简介
什么是优雅停止?在谈优雅停止前,我们可以说说什么是优雅重启,或者说热重启。
简言之,优雅重启就是在服务升级、配置更新时,要重新启动服务,优雅重启就是在服务不中断或连接不丢失的情况下,重启服务。优雅重启的整个流程中,新的进程将在旧的进程停止前启动,旧进程会完成活动中的请求后优雅地关闭进程。
优雅重启是服务开发中一个非常重要的概念,它让我们在不中断服务的情况下,更新代码和修复问题。它在维持高可用性的生产环境中尤其关键。
从上面的这段可知,优雅重启是由两个部分组成,分别是优雅停止和启动。
本文重点介绍优雅停止,而优雅启动的整个流程要借助于外部工具控制,如 k8s 的容器编排。
优雅停止
优雅停止,即要在停止服务的同时,保证业务的完整性。从目标上看,优雅停止经历三个步骤:通知服务停止、服务启动清理,等待清理确认退出。
要停止一个服务,首先是通过一些机制告知服务要执行退出前的工作,最常见的就是基于操作系统信号,我们惯例监听的信号主要是两个,分别是由 kill pid
发出的 sigterm 和 ctrl+c 发出的 sigint。 其他信号还有,ctrl+/ 发出的 sigquit。
当接收到指定信号,服务就要停止接受新的请求,且等待当前活动中的请求全部完成后再完全停止服务。
接下来,开始具体的代码实现部分吧。
从 http 服务开始
谈优雅重启,最常被引用的案例就是 http 服务,我将通过代码逐步演示这个过程。如下是一个常规 http 服务:
func hello(w http.responsewriter, r *http.request) { fmt.fprintf(w, "hello world\n") } func main() { http.handlefunc("/", hello) log.println("starting server on :8080") if err := http.listenandserve(":8080", nil); err != nil { log.fatal("listenandserve: ", err) } }
我们通过 time.sleep
增加 hello 的耗时,以便于调试。
func hello(w http.responsewriter, r *http.request) { fmt.fprintf(w, "hello world\n") time.sleep(10 * time.second) }
运行:
$ go run main.go
通过 curl 请求访问 http://localhost:8080/
,它进入到 10 秒的处理阶段。假设这时,我们 ctrl+c 请求退出,http 服务会直接退出,我们的 curl 请求被直接中断。
我们可以使用 go 标准库提供的 http.server
有一个 shutdown
方法,可以安全地关闭服务器而不中断任何活动的连接。而我们要做的,只需在收到停止信号后,执行 shutdown
即可。
信号方面,我们通过 go 标准库 signal
实现,它提供了一个 notify
函数,可与 chan nnel 配合传递信号消息。我们监听的目标信号是 sigint
和 sigterm
。
重新修改 http 服务入口,使用 http.server
的 shutdown
函数关闭 server
。
func main() { mux := http.newservemux() mux.handlefunc("/", hello) server := http.server{addr: ":8080", handler: mux} go server.listenandserve() quit := make(chan os.signal, 1) // 注册接收信号的 channel signal.notify(quit, syscall.sigint, syscall.sigterm) <-quit // 等待停止信号 if err := server.shutdown(context.background()); err != nil { log.fatal("shutdown: ", err) } }
我们将 server.listenandserve
运行于另一个 goroutine 中同时忽略了它的返回错误。
通过 signal.notify
注册信号。当收到如 ctrl+c 或 kill pid 发出的中断信号,执行 serve.shutdown
,它会通知到 server 停止接收新的请求,并等待活动中的连接处理完成。
现在运行 go run main.go
启动服务,执行 curl
命令测试接口,在请求还没有返回之时,我们可以通过 ctrl+c 停止服务,它会有一段时间等待,我们可以在这个过程中尝试 curl
请求,看它是否还接收新的请求。
如果希望防止程序假死,或者其他问题导致服务长时间无法退出,可通过 context.withtimeout
方法包装下传递给 shutdown
方法的 ctx
变量。
ctx, cancel := context.withtimeout(context.background(), 3*time.second) defer cancel() if err := server.shutdown(ctx); err != nil { log.fatal("shutdown: ", err) }
到这里,我们就介绍完了 go 标准库 net/http
的优雅停止的使用方案。
抽象出一个常规方案
如果开发一个非 http 的服务,如何让它支持优雅停止呢?毕竟不是所有项目都是 http 服务,不是所有项目都有现成的框架。
本文开头提到的的三步骤,net/http
包的 shutdown
把最核心的服务停止前的清理和等待都已经在内部实现了。我们可解读下它的实现。
进入到 shutdown
的源码中,重点是开头的第一句代码,如下所示:
// future calls to methods such as serve will return errserverclosed. func (srv *server) shutdown(ctx context.context) error { srv.inshutdown.store(true) // ...其他清理代码 // ...等待活动请求完成并将其关闭 }
inshutdown
是一个标志位,用于标识程序是否已停止。为了解决并发数据竞争,它的底层类型是 atomic.bool
,。
在 server.go
中的 server.serve
方法中,通过判断 inshutdown
决定是否继续接受新的请求。
func (srv *server) serve(l net.listener) error { // ... for { rw, err := l.accept() if err != nil { if srv.shuttingdown() { return errserverclosed } // ... }
我们可以从如上的分析中得知,要让 http 服务支持优雅停止要启动两个 goroutine,shutdown
运行与 main goroutine 中,当接收中停止信号,通过 inshutdown
标志位通知运行中的 goroutine。
用简化的代码表示这个一般模式。
var inshutdown bool func start() { for !inshutdown { // running time.sleep(10 * time.second) } } func shutdown() { inshutdown = true } func main() { go start() quit = make(chan os.signal, 1) signal.notify(quit, syscall.sigint, syscall.sigterm) <- quit shutdown() }
大概看起来是那么回事,但这里的代码少了一个步骤,即 shutdown
没有等待 start
完成。
标准库 net/http
是通过 for 循环不断检查是否有活动中的连接,如果连接没有进行中请求会将其关闭,直到将所有连接关闭,便会退出 shutdown
。
核心代码如下:
func (srv *server) shutdown(ctx context.context) { // ...之前的代码 timer := time.newtimer(nextpollinterval()) defer timer.stop() for { if srv.closeidleconns() { return lnerr } select { case <-ctx.done(): return ctx.err() case <-timer.c: timer.reset(nextpollinterval()) } } }
重点就是那句 closeidleconns
,它负责检查是否还有执行中的请求。我就不把这部分的源代码贴出来了。而检查频率是通过 timer 控制的。
现在让简化版等待 start
完成后才退出。我们引入一个名为 isstop
的标志位以监控停止状态。
var inshutdown bool var isstop bool func start() { for !inshutdown { // running time.sleep(10 * time.second) } isstop = true } func shutdown() { inshutdown = true timer := time.newtimer(time.millisecond) defer timer.stop() for { if isstop { return } <- timer.c timer.reset(time.millisecond)) } }
如上的代码中,start
函数退出时会执行 isstop = true
表明已退出,在 shutdown
中,通过定期检查 isstop
等待 start
退出完成。
此外,net/http
的 shutdown
方法还接收了一个 context.context
参数,允许实现超时控制,从而防止程序假死或强制关闭。
需要特别指出的是,示例中用的 isstop
和 inshutdown
标志位为非原子类型,在正式场景中,为避免数据竞争,要使用原子操作或其他同步机制。
除了可用共享内存标志位在不同协程间传递状态,也可以通过 channel 实现,或你看到过类似如下的形式。
var inshutdown bool func start(stop chan struct{}) { for !inshutdown { // running time.sleep(10 * time.second) } stop <- struct{}{} } func shutdown() { inshutdown = true } func main() { stop := make(chan struct{}) defer close(stop) go start(stop) go func() { quit := make(chan os.signal, 1) signal.notify(quit, syscall.sigint, syscall.sigterm) <-quit shutdown() }() <-stop }
如上的代码中,start
通过 channel 通知主 goroutine,当触发停止信号,isshutdown
通知 start
要停止退出,它成功退出后,通过 stop <- struct{}
通知主函数,结束等待。
总的来说,channel 的优势很明显,避免了单独管理一个 isstop 标志位来标识服务状态,并且免去了基于定时器的定期轮询检查的过程,还更加实时和高效。当然,net/http
使用轮询检查机制,是它的场景所决定,和我们这里不完全一样。
一点思考
go 语言支持多种方式在 goroutine 间传递信息,这催生了多样的优雅停止实现方式。如果是在涉及多个嵌套 goroutine 的场景中,我们可以引入 context 来实现多层级的状态和信息传递,确保操作的连贯性和安全性。
然尽管实现方式众多,但其核心思路是一致的,而底层目标始终是我们要保证处理逻辑的完整性。
另外,通过将优雅停止与容器编排技术结合,并为服务添加健康检查,我们能够确保服务总有实例在活跃状态,实现真正意义上的优雅重启。这不仅提高了服务的可靠性,也优化了资源的利用效率。
总结
本文探索了 go 语言中优雅重启的实现方法,展示了如何通过 http.server 的 shutdown 方法安全地重启服务,以及使用 context 控制超时。基于此,我们抽象出了一般服务优雅停止的核心思路。
以上就是详解如何在go中实现优雅停止的详细内容,更多关于在go中实现停止的资料请关注代码网其它相关文章!
发表评论