使用Go实现优雅重启服务功能
暴力的重启服务方案
一般服务器重启可以直接通过kill命令杀死进程,然后重新启动一个新的进程即可。但这种方法比较粗暴,有可能导致某些正在处理中的客户端请求失败,如果请求正在写数据,那么还有可能导致数据丢失或者数据不一致等。
那么有什么方式可以优雅的重启服务呢?
优雅的重启服务方案
优雅的重启方式流程如下:
从上面的流程可以看出,旧进程必须等待所有的请求连接完成后才会退出,请求不会被强制关闭,所以是个优雅的重启方式。
使用Go实现优雅重启
下面我们使用Go语言来演示怎么实现优雅启动功能,我们先来看看原理图:
从原理图可以知道,重启时首先通过发送SIGHUP信号给服务进程,服务进程收到 SIGHUP信号后会 fork一个新进程来处理新的请求,然后新进程会发送 SIGTERM信号给旧服务进程(父进程),旧服务进程接收到 SIGTERM信号后会关闭监听的 socket句柄(停止接收新请求),并且等待未处理完成的请求完成后再退出进程。
下面通过代码来说明这个流程,代码主要参考endless这个库,有兴趣可以查看其源码。
首先我们定义一个名为endlessServer的结构并且继承 http.Server结构:
typeendlessServerstruct{ http.Server EndlessListenernet.Listener wgsync.WaitGroup sigChanchanos.Signal isChildbool stateuint8 lock*sync.RWMutex }
Go的继承很简单,就是在定义结构时把要继承的结构嵌入到里面就可以了。
这里说明一下endlessServer各个成员的作用吧:
- Server:用于继承http.Server结构
- EndlessListener:监听客户端请求的Listener
- wg:用于记录还有多少客户端请求没有完成
- sigChan:用于接收信号的管道
- isChild:用于重启时标志本进程是否是为一个新进程
- state:当前进程的状态
- lock:用于锁定一些资源
定义一个创建endlessServer结构的函数:
funcNewServer(addrstring,handlerhttp.Handler)(srv*endlessServer){ isChild:=os.Getenv("ENDLESS_CONTINUE")!="" srv=&endlessServer{ wg:sync.WaitGroup{}, sigChan:make(chanos.Signal), isChild:isChild, state:STATE_INIT, lock:&sync.RWMutex{}, } srv.Server.Addr=addr srv.Server.ReadTimeout=0 srv.Server.WriteTimeout=0 srv.Server.MaxHeaderBytes=0 srv.Server.Handler=handler return }
NewServer()函数的实现比较简单,就是创建一个 endlessServer结构,然后初始化其各个成员。要注意的是,是否为新进程是通过读取环境变量 ENDLESS_CONTINUE来判断的,如果定义了 ENDLESS_CONTINUE环境变量,就是说当前进程是新的服务进程。
用过Go语言的HTTP包的同学应该知道,要进行监听客户端请求的话必须调用其ListenAndServe()函数,所以我们要定义这个函数:
funcListenAndServe(addrstring,handlerhttp.Handler)error{ server:=NewServer(addr,handler) returnserver.ListenAndServe() }
函数的实现很简单,就是先调用NewServer()函数创建一个 endlessServer结构,然后调用其 ListenAndServe()方法。所以我们要为 endlessServer结构定义一个 ListenAndServe()方法:
func(srv*endlessServer)ListenAndServe()(errerror){ addr:=srv.Addr ifaddr==""{ addr=":http" } gosrv.handleSignals() l,err:=srv.getListener(addr) iferr!=nil{ log.Println(err) return } srv.EndlessListener=newEndlessListener(l,srv) ifsrv.isChild{ syscall.Kill(syscall.Getppid(),syscall.SIGTERM) } returnsrv.Serve() }
ListenAndServe()方法首先会创建一个协程处理 handleSignals()方法,这个方法主要是处理信号,下面会介绍。然后调用 getListener()方法获取一个类型为 net.Listener的对象,然后调用 newEndlessListener()函数创建一个类型为 endlessListener的对象。再通过判断当前进程是否为新的处理进程,如果是就调用 syscall.Kill()方法发送一个 SIGTERM信号给父进程(旧的服务处理进程),最后调用 Serve()方法开始处理客户端连接。
我们先来看看处理信号的handleSignal()方法:
func(srv*endlessServer)handleSignals(){ varsigos.Signal signal.Notify( srv.sigChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, ) pid:=syscall.Getpid() for{ sig=<-srv.sigChan srv.signalHooks(PRE_SIGNAL,sig) switchsig{ casesyscall.SIGHUP: err:=srv.fork() iferr!=nil{ log.Println("Forkerr:",err) } casesyscall.SIGINT: srv.shutdown() casesyscall.SIGTERM: srv.shutdown() default: log.Printf("Received%v:nothingicareabout...\n",sig) } } }
handleSignal()方法主要监听3种信号,syscall.SIGHUP、syscall.SIGINT和 syscall.SIGTERM。syscall.SIGHUP信号为重启信号,而 syscall.SIGINT信号为关闭服务信号,而 syscall.SIGTERM信号主要是新的服务进程发送给旧的服务进程,告诉其关闭监听处理客户端的socket。当收到 syscall.SIGHUP信号时,需要调用 fork()方法来创建一个新的服务进程,而收到 syscall.SIGINT和 syscall.SIGTERM信号主要调用 shutdown()方法来关闭当前进程。
再来看看创建新服务进程的fork()方法:
func(srv*endlessServer)fork()(errerror){ files:=[]*os.File{ srv.EndlessListener.(*endlessListener).File(), } env:=append( os.Environ(), "ENDLESS_CONTINUE=1", ) path:=os.Args[0] varargs[]string iflen(os.Args)>1{ args=os.Args[1:] } cmd:=exec.Command(path,args...) cmd.Stdout=os.Stdout cmd.Stderr=os.Stderr cmd.ExtraFiles=files cmd.Env=env err=cmd.Start() iferr!=nil{ log.Fatalf("Restart:Failedtolaunch,error:%v",err) } return }
fork()方法也比较简单,主要是使用 exec包的 Command()方法来创建一个 Cmd对象,然后调用其 Start()方法来启动一个新进。要注意的是,创建新进程前需要设置环境变量 ENDLESS_CONTINUE,这是告诉新进程需要发送 syscall.SIGTERM信号给父进程。还有就是通过 Cmd对象的 ExtraFiles成员把监听客户端连接的socket句柄传递给新服务处理进程了。
再来看看关闭服务进程的shutdown()方法:
func(srv*endlessServer)shutdown(){ err:=srv.EndlessListener.Close() }
这个方法很简单,就是调用net.Listener对象的 Close()方法来关闭监听客户端请求的socket。关闭监听客户端请求的socket后,主循环会退出处理,然后会退出进程。
接着我们来看看接收客户端请求的endlessListener.Accept()方法:
func(el*endlessListener)Accept()(cnet.Conn,errerror){ tc,err:=el.Listener.(*net.TCPListener).AcceptTCP() iferr!=nil{ return } tc.SetKeepAlive(true)//seehttp.tcpKeepAliveListener tc.SetKeepAlivePeriod(3*time.Minute)//seehttp.tcpKeepAliveListener c=endlessConn{ Conn:tc, server:el.server, } el.server.wg.Add(1) return }
主要要注意的是,函数最后会调用el.server.wg.Add(1)这行代码来增加客户端请求的计数器,这是优雅重启的关键。因为在 endlessServer.Serve()方法中会等待所有客户端请求处理完毕才会退出,我们来看看 endlessServer.Serve()方法的实现:
func(srv*endlessServer)Serve()(errerror){ err=srv.Server.Serve(srv.EndlessListener) srv.wg.Wait() return }
可以看到,endlessServer.Serve()方法最后会调用 srv.wg.Wait()这行代码来等待所有客户端请求完成。那么客户端连接计数器什么时候会减少呢?在 endlessConn.Close()方法中可以看到计数器减少的操作:
func(wendlessConn)Close()error{ err:=w.Conn.Close() iferr==nil{ w.server.wg.Done() } returnerr }
可以看到,endlessConn.Close()方法最后会调用 w.server.wg.Done()这行代码来减少客户端请求计数器。至此,优雅重启服务的实现就完成。
当然,本篇文章主要介绍的是优雅重启的原理,完成的源码实现还是要查看endless这个库。
总结
以上所述是小编给大家介绍的使用Go实现优雅重启服务功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。