Golang怎么使用channel实现一个优雅退出功能

免费教程   2024年05月10日 6:15  

这篇文章主要介绍了Golang怎么使用channel实现一个优雅退出功能的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Golang怎么使用channel实现一个优雅退出功能文章都会有所收获,下面我们一起来看看吧。

实现思路

通过一个 os.Signal 类型的 chan 接收退出信号,收到信号后进行对应的退出收尾工作,利用 context.WithTimeout 或 time.After 等方式设置退出超时时间防止收尾等待时间过长。

读源码

由于 Hertz 的 Hook 功能中的 ShutdownHook 是 graceful shutdown 的一环,并且 Hook 功能的实现也不是很难所以这里就一起分析了,如果不想看直接跳到后面的章节即可 :)

Hook

Hook 函数是一个通用的概念,表示某事件触发时所伴随的操作,Hertz 提供了 StartHook 和 ShutdownHook 用于在服务触发启动后和退出前注入用户自己的处理逻辑。

两种 Hook 具体是作为两种不同类型的 Hertz Engine 字段,用户可以直接以 append 的方式添加自己的 Hooks,下面是作为 Hertz Engine 字段的代码:

typeEnginestruct{...//HookfunctionsgettriggeredsequentiallywhenenginestartOnRun[]CtxErrCallback//HookfunctionsgettriggeredsimultaneouslywhenengineshutdownOnShutdown[]CtxCallback...}

可以看到两者都是函数数组的形式,并且是公开字段,所以可以直接 append,函数的签名如下,OnShutdown 的函数不会返回 error 因为都退出了所以没法对错误进行处理:

//OnRuntypeCtxCallbackfunc(ctxcontext.Context)//OnShutdowntypeCtxErrCallbackfunc(ctxcontext.Context)error

并且设置的 StartHook 会按照声明顺序依次调用,但是 ShutdownHook 会并发的进行调用,这里的实现后面会讲。

StartHook 的执行时机

触发 Server 启动后,框架会按函数声明顺序依次调用所有的 StartHook 函数,完成调用之后,才会正式开始端口监听,如果发生错误,则立刻终止服务。

上面是官方文档中描述的 StartHook 的执行时机,具体在源码中就是下面的代码:

func(engine*Engine)Run()(errerror){...//triggerhooksifanyctx:=context.Background()fori:=rangeengine.OnRun{iferr=engine.OnRun[i](ctx);err!=nil{returnerr}}returnengine.listenAndServe()}

熟悉或使用过 Hertz 的同学肯定知道 h.Spin() 方法调用后会正式启动 Hertz 的 HTTP 服务,而上面的 engine.Run 方法则是被 h.Spin 异步调用的。可以看到在 engine.Run 方法里循环调用 engine.OnRun 数组中注册的函数,最后执行完成完成并且没有 error 的情况下才会执行 engine.listenAndServe() 正式开始端口监听,和官方文档中说的一致,并且这里是通过 for 循环调用的所以也正如文档所说框架会按函数声明顺序依次调用。

ShutdownHook 的执行时机

Server 退出前,框架会并发地调用所有声明的 ShutdownHook 函数,并且可以通过 server.WithExitWaitTime配置最大等待时长,默认为5秒,如果超时,则立刻终止服务。

上面是官方文档中描述的 ShutdownHook 的执行时机,具体在源码中就是下面的代码:

func(engine*Engine)executeOnShutdownHooks(ctxcontext.Context,chchanstruct{}){wg:=sync.WaitGroup{}fori:=rangeengine.OnShutdown{wg.Add(1)gofunc(indexint){deferwg.Done()engine.OnShutdown[index](ctx)}(i)}wg.Wait()ch<-struct{}{}}

通过 sync.WaitGroup 保证每个 ShutdownHook 函数都执行完毕后给形参 ch 发送信号通知,注意这里每个 ShutdownHook 都起了一个协程,所以是并发执行,这也是官方文档所说的并发的进行调用。

服务注册与下线的执行时机

服务注册

Hertz 虽然是一个 HTTP 框架,但是 Hertz 的客户端和服务端可以通过注册中心进行服务发现并进行调用,并且 Hertz 也提供了大部分常用的注册中心扩展,在下面的 initOnRunHooks 方法中,通过注册一个 StartHook 调用 Registry 接口的 Register 方法对服务进行注册。

func(h*Hertz)initOnRunHooks(errChanchanerror){//addregisterfunctorunHooksopt:=h.GetOptions()h.OnRun=append(h.OnRun,func(ctxcontext.Context)error{gofunc(){//delayregister1stime.Sleep(1*time.Second)iferr:=opt.Registry.Register(opt.RegistryInfo);err!=nil{hlog.SystemLogger().Errorf("Registererror=%v",err)//passerrtoerrChanerrChan<-err}}()returnnil})}

取消注册

在 Shutdown 方法中进行调用 Deregister 取消注册,可以看到刚刚提到的 executeOnShutdownHooks 的方法在开始异步执行后就会进行取消注册操作。

func(engine*Engine)Shutdown(ctxcontext.Context)(errerror){...ch:=make(chanstruct{})//triggerhooksifanygoengine.executeOnShutdownHooks(ctx,ch)deferfunc(){//ensurethatthehookisexecuteduntilwaittimeoutorfinishselect{case<-ctx.Done():hlog.SystemLogger().Infof("ExecuteOnShutdownHookstimeout:error=%v",ctx.Err())returncase<-ch:hlog.SystemLogger().Info("ExecuteOnShutdownHooksfinish")return}}()ifopt:=engine.options;opt!=nil&&opt.Registry!=nil{iferr=opt.Registry.Deregister(opt.RegistryInfo);err!=nil{hlog.SystemLogger().Errorf("Deregistererror=%v",err)returnerr}}...}Engine Status

讲 graceful shutdown 之前最好了解一下 Hertz Engine 的 status 字段以获得更好的阅读体验ww

typeEnginestruct{...//Indicatestheenginestatus(Init/Running/Shutdown/Closed).statusuint32...}

如上所示,status 是一个 uint32 类型的内部字段,用来表示 Hertz Engine 的状态,具体具有四种状态(Init 1, Running 2, Shutdown 3, Closed 4),由下面的常量定义。

const(_uint32=iotastatusInitializedstatusRunningstatusShutdownstatusClosed)

下面列出了 Hertz Engine 状态改变的时机:

函数状态改变前状态改变后engine.Init0Init (1)engine.RunInit (1)Running (2)engine.ShutdownRunning (2)Shutdown (3)engine.Run defer?Closed (4)

对状态的改变都是通过 atomic 包下的函数进行更改的,保证了并发安全。

优雅退出

Hertz Graceful Shutdown 功能的核心方法如下,signalToNotify 数组包含了所有会触发退出的信号,触发了的信号会传向 signals 这个 ,并且 Hertz 会根据收到信号类型决定进行优雅退出还是强制退出。

//Defaultimplementationforsignalwaiter.//SIGTERMtriggersimmediatelyclose.//SIGHUP|SIGINTtriggersgracefulshutdown.funcwaitSignal(errChchanerror)error{signalToNotify:=[]os.Signal{syscall.SIGINT,syscall.SIGHUP,syscall.SIGTERM}ifsignal.Ignored(syscall.SIGHUP){signalToNotify=[]os.Signal{syscall.SIGINT,syscall.SIGTERM}}signals:=make(chanos.Signal,1)signal.Notify(signals,signalToNotify...)select{casesig:=<-signals:switchsig{casesyscall.SIGTERM://forceexitreturnerrors.New(sig.String())//nolintcasesyscall.SIGHUP,syscall.SIGINT:hlog.SystemLogger().Infof("Receivedsignal:%s\n",sig)//gracefulshutdownreturnnil}caseerr:=<-errCh://erroroccurs,exitimmediatelyreturnerr}returnnil}

如果 engine.Run 方法返回了一个错误则会通过 errCh 传入 waitSignal 函数然后触发立刻退出。前面也提到 h.Spin() 是以异步的方式调用 engine.Run,waitSignal 则由 h.Spin() 直接调用,所以运行后 Hertz 会阻塞在 waitSignal 函数的 select 这里等待信号。

三个会触发 Shutdown 的信号区别如下:

syscall.SIGINT 表示中断信号,通常由用户在终端上按下 Ctrl+C 触发,用于请求程序停止运行;

syscall.SIGHUP 表示挂起信号,通常是由系统发送给进程,用于通知进程它的终端或控制台已经断开连接或终止,进程需要做一些清理工作;

syscall.SIGTERM 表示终止信号,通常也是由系统发送给进程,用于请求进程正常地终止运行,进程需要做一些清理工作;

如果 waitSignal 的返回值为 nil 则 h.Spin() 会进行优雅退出:

func(h*Hertz)Spin(){errCh:=make(chanerror)h.initOnRunHooks(errCh)gofunc(){errCh<-h.Run()}()signalWaiter:=waitSignalifh.signalWaiter!=nil{signalWaiter=h.signalWaiter}iferr:=signalWaiter(errCh);err!=nil{hlog.SystemLogger().Errorf("Receiveclosesignal:error=%v",err)iferr:=h.Engine.Close();err!=nil{hlog.SystemLogger().Errorf("Closeerror=%v",err)}return}hlog.SystemLogger().Infof("Begingracefulshutdown,waitatmostnum=%dseconds...",h.GetOptions().ExitWaitTimeout/time.Second)ctx,cancel:=context.WithTimeout(context.Background(),h.GetOptions().ExitWaitTimeout)defercancel()iferr:=h.Shutdown(ctx);err!=nil{hlog.SystemLogger().Errorf("Shutdownerror=%v",err)}}

并且 Hertz 通过 context.WithTimeout 的方式设置了优雅退出的超时时长,默认为 5 秒,用户可以通过 WithExitWaitTime 方法配置 server 的优雅退出超时时长。将设置了超时时间的 ctx 传入 Shutdown 方法,如果 ShutdownHook 先执行完毕则 ch channel 收到信号后返回退出,否则 Context 超时收到信号强制返回退出。

func(engine*Engine)Shutdown(ctxcontext.Context)(errerror){...ch:=make(chanstruct{})//triggerhooksifanygoengine.executeOnShutdownHooks(ctx,ch)deferfunc(){//ensurethatthehookisexecuteduntilwaittimeoutorfinishselect{case<-ctx.Done():hlog.SystemLogger().Infof("ExecuteOnShutdownHookstimeout:error=%v",ctx.Err())returncase<-ch:hlog.SystemLogger().Info("ExecuteOnShutdownHooksfinish")return}}()...return}

以上就是 Hertz 优雅退出部分的源码分析,可以发现 Hertz 多次利用了协程,通过 channel 传递信号进行流程控制和信息传递,并通过 Context 的超时机制完成了整个优雅退出流程。

自己实现

说是自己实现实际上也就是代码搬运工,把 Hertz 的 graceful shutdown 及其相关功能给 PIANO 进行适配罢了ww

代码实现都差不多,一些小细节根据我个人的习惯做了修改,完整修改参考这个 commit,对 PIANO 感兴趣的话欢迎 Star !

适配 HooktypeEnginestruct{...//hookOnRun[]HookFuncWithErrOnShutdown[]HookFunc...}type(HookFuncfunc(ctxcontext.Context)HookFuncWithErrfunc(ctxcontext.Context)error)func(e*Engine)executeOnRunHooks(ctxcontext.Context)error{for_,h:=rangee.OnRun{iferr:=h(ctx);err!=nil{returnerr}}returnnil}func(e*Engine)executeOnShutdownHooks(ctxcontext.Context,chchanstruct{}){wg:=sync.WaitGroup{}for_,h:=rangee.OnShutdown{wg.Add(1)gofunc(hookHookFunc){deferwg.Done()hook(ctx)}(h)}wg.Wait()ch<-struct{}{}}适配 Engine StatustypeEnginestruct{...//initialized|running|shutdown|closedstatusuint32...}const(_uint32=iotastatusInitializedstatusRunningstatusShutdownstatusClosed)适配 Graceful Shutdown//PlaythePIANOnowfunc(p*Piano)Play(){errCh:=make(chanerror)gofunc(){errCh<-p.Run()}()waitSignal:=func(errChchanerror)error{signalToNotify:=[]os.Signal{syscall.SIGHUP,syscall.SIGINT,syscall.SIGTERM}ifsignal.Ignored(syscall.SIGHUP){signalToNotify=signalToNotify[1:]}signalCh:=make(chanos.Signal,1)signal.Notify(signalCh,signalToNotify...)select{casesig:=<-signalCh:switchsig{casesyscall.SIGTERM://forceexitreturnerrors.New(sig.String())casesyscall.SIGHUP,syscall.SIGINT://gracefulshutdownlog.Infof("---PIANO---Receivesignal:%v",sig)returnnil}caseerr:=<-errCh:returnerr}returnnil}iferr:=waitSignal(errCh);err!=nil{log.Errorf("---PIANO---Receiveclosesignalerror:%v",err)return}log.Infof("---PIANO---Begingracefulshutdown,waitupto%dseconds",p.Options().ShutdownTimeout/time.Second)ctx,cancel:=context.WithTimeout(context.Background(),p.Options().ShutdownTimeout)defercancel()iferr:=p.Shutdown(ctx);err!=nil{log.Errorf("---PIANO---Shutdownerr:%v",err)}}

关于“Golang怎么使用channel实现一个优雅退出功能”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“Golang怎么使用channel实现一个优雅退出功能”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注行业资讯频道。

域名注册
购买VPS主机

您或许对下面这些文章有兴趣:                    本月吐槽辛苦排行榜

看贴要回贴有N种理由!看帖不回贴的后果你懂得的!


评论内容 (*必填):
(Ctrl + Enter提交)   

部落快速搜索栏

各类专题梳理

网站导航栏

X
返回顶部