服务注册表的价值完全取决于它有多诚实。Consul 把注册的实例收进 catalog,并给它们挂上健康检查,于是「某个服务的健康实例在哪」这个问题的答案会随现实变化。注册本身很简单;检查才是全部的产品价值,因为一份列出死实例的注册表比静态清单更糟——它会主动把流量送进坑里。
这个答案的新鲜程度,取决于两个第一周之后没人再看的选择:检查多久跑一次,以及它到底测的是什么。每 30 秒跑一次的检查会瞎 30 秒;只证明进程存在的检查则永远瞎着。
注册,以及两个层级的检查
实例通过配置文件或注册 API 登记名字、地址、端口以及一条或多条检查。检查默认以 critical 状态起步,这是刻意的:在健康至少被验证过一次之前,服务无法进入可用池——想要初始就是 passing,必须显式写出来。检查的状态由它自己判定:脚本检查按退出码决定(0 为通过、非 0 为 critical),HTTP 检查按响应码决定,输出超过 4KB 会被截断,脚本检查默认 30 秒超时。也就是说,「检查通过」这几个字的含义完全取决于你写的那条命令。
同一台节点上有两个层级的检查。serfHealth 这样的节点级检查服务名为空,描述的是这台机器;service:web-1 这样的服务级检查绑定在某一条注册上。它们各自独立失败,但只取健康的过滤器要求节点上所有检查都通过,所以节点级检查变 critical 会一次带走这台机器上的所有服务,而服务级检查变 critical 只移除一个实例。
调用方实际拿到什么
catalog 会回答两个不同的问题。不加过滤时,/v1/health/service/web 返回全部注册以及每条检查的状态;加上 passing=true 时,只返回所有检查都通过的实例。DNS 与 SRV 永远按第二种方式回答,所以解析器不会拿到一个不能用的名字,而这个通过集合就是负载均衡池。
catalog 同时也是最终一致的,机制叫反熵。注册是即时的——agent 会通知 leader——但修复是周期性的:每个 agent 按一个间隔把自己的本地状态与 catalog 对齐,128 个节点以内的集群是 1 分钟,随集群规模增长而变长,并且每个 agent 的起始时间都会错开,以免所有人同时同步。同步失败只会被记进日志、等下一个周期重试,所以一条错误的 catalog 记录可以安静地存活长达一个间隔。读路径也有自己的取舍:默认情况下 agent 会把 catalog 读请求转发给 leader,并用一个假定领导地位短期稳定的 leader lease,因此很快但偶尔可能返回陈旧结果;consistent 模式要求 leader 先向多数派确认自己仍是 leader,stale 模式则允许任何 server 不做检查就回答。这三档没有对错,只有你愿意为一个多新的答案付出多少延迟。
坏掉的样子
第一种故障就是间隔本身。检查的新鲜度不会超过它的周期,所以一个在探测刚通过之后立刻死掉的服务,会一直留在轮换里直到下一次探测——30 秒的检查是 30 秒,60 秒的检查就是 60 秒。TTL 检查比慢间隔更糟,因为它是被动的:实例按自己的节奏上报,所以一个 TTL 为 120 秒的死实例会把 passing 状态保持满 120 秒。
第二种故障是检查回答了错误的问题。一个对进程跑 pgrep 的脚本会在整个死锁期间报告 passing——进程在、端口在监听,而每一条被路由过去的请求都挂住。catalog 无法把这种实例和健康实例区分开,所以对池子里的每个调用方来说,故障都是静默且彻底的。真正去探一次依赖的检查哪怕不完美,也会在一个间隔内收敛到真相;只探存活的检查永远不会。错误的代价还取决于方向:该通过却失败的检查会摘掉健康实例、白白损失容量;该失败却通过的检查则持续把流量喂给一个服务不了的东西,调用方只能从自己的错误率里发现。
快速验证
对比过滤与未过滤两个视图,并看看 DNS 发出去的是什么:
consul catalog services
consul health service web
curl -s 'http://127.0.0.1:8500/v1/health/service/web' | jq -r '.[] | "\(.Service.ID) \(.Checks[] | select(.Status!="passing") | .CheckID + "=" + .Status)"'
curl -s 'http://127.0.0.1:8500/v1/health/service/web?passing=true' | jq -r '.[] | "\(.Service.ID) \(.Node.Address):\(.Service.Port)"'
dig +short SRV web.service.consul
在未过滤结果里带着失败 CheckID 出现的实例,必须同时从 passing=true 列表和 SRV 应答里消失;如果它还在,说明这条检查根本没有被求值。节点级检查为 critical 的机器会从两个列表里整体消失,连带它上面的所有服务。
来源
- Service discovery,catalog、注册与健康检查的角色,已核验 2026-09-16。
- Health checks,检查类型、间隔与默认的 critical 初始状态,已核验 2026-09-16。
- Health HTTP API,
/v1/health/service与passing过滤器,已核验 2026-09-16。 - Anti-entropy,周期性同步间隔与其失败行为,已核验 2026-09-16。