登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

systemd.service notify 怎样处理 READY=1

来源:17golang原创

时间:2026-10-25 14:21:43 451浏览 收藏

如果一个服务进程已经启动,但配置加载、监听端口或关键依赖还没有准备好,systemd 不能只凭“进程存在”判断它已经可以接收流量。这时可以把单元类型设为 Type=notify,让应用在真正完成启动后发送 READY=1。systemd 收到这条通知后,才会把服务从 activating 推进到 active,并继续启动依赖它的后续单元。

官方地址:https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html

本文只围绕一个边界展开:应用什么时候算“准备完成”,以及 systemd 怎样可靠收到这个结论。重点不是把所有服务都改成 notify,而是先看启动负载,再选择合适的服务类型。

先判断启动完成的业务边界

Type=notify 适合“进程已经存在”与“服务已经可用”之间有明显间隔的场景。例如服务需要读取配置、建立数据库连接、加载本地模型、恢复缓存,最后才开始接受请求。如果只使用默认的 simple,systemd 通常会在启动进程后很快继续调度后续单元;这并不代表应用内部初始化已经结束。

因此,先把启动过程分成两个层次:

  • 进程启动:二进制已经被 systemd 拉起,进程可以执行初始化代码。
  • 服务就绪:应用已经完成自己承诺的初始化,并愿意接受由后续单元或客户端发来的工作。

READY=1 表示第二个结论,而不是“某一行代码执行到了”。如果服务还没有绑定监听地址、关键配置没有加载,或者依赖连接失败后仍会继续重试,就不应过早发送这条通知。

Type=notify 将进程启动、应用初始化和 READY=1 就绪通知分成不同边界的静态结构图
图1:进程启动、应用初始化与 READY=1 通知之间的静态关系说明图,不是截图或运行证据。

写出 Type=notify 与 NotifyAccess=

最小单元至少要表达三件事:应用使用 notify 协议、通知由谁发送、初始化等待多久。下面的配置把通知发送者限定为主进程,并为启动阶段保留一个明确的超时边界:

[Unit]
# 只有应用完成初始化后,依赖本服务的单元才继续启动
Description=Notify-ready demo service

[Service]
# 让 systemd 等待应用主动发送 READY=1
Type=notify
# 只接受主进程发出的通知,避免辅助进程误报就绪
NotifyAccess=main
# 启动初始化超过这个时间仍未 READY=1 时判定启动失败
TimeoutStartSec=60s
# 示例程序负责在初始化完成点调用 sd_notify 或等价接口
ExecStart=/usr/local/bin/notify-demo
Restart=on-failure

[Install]
# 仅示意启用关系,实际目标按部署拓扑决定
WantedBy=multi-user.target

systemd 官方说明,notify 会等待服务发送 READY=1;如果 NotifyAccess= 缺失或为 none,对 notify 服务会被强制调整为 main。在配置里显式写出 NotifyAccess=main 仍然有价值:它把通知归属写进了单元契约,后续读配置的人不必猜默认行为。

如果确实由服务启动的辅助进程发送通知,才需要重新评估 NotifyAccess=all 或其他合适边界。但权限放宽并不会自动解决归属问题,辅助进程发送通知后立即退出时,systemd 仍可能无法正确把消息归属于该服务。

在初始化完成点发送 READY=1

通知应该出现在“应用认为已经可用”的那个明确位置。以使用 libsystemd 的 C 程序为例,发送点应位于配置加载、关键依赖连接和监听端口准备完成之后:

#include 

int main(void) {
    // 读取配置;失败时直接退出,不向 systemd 报告就绪
    if (load_config() != 0) {
        return 1;
    }

    // 先建立关键依赖,避免 READY=1 掩盖启动阶段的硬失败
    if (connect_dependencies() != 0) {
        return 1;
    }

    // 监听端点已准备好后,再声明服务可以接受后续工作
    if (start_listener() != 0) {
        return 1;
    }

    // READY=1 只说明初始化完成,不替代后续运行状态上报
    sd_notify(0, "READY=1");
    return run_event_loop();
}

这里的关键不是函数名,而是顺序契约:启动硬失败要在通知之前返回;通知之后,依赖本服务的单元才可以把它当作已启动。如果服务还会继续做一段不能影响可用性的后台预热,可以把预热放在通知之后,但不要让真正的必需依赖留到通知之后。

