Go服务需手动监听SIGTERM并调用server.Shutdown()实现优雅退出,配合context超时、组件清理、preStop与terminationGracePeriodSeconds对齐,以及确保容器信号透传。

Go 服务收到 SIGTERM 后立刻退出?那是没接住信号
Go 的 http.Server 本身不自动响应系统信号,Kubernetes 发送 SIGTERM 时若没显式处理,进程会立即终止,正在处理的 HTTP 请求被粗暴中断,连接重置、数据丢失都可能发生。
必须手动监听信号,并在收到后触发 server.Shutdown() —— 它会拒绝新连接、等待已有请求完成(或超时),这才是“优雅”的实质。
- 用
signal.Notify监听os.Interrupt和syscall.SIGTERM,后者是 K8s 默认发送的终止信号 -
server.Shutdown()是阻塞调用,需在 goroutine 中执行,否则主流程卡住无法继续清理 - 务必设置合理的
context.WithTimeout,比如 10–30 秒;太短丢请求,太长拖慢滚动更新 - 别忘了关闭其他长期运行组件:数据库连接池、消息队列消费者、定时器等,它们也得在 Shutdown 阶段一并停掉
Shutdown 超时了但请求还在跑?检查 context 传递是否断链
server.Shutdown() 只负责 HTTP server 层,它等的是每个 handler 执行完毕。如果 handler 内部用了未受控的 goroutine、没传入 request context、或用了 time.Sleep 等硬等待,就会无视超时,死扛到底。
- 所有异步操作(如日志上报、下游调用、DB 查询)必须接收并传递
req.Context() - 避免在 handler 里起无 context 约束的 goroutine,例如
go doSomething();应改用go func() { ... }()并监听req.Context().Done() - 第三方库(如
database/sql)的查询方法支持 context,优先用db.QueryContext()而非db.Query() - 自己写的耗时函数,入口加
ctx context.Context参数,并在关键点检查ctx.Err() != nil
Kubernetes preStop hook 和 Go Shutdown 时间怎么对齐?
K8s 的 preStop 钩子只是触发时机,真正决定停机窗口的是容器的 terminationGracePeriodSeconds(默认 30 秒)和 Go 代码里的 Shutdown 超时时间——两者必须前者 ≥ 后者,否则 K8s 强杀前,Go 还没来得及完成清理。
立即学习“go语言免费学习笔记(深入)”;
- 在 Deployment 中显式设置
terminationGracePeriodSeconds: 45(比 Go 的 Shutdown timeout 多 10–15 秒缓冲) -
preStop不需要做任何事(比如发 curl),Go 自己监听信号就够了;加它反而可能重复触发或引入竞态 - 如果用了 readiness probe,确保在 Shutdown 开始后立即让 probe 失败(比如关掉健康检查端口或返回 503),防止新流量进来
- 验证方式:删 Pod 后立刻
kubectl logs <pod> --previous,看是否有 “shutting down…” 日志,以及最后一条日志是否在 terminationGracePeriodSeconds 内出现
为什么本地 ctrl+C 能停,K8s 里却直接 kill?
常见原因是容器镜像里没启用信号转发。Docker 默认启动的 PID 1 进程(比如你的 Go 二进制)若不是 init 系统,SIGTERM 无法透传给 Go 主 goroutine,导致监听失效。
- 确认 Dockerfile 使用
ENTRYPOINT ["./myapp"]而非SHELL模式(后者会套一层 /bin/sh,它不转发信号) - 避免在 CMD/ENTRYPOINT 前加
sh -c或bash -c,除非你明确用exec "$@"替换自身进程 - 在容器内执行
ps -o pid,ppid,comm,检查 Go 进程的 PPID 是否为 1;如果不是,说明信号没到它手上 - 极简验证:进容器
kill -TERM 1,看 Go 是否打印 shutdown 日志;没反应就是信号路径断了
信号链路不通,再严谨的 Shutdown 逻辑也白搭。K8s 环境下,从镜像构建到 Pod 配置,每层都要确认信号能落地到你的 main() 函数里。










