Golang http.Server安全退出:容易被误用的Shutdown()方法
我开了个新坑,错而知新这个栏目记录的是一些我犯了很久但又不自知,到头来焕然大悟的错误,用它们作为“认知的过程是螺旋的”这句话的论据简直再好不过了。
用Go写个HTTP服务很容易,但是让运行中的服务安全退出就不是那么直接了。
如果你对安全退出(graceful shutdown,也叫优雅退出)这个提法比较陌生,它是指HTTP服务在接到用户的退出指令后停止接受新请求,在处理和回复当前正在处理的这批请求后主动退出。区别于SIGKILL
(kill -9
或者“强行停止”),安全退出可以最小化程序在滚动更新时的服务抖动。
用户的退出指令一般是SIGTERM
(k8s的实现)或SIGINT
(常常对应bash的Ctrl + C
)。
监听信号
使用标准库signal即可完成信号监听,小场面:
|
|
值得注意的是,在没有使用signal.Notify()
时,Go默认有一套信号处理规则,比如 SIGHUP
, SIGINT
或SIGTERM
会让程序直接退出。
停止HTTP服务
调用运行中的Server实例的Shutdown()方法可以让服务安全退出:
|
|
GracefulServer
这么个用法:
|
|
有读者这时可能会问了,为什么要封装一层Server.ListenAndServe()
呢,加上shutdownFinished
这个channel的意义在哪里?别急,Server.Shutdown()
的文档里有一句:
When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately return ErrServerClosed. Make sure the program doesn’t exit and waits instead for Shutdown to return.
大意说Shutdown()
在调用后,Server.ListenAndServe()
立马返回,如果我们把ListenAndServe()
放在主函数main()
里,主函数很快就会退出。在Go中,不论此时其他goroutine是什么状态,主函数退出会让整个程序退出,那我们就没法安全地确保Server.Shutdown()
执行完毕了。于是,shutdownFinished
被放在这里提供保护。
有经验的读者这时也许又会问了,那么为什么不把Server.ListenAndServe()
的调用写到goroutine里,信号监听和Server.Shutdown()
写到主函数main()
里?就像这个例子里写的这样,还不用再封装一层Server,岂不美哉?
这里就见仁见智了,ListenAndServe()
在goroutine中的话,错误处理大概率是log.Fatal(err)
这样的操作,如果服务并不是主动退出的(比如启动时立马遇到端口占用的错误),主函数main()
中的defer是不会执行的。我这里用了一些额外的复杂度让安全退出的逻辑更圆满了一些。
如果你感兴趣,我在Github上放了一份完整的实现,你可以编译后自己请求再同时退出体验一下。
微妙的API
事实上,使用shutdownFinished
确保Server.Shutdown()
执行完毕这个操作是我最近才意识到的。大概有两年的时间,我一直都忽略了Server.ListenAndServe()
会在Server.Shutdown()
开始执行后立马返回这件事,一直使用着错误的实现。而且由于一些机缘巧合,如果在退出时没有正在处理的HTTP请求,这个错误的实现极大概率可以正确地安全退出。_(:зゝ∠)_
就我个人来看,Server.Shutdown()
是一个容易误用的API.
Server.Shutdown()
在2013年就开始讨论,最后在2016年的Go 1.8引入。而上面我们提到的文档里说明Server.ListenAndServe()
会在Server.Shutdown()
开始执行后立马返回的意外特性在半年之后专门加入到了文档中,这么说,也许我不是这个微妙而且容易不小心误用的API的唯一受害者。┑( ̄Д  ̄)┍
设计和增加对外的API真是一件困难的事情。最近Go Team在讨论怎么让标准库支持泛型,我衷心祝福他们好运。