Rust in Everything
基于 pingora-core 0.9.0

Graceful shutdown: grace period 300s starts

主线位置:服务主线的最后一步「SIGTERM 平滑停机」。停 accept、睡宽限期、关运行时三段各做什么,见平滑升级与停机。本页讲默认值和容器停机时限撞在一起的样子。

sequenceDiagram
  participant D as docker stop
  participant P as pingora
  participant R as 在途请求
  D->>P: SIGTERM
  P->>P: 关监听 广播停机
  P->>P: sleep 300 s
  R-->>P: 2 s 的请求完成
  D->>P: 10 s 后 SIGKILL
  P--xR: 12 s 的请求被切断

实验 crate exp-pingora/pita-lab(bin svc,pingora 0.9.0,tokio 1.53.2)和 exp-pingora/pita-gw(本站网关的拷贝,pingora 0.8.0,tokio 1.52.3,端口改到 19232 / 19233,环境变量 PITA_GRACE=1 打开本页的改法);脚本 tools/stop.py 模仿 docker stop:起服务,发两个在途请求(2 秒和 12 秒),0.5 秒时 SIGTERM,1.0 秒时开一个新连接,到 10 秒还没退出就 SIGKILL。M2 Max、macOS 27、rustc 1.99.0,debug。

现象

svc 不带配置文件:

== svc (0.9.0), no conf
  0.50s SIGTERM
  1.00s new conn: ConnectionRefusedError: [Errno 61] Connection refused
  2.01s in-flight 2s: HTTP/1.1 200 OK
 10.01s still alive at 10 s, SIGKILL, rc=-9
 10.01s in-flight 12s: closed without response

日志的最后三行,之后什么都没有:

[  1.502s INFO pingora_core::services::listening] Shutting down 127.0.0.1:19230
[  1.502s INFO pingora_core::server] Graceful shutdown: grace period 300s starts
[  1.502s INFO pingora_core::server] service 'lab' exited.

service 'lab' exited 是 run_service 里 start_service 返回后打的,意思是这个服务的监听循环都停了,不是进程退出。

网关的拷贝(0.8.0、TLS)同样:

== gateway copy (0.8.0), as is
  0.51s SIGTERM
  1.01s new conn: ConnectionRefusedError: [Errno 61] Connection refused
  2.04s in-flight 2s: HTTP/1.1 200 OK
 10.00s still alive at 10 s, SIGKILL, rc=-9
 10.00s in-flight 12s: closed without response
  • SIGTERM 那一刻监听 socket 就关了,新连接被拒。之后进程不再接活,却要睡满 300 秒。
  • 2 秒的请求在宽限期里正常完成,宽限期起了作用;12 秒的请求在第 10 秒随 SIGKILL 一起断掉。
  • 进程从没有走到「Waiting for runtimes to exit」和「All runtimes exited」,退出码是 -9。日志里看不出它是被杀的,只能从编排系统那边看。

在容器里,这意味着每次 docker stop、docker restart、docker compose up -d 换镜像,都要整整等满停机时限,再被 SIGKILL。网关就是这样被停的,tini 转发信号、compose 没有配 stop_grace_period,细节和监听端口空窗见在本站中。

出处

run 的停机段:

            let exit_timeout = self
                .configuration
                .as_ref()
                .grace_period_seconds
                .unwrap_or(EXIT_TIMEOUT);
            info!("Graceful shutdown: grace period {}s starts", exit_timeout);
            thread::sleep(Duration::from_secs(exit_timeout));
            info!("Graceful shutdown: grace period ends");

EXIT_TIMEOUT 是 60 * 5。这是主线程上的 thread::sleep,不看还有没有在途连接,所以哪怕一个请求都没有也要睡满。0.8.0 是同样的写法,main 上到 84f21ee 也没变。睡完之后才轮到 graceful_shutdown_timeout_seconds(默认 5 秒),它只约束阻塞线程池,见平滑升级与停机的「没有蠢问题」。

ServerConf 里这个字段是 Option<u64>,默认 None。docs/user_guide/conf.md 的配置表里没有它,只能从源码里查到。用 Server::new(None) 的程序读不到配置文件(见 -c 配置文件不生效),写了也没用。

修法

显式配一个比停机时限短的宽限期。svc 用 -c conf/grace5.yaml:

---
version: 1
grace_period_seconds: 5
graceful_shutdown_timeout_seconds: 1
== svc (0.9.0), -c conf/grace5.yaml
  0.50s SIGTERM
  1.01s new conn: ConnectionRefusedError: [Errno 61] Connection refused
  2.01s in-flight 2s: HTTP/1.1 200 OK
  5.51s in-flight 12s: closed without response
  5.53s exited by itself, rc=0
[  1.501s INFO pingora_core::server] Graceful shutdown: grace period 5s starts
[  6.511s INFO pingora_core::server] Graceful shutdown: grace period ends
[  6.511s INFO pingora_core::server] Waiting for runtimes to exit!
[  6.512s INFO pingora_core::server] Waiting for service runtime lab to exit
[  6.512s INFO pingora_core::server] Service runtime lab exited after 176.209µs
[  6.512s INFO pingora_core::server] All runtimes exited, exiting now

网关的拷贝没有配置文件,用 Server::new_with_opt_and_conf 传同样两个值(改法原文在在本站中):

== gateway copy (0.8.0), PITA_GRACE=1
  0.51s SIGTERM
  1.01s new conn: ConnectionRefusedError: [Errno 61] Connection refused
  2.03s in-flight 2s: HTTP/1.1 200 OK
  5.51s in-flight 12s: closed without response
  6.56s exited by itself, rc=0
  • 两边都在宽限期结束时切断 12 秒的请求,然后自己以 0 退出。
  • 0.8.0 比 0.9.0 晚一秒:0.8.0 关运行时之后还要多睡一次 graceful_shutdown_timeout_seconds,0.9.0 删掉了这一次多余的 thread::sleep(6f15714,来历见平滑升级与停机)。

宽限期怎么定是取舍:它要比最长的正常请求长,又要比编排系统的停机时限短,并且留出 0.8.0 那额外的一秒。反过来也可以把容器的停机时限调长(compose 的 stop_grace_period),但那样每次重启拒绝新连接的时间也一样长,因为监听 socket 在 SIGTERM 时就关了。

怎么确认没中招

  • 停一次服务,看日志里 grace period Ns starts 的 N,再看后面有没有 All runtimes exited, exiting now。没有这一行而进程没了,就是被杀的。
  • 对比两个数:grace_period_seconds(不配就是 300)加上 graceful_shutdown_timeout_seconds(0.8.0 还要再加一次),要小于编排系统的停机时限(docker 默认 10 秒,docker stop -t 或 stop_grace_period 可改)。
  • 确认配置真的读进来了:启动时打印 server.configuration.grace_period_seconds。

小结

  • 不配 grace_period_seconds 时,SIGTERM 后主线程无条件睡 300 秒;docker 默认 10 秒就 SIGKILL,每次停机都是被杀的。
  • 宽限期里在途请求能完成,监听却在 SIGTERM 时就关了,等得越久,拒绝新连接的空窗越长。
  • 显式配一个比停机时限短的宽限期(实验里 5 秒),0.9.0 在 5.53 秒、0.8.0 在 6.56 秒自己退出。

💬 关联讨论

发起讨论
加载中...