慢调用治理:从超时到熔断的链路优化 — vibrant cover for technical blog article

慢调用比慢SQL更危险:超时、熔断与异步化

一个慢调用拖垮整个服务——慢调用治理是分布式系统稳定性的关键。

2025年10月29日·返回文章列表
文章大纲

做 Matrix 营销系统那会儿,人群服务有一次 RT 从 30ms 飙到几秒,上游策略服务的调用全部排队等待,线程池在半分钟内耗尽,整个链路从前到后逐级变慢。最坑的是它不报错,监控里错误率还是零,只是 P99 在悄悄往上爬。等告警触发的时候,上游已经因为线程耗尽开始拒绝正常请求了。后来定位到是下游一个依赖服务的 GC 停顿。那次之后我们把慢调用单独拎出来做了一轮专项,超时、熔断、隔离全套补齐,人群服务 RT 稳回 30ms,上游 QPS 也扛回了 2800。

慢调用和直接故障是两种性质的问题。下游挂了抛异常,错误率上去,告警立刻响,处理路径清晰。下游慢了,请求一个个排队等,不报错但悄悄吃线程,等你发现的时候线程池已经满了,正常请求也跟着遭殃。慢调用比故障更危险,因为它不像故障那样给你明确的信号。

超时是第一道防线,也是最容易被忽略的一道

PlantUML
正在加载图表…
PlantUML diagram

图:超时逐级递减原则——链路上游的超时必须短于下游,给每一级留出处理降级的时间窗口

慢调用治理的第一道防线是超时,听起来最朴素,但它的失效往往最致命。一个没设超时的外部调用,是埋在线程池里的定时炸弹。依赖一旦卡住,调用线程永久挂起,永远等不到返回也永远不释放。线程池是有上限的,几十个这样的线程就能把整个服务拖死。

我见过太多代码里调 Redis 不设超时、调下游 HTTP 不设超时、连查个配置中心都不设超时。开发期的理由都差不多,本地调得快,觉得不需要。但生产环境的卡顿来自你预料不到的地方,网络抖动、对端 GC、连接池打满、慢 SQL 串联起来。任何一个环节卡住,没有超时的调用就把线程焊死在那里。

所有外部调用必须设超时

调 DB、调 Redis、调下游服务、调第三方 API,每一处都要设超时。这是分布式系统的铁律。没有超时的调用不是慢一点的问题,是服务崩溃的入口。这个习惯要写进代码规范和 review 清单,不能指望个人自觉。

超时设多少要分场景。读操作超时短,两三秒足够,读不到就降级,用户等不了太久。写操作可以稍长,因为事务要完成,半截写入比超时更危险。更关键的是超时要逐级递减,链路总超时是用户能接受的等待上限,假设网关 10 秒,那服务 A 给下游的超时要留出余量,比如 8 秒,服务 A 调服务 B 再短一点,5 秒。每一级都给自己留时间处理超时后的降级逻辑。如果下游超时比上游还长,上游已经超时返回了,下游还在那算,算完的结果没人接,纯浪费。

超时的具体数值要基于压测,盯 P99 这条线。正常请求都在 P99 以内,超时阈值设在 P99 附近,正常的请求不受影响,真正异常的慢请求被切断。凭感觉拍一个超时值,要么卡得太严把正常请求也切了,要么卡得太松等于没设。

熔断,针对慢比针对错更难

PlantUML
正在加载图表…
PlantUML diagram

图:熔断器三态轮转——正常、熔断、半开探测,针对慢调用应使用 RT 比例而非仅看错误率

超时切断的是单次慢调用,但下游持续变慢时,每来一个请求都要等满超时才失败,线程还是被持续占用。这时候需要熔断,让调用方在下游还没恢复时就主动放弃调用,快速失败而不是慢失败。

熔断器的状态机很简单,正常、熔断、半开探测三态轮转。关键是怎么定义触发条件。常见的有两种,错误率超阈值熔断,慢调用比例超阈值熔断。错误率熔断适合下游直接挂的场景,但对慢这种故障不敏感,因为慢调用如果最终超时返回,未必被统计成错误。

