1 分布式系统基础要回答什么
在单台主机上,脚本把配置写进文件,另一个进程再读取它,排查通常可以从路径、权限、退出码和进程状态入手。现在把同一份配置保存到节点 A、B、C,并要求任意节点都能提供读取:A 写成功后,B 什么时候能看到?A 与 B 失去联系时,谁还可以接受修改?客户端等不到应答,能否再提交?这些问题构成分布式系统的入门边界。这里的节点可以是独立主机,也可以是不同主机上的进程;进程数量增加,并不自动得到可靠性。
分布式系统最重要的变化,是没有观察全局状态的瞬间。进程看到的是本地内存、文件和收到的消息。网络消息可能延迟、丢失或重复,主机可能重启,磁盘写入可能失败,客户端可能在服务器完成工作之后断开。看到连接超时,只能说明规定时间内没有得到结果,不能直接证明对端没有执行。必须把“请求是否送达”“修改是否完成”“应答是否收到”分开记录,否则同一个超时会被不同岗位解释成不同事故。
本课采用配置文件发布场景。客户端向配置服务提交新版本,服务把内容写到多个节点,节点上的代理进程再将配置安装到本地文件系统。配置中有版本号、内容摘要和操作标识。我们关心的是:承诺发布成功时哪些副本已经确认,读取可能获得什么版本,重复请求如何识别,以及代理何时真正启用配置。这里“配置服务已保存”和“所有进程已加载”是不同状态,方案必须明确自己承诺到哪一步。

先划定故障假设:节点可能停机或变慢,链路可能中断,消息内容由传输和应用检查发现损坏;暂不研究节点故意伪造结果。多个节点还可能共用交换机、电源或机架,这使“有 3 个进程”和“能承受 1 个独立故障”之间存在距离。可靠性来自可解释的协议和部署边界,不能只用副本数量表示。
可以先写出可检验的目标:配置发布返回成功后,从指定读取接口应能读到该版本或更新版本;在 1 个副本不可用时,服务是否继续接受发布;客户端总等待时间最多是多少;同一操作重试是否只形成 1 次发布。目标越具体,日志、命令和故障演练才越有落点。本课将用这些目标串起分区、复制、时钟、重试和法定人数,暂不展开具体共识算法内部实现。
2 分区、复制与一致性模型
“分区”需要区分 2 种含义。数据分区是按键或目录范围分担数据,例如配置组甲由 A 管理,配置组乙由 B 管理,目的是分担容量与请求量。网络分区则是部分节点之间无法正常交换消息,例如 A 能访问 B,B 无法访问 C。后者是一种通信故障,并不要求所有连接同时断开。数据分区不一定带来冗余,只有 1 份的数据所在节点停机后,其他分区通常无法替代它。
复制是把同一份逻辑数据保存在多个节点。同步复制通常把若干副本确认放到成功应答之前,异步复制则允许应答后继续传播。这里必须继续追问“确认”的含义:只是收到消息、写入进程内存,还是已经按服务约定持久保存?进程收到内容后立即崩溃,与完成持久保存后重启,恢复结果可能不同。副本越多,并不意味着每次发布一定等待所有副本;等待范围决定延迟,也决定故障期间能否继续应答。
一致性模型描述读写在外部看来遵守什么规则。线性一致性要求每次操作看起来在调用与返回之间某个时刻生效,并尊重已经完成操作的先后关系。如果发布版本 42 已成功返回,此后才开始的读取不能返回版本 41。这个定义针对约定的数据对象和接口,不能据此推断所有代理进程都已重新加载,也不能推断跨多个独立键的修改自动成为事务。
最终一致性允许传播期间读到旧值,但通常要求在没有新写入、通信恢复且复制持续推进的条件下,副本最终收敛。它没有天然给出“最多落后 2 秒”的保证。若业务需要时限,就应额外规定传播目标、测量方式和超限处理。读己之写则是另一种有用约束:某个客户端完成发布后,自己的后续读取不能退回旧版本;其他客户端仍可能暂时读到旧值。选模型要从实际读取者的需求出发。
网络分区出现时,系统无法无条件同时承诺严格一致的最新结果和每个可达节点都成功响应。若 A、B 可以通信,C 被隔离,让 C 随意接受相互冲突的配置,恢复后便需要解决冲突;让 C 拒绝写入,则牺牲了这部分请求的可用性。可以为隔离节点提供带版本的旧配置读取,但必须把“可能过期”写进接口约定。不要把这项选择简化成日常运行时永远固定的口号。

