微服务调用链路太长怎么办?一篇文章讲透
1. 链路为什么越写越长
我几年前接手过一个老项目,最离谱的一次链路是这样的:
网关 → A → B → C → D → E → F
六跳。每一跳都 80~120ms,最后 RT 是 600ms 起步。用户点个按钮,像是点到异地机房。
那时候我特别不理解:为啥一个查订单的接口,要查到 F 服务?后来才搞明白——大家做需求的时候都觉得“反正我这个接口需要那个数据,就直接调一下它嘛”,于是越积越厚,链路越拖越长。
问题的本质其实就一句话:大家都觉得自己的那一次调用没事,但所有调用叠在一起就出事了。
链路长,就像高速公路上加了好多收费站,这点过路费不是大问题,但排队排成长龙,一辆接一辆慢吞吞,就会把整条高速拖死。
2. 什么叫服务雪崩
简单说,就是一个服务挂了,导致十几个、几十个一起挂,然后网关直接 502 一片。
之前遇到过一次最典型的,当时我们的库存服务突然 RT 飙升,平时 20ms,那次变成了 2s;订单服务每个请求都要查库存,上游就跟着变慢;再往上用户下单就卡住;最后所有下单相关的业务全挂。
当时是晚上 10 点,业务方直接跑过来问:“怎么下个单能要这么久?”
我也只能苦笑:“不是我们想慢,是下游在那里深呼吸。”
3. 雪崩的根本原因
我后来把问题总结成三个字:等、堵、炸。
- 等:超时设置太长,大家在那等结果,线程不释放,RT 越来越大
- 堵:大量调用挤在一起,下游撑不住
- 炸:线程池满了、连接池满了、队列爆了
有时候你看日志会发现:线程池 200 个线程,一个都没空,因为大家都在等待下游的 2s 超时。
这就是最可怕的:不是因为处理慢,而是因为一直在等,导致大家都没法工作。
很多团队(包括我们当年)都会犯同一个错:
默认超时值不改,库里多少就是多少。
然后就等着凌晨线上爆炸。
4. 超时时间到底怎么设
这件事我反复讲,讲到有点烦了,但真的太多人不重视。
我自己踩过一个坑:有一次一个接口因为访问第三方支付,超时时间设置成了 10 秒(对,就是我设的)。平时没感觉,但双十一那天因为对方偶尔卡一下,整个链路都跟着卡。
后来我总结了几条很简单但非常救命的原则:
超时设定原则
- 超时必须小于自身 SLA 你自己要求接口 200ms 返回,你给下游设个 2s,是不是有点离谱?
- 链路上每一跳都要递减 网关→A 最长 300ms A→B 200ms B→C 100ms
- 所有超时必须按指标(99 分位)来算 不看平均值,平均值在高并发下毫无意义
- 不要迷信下游 “我们这个服务很稳的,不会超时的” 这种话只适合写在墓志铭上。
5. 熔断器怎么工作的?Hystrix、Resilience4j 这些到底在干啥
很多人只知道“失败多了会熔断,但具体怎么判定的?”
我当年第一次用 Hystrix 时也只知道简单配置,直到某天线上被淹没,才研究得特别细。
核心逻辑其实就三个:
- 统计窗口(一般 10 秒)
- 阈值(失败请求达到一定比例就断)
- 半开探活(过一会儿放一部分流量试试是不是恢复了)
我第一次真正体会熔断的威力,是线上 C 服务突然 RT 飙到 5s,所有服务都在等它。
后来我们给 B→C 加了 Resilience4j,一旦失败率达到 50%,马上熔断,直接走降级逻辑。
结果非常明显:链路不再被拖垮,页面从 504 变成兜底数据,至少不至于挂死。
6. 隔离舱壁
讲人话就是:我不能因为你一个服务在拖延,把我的线程池全拖死。
Hystrix、Resilience4j 都有线程池隔离,重点就是——
每个下游调用自己的线程池,不许抢资源。
我们之前没有隔离时,库存服务 RT 2s 导致调用它的线程数占满了整个应用的线程池,其他接口完全没办法服务用户。
加了“舱壁”之后,库存服务爱慢不慢,慢死了也只是把自己的池子占满,不影响订单、商品详情那些正常流量。
这就是舱壁机制的本质。
7. 限流到底是令牌桶好还是漏桶好
我自己的体感是:
令牌桶更适合业务系统
因为能允许突发流量,比如突然来了 200 QPS,你不至于直接拒绝。
漏桶更适合对外接口
因为输出稳定,RT 可控。
当年我写过一个支付回调接口,用的是令牌桶,结果双十一凌晨,突发流量把桶打空了,直接拒绝了一堆回调。后来换成漏桶,流量下来了,但更稳。
对于内部系统,我一般建议使用令牌桶。因为微服务链路,不能太死板,总是要吃一点突发。
8. 降级策略
降级这事,大家都懂,但实际写出来的往往非常“羞羞的”。
比如我见过一个最离谱的降级:
代码语言:java
AI代码解释
catch (Exception e) {
return null;
}
然后就造成了 NPE 满天飞。
我自己的做法:
- 兜底返回(友好提示)
- 缓存返回(比如 5 分钟内的旧数据)
- 随机降级(比如 30% 的请求走兜底)
降级是让服务“趴着也能活”,不是让它“躺平”。
9. 链路追踪
以前没有 tracing 的时侯,排查问题就像摸黑找猫。加了 SkyWalking/Zipkin 后,真是一下清醒了。
有一次我们查一个慢接口,开发说“不可能是我这边,逻辑很简单的”。
链路追踪一打开:
A → B 100ms,B → C 1.4s
然后大家都沉默了。
追踪里看得最清楚的就是:
- 哪个节点慢
- 哪段网络延迟高
- 是否有重试导致的抖动
- 线程池是否排队
这东西平时没感觉,但线上爆炸的时候,是救命稻草。
10. 微服务架构里的 SLA 怎么定比较合理?
说实话,我以前以为 SLA 是业务方的事情,后来才明白:
没有清晰 SLA 的架构,是无法扩容、无法限流、无法保护的。
我的经验是:
- 核心链路 SLA 200ms(比如下单、支付)
- 查询类 SLA 500ms
- 非核心异步可超过 1s
为什么要定 SLA?因为:
- 限流要用 SLA
- 超时要用 SLA
- 要用 SLA
没有 SLA 的系统,就像没有体检报告的身体,看着好像还行,但随时可能倒下。
11. 如何避免级联失败
级联失败这个东西,戏剧性很强,轻则 504 一片,重则直接报警电话响一晚上。
我自己的经验:
- 所有下游调用都要单独线程池
- 每一跳都要有超时
- 每一跳都要有熔断
- 避免链路超过 3~4 跳
- 强依赖必须预热(特别是缓存)
我当年踩得最痛的坑是:
我们有个服务启动后,缓存要预热 2 分钟。结果部署后还没预热好,就有请求进来,导致大量查询,DB 压力飙升,整个集群挂了。
那次之后我深刻意识到:
启动预热也是链路稳定性的一部分。
12. 一个真实案例
这是一个我印象特别深的线上事故。
某年 618 晚上 20:00,秒杀开始,我们系统平时 QPS 600,峰值能冲到 3000 左右,也不算很夸张。
结果就那一天库存服务突然 RT 变成了 800ms,我们完全没料到。原因是某个同事改了库存的查询逻辑,加了个“统计未支付订单数量”的 join,而且没走索引。
这 800ms 就像砸在水面上的石头:
- 网关 RT 从 150ms 飙到 1500ms
- A 服务线程池从 150 用满到 300
- C 服务连接池爆掉,拒绝连接
- 后面所有请求直接被 504
整条链路像一串多米诺骨牌,全倒了。
当时我看着监控,整整愣了十秒。
最后解决是这样:
- 在 A→库存 加了熔断
- 线程池隔离拉开
- 缓存兜底库存(允许 5 秒的旧数据)
- 第三分钟,库存服务恢复正常
- 全链路才慢慢拉回来
更多推荐
所有评论(0)