☰
网络分区:微服务架构最隐秘的故障源与韧性设计
2026/10/1 11:18:14 网站建设 项目流程

1. 网络分区是什么,以及微服务为什么躲不开它

聊微服务架构的时候,网络分区(Network Partition)是绕不开的一个话题。我在不少团队评审过架构方案,发现大家对分区这个概念普遍有两个误解:一是觉得分区就是“网络慢”,二是觉得“我用了注册中心和熔断就没事了”。这篇文章结合我自己在多个项目里的实测和踩坑,把分区对微服务的影响掰开讲清楚。适合正在做微服务改造、或者被线上故障折磨过的同学,不管你用的是 Spring Cloud 还是 Dubbo,背后的道理是相通的。

1.1 一个最容易误解的概念:分区≠慢

先把这个概念说清楚。网络分区指的是,分布式系统里一部分节点和另一部分节点之间,处于一种“完全无法通信”的状态,而不是“通信变慢”。这个状态可能由交换机故障、光纤被挖断、机房断电、防火墙误配、甚至容器网络插件出问题导致。它和网络延迟升高是两回事:延迟高只是慢,节点之间仍然能交换数据;分区则是两边彻底断联,但各自内部仍然正常工作。

这个区别至关重要。因为如果只是慢,你的超时时间还有意义;如果是分区,两边节点都活着、各自的进程都不崩溃,但彼此收不到任何消息,系统就出现了“明明全员在线,却谁也联系不上谁”的怪现象。在这种状态下,如果一个服务同时在两个分区里都有实例在跑,就会出现“双主”或者“多点写入”,也就是常说的脑裂(Split Brain)场景。我见过最典型的案例是:两个机房之间链路中断,两边机房各自的服务都正常对外服务,但数据各写各的,等链路恢复之后才发现大量冲突。这时候你才意识到,微服务架构的韧性,本质上不是看单机稳不稳,而是看它对“部分失联”这件事处理得好不好。

1.2 CAP定理下,微服务必须先选边站

说到分区,就绕不开 CAP 定理。很多刚接触分布式的人把 CAP 背成“三选二”,其实是错的。分区(Partition tolerance)在网络环境下是必然存在的,你没法选择不要它。真正能选的,是当分区发生时,你是保一致性(Consistency)还是保可用性(Availability)。

微服务架构的尴尬就在这:它天生是跨进程、跨网络的调用模型,任何一个业务请求都可能经过网关、多个微服务、缓存、数据库。你没法保证网络永远不出问题,所以你在做架构设计时,其实已经隐含地给每个环节都做了一次 CAP 选择。比如注册中心用 Eureka,选的是 AP:分区时它宁可让你读到旧的服务列表,也要保证能继续返回数据;用 ZooKeeper 做注册中心,选的是 CP:分区时它宁可拒绝写请求,也要保证大家读到的是同一个版本。

这个选择没有绝对的对错,但你必须清楚每条链路上的选择是什么。我见过一个团队,业务数据存在 MySQL(CP),缓存用 Redis(保可用),注册中心用 Eureka(AP),分布式事务又要求强一致,结果一到分区故障,各组件各自为政,表现完全不一致,排查起来极其痛苦。所以我一直建议,做微服务之前先把每条链路的 CAP 属性画出来,知道自己哪些环节在分区时会牺牲什么,否则后面所有的加超时、加重试都是盲调。

2. 分区打在微服务架构上的真实痛处

网络分区对微服务的冲击不是单一维度的,它会沿着请求链路一层一层传导。我把这些年趟过的坑按影响面从大到小列一下。

2.1 注册中心的心跳风暴与“假死”实例

微服务最依赖的就是注册中心。正常情况下,服务实例每隔一段时间(比如 Eureka 默认 30 秒)发一次心跳续约,注册中心超过 90 秒没收到心跳就把它踢下线。这个机制平时没问题,但一发生分区,情况就完全变样。

假设你的服务部署在两个机房,机房 A 和机房 B 之间断连。此时机房 B 的实例发往注册中心(假设它部署在 A)的心跳全部丢失,注册中心会在大约 90 秒后把这些实例全部标记为不可用。但注意,机房 B 的实例本身完全健康,还在正常处理请求。消费者的服务列表里这些实例被清掉了,于是流量全部打到机房 A 的实例上;而机房 B 内部的调用还在走 B 实例,但消费者已经找不到它们了。这还不是最疼的,最疼的是恢复瞬间:链路一通,所有积压的心跳、注册请求、配置拉取请求同时涌向注册中心,直接把它打满,形成“心跳风暴”。

