Go程序收不到SIGTERM通常因shell封装导致信号未传递至主进程;需用exec启动或直接ENTRYPOINT,确保Go为PID 1,并手动管理WebSocket等长连接关闭,配合preStop与readinessProbe实现真正优雅退出。

Go 程序收不到 SIGTERM?先查是不是被 shell 挡住了
绝大多数“优雅退出不生效”的问题,根本不是代码写得不对,而是容器启动方式错了:你的 Go 二进制被 shell 包了一层(比如 sh -c "./app" 或没加 exec 的 entrypoint 脚本),导致 K8s 发的 SIGTERM 只打到 shell 进程上,根本传不到 Go 主进程。
- K8s 的
preStop和terminationGracePeriodSeconds都依赖主进程收到SIGTERM,否则整个优雅流程从第一步就断了 - Dockerfile 中避免写
CMD ["sh", "-c", "./myapp"];改用ENTRYPOINT ["./myapp"]或CMD ["./myapp"] - 如果必须用脚本启动,务必在最后一行写
exec ./myapp——exec会用 Go 进程替换当前 shell,让 Go 成为 PID 1,信号才能直达 - 验证方法:
kubectl exec -it <pod> -- ps aux,确认./myapp的 PID 是 1
server.Shutdown() 为什么没等 WebSocket 关闭就超时退出?
http.Server.Shutdown() 只管理 net/http 内部的连接生命周期,对 Upgrade 后的连接(如 WebSocket、SSE)完全无感——它们已脱离 HTTP 处理链,变成裸 TCP 连接,Shutdown() 不会主动 close 或等待它们。
- 现象:调用
Shutdown()后立刻返回,但客户端 WebSocket 还连着,几秒后突然断开(RST),日志里看不到连接关闭痕迹 - 解决核心:自己维护长连接列表,在收到
SIGTERM后主动通知并等待它们关闭 - 典型做法:用
sync.Map存 WebSocket*websocket.Conn,每次Upgrader.Upgrade()后存入;收到信号时遍历调用conn.Close(),再用time.AfterFunc()或sync.WaitGroup等待所有连接真正断开 - 注意:不要在
Shutdown()调用前就关掉 listener,否则新 Upgrade 会被拒绝,但已有连接仍可发消息
preStop + readinessProbe 怎么配合才能真正“先断流”?
只靠 Go 层关 server 不够,K8s endpoint controller 从发现 readiness 探针失败到把 Pod 从 Endpoints 列表摘除,有可观延迟(可能几百毫秒到几秒)。必须用 preStop 强制“立即断流”。
-
preStop必须是同步 HTTP 请求(如curl -X POST http://localhost:8080/shutdown),且该接口要立刻让服务进入“拒绝新请求”状态(比如关掉 listener、返回 503) -
readinessProbe的httpGet.path必须和 shutdown 接口一致(如/shutdown),并在 shutdown 触发后立即返回 503,这样 endpoint controller 下次 probe 就能快速摘除 -
preStop默认超时 30 秒,但实际执行必须远快于terminationGracePeriodSeconds(建议 ≤5s),否则会被跳过;别在 preStop 里做 DB 连接池 drain 这类耗时操作 - 关键检查点:
kubectl get endpoints <svc-name>,删 pod 后立刻执行,确认 IP:port 是否秒级消失
超时时间设多少?Shutdown() 的 context 别乱传
常见错误是把 Shutdown() 的 context 设成全局 long-lived context,或直接用 context.Background(),导致它永远等不到超时,卡死进程。
立即学习“go语言免费学习笔记(深入)”;
- 必须用带 deadline 的 context:
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second),且这个 25s 要比 K8s 的terminationGracePeriodSeconds(默认 30s)小至少 3–5 秒,留出 preStop、probe 更新、网络延迟的余量 - 别用
context.WithCancel(context.Background())然后手动 cancel —— 容易漏调或早调,Shutdown 逻辑反而更难测 - 调用
server.Shutdown(ctx)后,一定要cancel(),否则 goroutine 泄漏风险上升 - 如果你的服务还启了 gRPC server、metrics server 等其他监听器,每个都得单独 Shutdown,且共用同一个 timeout context
最常被忽略的一点:优雅退出不是“等固定时间”,而是“等活请求自然结束 + 主动切断长连接 + 让 K8s 主动停止转发”。三个环节缺一不可,任何一个断掉,都会在发布瞬间看到 5xx 或连接重置。特别是 WebSocket 场景,光靠 Shutdown() 就是掩耳盗铃。










