bigywater.tech
Gin · 51

优雅关机:Shutdown 等多久

Gin 没有自带优雅关机,要靠标准库的 http.Server.Shutdown。它到底等多久、等什么、不等什么?ctx 超时了请求会不会被中断?长连接和 WebSocket 怎么办?这篇用进程内实验和真实的 SIGTERM 把每种情形都测一遍。

650 ms
在途请求 503 ms 结束后 Shutdown 才返回(轮询间隔最长 500 ms)
deadline exceeded
ctx 先到期时 Shutdown 的返回值,但在途请求仍然继续(1002 ms 才结束)
2 ms vs 1050 ms
SIGTERM 之后进程退出的耗时:不处理信号 vs 优雅关机

Shutdown 做了什么

实测 · 本机执行输出
     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,所以返回时间比最后一个请求结束可能晚最多半秒。

ctx 到期之后

实测 · 本机执行输出
     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 的几篇)。

被 Hijack 的连接

实测 · 本机执行输出
       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 返回。

分步演示

优雅关机
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。进程内的实验用真实的 net.Listen + http.Server(时间是相对进程内 t0 的毫秒数);第五节编译成真实二进制并用 Python 的 subprocess 启动、发 SIGTERM,客户端用 urllib;读过的 Go 源码:net/http/server.go 的 (*Server).Shutdown(pollIntervalBase 起始 1 ms、每次翻倍)与常量 shutdownPollIntervalMax = 500 * time.Millisecond(第 3227 行);每种情形各一次
    • 没有验证:Kubernetes 里 preStop、readiness 与 SIGTERM 的配合;HTTP/2 下 Shutdown 对 GOAWAY 的处理;TLS 连接的 Shutdown;Windows 上的信号;Shutdown 其余分支(h3 等)的细节;并发大量在途请求下的 Shutdown 耗时
    京ICP备2026067502号-1京公网安备11010502063487号