go进程收不到sigterm主因是容器启动方式错误导致信号未传递至pid 1:应避免shell封装,改用entrypoint["./app"]或cmd["./app"],若需脚本则末行加exec;go中需监听syscall.sigterm,shutdown前关闭listener并设超时,主动管理长连接,配合prestop调/shutdown接口与合理readinessprobe、maxsurge配置实现真正优雅退出。

Go进程收不到SIGTERM?先查PID是不是1
绝大多数“优雅停机失效”问题,根源不在Go代码,而在容器启动方式。K8s发的SIGTERM只传给PID 1进程;如果你的Dockerfile写的是CMD ["sh", "-c", "./app"]或没加exec的entrypoint脚本,那shell就是PID 1,Go进程只是子进程——信号根本进不来。
验证方法:kubectl exec -it <pod> -- ps aux</pod>,看./app的PID是否为1。
- 正确写法:
ENTRYPOINT ["./app"]或CMD ["./app"] - 必须用shell时:最后一行必须是
exec ./app(不是./app) - 别在
preStop里用httpGet去调shutdown接口——它不保证在SIGTERM前执行,且可能被跳过
http.Server.Shutdown()为啥不等WebSocket断开?
http.Server.Shutdown()只管理HTTP handler生命周期内的连接,对Upgrade后的长连接(如websocket.Conn、SSE流)完全无感。它们已脱离net/http控制,变成裸TCP连接,Shutdown()既不关闭也不等待。
现象:调用Shutdown()后立刻返回,但客户端还在发消息,几秒后突然RST断连,日志里看不到Close()痕迹。
- 自己维护连接池:
sync.Map存*websocket.Conn,Upgrader.Upgrade()后存入 - 收到
SIGTERM后遍历调用conn.Close(),再用sync.WaitGroup或time.AfterFunc()等待全部真正关闭 - 别在
Shutdown()前就close(listener)——否则新Upgrade会被拒,但已有连接仍可通信
preStop + readinessProbe怎么配合才能“秒级断流”?
K8s endpoint controller从探测到readinessProbe失败,到把Pod从Endpoints摘除,有几百毫秒到数秒延迟。仅靠Go层关server,流量还会继续进来。
关键不是“等K8s发现”,而是“主动让K8s立刻发现”。preStop必须同步触发状态切换。
-
preStop.exec里发一个快速请求到http://localhost:/shutdown,让Go服务立即拒绝新连接 -
readinessProbe.httpGet.path也配成/shutdown或/healthz,并在shutdown触发后返回503 -
preStop默认超时30秒,但必须远小于terminationGracePeriodSeconds(建议≤5s),否则会被跳过 - 别在
preStop里做DB连接池drain、文件刷盘等耗时操作——这些该由Go主进程自己处理
terminationGracePeriodSeconds设多少才够用?
这个值不是“我打算等多久”,而是“K8s最多给我多少时间完成所有清理”。它包含preStop执行时间 + SIGTERM处理时间,且两者并行计时。
设太短:未完成清理就被SIGKILL强杀;设太长:滚动更新卡住,影响发布节奏。
-
http.Server.Shutdown()的ctx超时建议设为该值的70%~80%(如terminationGracePeriodSeconds: 30,则Shutdown(ctx)用25s) - 长连接等待逻辑(如WebSocket批量Close)必须在这个总窗口内完成,否则照样被强杀
- 真实压测很重要:模拟高并发WebSocket连接下,从
SIGTERM到所有连接真正EOF的实际耗时
最常被忽略的一点:WebSocket连接的Close()调用只是发FIN包,不代表对方已收到或已断开。真正在意平滑性,得监听conn.ReadMessage()返回io.EOF或websocket.CloseMessage才算彻底清理干净。











