本文转载自微信公众号《微服务实战》,作者hxl。转载本文请联系微服务实践公众号。服务器程序更新或重启时,如果我们直接kill-9杀掉旧进程,启动新进程,会出现以下问题:旧请求没有处理完,如果服务器进程直接退出,会导致客户端链接中断(receivedRST)有新的请求进来,服务还没有重启,导致连接被拒绝即使想退出程序,直接kill-9还是会中断正在处理的请求。直接的感受是:在重启过程中,会在一段时间内无法为用户提供正常的服务;同时,粗暴的关闭服务也可能污染业务所依赖的数据库等状态服务。因此,在服务重启或重新发布的过程中,我们必须做到新旧服务的无缝切换,同时保证变更服务零宕机!go-zero作为微服务框架,如何帮助开发者优雅退出?什么?下面我们一起来看看吧。优雅退出实现优雅重启首先要解决的问题之一就是如何优雅退出:对于http服务,一般的思路是关闭对fd的listen,保证没有新的请求进来,新的请求进来被处理。请求,然后退出。Go在http中原生提供了server.ShutDown(),我们来看看它是如何实现的:设置inShutdown标志关闭监听,保证没有新的请求进来,等待所有活动链接变为空闲,退出函数.结尾会单独解释一下,看看这几步的含义:inShutdownfunc(srv*Server)ListenAndServe()error{ifsrv.shuttingDown(){returnErrServerClosed}...//实际监听端口;生成一个listenerln,err:=net.Listen("tcp",addr)iferr!=nil{returnerr}//实际逻辑处理,将监听器注入returnsrv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})}func(s*Server)shuttingDown()bool{returnatomic.LoadInt32(&s.inShutdown)!=0}ListenAndServe是http启动服务器的必备函数。里面第一句是判断Server是否关闭。inShutdown是一个原子变量,非零表示关闭。listenersfunc(srv*Server)Serve(lnet.Listener)error{...//将注入的侦听器添加到内部映射//以方便后续控制从侦听器链接到的请求if!srv.trackListener(&l,true){returnErrServerClosed}defersrv.trackListener(&l,false)...}在Server内部的listenersmap中注册listener,在ShutDown中直接从listeners中获取,然后执行listener.Close(),TCP之后挥手四次,新的请求不会进入。closeIdleConns简单的说就是:将当前Server中记录的活动连接变为空闲状态,并返回。closefunc(srv*Server)Serve(lnet.Listener)error{...for{rw,err:=l.Accept()//此时accept会出错,因为在iferr之前已经关闭了listener!=nil{select{//Anotherflag:doneChancase<-srv.getDoneChan():returnErrServerCloseddefault:}}}}在getDoneChan之前监听器已经关闭时,推送到doneChan通道。总结一下:Shutdown可以在不中断已经活动的链接的情况下优雅地终止服务。但是在服务启动后的某个时刻,程序怎么知道服务中断了呢?服务中断时如何通知程序,然后调用Shutdown进行处理?接下来我们看一下系统信号通知函数的作用。服务中断取决于这个时间。操作系统本身提供的信号。对应gonative,Notifyofsignal提供了系统信号通知的能力。https://github.com/tal-tech/go-zero/blob/master/core/proc/signals.gofuncinit(){gofunc(){varprofilerStoppersignals:=make(chanos.Signal,1)signal.Notify(信号,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGTERM)for{v:=<-signalsswitchv{casesyscall.SIGUSR1:dumpGoroutines()casesyscall.SIGUSR2:ifprofiler==nil{profiler=StartProfile()}else{profiler.Stop()profiler=nil}casesyscall.SIGTERM://正在执行正常关闭的地方对错误分析很有用SIGUSR2->开启/关闭所有指标监控,自己控制profiling时间SIGTERM->真正开启gracefulStop,优雅关闭gracefulStop过程如下:取消监听信号,毕竟你必须退出,不需要反复监听wrapup,关闭当前服务请求,和资源time.Sleep(),等待资源处理完成,再关闭shut下来并通知出口。如果主goroutine还没有退出,它会主动发送SIGKILL退出进程。这样服务就不会再接受新的请求,service-active请求等待处理完成,同时等待资源关闭(数据库连接等),如果超时则强制退出。总体流程我们当前的go程序是运行在docker容器中的,所以在服务发布过程中,k8s会向容器发送一个SIGTERM信号,然后容器中的程序会收到该信号,开始执行ShutDown:这里,整个优雅关机过程就是这样。但是还是有顺利重启的,这就要看k8s了。基本流程如下:在老的pod退出之前,启动新的pod。旧的pod继续处理接受的请求,不再接受新的请求。新的pod接受并处理新的请求,这样旧的pod就退出了,这样整个服务重启才算成功。如果新的pod启动失败,老的pod也可以提供服务,不影响当前在线服务。项目地址https://github.com/tal-tech/go-zero欢迎使用go-zero和star支持我们!