对于入门方案,可选定“发布等待多数副本确认,读取走能保证约定一致性的入口”。同时明确代理安装属于后续阶段,靠状态上报追踪。这样就能分别回答数据是否保存、读取是否新鲜、进程是否生效。若某个接口允许旧读,应返回版本号或更新时间,让调用者有证据判断能否使用,而不是把所有成功响应都解释成最新。
3 时钟、超时与重试幂等
主机上的时间至少要分清用途。墙上时钟用于显示日期、跨机器日志对照,可能因人工校时或时间同步而跳变。单调时钟用于测量本机经过的时长,不随墙上时钟校正而倒退。实现还应核对所用计时接口在系统挂起期间如何计时。不同主机的单调时钟数值不能直接互相比较,墙上时间接近也不能证明 2 次写入的因果顺序。
因此,日志可以同时记录墙上时间、请求标识、版本号和本机耗时。比较一次请求是否超过 800 毫秒,使用适合测量时长的时钟;判断配置 43 是否晚于 42,使用服务分配的版本顺序;判断重试是否属于同一操作,使用稳定的操作标识。把这些职责都压到“毫秒时间戳”上,容易在校时、并发和重启时出错。
超时是调用者的等待上限,不是远端事务的撤销命令。连接建立、发送、服务器排队、复制以及返回都占时间。客户端总预算为 1200 毫秒时,不能让每层组件各自等待 1200 毫秒,再乘上多轮重试。应传播剩余预算,并给连接、执行和重试留出明确边界。取消请求也需要服务端协作;即使客户端已经关闭套接字,服务端仍可能完成持久写入。
幂等解决重复调用带来的语义问题。同一操作执行多次,应具有约定上与执行 1 次相同的效果。发布请求应带有在首次发送前生成、重试期间保持不变的操作标识。服务端把该标识、请求内容摘要、执行状态和结果关联保存。再次收到相同标识与相同内容时返回已有状态;相同标识却携带不同内容时拒绝,而不是误把另一项工作当成重试。并发到达的重复请求也要由服务端协调,不能都通过“尚未发现记录”的检查。
去重记录和配置修改还必须具有协调的持久语义。若先改配置、后存去重记录,中途崩溃就可能在重试时再改一次;若先记录“成功”、后改配置,中途崩溃可能产生虚假的成功。入门阶段不必实现完整事务机制,但必须选择已有服务提供的原子能力,或把处理中、已完成、恢复查询的状态明确设计出来。去重记录的保留期还应覆盖允许的重试和补偿窗口。
超时越长,调用者越久占用连接和并发名额;超时越短,正常的慢请求也可能触发重试。滑块用固定的假设延迟样本说明这项权衡,不负责给出生产环境的最佳数值。真实设置要基于请求耗时分布、故障目标及资源上限,并把“结果未知”作为可查询的状态。对于不可安全重复的操作,应优先查状态或交给人工处理流程。
4 动手:用时序图解释一次跨节点写
下面用已有配置服务的抽象接口观察一次发布。假定实验接口为本机 127.0.0.1:8080 上的 /config 和 /operations/{id};它们是接口示例,需要由练习服务实现,普通 Linux 主机不会自带这些路径。配置服务管理 A、B、C 共 3 个副本,本次写由 A 协调,约定至少 2 个副本完成持久确认后才能返回成功。实验只在隔离环境进行。
op_id=$(uuidgen)
printf '%s\n' 'worker_limit=8' > config.txt
curl --connect-timeout 0.3 --max-time 1.2 \
-X PUT http://127.0.0.1:8080/config \
-H "Idempotency-Key: $op_id" \
--data-binary @config.txt
curl --max-time 1 http://127.0.0.1:8080/operations/"$op_id"
uuidgen 需要环境安装相应工具;它在这里生成操作标识,不能用当前秒数代替。保存 op_id,后续重试沿用它,也沿用同一份内容。--connect-timeout 限制连接阶段,--max-time 限制本次命令的总等待时间。命令退出码、HTTP 状态码和操作状态是不同证据:传输完成也可能收到服务拒绝,传输超时也可能对应已经成功的修改。实际调用脚本应分别记录这些信息。

