一个永远只指向可达实例的分布式注册表

Consul 维护一份存活的健康服务清单

服务注册表的价值完全取决于它有多诚实。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 不做检查就回答。这三档没有对错,只有你愿意为一个多新的答案付出多少延迟。 ...

四月 6, 2028 · 1 分钟 · XB Field Notes
把流量与重启策略分开的两种探针。

liveness 与 readiness 告诉 Kubernetes Pod 的状态

慢启动不等于进程已死:readiness 负责遮挡流量,liveness 只负责让进程活着。把两者搞混是最常见的健康检查错误。团队把同一个接口同时接进两种探针,于是每个变慢的依赖都变成重启循环——应用需要时间,存活探针偏不给,Pod 恰恰死在本来再忍一会儿就能活的关口。两种探针并存,因为"没就绪"和"已死"要的是相反的动作:一个要时间,一个要重启。 防混的心智模型是医院分诊。就绪探针是分诊护士,决定谁现在能看医生;存活探针是抢救车,只在病人心跳停了才推出来。没人会因为坐在候诊区就给人除颤。记住这个画面,探针配置几乎不用想就写对了。 Readiness 把流量挡在门外 readiness 探针失败,会把 Pod 从 Service 的端点里剔除,但 Pod 仍在运行,好让进程完成热身或自行恢复。只要探针恢复通过,流量马上回流。所以就绪探针是可以放心失败的那一个:缓存可以慢慢热,迁移可以跑完,下游依赖可以回来,全程不丢 Pod 的内存状态,也不付重启的代价。 就绪接口要围绕真正的可服务性设计,而不是进程存在性。检查那些决定这个 Pod 此刻能否正确回答的东西:结构迁移是否应用、必需的缓存是否装满、关键下游是否可达。只检查"服务能响应"的就绪探针,会把注定失败每个请求的 Pod 放进流量——比不检查还糟,因为负载均衡器是信任它的。但也别把所有依赖都塞进去:非关键的推荐服务挂了,就绪失败等于为一件装饰品扔掉主干流量。 Liveness 守护进程本身 liveness 探针失败会触发重启,这是结束僵死状态的最后一招。它要处理的该是"进程卡死"这类问题,而不是容器只是暂时还没就绪。合格的目标很窄:死锁——此后任何请求都不会再成功;协程或线程泄漏到只有新进程能清;内存状态损坏到任何重试都救不回来。每种情况的共同点是不可逆:不重启就不可能恢复,而不只是恢复得慢。 这个门槛意味着很多服务应该只配极简的存活探针,甚至不配。把存活检查打到外部数据库上,每次数据库抖动都会变成全集群的重启风暴,还顺手毁掉本可以加速恢复的热缓存。优先用廉价的本地检查——主循环定时刷新的看门狗时间戳——不问进程之外的任何东西,只回答"这个进程是不是卡死了"。崩溃的容器本就会被重启;存活探针只为行尸走肉而存在。 给探针留足余量 把 initialDelaySeconds 设到足够大,别把冷启动误判成崩溃;timeout 也要配合真实工作量。把两类探针分开,让慢的 Pod 由 readiness 护住,而不是被 liveness 误回收。启动慢的容器最好再加一个启动探针:它运行期间存活探针保持关闭,于是需要预热大缓存的版本升级能拿到几分钟而不是几秒,稳态的存活检查强度一点不减。 失败阈值按观测到的行为调,别在服务之间抄整数。检查周期乘以失败次数就是真实反应时间——十秒周期配三次失败,等于坏了三十秒才动手。对就绪探针,这是三十秒失败请求继续被路由进来;对存活探针,这是卡死三十秒才重启。量出冷启动的分位耗时、依赖恢复时间、最坏的回收停顿,再定每个数字:比告警快,比噪声慢。 坏掉的样子 标志性故障是依赖故障期的重启循环:数据库变慢,本来就绪探针就够了,但存活探针共用同一个失败的接口,全集群一波波重启。每次重启清空热缓存,下一波恢复更慢,系统无限振荡。诊断看重启时间戳与依赖事故是否对齐;修法是拆接口——就绪查依赖,存活只查进程——让生病的下游只挡流量,不杀 Pod。 ...

七月 14, 2027 · 1 分钟 · XB Field Notes
浅色网格上的标题为‘三类探针,不同动作’,旁侧是观察、判断、行动三个步骤。

探针告诉平台何时出手

