首页 / 资讯中心 / 文章详情

网络分区引发微服务雪崩?从CAP理论到故障演练的应对实践

网络分区引发微服务雪崩?从CAP理论到故障演练的应对实践 ★ FEATURED ARTICLE
前阵子告警群半夜炸了订单服务核心接口成功率掉到60%用户端大量报错排查了两个小时才定位到问题机房A区和B区之间的光模块故障链路丢包率飙到50%。网络没有完全断但实际效果和断了差不多——服务之间时通时不通满足不了任何正常的RPC调用这就是典型的网络分区。网络分区对微服务架构的影响比单机宕机麻烦得多。单机挂了最多损失一个实例的容量还有冗余节点顶上去网络分区是整条链路上所有依赖同时半死不活超时、重试、线程池耗尽、实例误摘除这些坑会叠加在一起形成连锁雪崩。这篇文章我打算结合自己排查和演练的经验把网络分区的成因、对调用链路和数据面的影响、入口流量的失控过程以及应对方案和演练方法逐一拆开讲适合正在做微服务治理、分布式系统的开发和架构同学参考。1. 一次真实故障复盘网络分区是怎么发生的1.1 静默断网的两个小时那次光模块故障最有迷惑性的地方在于监控面板上看链路带宽使用率只有30%ping网关偶尔丢几个包TCP连接也建得起来但实际业务请求一旦需要传大包就会大量超时或者收到半截数据。服务A调用服务B20个请求里能成功10个就算运气好RT从正常的5ms一路涨到2秒以上。更坑的是由于丢包不是100%一些重试恰好成功导致告警波动性很大。订单监控一会儿红了一会儿又恢复正常我们一度以为是发版或者DB抖动。直到排查到中间件层面抓包看到大量TCP重传和乱序才发现问题出在物理链路上。这种时通时不通的状态在网络故障里是最难定位的因为它不符合彻底断网的直觉判断。1.2 分区背后是CAP的必然要理解网络分区为什么是微服务的天敌得先回到CAP理论。C是强一致性A是可用性P是分区容错性。分布式系统在物理网络上布了这么多节点网络故障是客观存在的所以P几乎是必须选的。选完P之后C和A就只剩二选一要么牺牲一致性保证可用AP要么牺牲可用性保证一致CP。绝大部分微服务系统默认走的是AP路线注册中心、配置中心、业务缓存都是这种思路。这意味着分区期间两边节点的数据可以短暂不一致等网络恢复后再通过最终一致去追平。理论上没问题但问题在于短暂有多短以及追平的过程中业务是否已经被重试、超时、熔断搅乱了。网络分区不是边界清晰的一块大铁板它更像一层时断时续的纱这让所有恢复策略的判定都变得非常困难。1.3 最容易触发分区的四个环节从我的经验看网络分区很少是单一根因常见触发点有四个物理设施故障光模块老化、光纤被施工挖断、交换机CPU打满导致转发延迟。这类故障通常伴随间歇性丢包最难发现。配置变更防火墙策略、安全组规则、路由表写错或者云平台上误操作导致部分网段隔离。有一次我们遇到的问题是某次安全规则批量更新把内网端口段过滤掉了服务register和心跳请求全被拦应用层面毫无感知。软件缺陷TCP keepalive配置不合理、网卡驱动bug、宿主机GC长停顿导致虚拟网卡处理不过来这类问题在虚拟化环境尤其常见。云平台可用区故障整个可用区出现网络异常连同一VPC下的机器都可能出现跨机架通信抖动。我见过很多团队把网络分区当小概率事件把精力全花在单机高可用上结果真出问题时才发现两地三中心的架构图画得再漂亮跨链路一断所有优雅设计瞬间归零。2. 调用链最先崩的是超时与重试2.1 分区之后调用链的现场网络分区发生的那一刻最先受冲击的一定是服务间调用。注册中心还没感知到异常时调用方已经发出大量请求。问题在于TCP层的表现非常迷惑三次握手可能成功SYN包轻能过去但在传业务数据时丢包严重于是客户端卡在写超时或读超时状态。很多框架的默认超时配置是很保守的某些版本HTTP客户端读超时默认给到30秒甚至更久。一个下单请求网关到订单服务10秒订单服务同时调库存、优惠券、用户积分三个服务如果每个都等满超时这个线程至少被占30秒以上。线程池大小是有限的假设默认200线程高并发场景下几秒就全部占满后面所有请求排队形成线程饥饿。这里有个容易被忽视的细节分区导致的超时不是均匀分布的而是批量失败偶尔成功的交替状态。如果重试配置得不合理一个成功的请求后面可能跟着十几个失败的尝试系统的有效吞吐会瞬间骤降但CPU和内存消耗反而因为大量连接建立、销毁而升高。2.2 重试风暴如何放大故障重试机制本来是为容错设计的但在分区场景下它是故障放大器。假设基础调用失败率50%如果每次失败都重试3次实际打到下游的请求量就是原来的6倍甚至更多。多级调用下更夸张网关重试订单、订单重试库存、库存重试数据库连接每一层放大一次最终流量可能是正常值的几十倍。我亲身经历过一次因为重试引发的雪崩式放大核心链路的失败率只有20%但每个服务都配了失败重试3次加上Ribbon/Feign层和业务代码层的双重重试一个请求最长被放大到42次调用。结果就是分区期间下游服务本来只是部分不可用硬生生被打成了完全不可用因为正常请求和重试请求混在一起把线程池彻底塞满了。解决重试风暴的思路不能只靠禁止重试而要知道什么时候不该重试。非幂等操作、写操作、超时类失败尤其是读超时因为服务端可能已经处理完但响应丢了都不应该盲目重试。重试一定要配指数退避和随机抖动且重试次数上限设成1次同时全链路要有重试预算从入口到出口累计重试次数不能超过N次。2.3 熔断和隔离是怎么兜底的熔断器是应对分区时超时堆积的最后一层防线。熔断器维护三种状态关闭、打开、半开。窗口期内错误率达到阈值比如滑动窗口1分钟内失败率超过50%就打开熔断直接快速失败不再等待超时半开会放少量探测请求去试探下游恢复情况成功率达到阈值再关闭。实际上很多团队配置熔断参数时是凭感觉的导致该熔断时不熔断、不该熔断时误熔断。我建议最少关注四个参数滑动窗口大小、最小请求数、错误率阈值、熔断打开后的sleep时间。例如一个商品详情接口QPS有几千熔断窗口可以设10秒错误率阈值设50%sleep 30秒但如果是一个低频管理接口最小请求数必须调低否则窗口期根本凑不够样本熔断永远不触发。另外一个重点是线程池隔离和信号量隔离。哪怕上游已经熔断也不能让正常流量被拖死。核心服务和边缘服务如果共享线程池边缘服务超时会把核心服务的线程吃掉。按依赖拆分线程池是成本最低的隔离方案而信号量隔离更轻适合低延迟场景但无法主动切断超时依赖。3. 更隐蔽的是数据面注册中心、缓存与事务的连锁反应3.1 注册中心的心跳误判与自我保护调用链路层面的故障大家看得见数据面的问题往往藏得更深。注册中心依赖心跳来感知实例健康状态每个实例每隔固定时间上报一次心跳超过阈值没上报就被摘除。网络分区时问题就来了——B节点的进程是健康的单纯因为网络不通导致心跳到不了注册中心于是注册中心判定B已死把实例从服务列表里摘掉。这就会造成一个悖论分区期间A区的服务发现列表里看不到B区的实例调用直接失败返回no provider available而不是等待超时等网络恢复后B的实例重新注册流量又突然灌进去引发新一轮超时。B节点全程健康却因为错误的心跳判定被当成故障节点处理。Eureka的自我保护机制就是为了应对这个场景设计的当短时间内心跳丢失比例达到阈值默认15分钟内低于85%时注册中心停止摘除实例宁可保留可能不健康的实例也不批量误杀。Nacos也提供类似的保护阈值配置。但自我保护是双刃剑分区时保护了B同时也会把真正故障的节点留在列表里流量打到死节点上一样失败。这里的取舍没有绝对正确只能根据业务特性去调阈值并配合客户端的本地缓存来兜底。3.2 缓存一致性与分布式事务的困局分区对数据一致性造成的破坏比很多人想象的严重。先看最常见的cache-aside模式正常情况下读缓存未命中再去查DB然后回填缓存。分区时DB连接也可能走的是故障链路查询超时后缓存里没有数据请求直接失败更麻烦的是写操作服务先更新DB成功再想删除缓存时网络断了缓存里还是旧数据其他节点继续读到过期值只能等缓存过期时间到了才能恢复。分布式事务在分区面前更是基本失效。2PC协议要求协调者和所有参与者保持通信分区时协调者收不到参与者的prepare或commit响应事务只能悬挂在那里要么等超时回滚要么无限阻塞。TCC的confirm和cancel阶段一旦遇到下游不可达补偿调用也会失败。Saga的本地事务在分区时还能部分提交但如果补偿操作也过不去局部已提交的数据就成了脏数据。所以我在设计分布式事务方案时有个原则网络分区期间绝对不能依赖任何跨节点同步提交的方案。合理的思路是让每个分区内的服务保持自身的完整性记录不可达的变更分区恢复后通过定时对账任务和状态机迁移去执行补偿最终一致。虽然复杂但这是唯一能扛过网络故障的思路。3.3 异步消息在分区里的表现异步消息规避了同步调用的超时问题但网络分区照样坑人。生产者发消息到MQ中间件时如果生产者和MQ正好分布在分区两侧发送就失败。此时如果用的是同步发送业务会感知到异常但很多团队用fire-and-forget模式发消息失败被吞掉业务链路看起来成功了实际下游根本没收到通知数据状态永远对不上。消费者的ACK丢失同样常见。消费者处理完消息准备ACK时网络断开消息在MQ侧一直处于未确认状态恢复连接后会重新投递。这意味着分区恢复后会有大量重复消息涌入如果消费者不做幂等处理订单可能被重复创建、资金可能被重复扣减、通知可能被重复发送。消息中间件的本地事务表和消息状态表在这里是保命手段消费者先落业务数据再落消费记录两者放同一个本地事务里通过状态字段控制是否处理过该消息。4. 入口流量的失控网关与全链路超时错位4.1 网关线程池是怎么被打满的网关是流量的第一道闸门也是网络分区时最先被打爆的一层。网关到上游服务的连接池和线程池都是有限的假设网关为每个上游服务维持一个200大小的连接池正常情况下每个请求几十毫秒就结束连接反复复用非常高效。分区时情况完全不同一个请求发到上游数据包在中途丢失客户端这边没有收到响应网关线程就一直被这个请求占用。200个线程看起来不少但每个请求占用10秒200个线程也只能扛住20个并发请求。业务高峰期每分钟进几万个请求等待队列瞬间堆满线程池开始拒绝新请求返回503。用户端看到503后会刷新重试新一波流量打进来连网关的CPU都开始吃紧。更麻烦的是网关和上游服务之间如果还配置了连接池连接建立后长时间没有数据交互连接空闲超时后又要重建重建过程又遇到分区丢包进一步拖慢响应。4.2 全链路超时的错位配置我排查过很多超时雪崩的案例最后发现根因是各层超时时间配置完全错位。常见做法是各服务自己定一个觉得够用的超时从不考虑上下游。比如网关给下游调用配置10秒超时订单服务调用库存服务却配置30秒超时订单服务本身处理逻辑又要等库存结果——结果就是网关10秒就断开了订单服务还在傻等库存的30秒超时线程被多占20秒。这种错位的本质是超时预算没有做全链路规划。超时预算的原则是从入口到出口逐层递减用户请求入口最多等2秒网关到核心服务超时应该控制在1.5秒核心服务到下游基础服务超时要控制在800毫秒而基础服务内部调用数据库就要控制在200毫秒以内。每一层都要为自己的下游预留时间不能都比上游配置得更长。以实际配置举例层级推荐超时范围说明客户端/浏览器2-3秒超出直接报错避免用户无限等待API网关1.5-2秒所有后端调用的总预算核心服务调用800毫秒-1秒针对业务下游必须小于网关超时基础服务调用300-500毫秒跨服务Redis/DB类短连接DB/缓存操作100-200毫秒单次数据访问极限4.3 用户侧看到的雪崩入口流量的失控最终会反馈给用户。分区期间用户点击下单按钮页面转圈十几秒然后报系统繁忙。如果前端没有做请求重试拦截用户下意识会狂点按钮每次点击都会发起新请求流量被进一步放大。即使恢复了短时间内用户积压的请求还会集中涌进来造成二次冲击。所以网关层一定要做客户端友好的快速失败策略而不是长时间超时等待。502、503不如直接返回一个明确的错误页或兜底数据让用户知道没成功稍后再试。分区域熔断、限流降级也要提前在网关层配置好否则后面所有服务层面的保护措施都会被入口流量冲垮。5. 写在设计里的应对超时、幂等、多活5.1 超时与重试的正确姿势把应对网络分区的策略写进代码里而不是等故障发生再去救火是我这几年最大的心得。先说超时的三个原则。第一连接超时和读取超时必须分开配置。连接超时表示TCP建连的等待时间网络分区通常表现为能连上但传不动所以连接超时短一点500毫秒内读超时长一点但有限度这样才能把连接成功但响应丢失的场景识别出来。第二超时时间不能是“拍脑袋”的固定值最好基于P99延迟动态调整。一个接口正常P99是100毫秒给它300毫秒的超时已经非常充裕没必要默认配30秒。动态调整可以用自适应超时算法根据实时延迟统计自动调整但维护成本偏高一般团队做到“按接口分级配置P99加缓冲”就足够。第三重试要遵循“退避抖动”原则。指数退避的意思是第一次重试等100毫秒第二次等200毫秒第三次等400毫秒抖动是在退避时间上增加随机偏移防止一堆请求在同一时间点集体重试。重试次数严格限制最好用重试预算统一管理比如整个调用链只允许最底层基础服务重试一次其他层一律快速失败。5.2 幂等与对账是保命符网络分区恢复后最头痛的不是服务恢复了而是到底哪些操作成功过、哪些没成功说不清楚。请求超时了服务端可能已经执行成功也可能根本没执行。这时候如果调用方直接重试可能会重复下单如果不重试又可能丢单。唯一可靠的解法就是幂等设计。幂等设计的关键不是把接口简单地加个幂等判断而是要让幂等键贯穿整个业务链路。用户端的每一次操作生成一个唯一的业务流水号网关层透传业务服务用这个流水号做唯一约束数据库对应字段建唯一索引。第一次请求执行成功之后的重复请求直接返回第一次的结果。没有唯一索引的幂等都是不保险的因为高并发下先查后插必然会有竞态窗口。对账是比幂等更进一步的兜底方案。每天凌晨跑一批对账任务把订单状态、支付金额、库存扣减放在一起比对查出来两边状态不一致的订单按照状态机推进或者补偿。网络分区期间的脏账往往要等到对账任务才能完全暴露出来所以状态机的设计里一定要有一个未知或待确认的中间态而不是简单的成功/失败二元结构。5.3 多活设计与故障域如果说超时、重试、幂等都是事后补救多活设计则是从架构层面降低网络分区的爆炸半径。常见的多活形态有同城双活、两地三中心、单元化部署。同城双活强调的是两个可用区同时分担读写流量一个可用区整体故障时另一个可用区能独立支撑单元化则是把业务按用户维度切分每个单元内有完整的服务栈和数据分片做到我的数据在哪个单元我的请求就去哪个单元。多活设计最核心的难点在数据库层。跨可用区的数据库强同步会引入很长的写延迟分区时主备切换又面临数据丢失风险。所以真实情况往往只能做同城主备跨城异步复制或者基于分片的独立多活每个单元持有自己的一部分数据分片不同分片之间独立写入再通过异步同步汇总。这样网络分区时每个分片都是天然自洽的业务可以在分片级别继续提供读写服务。故障域的设计也要跟着业务来划分。把下单链路、支付链路、库存链路放在同一个故障域内分区时这个域内的服务还能互相通信用户业务基本不受影响跨域调用要有降级预案比如支付不可用时允许用户先下单后付款。我见过很多系统把核心依赖部署在跨机房的多个节点表面上是高可用实际上每次跨机房调用都是分割网络的定时炸弹。6. 常态化的功夫故障演练与监控告警6.1 演练要演什么网络分区不是靠想就能想明白的故障它涉及链路层、网络层、应用层多个层面的复杂交互。所以我强烈建议团队做常态化的混沌工程演练模拟真实丢包、延迟和断连场景。工具方面ChaosBlade是国内团队用得比较顺手的支持注入网络丢包、延迟、DNS故障等多种异常。实际演练时可以分三个层级去注入最轻的是网络延迟比如对指定服务加100ms的延迟观察超时和熔断是否按预期触发加重一点是丢包注入30%的丢包率验证重试和退避策略是否生效最重的是直接断连用防火墙规则或ChaosBlade的断网能力模拟A区和B区完全隔离验证注册中心自我保护、网关降级、消息补偿机制是否都能正常工作。命令示例# 模拟对目标服务增加100ms网络延迟 blade create network delay --time 100 --remote-port 8080 # 模拟30%的网络丢包 blade create network loss --percent 30 --remote-port 8080 # 模拟指定IP端口的网络完全断开 blade create network drop --remote-ip 10.0.0.5 --remote-port 8080 --interface eth0演练最容易出现的问题是演了个寂寞注入丢包后只有少量告警业务表面正常你以为系统没事实际上是因为流量还没打足。所以演练要配合压测工具一起做在注入异常的同时维持一定流量水位才能真正暴露问题。6.2 监控指标的设定分区故障的排查之所以慢很大程度是因为监控指标选错了。很多人只盯着机器CPU、内存、磁盘这些指标在分区时反而可能是正常的。真正能在分区时帮你快速定位问题的指标应该是调用链维度的数据。监控指标预警阈值建议反映的问题服务间调用成功率低于99.9%持续1分钟分区可能刚开始P99/P95 RT偏离基线超过基线3倍链路延迟增加线程池活跃线程数和拒绝数活跃接近上限且出现拒绝超时堆积导致线程饥饿熔断器打开次数每分钟出现熔断事件降级是否生效注册中心心跳失败比例超过15%实例摘除或自我保护TCP重传率和丢包率重传率超过静态基线物理链路异常分区发生的瞬间最先反应的往往是P99 RT和TCP重传率然后是调用成功率再往后才是线程池和熔断。我自己的经验是把调用成功率和P99 RT做成所有监控大盘的第一屏这两个指标一起异常波动基本可以直接判断为网络层问题不用先去查代码。6.3 演练踩过的坑说几个我在演练过程中踩过的坑给准备做混沌工程的团队提个醒。第一个坑是演练工具自身依赖网络注入丢包的时候把执行命令的控制通道也丢掉了。我遇到过用SSH登录目标机器后执行断网命令结果SSH连接立刻断掉最后只能靠带外管理口进去恢复的情况。现在演练前都会先确认有独立的带外控制通道或者让演练平台通过API下发指令而不是直连业务机器。第二个坑是时机的选择。在不通知周边团队的突袭式演练里如果正好赶上业务高峰期演练引发的告警会和真实故障混在一起运维都分不清是演练还是出事了。稳妥的做法是先从低峰期、局部流量开始确认整个告警、定位、恢复链路都通了再逐步增加演练的随机性和覆盖面。第三个坑是告警风暴。网络分区一旦被模拟必然触发几十个服务的告警如果告警聚合和降噪策略没做好所有告警同时轰炸真正有用的信息反而被淹没。演练前先把告警级别和归并策略调好让告警按链路维度收敛而不是按服务维度广播。每次演练结束我都会带着团队做一次复盘清单超时预算表有没有更新、降级预案能不能在控制台一键生效、注册中心保护阈值是否贴合当前流量模型。说实话网络分区很难完全避免但每演练一次系统就皮实一点真出问题的时候大家也知道先看什么、先按哪个开关而不是一群人扎堆看日志。
阅读完成 · 觉得有帮助?
咨询建站