按图解释事件:客户端发起版本发布,A 校验内容与标识,A 完成本地持久保存并向 B、C 传播;B 确认后达到本次约定的数量,服务才能形成成功结果。图中的传播是协议简化图,不能把任意文件复制脚本都视为具有相同保证。C 暂时没有确认,只表示它可能落后,不能据此说已经成功的发布丢失。此时若应答在返回途中丢失,客户端看到超时,服务却已有成功记录。
接下来查询同一个操作标识。若得到已完成及版本 42,应记录该结果,不再创建新标识重新发布。若得到处理中,应在剩余预算内退避查询。若查询接口返回未知,还要结合接口对记录保留期和读取一致性的定义,不能立即推断“从未执行”。只有服务明确支持对同一标识安全重试时,才重新提交同一内容;超出预算则交由后续恢复流程处理。
最后观察代理安装。代理可以先把内容写进目标目录内的临时文件,校验后再使用同一文件系统内的重命名替换目标,以避免读者看到半份文件。重命名的可见性原子性不等于断电持久性,后者需要正确的文件与目录同步策略。进程还可能缓存旧配置,必须通过其约定的重新加载接口或重启流程生效,不能随意对未知服务发送信号。验收时分别检查保存版本、安装版本和进程报告的生效版本。
5 脑裂、法定人数与故障域
脑裂常见于网络隔离后,多个节点都认为自己有权处理互相冲突的写入。它未必表现为进程崩溃:A 和 C 的本地健康检查都可能通过,各自还能读写磁盘。若只把“本机进程存活”当作写入资格,隔离两侧便可能独立推进版本。恢复连接后,选择时间戳更大的文件并不必然正确,因为时钟可能偏移,文件修改时间也无法证明哪项操作获得了合法确认。
法定人数是协议要求的一组参与者。对于固定的 3 个投票副本,多数为 2;任意 2 个这样的多数集合至少共享 1 个副本。这个交集为保留已确认历史提供基础,但它本身不是完整一致性协议。还需要约束谁能协调写入、副本按什么规则接受历史、如何读取已确认结果,以及旧协调者何时失去资格。随意向任意 2 台机器写文件,不能自动获得线性一致性。
当 A、B 连通而 C 隔离时,具备正确协议的多数侧可以按约定继续工作,C 应拒绝需要写入资格的请求。若只剩 A 可达,安全优先的配置发布服务就应停止发布,并保留查询或旧版本读取等受限能力。此时绕过限制“先强制写一下”,会改变原先的正确性前提。需要应急变更时,应采用预先审定的恢复流程,而不是把普通健康检查结果当成授权。
旧协调者还可能继续执行外部动作。若发布最终要写入共享存储或控制某项外部资源,可以使用递增的隔离令牌:新的合法协调者得到更大的令牌,目标资源拒绝旧令牌。前提是目标资源真正检查令牌;只把数字写入日志并不能阻止旧进程。这里先掌握“失去资格后,外部副作用也要被阻止”的要求,令牌分配与选举机制留待后续学习。

故障域决定副本是否会一起消失。3 个进程都在同一主机,不能承受该主机停机;3 台主机共用同一电源回路,仍可能同时断电。跨故障域部署也有代价:链路更长,确认时间可能增加,分区时的运行规则更重要。对于 3 个投票副本,设计目标通常可以包含承受 1 个副本不可用,但是否达到目标还取决于部署、协议、容量以及恢复操作,不能只从数量推出。
6 常见坑:无限重试放大故障面
配置发布开始变慢后,每个客户端都立即重试,通常会让系统更慢。最初的请求可能还在服务器排队,重试又占据连接、线程、内存和磁盘工作量。即使服务端保证幂等,重复请求仍会消耗鉴权、查询和应答资源。幂等限制重复副作用,不负责让重复流量免费。资源耗尽以后,原本正常的读取也可能受影响,故障范围从发布功能扩大到整个服务。
多层重试会相乘。外层脚本最多尝试 3 次,代理每次又尝试 3 次,内部调用再尝试 3 次,最坏可触发 27 次下游尝试。还没有计算结果查询和健康探测。方案应指定主要重试层,并给总尝试数、总截止时间和并发数设上限。其他层应快速传递能解释的失败原因,而不是各自默默重新开始完整预算。
退避让尝试之间的间隔逐步增大,例如以 100 毫秒为基础增长,并设定最大等待间隔。抖动是在允许范围内加入随机差异,避免大量客户端在相同节拍醒来。达到服务返回的限流条件时,应按接口说明延后或停止;权限错误、内容格式错误等确定失败不应通过重复提交解决。超时与暂时不可达可以进入受限恢复流程,但始终需要稳定的操作标识。
观察时要沿请求路径收集证据。客户端记录逻辑操作数和实际尝试数;入口记录活跃请求与拒绝数;配置服务记录排队时间、持久写入耗时、复制确认时间和副本版本差。若业务操作量基本不变,实际尝试量持续上升,同时排队增长,就应怀疑重试正在放大故障。只看 CPU 不够:线程可能等待网络或存储,CPU 仍然不高。

