使用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(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。