systemd 服务长期 activating:Type=notify、READY 与 Watchdog 排查
systemd 服务长期 activating:Type=notify、READY 与 Watchdog 排查
先确认实际 Unit 与状态
使用 systemctl cat 和 systemctl show 查看实际加载配置,记录 Type、NotifyAccess、TimeoutStartUSec、WatchdogUSec、MainPID、ActiveState、SubState、Result 和 drop-in 路径。再用 journal 保存本次启动从 ExecStart 到超时的完整时间线。
不要只看仓库中的 unit 文件。发行版默认、/etc 覆盖、运行时 drop-in 和生成器都可能改变最终属性。修改后执行 daemon-reload 并再次查看实际配置,不能凭编辑器内容确认生效。
Type=notify 的启动完成条件
Type=notify 与 Type=simple 不同。simple 通常在主进程启动后就认为服务已开始,notify 则等待服务发送 READY=1。在 READY 到达前,依赖该服务“启动完成”的后续 unit 可能继续等待。
如果程序根本没有实现 systemd notify 协议,却配置成 notify,就会一直 activating 直到启动超时。要么让程序在真正准备好时正确通知,要么选择符合程序行为的服务 Type;不要仅把 timeout 调到无限大。
检查 NOTIFY_SOCKET 是否存在
systemd 会通过环境变量 NOTIFY_SOCKET 告知服务通知 socket 地址。应用应调用 sd_notify() 或兼容实现向它发送 datagram。若启动脚本清空环境、容器边界未传递 socket,或程序在 systemd 之外手工运行,变量可能不存在。
在受控调试中记录变量是否存在及地址类型,但不要把内部路径当成固定配置写死。应用应在变量缺失时安全运行于非 systemd 环境,在变量存在时按协议通知。
READY=1 应在真正就绪后发送
发送 READY 太早会让 systemd 启动依赖服务,但应用可能尚未绑定端口、加载配置或完成必要恢复;发送太晚则会触发 TimeoutStartSec。把就绪点定义为“能够可靠处理核心请求”,而不是“main 函数开始”或“后台线程已创建”。
对于需要数据库迁移或缓存预热的服务,明确哪些步骤是启动阻断条件。可用 STATUS= 更新可读状态,帮助 systemctl status 展示当前阶段,但 STATUS 不能替代 READY。
NotifyAccess 决定谁能通知
默认允许通知的进程范围取决于服务配置和 Type。若实际发送 READY 的是主进程派生的辅助进程,而 NotifyAccess=main,systemd 可能忽略它;设置为 all 会扩大同一 cgroup 内可通知的进程范围,需要安全评估。
优先让主进程发送通知。若必须由其他进程通知,明确配置适合的 NotifyAccess,并确保发送进程仍可被 systemd 正确归属。不要为了让一次测试通过就无条件扩大权限。
短命辅助进程的竞态
通知由一个很快退出的 helper 发送时,systemd 处理 datagram 前可能已无法确定发送者属于哪个 unit,通知会被忽略。systemd-notify 提供等待/屏障相关能力来降低这类竞态,但最稳妥的设计仍是让长期存在的主进程直接通知。
检查 journal 中是否出现来源归属或通知被忽略的信息。使用 shell 包裹 systemd-notify --ready 时,也要确认它通知的是正确 unit,且脚本退出不会改变主进程判断。
MAINPID 与派生进程
如果服务启动器再 fork/exec 实际守护进程,systemd 可能跟踪错误 PID。notify 协议可涉及 MAINPID= 更新,但调用者权限、归属和时序必须正确。错误 MainPID 会影响信号、资源统计、停止和 watchdog 归因。
尽量让 systemd 直接启动前台主进程,避免传统 double-fork。若软件只能 fork,评估 Type=forking 与 PIDFile 是否更符合实际,而不是同时混用多个进程发现机制。
TimeoutStartSec 为什么触发
systemd 在启动阶段等待完成信号,超过 TimeoutStartSec 后会把启动判为失败并按配置终止服务。notify 服务可在特定条件下用 EXTEND_TIMEOUT_USEC= 延长启动时间,但必须在原超时到期前持续按规则发送,不能在超时后补发。
延长只适合可证明仍在推进的长任务。应用应通过 STATUS 和内部进度指标证明进展,并设置绝对业务上限。永久循环发送延长通知会把死锁伪装成健康启动。
Watchdog 与启动通知是两件事
配置 WatchdogSec= 后,systemd 会通过环境向服务提供 watchdog 间隔。服务就绪后需要周期发送 WATCHDOG=1,频率应足够早于截止时间。READY 解决启动完成,WATCHDOG 证明运行期事件循环仍活跃。
只发送 READY 不会满足后续 watchdog;只发送 WATCHDOG 也不代表服务已 READY。检查应用是否读取 WATCHDOG_USEC、是否按预期线程发送,以及进程暂停、GC、死锁或 CPU 饥饿是否阻止通知。
WATCHDOG_PID 与多进程架构
多进程服务中,只有正确进程应承担 watchdog。若 worker 仍发通知但主协调进程已死,可能产生假健康;若通知线程在无关子进程,systemd 也可能忽略。根据上游协议核对 WATCHDOG_PID 和主进程身份。
健康信号应覆盖真正决定服务能力的事件循环。不要创建一个永远独立发送 WATCHDOG 的线程,它即使业务线程全部死锁也会继续报活。
sd_notify 返回值与错误处理
应用调用 sd_notify() 后应检查返回值和错误路径。通知函数返回成功通常表示消息已发送,不一定等于 systemd 已完成所有状态处理。对关键启动可在适合环境使用 barrier 机制或查看 systemd 状态确认。
不要在每个 watchdog 周期打印高频成功日志。只记录状态变化、连续发送失败和明显延迟,避免日志风暴影响服务本身。
权限与沙箱限制
PrivateNetwork、ProtectSystem、用户命名空间、容器封装或自定义 seccomp 可能影响通知 socket 可达性。通常 systemd 会为服务安排正确环境,但额外启动器或跨容器代理可能破坏 Unix datagram socket 访问。
从 unit 直接启动的实际进程环境验证,而不是用交互 shell模拟。检查 socket 地址、进程凭据和 cgroup 归属;不要通过放宽全部沙箱选项来试错,逐项验证最小必要权限。
容器内 systemd 与宿主 systemd
应用在容器内运行时,NOTIFY_SOCKET 可能属于容器内的 systemd,也可能由宿主通过代理暴露。把容器内 systemd-notify 发往错误管理器,会让宿主 unit 永远等待。编排平台的健康检查也不等同于 systemd READY。
明确谁是服务管理器、谁创建 unit、谁接收通知。跨边界转发需要平台正式支持和正确挂载/代理,不能把宿主 socket 路径直接硬编码进容器镜像。
重载通知与启动通知不同
支持 Type=notify-reload 的服务在 reload 流程中还需要按协议通知重载开始与完成。只实现启动 READY 而未实现 reload 协议,可能让 systemctl reload 卡住或超时。
若程序不支持通知式 reload,就不要声明相应 Type。为 reload 单独测试配置解析、旧连接保留和完成通知,不要用重启成功代替重载验收。
一套逐层排查流程
第一步,保存实际 unit 属性和 journal。第二步,确认程序是否真的支持 notify。第三步,检查 NOTIFY_SOCKET 与发送进程。第四步,验证 NotifyAccess 和 cgroup 归属。第五步,在 READY 前后记录应用真实就绪条件。第六步,核对 TimeoutStartSec 与延长逻辑。第七步,单独验证 WatchdogSec、WATCHDOG_USEC 和主事件循环。第八步,测试 restart、stop 和 reload。
修复后从干净启动验证:服务先为 activating,核心能力就绪后进入 active;依赖 unit 只在 READY 后启动;模拟主事件循环卡住时 watchdog 能检测;正常停止不会被误判超时。
常见错误
常见误区包括:不支持 notify 的程序配置 Type=notify;启动脚本吞掉 NOTIFY_SOCKET;辅助进程发送 READY 但 NotifyAccess=main;端口未就绪就提前通知;无限延长启动超时;独立线程无条件发送 WATCHDOG;混淆容器与宿主管理器;以及用加大 TimeoutStartSec 掩盖死锁。
常见问题
进程和端口都存在,为什么仍是 activating?
Type=notify 要等待 READY=1。进程存在或端口监听不是 systemd 的完成信号,检查通知是否发送、来源是否允许以及是否被接收。
可以改成 Type=simple 解决吗?
只有当依赖服务不需要等待真实就绪,且程序行为符合 simple 语义时才考虑。否则会把启动未完成伪装成 active,可能引发依赖竞态。
WatchdogSec 设置后为什么服务被重启?
应用必须在规定间隔内发送 WATCHDOG=1,并确保信号来自正确进程且代表核心事件循环健康。长暂停或死锁都会造成超时。
NotifyAccess=all 是否更省事?
它扩大 cgroup 内可发送通知的进程范围。优先由主进程通知;确有多进程需求时才配置 all,并评估错误或恶意子进程伪造状态的风险。
systemd-notify --ready 执行成功为何状态没变?
可能发往错误 socket、发送进程不属于该 unit、NotifyAccess 不允许,或短命进程归属竞态。查看 journal、实际环境和 cgroup,而不只看命令退出码。
总结
Type=notify 的核心契约是:服务在真正可用时通过正确通知 socket、由允许的进程发送 READY;运行期 Watchdog 则必须代表关键事件循环持续存活。可靠排障应同时核对 unit 实际属性、发送进程归属、超时和容器边界。修复后验证依赖顺序、watchdog 故障检出及 reload/stop,才能证明服务状态不是表面变绿。
来源资料
- systemd, systemd.service: https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
- systemd, sd_notify: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html
- systemd, systemd-notify: https://www.freedesktop.org/software/systemd/man/latest/systemd-notify.html
- systemd, systemd.unit: https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html