主机侧可以用 ss -tan 查看 TCP 连接状态,用 ps -eo pid,stat,etime,comm 查看进程是否存在及其基本状态;使用 systemd 的主机可以用 journalctl -u config-agent --since '-10 min' 查看对应服务日志,实际单元名应替换为部署名称。这些命令帮助定位连接积压、进程退出和错误时间,却不能单独证明某项配置已提交。提交结论仍要关联操作标识、版本和协议状态。
缓解时优先限制新流量和重试并发,为已经进行的请求释放空间,再检查依赖服务及链路。恢复后也要缓慢释放积压,避免客户端同时补发。若只重启服务而不限制流量,重启后的冷缓存和恢复工作可能立刻被重试淹没。处置记录应写清停止条件、回滚办法和状态查询方式,尤其不要把所有“未知”操作直接当成失败重新执行。
7 案例:时钟回拨破坏去重键
某配置代理用“主机名加当前秒数”构造去重键,认为同一主机每秒只会提交 1 次变更。初期低负载时看似正常。后来主机校时回拨 90 秒,代理再次走过已经使用过的时间区间。新配置带上旧去重键,服务把它当成历史请求返回;代理又只检查成功状态,没有比较结果中的版本和内容摘要,于是日志显示发布成功,进程却持续使用旧配置。
这里即使没有时钟回拨,同一秒内的并发发布也会冲突。回拨只是让原先隐藏的假设更容易暴露。另一种错误修复是每次重试都重新读时间来生成键,这会把同一项操作变成多个不同请求,丢掉去重作用。若服务把键与内容摘要绑定并拒绝冲突,该事故会表现为显式错误,而不是静默误认;因此服务端校验和客户端结果核对同样必要。
排查应从具体操作开始:找到客户端生成的键、提交内容摘要、服务返回的历史操作和配置版本,再对照代理报告的生效版本。使用 date -Is 能看到当前墙上时间,但不能证明过去发生过回拨。需要查看时间同步服务及系统日志中的调整记录,并结合请求顺序、进程启动记录和本机耗时。日志时间在回拨区间可能重复,单纯按显示时间排序会误导分析。

