go进程收不到sigterm主因是未成为pid 1:shell封装导致信号止于sh,须用entrypoint["./app"]或脚本末行加exec;shutdown()不感知websocket,需sync.map手动管理并等待关闭;prestop与readinessprobe须同路径快速响应以实现先断流。

Go 服务在 K8s 里收不到 SIGTERM,或 WebSocket 连接被 RST 中断,基本不是代码漏写了 shutdown,而是信号根本没传到 Go 进程、或者 Shutdown() 根本不感知长连接。
为什么 Go 进程收不到 SIGTERM?
绝大多数“优雅退出卡住”或“直接断连”的根源,是容器启动时 Go 二进制没成为 PID 1。
常见错误写法:
-
CMD ["sh", "-c", "./myapp"]—— sh 是 PID 1,SIGTERM 打到 shell 上就停了,Go 完全无感 - ENTRYPOINT 脚本里最后一行没加
exec ./myapp—— shell 进程残留,Go 是子进程,收不到信号
验证方法:kubectl exec -it <pod> -- ps aux</pod>,确认 ./myapp 的 PID 是 1。
正确做法:
- 直接
ENTRYPOINT ["./myapp"](推荐) - 或
CMD ["./myapp"](前提是镜像 base 不带 shell 封装) - 必须用脚本?结尾务必写
exec ./myapp,不能只写./myapp
server.Shutdown() 为什么不管 WebSocket?
http.Server.Shutdown() 只等待标准 HTTP 请求完成,对 Upgrade 后的连接(如 WebSocket、SSE、HTTP/2 流)完全不跟踪 —— 它们已脱离 net/http 管理,变成裸 TCP 连接。
现象:调用 Shutdown() 后立刻返回,但客户端仍连着,几秒后突遭 RST。
必须自己管理:
- 用
sync.Map或sync.WaitGroup记录每个*websocket.Conn - 在
Upgrader.Upgrade()成功后存入,并在conn.Close()后清理 - 收到 SIGTERM 后,遍历调用
conn.Close(),再用time.AfterFunc()或WaitGroup.Wait()等待真正断开 - 别提前
lis.Close()—— 已建立的 WebSocket 还能发消息,新 Upgrade 才会被拒
preStop + readinessProbe 怎么配合才真“先断流”?
K8s 的 endpoint controller 从探针失败到摘除 Pod,有几百毫秒到数秒延迟。单靠 Go 层关 server,新请求仍可能打进来。
关键动作必须在 preStop 钩子里触发:
-
preStop里发一个快速请求(如curl -X POST http://localhost:8080/shutdown),让 Go 服务立即切换为“拒绝新连接”状态 -
readinessProbe必须探测同一路径(如/healthz),且该接口在 shutdown 触发后立刻返回503 -
preStop默认超时 30 秒,但实际执行应 ≤5s;别在里面做 DB 连接池 drain 等耗时操作 -
terminationGracePeriodSeconds建议设为 30,Shutdown()超时设为 25,留出缓冲
最容易被忽略的三个点
一是 preStop 和 readinessProbe 探测路径必须一致且响应极快 —— 慢了就跳过摘除;二是 WebSocket 关闭必须主动调用 conn.Close() 并等待,不能依赖 Shutdown();三是所有监听器(HTTP、gRPC、自定义 TCP)都要单独处理关闭逻辑,http.Server 的 shutdown 对它们无效。