针对这种情况,我实测下来最有效的组合是三条:第一,消费者侧一定要用本地缓存的服务列表,并且缓存过期时间要大于注册中心的租约剔除时间;第二,注册中心的心跳间隔和剔除阈值不能盲目照抄默认值,要结合你机房间的真实链路质量去配,别把 RTT 2ms 的局域网和 RTT 几十毫秒的跨机房放在同一套参数下;第三,健康检查不能只看心跳,要配合实际的业务健康探针,避免“假活”实例还在被调。

2.2 配置中心与“最后一版可用配置”

配置中心是分区时最容易被忽略、却往往最先出问题的组件。配置中心的设计思路是“中央式发布、客户端拉取”,这决定了它天生对网络分区敏感。分区时,部分服务拉不到最新配置,只能继续用本地缓存的旧配置,这本身不算大问题;真正的坑在于“部分节点拉到新配置、部分节点没拉到”的中间状态。

我遇到过一起事故:运营在配置中心改了某个开关,想灰度放量。结果刚改完,机房链路抖动,一部分实例拉到了新配置,另一部分没拉到。两个分区的行为逻辑瞬间不一致,同一套服务对不同流量表现完全不同,业务方以为是程序出 bug 了,排查了半天最后发现是配置不一致。后来我们定了一条规矩:凡是涉及流量、定价、权限这类敏感配置的变更,必须通过配置中心的灰度发布能力配合版本校验,宁可全部拉不到也不要拉一半;同时配置监听一定要有版本号比对,本地缓存更新失败要能够回滚到上一个已知可用版本。

2.3 事务与数据一致性:最贵的一课

如果上面说的还只是“行为不一致”,那么分布式事务在分区面前就是彻底的“钱的事”。微服务拆库之后,一次业务操作往往要写多个服务的数据。常见的方案有二阶段提交(2PC)、TCC(Try-Confirm-Cancel)、Saga、本地消息表等。这些方案对网络分区的容忍度差异极大。

2PC 是最脆弱的:协调者向多个参与者发送 Commit 或 Abort,一旦分区导致部分参与者收不到指令,它们就会一直处于“预提交”的阻塞状态,直到超时。这条链路上任何一环失联,事务就可能僵死,数据库连接池打满,整个服务跟着瘫。TCC 和 Saga 相对好一些,因为它们把决策分散到各参与方,允许在某一步失败时做补偿,但补偿动作本身也要跨网络,如果补偿消息也发不过去,你还是需要重试队列和本地消息表来兜底。

我个人的经验是:核心交易链路不要试图用“一次请求内完成强一致”的思路。网络分区存在的现实下,强一致要么靠 CP 存储(比如把核心状态写入单一主库),要么就得接受最终一致的补偿闭环。把 Saga 的补偿步骤做成异步消息+事务性消息表(本地消息表/Outbox),再配合重试和幂等,是实践中最稳的组合。注意补偿必须是幂等的,补偿失败要有死信队列和告警,不然分区的账最后要人工拿 Excel 对,那感觉不想来第二次。

2.4 网关超时与重试引发的雪崩效应

网络分区对网关层的影响最直观,但也最容易被错误应对。分区时,网关发往下游服务的请求要么超时、要么连接被拒绝。此时如果网关的重试策略是“失败自动重试其它实例”,那灾难就来了:下游服务实际是健康的,只是网络不通,重试到其它实例同样大概率失败,重试风暴会成倍放大流量,打到本来就紧张的链路上,最终把系统打挂。

这里要区分两种失败:连接失败(ConnectException)和超时。连接失败通常意味着对端不可达,重试还有意义;读超时意味着请求已经发出去了,你重试反而可能造成重复执行,必须谨慎。我见过一个团队把 Feign 的请求默认重试次数从 0 调到了 3,分区时下游接口被重复调了 3 次,数据库压力直接翻了几倍,最后还是靠熔断器兜底才没全挂。重试一定要配指数退避和抖动(jitter),不要所有实例同时重试,避免请求整齐划一地打过去形成“惊群”。

2.5 可观测性断片:排查故障时最难受的部分

分区时大家忙着救火,往往最后才想起来看监控,这时候发现链路追踪(Trace)断成一段一段的:调用链路过不了分区边界,TraceID 传不过去,日志散落在各个服务里。而分区本身又是最需要全局视图去判断“到底哪两段网络不可达”的场景。

