Gin 没有自带优雅关机,要靠标准库的 http.Server.Shutdown。它到底等多久、等什么、不等什么?ctx 超时了请求会不会被中断?长连接和 WebSocket 怎么办?这篇用进程内实验和真实的 SIGTERM 把每种情形都测一遍。
102 ms 调用 Shutdown
154 ms Shutdown 之后的新请求:err=connection refused
503 ms 在途请求结束:200 slow done err=<nil>
650 ms Shutdown 返回 err=<nil>先关闭所有监听(新连接立刻被拒绝),然后反复检查连接是否都已空闲,空闲的 keep-alive 连接会被关闭,正在处理请求的连接则等它们处理完。检查的间隔从 1 ms 开始翻倍、最长 500 ms,所以返回时间比最后一个请求结束可能晚最多半秒。
404 ms Shutdown(ctx 300ms) 返回 err=context deadline exceeded;强制 Close=false
1002 ms 在途请求结束:200 slow done
---
403 ms Shutdown(ctx 300ms) 返回 err=context deadline exceeded;强制 Close=true
404 ms 调用 srv.Close()
404 ms 在途请求结束:错误:EOF
---Shutdown 的 ctx 只决定“等多久”:到期就返回 context deadline exceeded,但并不会终止在途请求。你必须决定接下来是继续等还是调用 srv.Close() 强制断开(在途请求会失败)。常见写法是给 Shutdown 一个略短于编排系统宽限期的 ctx(例如 K8s 的 30 秒),超时后 Close。
RegisterOnShutdown 通知 handler=false → 1605 ms Shutdown 返回 err=context deadline exceeded
1605 ms 流式 handler 看到取消:context canceled
---
102 ms 流式 handler 看到取消:context canceled
RegisterOnShutdown 通知 handler=true → 104 ms Shutdown 返回 err=<nil>
---SSE、长轮询这样的 handler 不会因为 Shutdown 而结束,Shutdown 会一直等到 ctx 到期。办法是给 http.Server 设 BaseContext 返回一个你控制的根 context,再用 RegisterOnShutdown 在关机时取消它,handler 里监听 Request.Context().Done()(见 context 的几篇)。
0 ms 客户端读到 43 字节(handler 已 Hijack,还拿着连接)
0 ms Shutdown 返回 err=<nil>(没有等被 Hijack 的连接)
202 ms 被 Hijack 的连接仍然开着(读超时而不是 EOF)WebSocket 等被 Hijack 的连接完全不归 Shutdown 管:它既不等也不关。要自己维护这类连接的集合,收到关机通知时发关闭帧并断开,否则进程退出时它们会被粗暴地切断。
[plain] SIGTERM 发出 2 ms 后进程退出,退出码 -15;在途请求(耗时 209 ms)结果:错误:RemoteDisconnected Remote end closed connection without res;进程输出:(无) [graceful] SIGTERM 发出 1050 ms 后进程退出,退出码 0;在途请求(耗时 1004 ms)结果:slow done;进程输出:收到信号,开始 Shutdown | Shutdown 完成,退出
只用 r.Run、不处理信号时,SIGTERM 会直接终止进程,在途请求丢失。用 signal.NotifyContext 接收 SIGTERM / SIGINT,收到后调用 Shutdown,再让 main 返回;注意 ListenAndServe 在 Shutdown 之后立刻返回 http.ErrServerClosed,不要在那里退出进程,要等 Shutdown 返回。