bigywater.tech
Go · 95

os/signal 与优雅退出:Shutdown 到底等什么

Kubernetes 一类系统停服务时先发 SIGTERM,服务如果不处理,正在处理的请求就会被直接掐掉。优雅退出的核心是 Shutdown,但它到底等什么、不等什么?这篇把服务做成单独的进程,用脚本发真实的 SIGTERM,看客户端和进程的结果。

0.51 秒 vs 2.00 秒
不处理信号与优雅关闭时,2 秒的在途请求的结局:被断开 vs 正常完成
1.63 秒
优雅关闭时从发信号到进程退出的时间(等在途请求结束)
不等 Hijack
Shutdown 不会等被劫持的连接,返回 nil 后进程立即退出

不处理信号

实测 · 本机执行输出
  客户端发出一个要处理 2 秒的 /slow 请求,0.5 秒后给服务进程发 SIGTERM:
    客户端的这个请求:0.51 秒后失败:RemoteDisconnected: Remote end closed connection without response
    SIGTERM 之后新发的请求:0.00 秒后失败:ConnectionRefusedError: [Errno 61] Connection refused
    服务进程退出码 -15(负数表示被信号杀死),发信号后 0.20 秒内退出

SIGTERM 的默认行为就是终止进程。在途的 2 秒请求在 0.51 秒时被断开,客户端看到没有响应的连接关闭;新请求直接被拒绝(连接被拒)。

NotifyContext + Shutdown

实测 · 本机执行输出
    客户端的这个请求:2.00 秒后收到 200 'slow done'
    SIGTERM 之后新发的请求:0.00 秒后失败:ConnectionRefusedError: [Errno 61] Connection refused
    服务进程退出码 0,发信号后 1.63 秒退出
  [服务进程 +   0 ms] 开始服务,等待 SIGINT/SIGTERM
  [服务进程 + 755 ms] 收到信号,调用 srv.Shutdown
  [服务进程 +2360 ms] Shutdown 返回:<nil>

signal.NotifyContext 把信号变成 context 的取消;收到后调用 srv.Shutdown:先关监听(新请求被拒绝),再等在途请求完成。在途请求 2.00 秒后正常返回 200,进程在发信号后 1.63 秒以 0 退出。注意 main 要等 Shutdown 返回再退出。

Shutdown 的超时

实测 · 本机执行输出
    客户端的这个请求:1.01 秒后失败:RemoteDisconnected: Remote end closed connection without response
    服务进程退出码 0,发信号后 0.50 秒退出
  [服务进程 +   0 ms] 开始服务,等待 SIGINT/SIGTERM
  [服务进程 + 750 ms] 收到信号,调用 srv.Shutdown
  [服务进程 +1251 ms] Shutdown 返回:context deadline exceeded

给 Shutdown 的 context 设了 500 ms,而请求需要 2 秒:500 ms 一到它返回 context deadline exceeded,进程退出,请求被截断。超时要略大于最长请求,但也要小于编排系统给的强制杀死期限。

被 Hijack 的连接

实测 · 本机执行输出
    客户端的这个请求:0.51 秒后失败:RemoteDisconnected: Remote end closed connection without response
    服务进程退出码 0,发信号后 0.20 秒退出
  [服务进程 +   0 ms] 开始服务,等待 SIGINT/SIGTERM
  [服务进程 + 755 ms] 收到信号,调用 srv.Shutdown
  [服务进程 + 755 ms] Shutdown 返回:<nil>

WebSocket 之类被 Hijack 的连接不在 Shutdown 的管理范围内:它立刻返回 nil,随后进程退出,连接被断。需要自己通过 RegisterOnShutdown 通知这些连接并等待它们结束。

文档

实测 · 本机执行输出
  Shutdown gracefully shuts down the server without interrupting any active connections. Shutdown works by first closing all open listeners, then closing all idle connections, and then waiting indefinitely for connections to return to idle and then shut down. If the provided context expires before the shutdown is complete, Shutdown returns the context's error, otherwise it returns any error returned from closing the Server's underlying Listener(s). When Shutdown is called, Serve, ServeTLS, ListenAndServe, and ListenAndServeTLS immediately return ErrServerClosed. Make sure the program doesn't exit and waits instead for Shutdown to return. Shutdown does not attempt to close nor wait for hijacked connections such as WebSockets. The caller of Shutdown should separately notify such long-lived connections of shutdown and wait for them to close, if desired. See Server.RegisterOnShutdown for a way to register shutdown notification

文档的描述和实验一致:先关监听器,再关空闲连接,然后无限期等待活动连接;调用后 Serve 返回 ErrServerClosed,程序应等待 Shutdown 返回。

分步演示

优雅退出
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。服务端是单独编译的 Go 程序,由 Python 脚本启动、用 os.kill 发 SIGTERM,并用 http.client 发请求;时间用 Python 的 time.time,精度约 10 ms;每种情形只运行一次;读过的文档:go doc net/http.Server.Shutdown
    • 没有验证:SIGINT/第二次信号处理(NotifyContext 的 stop 之后默认行为恢复)、Kubernetes 的 preStop 与 terminationGracePeriod、keep-alive 空闲连接的关闭细节、HTTP/2 下的 GOAWAY、Linux 上的差别、RegisterOnShutdown 的实际写法
    京ICP备2026067502号-1京公网安备11010502063487号