我现在的做法是两件事:一是把基础设施层的网络质量监控做在前头,对所有跨机房/跨可用区的链路做主动探测,ping/TCping 的指标单独建一个看板,分区一发生先看它;二是选型时尽量让日志、链路追踪数据走独立的管理网络或独立的 topic/管道,避免业务熔断时把可观测性数据也一起熔断了。没有这条,分区恢复后你连“故障从几点开始”都回答不了,复盘就没法做。

3. 怎么模拟分区、怎么调参:实测记录

理论说得再多,都不如亲手按一次。下面这部分是我在测试环境里模拟分区、调参的完整过程,参数都是我实际用过的。

3.1 用最小代价构造一个分区实验环境

模拟网络分区不需要很贵的设备。我常用两种手段:一是用 Linux 的 tc 配合 netem 和 iptables,二是用 Chaos Mesh 这类混沌工程工具。最简单的场景如下:在三台机器(或者三个容器)上,把目标服务的两台实例之间的网络直接丢弃所有包。

# 模拟从本机到目标 IP 的完全丢包(分区) iptables -A INPUT -s 192.168.1.20 -j DROP iptables -A OUTPUT -d 192.168.1.20 -j DROP # 恢复 iptables -D INPUT -s 192.168.1.20 -j DROP iptables -D OUTPUT -d 192.168.1.20 -j DROP

如果你用 Kubernetes,可以给某个 Deployment 打一个网络分区故障,比如 Chaos Mesh 的 NetworkChaos 配置 partition,指定 duration 和 targets。我建议实验从“单条链路完全断开 2 分钟”开始,先看服务发现和熔断器的表现,再逐步增加同时断开的链路数量。实验顺序很重要:先只断一个实例,再断整个机房,再断数据库链路的网络,分别观察。一上来就全员乱断,你根本分不清是哪个环节先崩溃的。

3.2 超时、重试、熔断参数怎么定

参数调优这块,最忌讳拍脑袋。我给一个从实测中总结的参考:连接超时建议设为网络链路正常 RTT 的 5~10 倍,比如跨机房正常 RTT 是 3ms,连接超时给 30~50ms 足够;读超时则要结合业务复杂度,一般 200ms~1s,超过 1s 的接口应该先优化性能而不是无脑调超时。重试次数,我认为大部分场景 1 次是上限,最好 0 次,靠熔断和降级兜底,而不是靠重试。

熔断器参数我常用的配置是:滑动窗口 10 秒,请求阈值 20,错误比例 50%,熔断打开后 sleep window 5 秒进 half-open,half-open 状态放 5 个探测请求,全部成功才关闭。这是单体服务的经验值,你可以作为初始值,之后再通过压测微调。特别提醒:熔断打开后的 fallback 不要写“返回 null”就算完,要返回一个明确的降级结果并打点,否则前端拿到 null 还以为是正常空数据,故障会被掩盖。

3.3 注册中心选型:AP还是CP

注册中心在分区下的表现,本质是 AP/CP 的取舍。我把常见方案整理成一张表:

注册中心CAP 取向分区时的表现典型场景
EurekaAP保留旧服务列表,牺牲部分一致性,客户端可能调到已下线的实例对可用性要求高、可以容忍少量失败调用的互联网场景
ZooKeeperCP失去多数节点时拒绝写,服务可能短暂不可注册对一致性要求高、注册量不大的场景
ConsulCP(默认)选举新 leader,分区期间可能无法更新注册信息服务数量可控、重视一致性的场景
NacosAP/CP 双模式AP 模式类似 Eureka,CP 模式类似 ZooKeeper,可动态切换需要灵活切换策略的场景

我用 Nacos 比较多,因为它在 AP 和 CP 间切换很方便,但要注意切换不是零成本的,配置变更会触发一次全量通知,别在业务高峰期乱动。无论选哪家,我都建议把“客户端本地缓存服务列表”这个能力打开,并且缓存失效时间别小于注册中心最长故障转移时间,否则分区时消费者还是会因为拿不到列表而报错。

4. 常见问题排查与避坑指南

这一节直接给结论。很多问题表面看是业务代码问题,实际都是分区引发的次生灾害。

4.1 分区场景问题速查表

