Rust in Everything
基于 pingora-core 0.9.0

Upgrade is not currently supported outside of Linux platforms

主线位置:服务主线的「SIGQUIT 平滑升级」。Linux 上旧进程怎么把监听 fd 经 Unix socket 交给新进程、两边各重试几次,见平滑升级与停机。本页讲 macOS 上这两端都是空实现时,按 Linux 的步骤操作会得到什么。

sequenceDiagram
  participant N as 新进程 -u
  participant O as 旧进程
  participant C as 客户端
  N->>N: get_fds_from 返回错误
  N->>N: exit 1
  O->>O: 收到 SIGQUIT
  O->>O: send_fds_to 返回 Ok 0
  O->>O: 日志 listener sockets sent
  O->>O: 等 5 s 再停 accept
  C->>O: 新连接被拒

实验 crate exp-pingora/pita-lab,bin svc(Opt::parse_args),脚本 tools/upgrade.py:旧进程先起;0.5 秒后起新进程;再过 0.5 秒给旧进程发 SIGQUIT;同时每 50 ms 新建一个连接请求 /,记录应答的 pid。配置 conf/up.yaml 把宽限期设成 3 秒。M2 Max、macOS 27、rustc 1.99.0,debug,tokio 1.53.2。

现象

照 docs/user_guide/graceful.md 的步骤,新进程带 -u:

  0.51s SIGQUIT to old pid=44772; new pid=44773 args=-c conf/up.yaml -u
probe   0.51s..  5.47s  pid=44772    x95
probe   5.52s.. 15.95s  refused      x199
new process: exited rc=1

新进程的全部输出:

[  0.001s INFO svc] conf threads=1 work_stealing=true grace=Some(3) shutdown_timeout=Some(1) pid=44773
[  0.002s INFO pingora_core::server::bootstrap_services] Bootstrap starting
[  0.002s ERROR pingora_core::server::transfer_fd] Upgrade is not currently supported outside of Linux platforms
[  0.002s ERROR pingora_core::server::bootstrap_services] Bootstrap failed on error: ECONNREFUSED, exiting.

旧进程:

[  1.013s INFO pingora_core::server] SIGQUIT received, sending socks and gracefully exiting
[  1.013s INFO pingora_core::server] Trying to send socks
[  1.014s INFO pingora_core::server] listener sockets sent
[  6.016s INFO pingora_core::server] Broadcasting graceful shutdown
...
[  6.016s INFO pingora_core::services::listening] Shutting down 127.0.0.1:19240
[  6.016s INFO pingora_core::server] Graceful shutdown: grace period 3s starts
...
[  9.017s INFO pingora_core::server] All runtimes exited, exiting now
  • 新进程在 bootstrap 的第一步就以 1 退出,报错里的 ECONNREFUSED 不是真的连接被拒,是桩函数返回的固定错误码。
  • 旧进程收到 SIGQUIT,日志写 listener sockets sent,实际什么都没发,然后照常等 5 秒、停 accept、走完宽限期退出。
  • 从 SIGQUIT 之后 5 秒起,端口上没有任何进程,探针 199 次全部 refused。用 -u 的结果比不升级还糟:不升级,旧进程至少还在服务。

出处

transfer_fd/mod.rs 的 Linux 版本包在 #[cfg(target_os = "linux")] 里,其他平台是两个桩。接收端 get_fds_from 打一条 error、返回 Err(Errno::ECONNREFUSED);发送端 send_fds_to 什么都不做:

#[cfg(not(target_os = "linux"))]
pub fn send_fds_to<P>(
    _fds: Vec<RawFd>,
    _payload: &[u8],
    _path: &P,
    _max_retry: Option<usize>,
) -> Result<usize, Error>
where
    P: ?Sized + NixPath + std::fmt::Display,
{
    Ok(0)
}

main_loop 的 SIGQUIT 分支只看 send_to_sock 是不是 Ok,Ok(0)(发了 0 个字节)也打 listener sockets sent,然后进入 CLOSE_TIMEOUT 的 5 秒等待和正常停机。这条误导性的日志在 0.9.0 和 main(84f21ee)上都一样。README 对平台的说法是 Linux 为第一梯队,macOS「though some features might be missing」,平滑升级就是缺的那一个。

修法

macOS 上不带 -u:新进程按普通启动去 bind,撞上 AddrInUse 后每秒重试(见 still in use),旧进程一关监听 socket,它下一次重试就绑上。同一个脚本,新进程不带 -u:

  0.50s SIGQUIT to old pid=44994; new pid=44995 args=-c conf/up.yaml
probe   0.50s..  5.47s  pid=44994    x93
probe   5.52s..  5.99s  refused      x10
probe   6.05s.. 16.00s  pid=44995    x189
new process: alive

空窗是 5.52 到 5.99 秒之间的 10 次 refused,约半秒,最长不超过新进程的重试间隔 1 秒。这不是平滑升级:空窗里的连接会被拒,旧进程手上的长连接在宽限期结束时被切断。真正不丢连接的升级只能在 Linux 上做,本页没有 Linux 机器验证。

升级脚本里还应当加一道检查:起完新进程,确认它还活着(或者等到它的端口能连上)再给旧进程发 SIGQUIT。上面带 -u 的那次,新进程在 2 毫秒内就退出了,这一道检查就能拦住后面的停机。

怎么确认没中招

  • 新进程日志里出现 Upgrade is not currently supported outside of Linux platforms 或 Bootstrap failed on error,它已经退出了,不要再给旧进程发 SIGQUIT。
  • 旧进程的 listener sockets sent 在非 Linux 平台上不说明任何事;看新进程有没有 Bootstrap done。
  • 升级后对端口做一次探测,而不是只看旧进程是否退出。

小结

  • macOS 上 transfer_fd 两端都是桩:新进程带 -u 在 bootstrap 里 exit(1),旧进程的 send_fds_to 返回 Ok(0)。
  • 旧进程照样打 listener sockets sent、照常停机,从 SIGQUIT 后 5 秒起端口上没人监听。
  • macOS 上只能不带 -u、靠 bind 重试接上,空窗约半秒;脚本要先确认新进程活着再发 SIGQUIT。

💬 关联讨论

发起讨论
加载中...