READY=1 只对 Type=notify 或 Type=notify-reload 有启动完成语义。官方手册还指出,READY=0 没有定义的非就绪语义,因此不要用它来模拟“暂时未准备好”;遇到需要表达运行状态的场景,应结合 STATUS= 或应用自己的健康检查边界。

应用初始化完成后通过 NOTIFY_SOCKET 发送 READY=1 并让 systemd 继续调度后续单元的静态关系图
图2:应用、NOTIFY_SOCKET、READY=1 与后续单元之间的静态契约关系图,不是截图或运行证据。

按启动约束选择 notify、exec 或 simple

把服务类型当成启动契约来选,比把 Type=notify 当成通用最佳实践更稳妥。可以按下面的约束做取舍:

启动约束更合适的类型原因
只需确认二进制已成功执行,应用初始化不影响后续调度Type=execsystemd 会等到服务二进制成功执行,再继续推进。
必须等应用完成配置、依赖或监听准备Type=notify应用用 READY=1 给出真正的就绪时刻。
兼容旧式服务且由父进程退出表示启动完成Type=forking依赖传统 fork 协议,但维护成本通常更高。
启动后立即推进,不需要应用报告内部状态Type=simple配置更少,但 systemd 不知道应用内部初始化何时完成。

这个选择可以归结为一句话:如果后续单元必须等到“应用内部准备完成”,就要让应用提供一个可靠的就绪信号;如果只需要验证进程能否被执行,exec 往往已经足够。不要为了获得一个看起来更完整的状态而发送没有业务含义的 READY=1。

READY=1 没有生效时按边界排查

服务长时间停在 activating,通常不是 systemd “没有看到输出”,而是通知协议的某个边界没有满足。可以按以下顺序检查:

  1. 检查服务类型:确认单元确实使用 Type=notify,而不是把 READY=1 发给了一个仍按 simple 运行的服务。
  2. 检查发送时机:确认通知发生在必需配置、依赖和监听端点都成功之后,而不是初始化一开始。
  3. 检查通知权限:确认发送进程符合 NotifyAccess=main;若由辅助进程发送,要重新设计进程归属和退出时机。
  4. 检查启动超时:确认 TimeoutStartSec= 覆盖了真实初始化上限;延长超时不能替代修复永远不会发送通知的逻辑。
  5. 检查单元是否已重新加载:修改单元后先让 systemd 重新读取配置,再重新启动服务,避免排查的是旧配置。
# 重新读取单元文件,让后续启动使用最新的 Type=notify 配置
sudo systemctl daemon-reload

# 重新启动服务,观察它是否能在启动超时前发送 READY=1
sudo systemctl restart notify-demo.service

# 查看当前状态和最近日志,重点看 activating、timeout 与启动错误
systemctl status notify-demo.service
journalctl -u notify-demo.service -b --no-pager

排查时不要只看服务进程是否存在。进程还活着、服务仍处于 activating、依赖单元没有继续启动,这三个现象可以同时出现;真正的分界点是 systemd 是否收到一条能归属于该服务的 READY=1。

落地清单

正式采用前,我会把以下内容写进服务的维护说明:

  • 明确列出哪些初始化动作完成后才算 ready。
  • 在单元中显式写出 Type=notify、NotifyAccess= 和合理的 TimeoutStartSec=。
  • 让应用只在成功路径发送 READY=1,硬失败直接退出并交给 systemd 处理。
  • 记录通知发送者是主进程还是辅助进程,避免进程退出造成通知归属不确定。
  • 把 daemon-reload、重启、状态查看和日志查看纳入单元变更流程。

最终可以用这张关系记住它:Type=notify 定义等待模型,应用初始化决定何时满足条件,sd_notify(..., "READY=1") 传递完成信号,systemd 再推进后续调度。只要四者的边界一致,READY=1 就不是一行装饰性的字符串,而是服务启动契约的一部分。

相关问题

READY=1 发出后 systemd 会持续检查服务健康吗?

不会。READY=1 主要表示启动或重新加载完成;运行期健康仍需要应用自身监控、Watchdog 或外部健康检查等独立机制。

NotifyAccess=all 能解决所有 READY=1 丢失问题吗?

不能。它只改变允许哪些进程发送通知,发送时机、通知归属和启动超时仍然需要分别满足。

服务没有代码支持 sd_notify 还能直接使用 Type=notify 吗?

不建议。若应用不会发送等价的 READY=1 通知,服务会一直等待到启动超时;应选择与现有启动协议匹配的类型,或先补上通知支持。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>