修复时,在逻辑操作创建时生成具有足够唯一性的标识并持久保存,直到操作完成或进入明确的终止状态;进程重启后继续使用原标识查询和恢复。随机标识的质量取决于可靠的生成工具与足够的随机空间,也要避免把克隆镜像中的固定值当作节点身份。服务端将标识绑定请求摘要,重复内容返回原结果,不同内容明确报冲突。时钟用于展示和计时,版本用于说明发布顺序。
历史数据也要处理。找出受影响时间范围内的标识冲突,比较期望配置与实际版本,把尚未执行的新变更作为新的逻辑操作重新提交。不要批量删除所有去重记录:迟到的重试可能因此再次执行。修复验收应包含同秒并发、应答丢失后重试、客户端重启和墙上时钟回拨的隔离实验;同时核对保存、安装、生效这 3 个阶段的版本是否符合约定。
8 把一致性与超时写进分布式方案
方案首先写清对象与成功边界。例如“配置组内版本按服务确认顺序递增,发布成功表示达到约定数量的持久副本确认;代理加载另行报告”。然后列出读取入口:管理端读取要求看到已完成发布或更新版本;隔离节点上的代理可以继续使用最后已确认的本地配置,但要报告版本和失联时长。这样的描述可以直接变成接口验收条件,而不是只写“高一致、高可用”。
再描述失败状态。正常完成、确定拒绝和结果未知应有不同返回与处理路径。查询接口应说明状态从哪里读取、记录保留多久、何时可能返回未知。超时预算覆盖连接、服务执行、复制及返回,重试消耗同一总预算。还要说明调用者放弃后谁负责后续查询,避免无人认领的处理中操作长期积累。版本过旧时是继续运行、拒绝新任务还是报警,需要由使用配置的业务风险决定。
部署图应标出投票副本与故障域,容量说明应覆盖故障后的剩余节点。能容忍 1 个副本停机,不代表剩余节点必定承担得起全部请求。观测至少把操作标识、请求摘要、发布版本和生效版本串起来,指标同时反映正常耗时与重试放大。日志中避免记录配置里的口令等敏感内容,关联通常使用摘要和版本即可。
验收可以选择 4 类边界:断开 1 个副本链路,确认多数侧行为;丢失成功应答,确认重试不产生新发布;在持久化附近终止进程,检查恢复状态;让代理暂时离线,检查重新连接后的追赶与生效报告。每类实验都先写预期,再记录接口结果和节点证据。实验只能支持所覆盖的故障假设,不应把少量成功演练扩展成对所有故障的承诺。
9 从分布式系统基础走向供电与制冷(H36)
分布式协议最终依赖实际运行的计算、存储和网络设备。把副本放在不同主机,只隔离了部分故障;若主机共用供电、制冷或上联链路,仍可能一起不可用。持续过热也可能先表现为响应变慢,随后触发超时和重试。因此观察到复制确认变慢时,除了检查进程、磁盘和连接,还应结合基础设施告警,确认多个异常节点是否集中在相同物理区域。
下节 H36 将进一步讨论供电与制冷如何影响连续运行。本课先保留 1 个迁移原则:协议里的独立节点,需要映射到实际的故障域。电力中断可能同时打掉多个副本,恢复供电也不等于立即恢复服务;主机启动、存储检查、数据追赶和客户端重连都有自己的阶段。恢复计划应为这些阶段留出容量和等待时间,避免所有进程同时启动、所有客户端同时重试。
阅读机房或部署图时,可以把“这组设备共享什么”写在旁边,再对应配置服务的多数集合。即使暂时不掌握电气和热设计细节,也能提出可执行的问题:哪些副本会同时停止,谁还能提供合法写入,代理能用旧配置多久,恢复后的状态如何确认。这些问题把软件正确性与物理可用性连接起来,也为下一课建立共同语言。
放进方案里
为配置发布服务写方案时,先列出客户端、配置副本和代理进程,并明确发布成功只承诺配置已按约定持久保存。代理安装与进程加载分别上报版本,验收同时核对期望版本、保存版本和生效版本。配置组采用什么读取保证、隔离节点能否读取旧值、旧值允许使用多久,都要写成具体规则。
部署使用 3 个投票副本,标出主机、电源与网络故障域。写入资格由既定协议控制,达到多数确认才能成功;失去资格的节点拒绝发布。多数集合有交集并不代替完整协议,选用的服务必须说明恢复和读取保证。发生超出设计范围的故障时,应执行预定恢复流程,不能临时让隔离两侧独立修改。
每项逻辑操作首次发送前生成稳定标识,服务把标识、内容摘要和结果协调保存。相同标识与不同内容必须报冲突。规定去重保留期、查询路径及结果未知的处理人;进程重启后仍可恢复查询。持续时间使用合适的单调计时接口,日期用于日志展示,发布顺序由版本表达。
为请求设置总截止时间、最大尝试数和并发上限,指定负责重试的层,采用退避与抖动。将连接、排队、持久写入和复制耗时纳入观测,同时记录逻辑操作数与实际尝试数。告警既看成功率,也看版本落后、未知结果和代理生效滞后。
上线前演练副本失联、应答丢失、进程崩溃和客户端重启,逐项记录预期、实测、停止条件与回退路径。演练期间核对稳定标识能否防止重复发布,以及剩余节点能否承担流量。最后写清共同供电或制冷故障超出哪些承诺,并把恢复后的副本追赶与客户端流量释放纳入运行手册。
10 小结:下一步供电与制冷(H36)
分布式系统需要把局部观察变成明确的外部承诺:复制说明数据在哪里,一致性说明能读到什么,超时限制等待,幂等约束重复副作用,法定人数与故障域共同影响故障期间的行为。
自测时解释这 3 个问题:客户端超时后为什么不能直接认定写入失败?3 个副本为什么还要检查是否共用电源?同一个发布请求重试时为什么必须保留操作标识和内容?能把答案对应到时序、接口和日志,就具备了继续学习 H36 供电与制冷的基础。