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。