ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

网络分区:微服务架构最隐秘的故障源与韧性设计

网络分区:微服务架构最隐秘的故障源与韧性设计 1. 网络分区是什么以及微服务为什么躲不开它聊微服务架构的时候网络分区Network Partition是绕不开的一个话题。我在不少团队评审过架构方案发现大家对分区这个概念普遍有两个误解一是觉得分区就是“网络慢”二是觉得“我用了注册中心和熔断就没事了”。这篇文章结合我自己在多个项目里的实测和踩坑把分区对微服务的影响掰开讲清楚。适合正在做微服务改造、或者被线上故障折磨过的同学不管你用的是 Spring Cloud 还是 Dubbo背后的道理是相通的。1.1 一个最容易误解的概念分区≠慢先把这个概念说清楚。网络分区指的是分布式系统里一部分节点和另一部分节点之间处于一种“完全无法通信”的状态而不是“通信变慢”。这个状态可能由交换机故障、光纤被挖断、机房断电、防火墙误配、甚至容器网络插件出问题导致。它和网络延迟升高是两回事延迟高只是慢节点之间仍然能交换数据分区则是两边彻底断联但各自内部仍然正常工作。这个区别至关重要。因为如果只是慢你的超时时间还有意义如果是分区两边节点都活着、各自的进程都不崩溃但彼此收不到任何消息系统就出现了“明明全员在线却谁也联系不上谁”的怪现象。在这种状态下如果一个服务同时在两个分区里都有实例在跑就会出现“双主”或者“多点写入”也就是常说的脑裂Split Brain场景。我见过最典型的案例是两个机房之间链路中断两边机房各自的服务都正常对外服务但数据各写各的等链路恢复之后才发现大量冲突。这时候你才意识到微服务架构的韧性本质上不是看单机稳不稳而是看它对“部分失联”这件事处理得好不好。1.2 CAP定理下微服务必须先选边站说到分区就绕不开 CAP 定理。很多刚接触分布式的人把 CAP 背成“三选二”其实是错的。分区Partition tolerance在网络环境下是必然存在的你没法选择不要它。真正能选的是当分区发生时你是保一致性Consistency还是保可用性Availability。微服务架构的尴尬就在这它天生是跨进程、跨网络的调用模型任何一个业务请求都可能经过网关、多个微服务、缓存、数据库。你没法保证网络永远不出问题所以你在做架构设计时其实已经隐含地给每个环节都做了一次 CAP 选择。比如注册中心用 Eureka选的是 AP分区时它宁可让你读到旧的服务列表也要保证能继续返回数据用 ZooKeeper 做注册中心选的是 CP分区时它宁可拒绝写请求也要保证大家读到的是同一个版本。这个选择没有绝对的对错但你必须清楚每条链路上的选择是什么。我见过一个团队业务数据存在 MySQLCP缓存用 Redis保可用注册中心用 EurekaAP分布式事务又要求强一致结果一到分区故障各组件各自为政表现完全不一致排查起来极其痛苦。所以我一直建议做微服务之前先把每条链路的 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、TCCTry-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-openhalf-open 状态放 5 个探测请求全部成功才关闭。这是单体服务的经验值你可以作为初始值之后再通过压测微调。特别提醒熔断打开后的 fallback 不要写“返回 null”就算完要返回一个明确的降级结果并打点否则前端拿到 null 还以为是正常空数据故障会被掩盖。3.3 注册中心选型AP还是CP注册中心在分区下的表现本质是 AP/CP 的取舍。我把常见方案整理成一张表注册中心CAP 取向分区时的表现典型场景EurekaAP保留旧服务列表牺牲部分一致性客户端可能调到已下线的实例对可用性要求高、可以容忍少量失败调用的互联网场景ZooKeeperCP失去多数节点时拒绝写服务可能短暂不可注册对一致性要求高、注册量不大的场景ConsulCP默认选举新 leader分区期间可能无法更新注册信息服务数量可控、重视一致性的场景NacosAP/CP 双模式AP 模式类似 EurekaCP 模式类似 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 分区演练要有固定节奏我建议把分区演练纳入常规发布流程每个月至少一次从低风险场景开始逐步加码。演练不是整天真的把机房停掉可以用上面说的模拟丢包方式先在预发环境做再灰度到生产的一小部分流量。演练的目的不是证明系统不会挂而是把“分区发生时谁会先挂、挂了怎么恢复、恢复要多长时间”这三个问题提前回答掉。每次演练之后把记录沉淀成一份操作手册下次真的出事时照着手册执行不要临时发挥。从我个人的实际体会来说微服务架构的韧性不是在网络好好的时候体现的恰恰是在网络坏掉的时候体现的。分区是分布式系统的常态而非异常这句话听起来像废话但真正按这个前提去设计系统的人和只是嘴上说说的人稳定性差距非常大。希望这篇东西能帮你少踩几个我踩过的坑。
返回列表