如何通过systemctl优雅停止Rust多线程守护进程确保任务完成
systemd 下 Rust 高负载服务优雅停机实现方案
核心逻辑是对接systemd标准的SIGTERM停止信号流程,收到停止指令后先完成所有在途任务、回收全部工作线程再退出进程,既保证核心数据不丢失,也能让systemd准确感知服务状态。
第一步:调整systemd服务配置
替换原来依赖轮询配置文件的触发方式,直接用systemd原生的信号机制下发停止指令,服务配置参考:
[Service] Type=simple ExecStart=/path/to/your/compiled/rust_binary # 指定停止时发送SIGTERM信号 KillSignal=SIGTERM # 根据业务单任务最大处理时长配置最长优雅停机窗口,比如配置300秒,超过这个时间程序还没退出systemd才会强制发SIGKILL TimeoutStopSec=300
配置完成后执行systemctl daemon-reload重载配置即可生效。
第二步:Rust端实现信号处理+优雅停机逻辑
核心要做三个改造:
- 用跨线程可见的原子布尔值替代普通变量作为停机标记,无锁开销完全适配高负载场景
- 注册SIGTERM信号监听,收到信号时仅修改停机标记,不在信号处理逻辑里做重操作
- 维护所有工作线程的句柄,触发停机后等待所有工作线程完成当前正在处理的任务(包括进行中的HTTP请求、核心数据计算/落盘),全部线程回收完成后进程再主动退出。
核心代码参考
如果不想写unsafe的信号处理逻辑,可以引入轻量无运行时依赖的signal-hook库处理信号,生产环境踩坑概率更低:
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; use signal_hook::consts::SIGTERM; use signal_hook::iterator::Signals; fn main() { // 全局停机标记,跨线程共享 let is_alive = Arc::new(AtomicBool::new(true)); let signal_is_alive = is_alive.clone(); // 单独启动线程监听SIGTERM信号 let mut signals = Signals::new([SIGTERM]).expect("register signal handler failed"); thread::spawn(move || { for _ in signals.forever() { // 收到systemctl stop发来的信号,仅修改停机标记,不做其他重操作 signal_is_alive.store(false, Ordering::SeqCst); } }); let mut worker_handles = Vec::new(); // 这里替换成你的动态线程调度逻辑,动态增减线程时同步维护句柄列表即可 for worker_id in 0..thread::available_parallelism().unwrap().get() { let worker_is_alive = is_alive.clone(); let handle = thread::spawn(move || { loop { // 轮询检查停机标记 if !worker_is_alive.load(Ordering::SeqCst) { // 标记已触发停止,先处理完当前线程手里已经拿到的所有任务 // 包括未完成的HTTP请求收尾、核心数据提交/落盘等操作 finish_worker_pending_task(worker_id); break; } // 正常执行业务逻辑 run_business_logic(worker_id); } }); worker_handles.push(handle); } // 主线程阻塞等待所有工作线程全部处理完任务退出 for handle in worker_handles { handle.join().expect("worker thread exited with panic"); } // 所有任务完成,进程正常退出,systemd会立刻感知到服务停止完成 }
如果不想引入第三方依赖,也可以用标准库+libc做信号注册,注意信号处理器里只能执行异步信号安全的操作,也就是只能修改原子变量,不能做内存分配、锁操作等其他逻辑:
use std::sync::atomic::{AtomicBool, Ordering}; use libc; // 静态原子变量作为停机标记 static IS_ALIVE: AtomicBool = AtomicBool::new(true); extern "C" fn sigterm_handler(_sig: i32) { // 仅修改原子标记,禁止加其他逻辑 IS_ALIVE.store(false, Ordering::SeqCst); } fn main() { // 注册SIGTERM处理器 unsafe { libc::signal(libc::SIGTERM, sigterm_handler as libc::sighandler_t); } // 剩下的工作线程调度、等待回收逻辑和上面的示例一致 }
关键注意事项
- 动态增减线程的场景下,用线程安全的Vec(比如加Mutex的Vec)存所有活跃工作线程的JoinHandle,新启动线程时把句柄存入,线程正常退出时移除对应句柄,停机时等待列表里所有句柄join完成即可,不需要提前预估停机耗时。
- 工作线程检查到停机标记后,不能直接丢任务退出,必须先把当前已经从队列里取到的任务处理完再break,避免核心数据丢失。
- 所用的HTTP客户端建议配置合理的请求超时,超时时间要小于systemd配置的
TimeoutStopSec,避免卡在僵死请求上被systemd强制杀进程。 - 这套方案下,执行
systemctl stop命令时会阻塞到进程完全退出才返回,命令返回就代表所有在途任务已经处理完成、进程正常停止,systemctl显示的服务状态和实际状态完全一致,不会出现之前轮询配置文件时的状态不同步问题。
内容的提问来源于stack exchange,提问作者Dykeiichi
相关产品推荐
相关产品推荐

