下游服务慢了,我的系统为什么跟着崩?

去年一个大促,客户的核心交易系统挂了。不是流量太大,也不是代码有bug。是下游一个非核心服务——用户画像——突然变慢了。
那个服务平时响应时间20ms,大促当天涨到了800ms。订单服务调用它获取用户的会员等级,等800ms才返回。订单服务的线程池很快被占满,新请求进不来,开始排队。排队一长,订单服务自己的上游也开始超时。最后,整个交易链路雪崩。
一个非核心服务慢了几百毫秒,整站瘫痪。
这是微服务架构最容易被忽视的真相:你崩了,不一定是你自己的问题。往往是下游拖累的。
今天聊聊下游服务故障怎么传到上游,以及怎么防。
01 下游慢,上游为什么会被拖死?
很多人以为,下游服务挂了,直接报错就行了。下游服务只是慢,比挂了更可怕。
慢,才是慢性毒药。
过程是这样的:
线程被占住:下游响应变慢,上游调用的线程一直在等。本来20ms释放,现在要等800ms。线程释放不出来。
线程池满:新请求进来,没有空闲线程,只能排队或直接拒绝。上游自己的响应时间也开始变长。
连锁反应:上游变慢,影响到上游的上游。一层一层往上,直到入口。
那家客户的订单服务,线程池上限200。平时每个请求平均等下游20ms,200个线程够用。大促期间下游变慢到800ms,同等QPS下,需要的线程数暴增。线程池满了,新订单进不来。
02 超时是第一道防线
很多开发设超时很随意:3秒、5秒、10秒。觉得“设大点总没错”。
但超时太长,线程等得久;超时太短,正常请求会被误杀。
超时设多少?
先搞清楚下游服务的正常响应时间,P99是多少。超时设成P99的2-3倍。
正常P99=50ms → 超时设100-150ms
正常P99=200ms → 超时设400-600ms
正常P99=1秒 → 超时设2-3秒
那家客户的用户画像服务,正常P99=30ms,他们超时设了3秒。下游变慢到800ms,还在超时范围内,线程继续等。把超时改成100ms后,慢请求快速失败,线程不被长期占用。
03 重试是把双刃剑
下游请求失败,重试一次,有时候能成功。但重试不当,会放大故障。
重试风暴:下游服务变慢,上游大量请求超时,每个超时请求又重试一次,压力翻倍。下游更慢,更多超时,更多重试。恶性循环。
怎么设重试?
限制重试次数:最多1-2次,不要无限重试
指数退避:第1次等100ms,第2次等200ms,第3次等400ms
只对特定错误重试:网络超时、5xx可以重试;4xx(业务错误)不要重试
重试要有熔断:连续失败N次,熔断器打开,直接返回降级结果,不再重试
那家客户之前重试了3次,没有退避,失败就立刻重试。大促时下游变慢,重试风暴把下游打得更慢。改成了最多重试1次,指数退避,连续失败5次触发熔断。
04 熔断器:自动切断
熔断器保护你的系统,不被下游拖死。
三状态:
关闭:正常调用下游,统计失败率
打开:失败率超过阈值,直接返回降级结果,不调用下游
半开:过一段时间,放少量请求试探下游。成功则关闭,失败则继续打开
那家客户加了熔断器,阈值50%,统计窗口10秒。下游响应变慢后,失败率很快超50%,熔断器打开。订单服务不再调用用户画像,直接走降级,线程池恢复。
05 舱壁隔离:不互相影响
船舱分段隔离,一个舱进水,不会沉掉整条船。用在系统上同理。
线程池隔离:调用不同下游服务,用不同的线程池。用户画像用线程池A,订单服务用线程池B。画像服务崩了,只影响线程池A,订单核心链路不受影响。
信号量隔离:用计数器限制并发数,不创建独立线程池,更轻量。
那家客户把核心依赖(订单、库存)和非核心依赖(用户画像、推荐)的线程池分开。大促时画像服务变慢,只影响非核心线程池,订单核心链路不受影响。
06 降级:有损服务比没服务强
下游挂了或慢了,返回个替代结果,比等超时强。
降级方案:
返回缓存数据:画像服务慢,返回上次缓存的会员等级
返回默认值:画像服务不可用,默认用户是普通会员
关停非核心功能:大促期间关掉“猜你喜欢”,只保核心下单
那家客户降级方案:画像服务不可用时,默认用户等级为普通。虽然有部分用户该给的高级会员折扣没给,但总比不能下单强。
写在最后
下游服务慢,你的系统跟着崩,不是代码写得差,是防护没做够。
那家客户的运维负责人后来总结了一个口诀:“超时设短防积压,重试退避不放大;熔断隔离两把锁,降级保底不崩盘。”
你的系统里,有这些防护吗?超时设对了吗?重试带退避了吗?熔断器装了吗?舱壁隔了吗?降级方案写了吗?
没做的,今天补上。下次再遇到下游慢,你的系统就不会跟着崩了。