Research

服务已经 Running,为什么请求仍然失败?

从 Windows 服务控制管理器与 systemd 的状态语义出发,区分进程已启动、依赖满足与业务实际就绪。

为《服务已经 Running,为什么请求仍然失败?》确定性生成的文章图形
图形生成说明
函数家族
phase-orbit
简化公式
x(t)=sin(t)+αsin(3t), y(t)=cos(2t)+βsin(t)
短哈希
1b1cbe6c21d3
生成器版本
2

这是由文章身份与结构生成的装饰图形,不是数据可视化。

下载 SVG下载生成清单

Running 常被当作一句结论:服务起来了,问题应该在别处。但它首先是服务管理器保存的一项状态,回答的是“管理器是否认为启动过程已经完成”,并不直接回答端口能否接受请求、认证是否可用,或依赖的数据是否已经恢复。把这几个问题揉成一个检查项,故障发生时就很容易把排查带回重启。

本文只比较状态语义和可验证的时序,不假定某个具体软件、网络或生产环境。这里的“就绪”指一次有边界的业务判定:从预期入口发出一个不会改变数据的请求,得到符合契约的响应;它不是笼统的“进程还活着”。

先把三个状态拆开

可把启动后的观察拆成三层。第一层是管理层:Windows SCM 中的 SERVICE_RUNNING,或 systemd 认为 unit 已启动。第二层是进程层:主进程仍在、监听套接字存在,或子进程没有立刻退出。第三层才是业务层:请求经过入口、应用、必要的依赖后,得到可判定的结果。

不能从前一层直接推导后一层正常。进程活着可能没有监听目标端口;端口监听也可能只暴露了启动页;健康接口返回成功,还可能绕开了真正要访问的数据库、目录服务或队列。反过来,业务请求失败也不必然说明服务管理器报告错误。研究时应先写清每层的判据和采样时刻,避免把“后一次看见正常”误作“故障期间正常”。

一个可反驳的推演是:若在时刻 t0 管理状态变为运行,t1 才建立监听,t2 才完成依赖连接,而请求在 t0 到 t2 之间失败,那么“运行即就绪”被这段时序直接否定。若同一入口的只读探测在 t0 后立刻成功,且其路径确实覆盖关键依赖,才有证据支持更强的结论。这里最容易遗漏的是“确实覆盖”:只检查 HTTP 200 并不能证明后面的鉴权或写入路径可用。

Windows 的 Running 实际报告了什么

Windows 服务向 SCM 报告状态,控制程序和系统再从 SCM 读取它。微软对状态转换的描述很明确:服务完成初始化、准备接收控制请求后,调用 SetServiceStatus 报告 SERVICE_RUNNING;这一转换表示它已被 SCM 和服务监视工具认定为启动成功。Service State Transitions

这段定义的边界也正是问题所在。控制请求是停止、暂停等管理控制,并非某个应用协议的真实请求。SCM 无法从 SERVICE_RUNNING 推导出应用的每项外部依赖都已经可用;它只能依据服务上报的 SERVICE_STATUS 与启动过程中的检查点、等待提示来处理状态。官方结构说明中,dwCurrentStatedwCheckPointdwWaitHint 都是服务向 SCM 提供的进度信息,而不是端到端业务探测结果。SERVICE_STATUS

因此,观察到 Running 后仍应保留至少两条独立证据:入口侧的只读请求,以及应用对关键依赖的明确状态。若服务自身没有可用的健康语义,日志中“已经开始监听”、依赖连接成功或失败的时间戳,往往比一次服务列表查询更有解释力。它们也可能暴露另一种反例:服务过早报告 Running,随后才异步加载配置或建立连接。

systemd 的启动完成也有边界

systemd 的启动完成点随服务类型而变。Type=simple 在 fork 出服务进程后就可继续启动后续 unit,此时甚至可能还没执行服务程序的 execve;Type=exec 才会等到程序执行成功,但仍不等待应用初始化完成。依赖单元即使配置了 After=,也只能按对应类型的完成点排序,不能据此推断业务请求已经可用。具体差异见 systemd.service 手册

Type=notify 提供了较窄但有用的补充:支持该协议的守护进程通过通知套接字发送 READY=1,自行声明启动完成;notify-reload 也采用这一启动通知机制。通知还须符合 NotifyAccess= 的接收范围。sd_notify 文档说明了协议,但不会替应用定义业务成功。若 READY=1 发在必需的缓存预热、证书加载或下游握手之前,管理层的成功仍会早于实际可用。只改 unit 类型、不给应用实现通知,会让启动等待甚至超时。

这里可以提出一个可检验的设计假设:若服务需要依赖 D 才能服务请求 P,那么就绪探测必须让 P 或等价路径实际触达 D;若只验证 PID、unit 状态或独立的静态端点,探测结果不能反驳“D 尚未可用”。这不是要求每个服务都做昂贵的全链路测试,而是要求探测声明覆盖范围,并把范围写进监控告警的解释里。

一次排查应留下什么证据

面对“Running 但失败”,更有价值的顺序是重建时间线:管理状态改变的时刻、进程或监听出现的时刻、第一次有效请求及其结果、关键依赖的状态,再标出各记录的时钟来源。这样可以区分三种结论:状态先于业务就绪、业务在就绪后退化,或请求根本没有走到预期服务。

本文没有对任何 Windows 主机或 Linux unit 做实测,也没有证明特定产品应采用哪一种探测。不同应用的读操作、鉴权流程和依赖边界不同;尤其是写入型服务,不能把会改变数据的动作伪装成健康检查。下一步若要把上述推演落到某个服务,应先定义一条无副作用请求及其成功条件,再用同一时间源记录管理、进程和业务三层证据。如果首次成功请求明显晚于 Running,再检查这段间隔是预期的初始化过程、依赖延迟,还是服务过早报告了状态。