直接kill -9会丢请求,因为其强制终止进程导致内核立即回收所有文件描述符,包括监听套接字和已建立的TCP连接,客户端收到RST包而连接瞬断;而优雅关闭需先停新连接、再等存量请求完成或超时。

为什么直接 kill -9 会丢请求
因为 HTTP 连接是 TCP 状态机,不是“发完就扔”。一个正在传输大文件、执行慢查询、或维持 WebSocket 长连接的请求,其底层 socket 仍由旧进程持有。kill -9 强制终止进程,内核会立即回收所有 fd,包括监听套接字和已建立连接,客户端收到 RST 包,连接瞬间断开。
真正要保留的是:已 accept 的连接(ESTABLISHED)、半连接队列中的 SYN 请求(SYN_RECV)、以及 TIME_WAIT 中的残留连接。这些都依赖监听套接字持续存在——而 kill -9 直接抹掉它。
- 监听套接字关闭 → 新连接被拒绝(SYN 被丢)
- 已建立连接 fd 被回收 → 客户端读写失败(ECONNRESET)
- 没有 graceful shutdown 流程 → 无机会调用
http.Server.Shutdown()或清理资源
用 http.Server.Shutdown() 实现优雅关闭
Shutdown() 不是“等所有请求结束”,而是主动控制关闭节奏:先停新连接,再等活跃请求自然完成,超时则强制中断。它必须配合信号监听和 context 使用,否则会卡死。
- 必须在
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)后触发,不能只监听SIGUSR2(那是 Nginx 的习惯) - 传入的
context.Context要设超时,例如context.WithTimeout(context.Background(), 30*time.Second),避免慢请求拖垮整个升级流程 - 调用前需确保
http.Server已启动(即srv.ListenAndServe()已在 goroutine 中运行),否则Shutdown()会立即返回ErrServerNotStarted - 调用后要
wait主 goroutine,否则主函数退出导致进程终止,Shutdown()就没意义了
示例关键片段:
srv := &http.Server{Addr: ":8080", Handler: router}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<!-- 信号监听与 Shutdown -->
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<!-- 收到信号后触发 -->
<!-- 注意:这里不能用 log.Fatal,否则进程立刻退出 -->
<!-- 必须用 goroutine 防止阻塞 -->
go func() {
<!-- 等待信号 -->
<!-- ... -->
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown error: %v", err)
}
}()
用 endless 库实现真正的零停机(非优雅关闭,而是热替换)
endless 的核心不是等待旧进程退出,而是 fork 出子进程,并把监听套接字通过 SCM_RIGHTS 传递过去。新进程启动后立即接管端口,旧进程继续服务剩余请求——这才是“零停机”的物理基础。
- 它依赖 Linux 的
fork()+exec()+sendmsg()传递 fd,不兼容 Windows - 必须保证新二进制文件路径与旧的一致(或通过环境变量
ENDLESS_NEW_BINARY显式指定),否则子进程找不到自己 - 旧进程不会自动退出,需靠子进程发送
SIGTERM给父 PID(即旧进程),触发其内部Shutdown()流程 - 它绕过了
net.Listener的生命周期管理,因此无法和标准库的http.Server.Shutdown()混用;若同时启用,可能造成双 shutdown 冲突
常见错误:升级后新进程启动失败,但旧进程仍在跑,此时 ps aux | grep gin 会看到两个进程,却误以为升级成功——必须检查新进程是否真正 bind 成功并响应健康检查。
配置项和信号交互容易踩的坑
零停机不是加个库就完事,很多问题出在部署链路的衔接上。比如 Docker 容器里没正确透传信号、Kubernetes 的 terminationGracePeriodSeconds 小于 Shutdown 超时、或者 systemd 服务 unit 文件里没配 KillMode=process。
- Docker Compose 中必须显式声明
stop_signal: SIGTERM,否则默认发SIGKILL - Kubernetes 的
terminationGracePeriodSeconds至少要比Shutdowncontext 超时多 5 秒,留出容器 runtime 杀进程的时间 - systemd service 文件中,
KillMode=control-group会导致整个 cgroup 被杀,应改为KillMode=process,只杀主进程 - 不要在
Shutdown()回调里做耗时操作(如写日志到远程 ES、发告警),这会拖长关闭时间,影响滚动更新节奏
最隐蔽的问题是:你写的 shutdown 逻辑没问题,但上游负载均衡器(如 Nginx、ALB)健康检查间隔太长,导致它还没发现新实例 ready,就先把旧实例从 upstream 删了——这时才是真正丢请求的地方。


