探针结果是控制循环使用的证据,不是平台对应用健康的直接理解。Kubernetes 不会从 healthz 这个路径名称推断你希望怎样恢复服务;真正决定下一步的是探针类型、结果、阈值与容器生命周期。把每次失败都描述成“重启 Pod”,会抹掉直接影响可用性的关键区别。 三个问题,对应不同动作 启动探针为初始化提供独立的检测政策。配置后,在它成功之前,存活和就绪探针都不会执行。启动探针成功后,在当前容器生命周期内便不再持续检查;如果容器重新启动,则需要再次通过启动阶段,不能把上一次成功沿用到新生命周期。 存活检查用于识别适合通过重启当前容器恢复的状态。达到失败阈值后,kubelet 终止受到影响的容器,并应用对应的重启政策。这不是保证重建整个 Pod、改变其身份或修复外部数据库,也不意味着容器之间的依赖与在途工作都能自动安全恢复。 就绪失败则让容器继续运行,并持续检查是否恢复。它影响通常按就绪状态选择目标的 Service 流量,但不是防火墙,也不会取消已经通过连接到达的请求。直接访问实例的客户端和现有连接,仍需要自己的生命周期处理,不能把就绪状态误读成网络阻断保证。 阈值统计连续观察结果 failureThreshold 为三,不是从启动以来累计失败三次就动作,而是连续失败达到三次;一次成功会打断这段失败序列。就绪恢复可以要求多次连续成功,而启动和存活探针的 successThreshold 必须为一。阅读结果轨迹之前,应先明确进入失败和恢复成功两个方向的阈值。 超时、初始延迟、执行周期和终止宽限期同样影响实际表现。阈值乘周期可以辅助估算,但不能当成端到端故障切换的精确秒表;调度、探针执行和路由传播都参与其中。容器尚未就绪时,就绪探针还可能比配置周期更频繁地执行,以便更快恢复就绪。 实验选择四次启动失败、三次存活失败、两次就绪失败,并要求连续两次就绪成功才能恢复。这组数字只是为了看清不同计数器的边界,不是生产推荐。真实门槛应结合启动时间、检查成本、错误恢复方式以及误判代价确定,不能照搬实验参数。 检查机制决定你拿到了什么证据 HTTP 检查使用文档定义的 200 至 399 成功范围。即使响应正文写着“内部错误”,返回 200 也不会因为人类读出了坏消息,就自动变成失败探针。应沿实际探测路径检查认证和重定向,而不是默认平台与交互式浏览器具有完全相同的访问结果。 TCP 探针证明的是连接可以打开,不是查询或事务能够完成。exec 检查则需要镜像里确实存在相应命令,并且退出状态有明确含义;频繁创建进程还会消耗节点资源。应该选择能够回答当前问题的低成本机制,而不是认为检查越复杂、调用越多就越可靠。 就绪成功并不是存活执行的前提。没有启动探针隔离时,初始化较长的应用可能还没来得及就绪,就因为存活失败反复被终止。相反,启动容忍窗口过大,也会推迟发现真正无法完成的启动。两边都需要依据实际启动分布和恢复目标权衡。 坏掉的样子 把外部依赖可用性直接作为存活条件,可能在共享故障期间不断重启健康进程;另一个独立线程上始终绿色的处理器,又可能遗漏业务工作线程的死锁。重启并不天然便宜,它会中断在途工作、重复初始化,也可能进一步增加故障依赖的压力。 如果就绪涵盖每一项可选依赖,本来还能服务的容量也会被撤走;如果状态频繁抖动,负载会在副本之间来回转移,进一步恶化饱和。因此,既要测试明确故障能否被发现,也要测试误判和恢复行为,不能只看一次故障注入能不能把状态变红。 应对照探针结果、容器重启次数、事件记录、就绪变化和真实客户端流量,并确认部署中的配置与路由消费者。一个本地计数模型通过,不代表 kubelet、网络路径或负载均衡器已经正确执行同样规则。平台侧证据不能由应用侧自述替代。 快速验证 保存为 probe_policy_check.py,用 Python 3.10 以上版本执行 python3 probe_policy_check.py,实验已在 Python 3.14.4 上运行。这个确定性模型接收成功或失败两个值,不部署 Pod,不调用 Kubernetes,不安排真实探测时间,也不会重启操作系统进程。 ...

六月 18, 2027 · 2 分钟 · XB Field Notes