暴力的重启服务方案
一般服务器重启可以直接通过 kill
命令杀死进程,然后重新启动一个新的进程即可。但这种方法比较粗暴,有可能导致某些正在处理中的客户端请求失败,如果请求正在写数据,那么还有可能导致数据丢失或者数据不一致等。
那么有什么方式可以优雅的重启服务呢?
优雅的重启服务方案
优雅的重启方式流程如下:
从上面的流程可以看出,旧进程必须等待所有的请求连接完成后才会退出,请求不会被强制关闭,所以是个优雅的重启方式。
使用Go实现优雅重启
下面我们使用Go语言来演示怎么实现优雅启动功能,我们先来看看原理图:
从原理图可以知道,重启时首先通过发送 SIGHUP信号
给服务进程,服务进程收到 SIGHUP信号
后会 fork
一个新进程来处理新的请求,然后新进程会发送 SIGTERM信号
给旧服务进程(父进程),旧服务进程接收到 SIGTERM信号
后会关闭监听的 socket句柄
(停止接收新请求),并且等待未处理完成的请求完成后再退出进程。
下面通过代码来说明这个流程,代码主要参考 endless 这个库,有兴趣可以查看其源码。
首先我们定义一个名为 endlessServer
的结构并且继承 http.Server
结构:
type endlessServer struct {
http.ServerEndlessListener net.Listener
wg sync.WaitGroup
sigChan chan os.Signal
isChild bool
state uint8
lock *sync.RWMutex
}
Go的继承很简单,就是在定义结构时把要继承的结构嵌入到里面就可以了。
这里说明一下 endlessServer
各个成员的作用吧:
Server:用于继承
http.Server
结构EndlessListener:监听客户端请求的
Listener
wg:用于记录还有多少客户端请求没有完成
sigChan:用于接收信号的管道
isChild:用于重启时标志本进程是否是为一个新进程
state:当前进程的状态
lock:用于锁定一些资源
定义一个创建 endlessServer
结构的函数:
func NewServer(addr string, handler http.Handler) (srv *endlessServer) {isChild := os.Getenv("ENDLESS_CONTINUE") != ""
srv = &endlessServer{
wg: sync.WaitGroup{},
sigChan: make(chan os.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 = handlerreturn
}
NewServer()
函数的实现比较简单,就是创建一个 endlessServer
结构,然后初始化其各个成员。要注意的是,是否为新进程是通过读取环境变量 ENDLESS_CONTINUE
来判断的,如果定义了 ENDLESS_CONTINUE
环境变量,就是说当前进程是新的服务进程。
用过Go语言的HTTP包的同学应该知道,要进行监听客户端请求的话必须调用其 ListenAndServe()
函数,所以我们要定义这个函数:
func ListenAndServe(addr string, handler http.Handler) error {server := NewServer(addr, handler)return server.ListenAndServe()
}
函数的实现很简单,就是先调用 NewServer()
函数创建一个 endlessServer
结构,然后调用其 ListenAndServe()
方法。所以我们要为 endlessServer
结构定义一个 ListenAndServe()
方法:
func (srv *endlessServer) ListenAndServe() (err error) {addr := srv.Addrif addr == "" {
addr = ":http"
}go srv.handleSignals()l, err := srv.getListener(addr)if err != nil {
log.Println(err)return
}
srv.EndlessListener = newEndlessListener(l, srv)if srv.isChild {
syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
}return srv.Serve()
}
ListenAndServe()
方法首先会创建一个协程处理 handleSignals()
方法,这个方法主要是处理信号,下面会介绍。然后调用 getListener()
方法获取一个类型为 net.Listener
的对象,然后调用 newEndlessListener()
函数创建一个类型为 endlessListener
的对象。再通过判断当前进程是否为新的处理进程,如果是就调用 syscall.Kill()
方法发送一个 SIGTERM信号
给父进程(旧的服务处理进程),最后调用 Serve()
方法开始处理客户端连接。
我们先来看看处理信号的 handleSignal()
方法:
func (srv *endlessServer) handleSignals() {var sig os.Signal
signal.Notify(
srv.sigChan,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
)pid := syscall.Getpid()for {
sig = srv.sigChan
srv.signalHooks(PRE_SIGNAL, sig)switch sig {case syscall.SIGHUP:err := srv.fork()if err != nil {
log.Println("Fork err:", err)
}case syscall.SIGINT:
srv.shutdown()case syscall.SIGTERM:
srv.shutdown()default:
log.Printf("Received %v: nothing i care about...\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() (err error) {files := []*os.File{
srv.EndlessListener.(*endlessListener).File(),
}env := append(
os.Environ(),"ENDLESS_CONTINUE=1",
)path := os.Args[0]var args []stringif len(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()if err != nil {
log.Fatalf("Restart: Failed to launch, 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() (c net.Conn, err error) {tc, err := el.Listener.(*net.TCPListener).AcceptTCP()if err != nil {return
}
tc.SetKeepAlive(true) // see http.tcpKeepAliveListener
tc.SetKeepAlivePeriod(3 * time.Minute) // see http.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() (err error) {
err = srv.Server.Serve(srv.EndlessListener)
srv.wg.Wait()return
}
可以看到,endlessServer.Serve()
方法最后会调用 srv.wg.Wait()
这行代码来等待所有客户端请求完成。那么客户端连接计数器什么时候会减少呢?在 endlessConn.Close()
方法中可以看到计数器减少的操作:
func (w endlessConn) Close() error {err := w.Conn.Close()if err == nil {
w.server.wg.Done()
}return err
}
可以看到,endlessConn.Close()
方法最后会调用 w.server.wg.Done()
这行代码来减少客户端请求计数器。至此,优雅重启服务的实现就完成。
当然,本篇文章主要介绍的是优雅重启的原理,完成的源码实现还是要查看 endless 这个库。