go服务需捕获sigterm并调用http.server.shutdown()实现优雅停机,否则请求被强制中断;须确保进程为pid 1、正确配置prestop、统一管理grpc/db/ws等资源及goroutine生命周期,并设置合理的terminationgraceperiodseconds。

Go 服务收到 SIGTERM 后立即退出?必须加 http.Server.Shutdown()
K8s 发送 SIGTERM 后,Go 默认的 http.ListenAndServe() 会立刻返回、进程退出,正在处理的 HTTP 请求被强制中断。这不是优雅停机,是粗暴砍断。
真正要做的,是捕获信号、调用 srv.Shutdown() 等待活跃连接完成,再退出:
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
<-sig // 阻塞等待信号
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx) // 关键:传入带超时的 ctx
-
srv.Shutdown()不会关闭监听 socket,但会拒绝新连接,并等待已有请求完成(或超时) - 超时时间建议设为 K8s
terminationGracePeriodSeconds的 70%~80%,比如后者是 30s,这里设 25s - 别漏掉
err != http.ErrServerClosed判断——Shutdown()触发后ListenAndServe()就会返回这个错误,不是异常
K8s preStop 钩子不执行?检查容器是否真收到了 SIGTERM
常见现象:Pod 删除后秒退,日志里完全没看到 shutdown 日志,preStop 也没触发。大概率是容器主进程没在前台运行,或者被 shell 包了一层。
典型错误写法:command: ["sh", "-c", "exec ./myapp"] —— 这会让 sh 成为 PID 1,它不转发信号给子进程。
立即学习“go语言免费学习笔记(深入)”;
- 必须让 Go 二进制直接作为 PID 1:用
command: ["./myapp"],不要套sh或bash - 确认
preStop配置中用了exec类型而非httpGet(后者无法保证在 SIGTERM 前执行) - 如果用了
preStop+sleep 2,只是“假装优雅”,实际没等请求结束;它只能作为兜底延时,不能替代Shutdown()
HTTP 之外还有长连接怎么办?比如 gRPC、WebSocket、DB 连接池
http.Server.Shutdown() 只管 HTTP 连接,其他资源得手动收尾,否则照样丢数据或 panic。
常见遗漏点:
- gRPC server:调用
grpcServer.GracefulStop()(不是Stop()),它会等流式 RPC 完成 - WebSocket:每个连接需自行维护,通常靠
context.WithCancel()通知读写 goroutine 退出 - 数据库连接池:
sql.DB.Close()是同步阻塞的,应在Shutdown()后调用,且要给足时间(连接池可能有正在执行的 query) - 后台 goroutine:用
sync.WaitGroup+context控制生命周期,避免 shutdown 时还在往已关闭 channel 发数据
为什么本地测试正常,上线就超时失败?关注 K8s terminationGracePeriodSeconds
本地 kill -15 可能等够了 30 秒,但 K8s 默认只给 30 秒 grace period,一旦超时就发 SIGKILL 强杀——这时 Shutdown() 的超时还没到,但进程已经没了。
- 务必在 Deployment 中显式设置
terminationGracePeriodSeconds: 60(根据你最慢的清理逻辑定) - 检查 kubelet 日志:
journalctl -u kubelet | grep "killing pod",看是否出现Container exited with code 137(即被 SIGKILL) - 别依赖
preStop延长总时间:它的执行包含在 grace period 内,不是额外加时
真正难的不是写几行 Shutdown(),而是把所有异步出口、资源持有者、goroutine 生命周期都串到同一个 context 里——漏一个,就可能卡住整个退出流程。