针对慢调用,RT 维度的熔断更合适。统计单位时间内 RT 超过某个阈值的请求占比,超过比如 50% 就熔断。这样下游变慢但还没完全挂时,熔断器先一步反应,保护调用方的线程池。Sentinel 的慢调用比例规则就是这套逻辑,慢调用 RT 阈值和比例阈值两个参数配合。

熔断后的半开探测是恢复机制。熔断状态持续一段时间后放少量请求试探,探测成功说明下游恢复了,回到正常态。探测失败就继续熔断。这个间隔要设合理,太短频繁试探给下游压力,太长恢复慢。

资源隔离,让一个慢依赖不连累其他

超时和熔断保护的是调用方不被下游拖死,但还有一种情况,同一个服务调多个下游,其中一个慢了,把公共线程池吃光,导致连正常下游的调用也被挡在池外。这就是资源隔离要解决的问题。

做法是给不同下游分配独立的线程池或信号量。服务 A 调服务 B 用一个池,调服务 C 用另一个池。B 慢了只耗尽调 B 的那个池,调 C 的请求照样能拿到线程正常执行。资源隔离的代价是线程数变多,调度开销和内存占用都上升,所以一般只给关键路径或历史上不稳定的依赖做隔离,不是每个下游都单独开池。

信号量隔离比线程池隔离轻量,不切换线程,开销小,但没法做异步等待和超时中断。对响应快、只是想限制并发的下游用信号量足够。对可能慢、需要超时控制的下游还是用线程池。

发现和定位,链路追踪是关键

慢调用治理的前提是发现得了。APM 看慢调用排行,知道哪个接口慢。但这不够,还要知道慢在哪一层。链路追踪的价值就在这里,一个请求从网关到服务 A 到服务 B 到 DB,每一跳的耗时都在 trace 上,哪一层突出来一目了然。

定位到层之后再深挖。DB 层慢就去看慢 SQL,可能是索引或锁的问题。下游服务慢就去看它自己的链路和资源指标。资源争抢看 GC 时间、CPU 负载、线程池水位。网络看延迟和丢包率。自身逻辑慢就去做代码级分析。这步没法省,没定位准就动手优化,往往改错地方。

异步化,把逃不掉的慢挪出主链路

有些慢是结构性慢,报表导出、批量验真、大文件处理,这类操作天生就快不了。Matrix 系统的报表导出和联易融的批量发票验真都属于这类,同步做会占住线程几十秒甚至几分钟。把它们塞进主链路,等于让一个慢请求长期占着一把锁。

这类操作的正确做法是异步化。请求进来立刻投到 MQ,返回处理中的状态给调用方,后台消费者慢慢处理,完成后通过回调或通知告诉用户结果。联易融用 RabbitMQ 解耦批量验真和报表导出就是这套思路,把串行的慢操作从用户请求里剥出来,用户感知到的 RT 从几十秒降到几百毫秒。

异步化的代价是复杂度上升。你要管 MQ 的可靠性投递、消息幂等、补偿和重试、状态同步。同步改异步不是免费午餐,但对结构性慢的操作,这笔成本值得付,因为同步方案无论怎么优化都突破不了操作本身的耗时下限。

治理的两条路径

PlantUML
正在加载图表…
PlantUML diagram

图:慢调用治理的两条路径——防御与根治缺一不可,必须并行推进

慢调用治理分两条路径同时走。一条是防御,超时、熔断、隔离,保证自己不被下游拖垮,这是应急,解决生存问题。另一条是根治,定位到根因推动优化,下游慢就推动下游改,DB 慢就治慢 SQL,资源不够就扩容,这是长期,解决发展问题。

两条路径缺一不可。只做防御不根治,等于一直靠熔断和降级兜着,用户体验是降级的体验。只做根治不防御,下次下游再慢,自己先被打挂。Matrix 那次故障之后我们就是两条线一起推,防御侧把超时熔断隔离补齐,根治侧推动人群底座缓存改造,命中率从 40% 提到 99%,慢的根因才真正消除。

Continue Reading

关联文档推荐

查看全部
慢查询治理:从索引到 SQL 重写的全面优化 — editorial cover photo多级缓存:从浏览器到 DB 的全链路缓存设计 — vibrant cover for technical blog article容量评估:从 QPS 推导到资源规划的全流程 — editorial cover photo
作者:archy.shawn
声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。