现象很可能的原因处理思路
大量接口报 Connection refused消费者还在调用已被注册中心剔除的实例检查本地服务列表缓存、开启健康探针
下游时好时坏,一会通一会不通多个机房之间存在间歇性丢包用主动探测确认链路,优化重试退避策略
数据库连接池被打满2PC 或长事务在分区时僵死降低事务粒度,补偿改异步,检查连接超时
所有服务都在重试同一请求网关/客户端重试策略无退避关闭自动重试或加指数退避+抖动
服务列表频繁上下线心跳超时参数不符合真实链路按机房链路质量分别配心跳参数
监控图上 Trace 断链可观测性数据链路也被分区影响可观测性管道独立部署,优先保障

4.2 一次真实分区故障的复盘

讲一个我亲身处理过的案例,场景是双机房部署,Spring Cloud 微服务,注册中心 Eureka。那天下午某个机房之间链路发生抖动,先是监控报警大面积接口超时。我第一反应是某服务挂了,看了半天发现所有进程都活着,数据库也正常。然后看 Eureka 控制台,发现其中一个机房的实例被大量移出了服务列表。

当时的处理过程三步走:第一步,确认是分区而非应用故障,用 TCping 对比两机房之间的链路和机房内部链路,很快定位是跨机房链路问题;第二步,把网关的重试和熔断参数降下来,避免重试风暴,同时把请求尽量集中打到本机房的实例上,减少跨机房调用;第三步,等链路恢复后,观察注册中心的恢复过程,发现恢复瞬间确实有一波心跳风暴,但由于消费者本地缓存还在,实际影响被控制在很小范围。这次事故让我把“本机房优先”的策略正式写进了配置,而不是靠负载均衡的权重去隐性实现。

4.3 几个我踩过坑之后才明白的细节

第一个细节是健康检查。只用 TCP 端口探测远远不够,服务可能端口活着但依赖的 MySQL 已经连不上了,这时候实例依然是“假活”。健康检查一定要下钻到核心依赖,但不建议把所有依赖都放进探针,否则数据库抖动会把整个实例搞下线,反而放大故障。

第二个细节是 DNS 缓存。很多服务间调用走了域名而不是注册中心,DNS 在分区时也会有缓存过期问题。我见过一次故障,链路恢复后服务半天不通,最后发现是消费者本地 DNS TTL 太长,一直解析到旧的 IP。给服务间调用做域名解析时,TTL 要调小,并且要有本地的 DNS 缓存兜底。

第三个细节是“优雅上线”。分区恢复时,实例重新注册会有时间差,服务列表更新有先后,调用方可能在短时间内把这个实例当作已恢复就去调,但它的本地缓存依赖(比如 Redis、DB 连接池)其实还没完全建立,会有短暂的不稳定。这个可以通过注册中心的状态流转机制(先 OUT_OF_SERVICE 再 UP)来平滑处理,别让实例上线的一瞬间就接满流量。

5. 分区之后,做对什么才算真的稳

最后这部分不谈具体组件了,聊点更接近经验层面的东西。

5.1 先把“降级”当成一等公民

我观察到一个规律:准备过分区演练的团队,比没有演练过的团队,在故障处理心态上完全是两个量级。没有演练过的团队,分区一发生,第一反应是“能不能让网络马上恢复”,把所有希望寄托在基础设施上;演练过的团队,第一反应是“我先把哪些流量降下来、哪些功能停掉,保住核心链路”。降级不是认怂,是在分区这种既定事实面前最理性的选择。设计系统时,把非核心功能(消息通知、报表、推荐位)和核心功能(下单、支付、查询余额)从基础设施层面隔离,做独立的熔断降级策略,比事后临时改配置靠谱得多。

5.2 分区演练要有固定节奏

我建议把分区演练纳入常规发布流程,每个月至少一次,从低风险场景开始逐步加码。演练不是整天真的把机房停掉,可以用上面说的模拟丢包方式,先在预发环境做,再灰度到生产的一小部分流量。演练的目的不是证明系统不会挂,而是把“分区发生时谁会先挂、挂了怎么恢复、恢复要多长时间”这三个问题提前回答掉。每次演练之后把记录沉淀成一份操作手册,下次真的出事时,照着手册执行,不要临时发挥。

从我个人的实际体会来说,微服务架构的韧性,不是在网络好好的时候体现的,恰恰是在网络坏掉的时候体现的。分区是分布式系统的常态而非异常,这句话听起来像废话,但真正按这个前提去设计系统的人,和只是嘴上说说的人,稳定性差距非常大。希望这篇东西能帮你少踩几个我踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询