多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

服务端熔断与客户端熔断:本质区别与配合实践

服务端熔断与客户端熔断:本质区别与配合实践 1. 从一次线上事故说起为什么要区分服务端熔断和客户端熔断早几年我接手过一个支付回调系统的维护高峰期每秒要处理几千笔回调下游对接的是银行接口和一个内部风控服务。有一天风控服务的一个节点发生了FGCFull GC响应时间从50ms直接飙到3秒以上接着支付回调服务这边像多米诺骨牌一样线程池被打满HTTP连接池被占死最后整个应用无响应健康检查都过不了。当时最让人憋屈的是出问题的是风控服务但先挂掉的却是我们负责的支付回调服务。复盘时架构师问了两个问题直接把这次事故的性质给定了性“你们在调用风控服务的时候有没有在客户端做熔断” “风控服务自己有没有做服务端熔断”答案都是没有。我们当时以为两边只要有一个做了熔断就够了。这个认知后来被证明是错的——服务端熔断和客户端熔断是两个层面、两种目标、两种触发逻辑完全不同的机制它们不是同一个东西也不能互相替代。服务端熔断是保护“自己”的。它的意思是当服务提供方发现自己快扛不住了CPU高、线程池忙、错误率飙升主动拒绝一部分请求不再硬扛让系统先活下来。它管的是“我这个服务自己的生死”。客户端熔断是保护“调用方”的。它的意思是当我调用别人家服务时发现对方已经不健康了我这边直接在本地拦下请求快速失败不去傻等不把调用线程和连接池占死。它管的是“调用方自己的性能和稳定性”。这样一说很清晰但实际工作中很多团队把这两个概念混在一起讲甚至把“我配置了熔断器”挂在嘴边结果排查下来要么只做了客户端熔断要么只依赖对方的服务端熔断最后故障一来依然雪崩。这篇文章就专门把这两者的本质、触发逻辑、配置参数、落地方式、以及两者如何配合讲透。不管你是刚接触微服务的开发还是已经在线上背过锅的运维多少能从里面找到对应的场景。顺便把我自己调熔断参数踩过的坑一并交代了这些常规文档里可不会写。2. 服务端熔断先把自己保住才能谈别的2.1 服务端熔断的本质拒绝流量而不是等待服务端熔断说的是服务提供方在入口处做自我保护。它保护的对象是“服务自身这个进程”的可用性。拿现实中的例子来类比一个高速路口收费站如果入口不设卡所有车都放进来那闸口车道迟早堵死所有车包括救护车都过不去。服务端熔断就是收费站在入口设个关卡当里面车道已经开始堵了就直接告诉后面的车“这里堵死了请绕行”而不是把它们全放进去一起堵。在技术实现上服务端熔断的常见做法有几种通过容器线程池或者工作线程池设置了最大并发当活跃线程数超过了水位线入口直接抛异常拒绝新请求。通过限流器比如令牌桶、漏桶、滑动窗口计数器在入口层做速率限制超过阈值的请求直接返回“系统繁忙”类提示。基于错误率或慢请求比例触发熔断当连续一段时间内错误率超过阈值熔断器打开直接短路后续请求不再进入业务逻辑快速返回降级响应。服务端熔断的一个核心特征是它不需要知道“调用方是谁”它只需要衡量“自己是否健康”。这种机制对每个请求一视同仁只要系统指标恶化了就统一处理。2.2 服务端熔断最常见的坑把超时当成熔断有一个问题我见过很多次服务端配置了连接超时、读超时然后大家就说“我们加了熔断了”。实际上超时只是请求的截止时间它管的是单次请求能等待多久不管整体系统的承受能力。超时和服务端熔断的区别在于超时是“等不到就走”熔断是“不等了直接拒绝流量”。超时只能节省调用方的等待时间但对服务端本身没有保护作用——请求还是会进入业务逻辑还是会消耗CPU、内存、连接只是结果可能是超时返回。真正的服务端熔断必须在入口处拦截。常见的实现方式包括基于网关做全局限流比如用Nginx的limit_req或者Sentinel的集群流控能力在应用框架层面接入熔断限流组件比如Spring Cloud Gateway Sentinel或者是自己写Servlet Filter统计并发量和错误率如果用的是Dubbo框架服务端可以在Provider端配置actives参数限制同一个方法的最大并发调用数超过就算拒绝。对于自己实现的场景一个相对简单且有效的方案是用一个原子并发计数器在进入业务方法前自增执行完自减当并发数超过阈值时直接抛出拒绝异常。这个方案的优点是轻量、无侵入、性能开销小缺点是只能防护并发型过载防护不了CPU飙升导致的慢调用场景。所以更全面的做法是结合错误率指标一起判断。2.3 服务端熔断的配置要点和参数参考服务端侧即使是用成熟组件可配的参数也就那么几个但每个参数的物理意义需要理解清楚。以Sentinel的服务端流量防护来举例核心配置项有maxConcurrency最大并发超过最大并发的请求直接拒绝。这个值一般设置为理论最大QPS × 平均RT秒 × 1.2 Buffer系数。比如某接口压测下来最大QPS是1000平均RT是0.5秒那理论并发上限就是500你可以把maxConcurrency设为600左右留20%缓冲。maxQueueingTimeMs排队等待时间需开启公平排队这个参数是否该开实战中要慎重。排队意味着请求虽然不立即执行但依然占用着调用方的连接和线程排队时间过长反而会拖死调用方。一般建议只在拒绝不友好的场景比如用户直接访问页面时才用排队内部服务调用之间最好直接快速失败。slowRatioThreshold慢调用比例阈值当耗时超过maxRt的请求占比大于该阈值时触发熔断。这个参数对那种“流量不大但就是慢”的场景特别有用。这里强调一点服务端熔断的降级响应必须是占用资源极小的操作否则会在拒绝时反而把CPU打满。我看到有人做降级响应时还去查数据库拼装富文本页面那就是火上浇油。降级响应的正确姿势是直接返回预置好的静态字符串或缓存结果或者直接抛一个无堆栈的轻量异常。2.4 为什么服务端不能只靠“别人帮你熔断”这是一个特别关键的观点你在自己的服务端做不做熔断取决于你是否能接受“自己被拖死”。如果你不做服务端熔断你把希望寄托在“调用方会对我做客户端熔断”那等于你在说只要调用方发现我不行了他们会主动减少对我的请求量。但这个依赖链条有几个致命漏洞调用方可能没有做客户端熔断。这是常态很多团队只会要求别人做自己不落实。调用方发现“你不健康”需要时间这个时间窗口里你已经扛不住了。不是所有流量都来自同一个调用方可能有20个来源只要有一个来源没有做客户端熔断并且它的流量足够大照样把你打死。所以说服务端熔断是系统自我保护的最后一道防线它不需要依赖任何人的自觉自己先保住命。就像飞机上的氧气面罩你自己先戴好再去帮别人。3. 客户端熔断调用方活好的前提是不被拖死3.1 客户端熔断的本质别傻等快速失败客户端熔断是调用方视角的自我保护。它解决的核心问题是当被调用的下游服务变慢或者已经故障时调用方的请求不必傻傻地等满超时时间而是在本地通过熔断器判定“下游已经不健康”直接短路快速返回降级结果。为什么这个很重要因为超时等待是会累积的。一个线程最多只能并发跑那么多请求如果每个请求都在等下游3秒钟那这个线程池很快就打满了。线程池满了之后新来的请求连执行的机会都没有只能在队列里排队等着排队也需要时间排满了就直接拒绝。这就是典型的“业务线程饥饿”问题。客户端熔断要做的事就是在下游明显不健康时把这些请求拦截在最外层压根不去占用业务线程和连接资源直接给它一个快速的失败响应。它通过“快速失败”来保住调用方自身的资源不被消耗殆尽大致的指标就是调用方线程池使用率降下来了连接池也松了整体服务的吞吐得到保障哪怕下游全挂了调用方自己还能正常处理别的事情。3.2 客户端熔断的实现方式和核心隔离策略目前业界的客户端熔断实现从隔离粒度上来分主要有两种模式线程池隔离把对不同下游服务的调用放在不同的线程池中执行。某下游故障时只是它对应的那个线程池的线程被打满其他线程池不受影响。典型实现是Hystrix的THREAD隔离策略。信号量隔离不额外创建线程调用方仍然使用自己的请求线程去调用下游但通过信号量限制“同时调用下游”的数量。数量超过阈值时直接拒绝新调用。典型实现是Hystrix的SEMAPHORE策略以及Resilience4j内部的默认模式。线程池隔离的优点是隔离效果最彻底缺点是需要额外的线程切换开销每个下游调用多一次线程切换的上下文切换成本在超高并发场景下这个开销会比较明显。信号量隔离则相反几乎没有额外开销但是隔离不够彻底同一个业务线程还是会被慢调用占住如果这个业务线程同时还处理其他逻辑那影响会扩散出去。我的建议是这样如果下游是内部关键服务并且调用量很大、对RT敏感用线程池隔离宁可多掏一点线程切换的代价也要把故障隔离在局部。如果下游是缓存类、短RT服务或者是那种你不希望它去开辟额外线程去兜底的场景信号量隔离性价比更高。关于这两种模式的选型有一个容易被忽略的细节线程池隔离时线程池的核心线程数、最大线程数、队列长度的配置直接决定了隔离的效果。如果线程池配了200个线程而下游并发一上来就全被打满那和没有隔离几乎没区别。我的经验值是线程池核心线程数设置在“正常高峰期并发量的1.5倍到2倍”队列长度不宜过长一般建议0或者非常短比如10否则排队时间会掩盖下游故障的严重性。3.3 客户端熔断器工作状态机CLOSED、OPEN、HALF_OPEN熔断器内部核心逻辑就是一套状态机。每种状态的含义和流转逻辑直接决定了故障时系统的表现。我尽量用最简单的语言把这部分讲明白CLOSED关闭一切正常请求可以正常调用下游同时统计窗口内的失败率/慢调用率。当失败率超过阈值状态变为OPEN。OPEN打开熔断器打开所有请求直接短路不调用下游直接走fallback。这个状态下调用是“立刻失败”的不耗任何资源。HALF_OPEN半开经过一段冷却时间后熔断器放一小部分请求过去试探下游是否恢复。如果这些探测请求成功熔断器转回CLOSED只要有一个失败重新回到OPEN。这个状态机的设计很关键。很多人配置熔断器只配置了阈值却忘了配置“冷却时间”即从OPEN到HALF_OPEN需要等待的时间也不了解“最小请求数”即统计窗口内至少有多少请求后才开始计算失败率。这两个参数不配置好熔断器很容易出现“抖动”——一会儿打开一会儿关闭下游刚恢复一点又被你打开熔断压回去形成恶性循环。参数建议基于Hystrix也基于Resilience4j我们项目直接用Resilience4jfailureRateThreshold失败率阈值50%minimumNumberOfCalls最小调用次数达到该值后才触发熔断计算10slidingWindowSize滑动窗口大小单位秒或次数10秒内waitDurationInOpenState打开状态持续时间10秒到30秒之间不要小于5秒太短会导致“还没恢复就放流量进去探自己先崩溃”的问题。3.4 客户端熔断的Fallback不是随便返回一个默认值那么简单Fallback是客户端熔断的重要一环。没有Fallback即使熔断器打开了也只是从“慢失败”变成了“快失败”对业务来说没有任何补偿措施。但是Fallback的设计要区分场景对于查询类接口如果下游熔断了可以考虑返回上次成功缓存的旧数据所谓降级值配合上Cache Aside模式让用户感知不到下一跳。对于写操作类接口Fallback不能直接吞掉应该进入本地任务表或消息队列后续补偿执行。对于纯C端导流类场景Fallback兜底是返回一个静态提示页面比如“活动火爆请稍后再试”。还有一个非常重要的实操细节Fallback本身不应该调用外部服务否则会导致熔断后的请求在Fallback链路上再次打爆其他依赖。Fallback必须是纯本地的、资源占用极低的操作。我在代码审查中看到过有人把Fallback里写了一遍写数据库的操作结果熔断本来是为了保护数据库反而在Fallback路径上把数据库打崩了这就是典型的本末倒置。4. 两者如何配合分层容错的最佳实践4.1 服务端熔断和客户端熔断的分工关系如果说整个系统是一个水管网络服务端熔断就像自来水厂的蓄水池管理水压太高了阀门自动关小、限量供水客户端熔断就像小区住户自家装的防倒灌阀门外面水管爆了不至于淹到自己家里。这两个机制处理的是不同环节的过载风险服务端熔断应对的是“自己被打垮”的风险。触发源头是自身的资源指标CPU、线程、连接、错误率。客户端熔断应对的是“被下游拖垮”的风险。触发源头是下游的响应指标RT、错误率、慢调用率。只有两端的熔断都生效容错链路才是完整的。打个比方你负责的A服务调用B服务B服务做了服务端熔断那么当B压力过大时B会主动拒绝A的请求这部分是B在保自己同时A做了客户端熔断那么当B出现慢调用或大量报错时A会快速失败不去死等这部分是A在保自己。两者同时生效后整条链路才不会因为任意一端的故障导致两端同时崩溃。实际情况中最理想的状态是每个服务都要有服务端层面的自我保护机制无论流量来源有多少都能在自身资源达到危险水位时直接拒绝流量同时每个服务对它的关键下游都要有客户端层面的熔断能力当下游明显不健康时快速失败并输出降级结果把影响限制在自己这个服务内部。4.2 配置参数联动超时、重试、熔断必须一起调很多人只知道配熔断器的参数却忽略了超时和重试策略会直接影响熔断器的行为。这三者是一个互相关联的系统。先说超时。客户端对下游的超时时间如果设置过长比如10秒那么熔断器统计窗口内的“慢调用”比例就会偏低因为大部分请求还没到超时就被正常返回了熔断器触发的前提“慢调用比例超标”就很难成立。这样即使下游实际已经快不行了熔断器也不会打开调用方还是会被拖死。反过来超时设得太短比如100ms那么下游稍微抖动一下就是一大片超时熔断器频繁打开正常流量也会被殃及。我的建议是一套经验值适用于内部服务调用的常见场景假设网络延迟在1ms~10ms之间参数建议值说明连接超时200ms ~ 500ms如果连TCP握手都完不成说明网络或对端负载已经非常严重读超时1s ~ 3s超过这个时间还没返回对调用方来说等于失败重试次数0 ~ 1次只有对GET类幂等请求才建议重试写操作严禁自动重试熔断器失败率阈值50%过高则保护不及时过低则容易误伤最小调用次数10次窗口内调用太少时熔断不生效避免偶发抖动触发误熔断这里特别强调一下重试和熔断的配合。重试会让下游故障期间的流量放大比如下游本来已经只有20%的成功率如果你做了2次重试等于把打到下游的流量放大了3倍。这会让下游恢复的难度变得更高所以重试次数务必设置为0或1并且要配合一个“重试退避时间”不要瞬间重试。4.3 一个完整的分层容错落地清单我每次给团队评审服务容错方案时都会按下面的清单来逐项检查。这份清单也是实践中总结出来的现在分享给你可以作为自查表[ ] 服务入口是否有限流能力至少要在网关层或Filter层实现基于QPS/并发的快速拒绝。[ ] 服务进程是否在资源水位过高时主动自我保护比如Tomcat线程池满了之后返回503而不是继续往队列塞[ ] 所有内部调用是否有客户端熔断开关关键是“打开后能自动恢复”而不是一次性关闭。[ ] Fallback是否是纯本地实现有没有在Fallback里继续调用外部依赖[ ] 超时时间是否符合下游SLO是否发生过“超时比对方平均RT长10倍”的不合理配置[ ] 是否有针对“慢调用比例”触发的熔断而不是只盯着错误率[ ] 是否有监控面板能看到熔断器当前是OPEN还是CLOSED没有这面板调参就是闭眼开车。5. 常见问题与排查技巧实录5.1 熔断器打开了但系统还是挂了怎么回事熔断器打开后请求确实不再调用下游了但是Fallback却在消耗资源或者是降级路径上仍然有大量高耗时操作那瓶颈就从下游转移到自己这边了。排查思路如下打开熔断器控制面板看当前OPEN状态期间打进来的流量是多少QPS是否明显飙升。看Fallback方法的执行耗时。如果Fallback方法平均耗时超过100ms先检查它有没有查询数据库或调用其他外部服务。看业务线程池的“排队等待时间”。如果线程池排队积压严重即便是快速失败的请求也会因为排不上线程而表现得像卡死一样。我踩过一个具体坑某次我们对下单接口做了客户端熔断Fallback返回的是“下单繁忙”。看起来没问题但那个Fallback方法里有个Bug每次调用都会写一行日志到本地磁盘磁盘IO打满之后应用整体卡顿。这个案例告诉我们熔断后的降级路径也必须做性能预算不能因为是“降级”就放松要求。5.2 熔断器频繁打开又关闭出现“抖动”怎么办这种“熔断抖动”的典型表现是熔断器刚打开过了几秒又关闭然后因为失败率再次突破阈值又重新打开周而复始。根源通常是配置的minimumNumberOfCalls太小或者waitDurationInOpenState太短。具体来说minimumNumberOfCalls设成1或2时只要碰巧有一次失败就达到触发条件熔断器非常敏感waitDurationInOpenState设成2秒或3秒时下游还没恢复就被半开状态的探测请求打进来失败率飙升重新进入OPEN。合理的组合建议是minimumNumberOfCalls至少10次以上waitDurationInOpenState至少10秒。另外半开状态下放行的试探请求数量也很关键。如果半开状态放行的是全量流量的一部分比如50%而不是一个很小的固定值会造成“恢复还没跑稳又被压下去了”。Resilience4j中可以通过permittedNumberOfCallsInHalfOpenState来限制半开时的并发试探数建议设置为1到3让下游在恢复期间以最小代价接受试探。5.3 哪些异常该计入熔断统计业务异常算吗这个问题经常被忽视但影响很大。默认情况下很多熔断组件会把所有异常都算进失败率里包括正常的业务异常比如参数校验失败、账户余额不足。这会让熔断器产生“误杀”。正确做法是只把连接超时、读超时、连接拒绝、下游返回5xx错误码等外部故障类异常计入熔断统计业务异常直接抛出不参与熔断统计。在使用Resilience4j时可通过recordExceptions参数指定哪些异常计入失败率或者用ignoreExceptions把业务异常排除在统计之外。我在一个合同服务里就遇到过这种情况下游业务接口有不少“余额不足”业务异常结果异常率始终很高熔断器频繁误打开引起线上事故。排查了半天才意识到是统计口径出了问题调整ignoreExceptions之后马上恢复正常。5.4 服务端熔断触发后返回什么状态码和限流怎么区分服务端熔断触发时返回给调用方的响应要明确表达“是服务端主动拒绝的”通常选用503Service Unavailable或者429Too Many Requests。503适合“服务端过载暂时无法处理”的场景它能明确告诉客户端这是服务端故障一般客户端会对503做特殊处理不重试或延迟重试。429适合“超过限流阈值”的场景带上Retry-After头信息告诉客户端多久之后可以重试。注意区分这两个状态码很重要因为很多客户端组件对429的重试策略和对503的重试策略是不同的。如果混用可能会造成客户端调用方对整个服务做无脑重试反而放大了流量。同时服务端熔断触发的拒绝响应一定不要带大量body更不要走复杂的渲染模板否则会加剧入口处的I/O开销。最佳实践是在网关统一拦截返回预置好的JSON字符串或者空响应响应体控制在几十字节以内从网关直接回到调用方不进入业务进程。5.5 熔断参数的调优策略如何确定阈值熔断阈值不能拍脑袋拍出来它必须基于容量评估和历史监控数据。梳理一套简化的调优思路你在实际项目中可以照着走先压测出下游服务的真实容量瓶颈多大的QPS下RT开始劣化错误率在什么流量下开始飙升。记录正常情况下的P99 RT和错误率基线。如果没有监控数据那就从最近一周的日志里拉一下各项指标的中位数和P99。熔断阈值设定为“基线P99 RT的1.5倍到2倍”以及“正常最大流量下的并发数加20%缓冲”。比如正常P99 RT是1s那阈值可以设为1.5s或2s超过这个RT算慢调用。上线后持续观察调参周期建议至少一周。不要第一天改完第二天就大动线上流量天然有波动一两天的数据往往说明不了问题。有一个观察指标特别有用熔断器触发次数和下游状态码的关联图。如果下游5xx的比例开始上升熔断器也开始打开说明阈值设得基本合理如果熔断器经常触发但下游QPS很低、RT正常那就是阈值过于敏感需要放大。5.6 客户端熔断和服务端熔断谁优先配置如果一个系统完全没有熔断能力资源有限只能先做一端我建议先做服务端熔断再做客户端熔断。理由很简单服务端熔断保护的是你自己你是自己这个服务唯一的负责人。你不做服务端熔断别人帮不了你别人也不会一定帮你。先把你能直接控制的那一道防线做好再谈调用链路上的防御。时序上可以这样安排第一周在网关和所有服务入口接入限流和并发控制落地快速失败返回503。第二周为每个服务梳理关键下游依赖清单按调用量和重要性排序。第三周起逐个为TOP下游配上客户端熔断观察指标持续两周。顺手把超时、重试、连接池大小统一收编到一个配置平台管理避免每个服务各自为政。6. 回归实践这份经验能给你什么帮助说实在的熔断这个话题在分布式系统里已经被讲烂了但真正能把服务端熔断和客户端熔断分清楚、并且在线上把它们配合用好的人我见过的并不多。大家更多是停留在“用过某个组件配了几个参数”的层面上很少有人从头到尾理解两者的分工和边界。这些年我经手的系统里凡是线上稳定性出过问题且长期解决不了的最后排查下来往往都有同一个共性调用链路上要么服务端不做自我保护要么客户端不做快速失败更有甚者两个都不做光靠一堆超时时间硬扛。最后再分享一个小技巧熔断相关的经验值最好每次故障后用文字沉淀下来包括累计调用量、触发时段、触发前的RT曲线、熔断持续了多久、用户体感影响范围。等到下次调参时这些记录比任何公式都管用。毕竟熔断参数不是配一次就一劳永逸的它在流量高峰、活动大促、下游重构时都值得重新审视一遍而你之前记录的每一笔经验都会在那时变成你判断“阈值到底应该调高还是调低”的依据。
返回列表