付同学的技术客栈 - 服务治理
type: channel
link: https://www.fuanyi.com/tag/%E6%9C%8D%E5%8A%A1%E6%B2%BB%E7%90%86/
items: 接口和服务高并发：别让重试把系统拖垮

接口和服务高并发：别让重试把系统拖垮
type: item
date: 2023-02-19T13:20:00+00:00
description: 活动高峰时，库存服务先慢了。订单服务调用库存服务，原本 100ms 返回，现在变成 2 秒。订单服务等不住，开始超时；客户端看到失败，又发起重试；订单服务自己也配置了重试；库存服务本来已经吃紧，又被更多重复请求打上来。几分钟后，问题已经不只是库存服务慢了。订单线程池被占满，网关错误率升高，数据库连接也开始紧张。这就是服务高并发里很典型的连锁反应：一个局部依赖变慢，最后把整条链路拖下水。服务保护这件事，平时最容易被忽略。系统流量不大的时候，限流、熔断、降级看起来像是“以后再补”的东西。直到某个接口被打爆，线程池耗尽，连正常用户都进不来，大家才发现保护策略不是锦上添花。我现在更愿意提前问一句：如果这个接口突然慢了，谁会被拖住？如果答案是“整个链路都会等它”，那它就不能裸奔。 现场判断 服务保护不是为了拒绝请求，而是为了不让一个慢服务拖垮一整条链路。 真正危险的不是失败，而是大家都在等、都在重试、都在占资源。 一、限流是先承认系统有边界限流听起来像是在拒绝用户，但它本质上是在保护系统。任何系统都有容量边界。连接数、线程数、数据库连接池、CPU、IO、下游接口，都不是无限的。如果入口完全不限制流量，超过容量后系统可能不是慢一点，而是整体雪崩。限流可以按接口、用户、IP、租户、业务场景做。常见算法有固定窗口、滑动窗口、令牌桶、漏桶。我更关心的是限流后的体验：返回什么错误？能不能提示稍后再试？核心用户和普通流量是否要分级？后台任务和前台请求是否要隔离？限流不是为了简单拒绝请求，而是把系统能力花在更重要的请求上。二、熔断和降级是在给系统止损熔断解决的是下游持续失败或持续变慢的问题。如果库存服务已经明显不可用，订单服务还不断调用它，只会浪费线程和时间。熔断的作用是短时间内停止调用，让请求快速失败或走备用逻辑。降级更偏业务取舍。比如推荐服务不可用时，首页先展示默认内容；评论数暂时查不到时，先隐藏计数；活动排行榜晚一点刷新；非核心统计延迟处理。这里的关键是提前定义“哪些功能可以降级”。不要等故障发生时才讨论哪个按钮能关、哪个模块能不显示。成熟系统里，降级开关应该可配置、可灰度、可回滚。三、超时控制比无限等待更重要很多系统不是死在失败上，而是死在等待上。一个接口调用下游，如果没有合理超时，线程可能长时间被占住。高并发下，这会迅速耗尽线程池和连接池。超时时间不能随便拍脑袋。它要结合接口重要性、下游 P99 延迟、用户体验和整体链路预算。比如一个页面请求总预算是 1 秒，里面有 5 个下游调用，就不能每个调用都设置 3 秒超时。否则任何一个下游慢了，整个页面都会被拖住。服务治理里有个很朴素的原则：失败要快，等待要有边界。四、重试可能会放大故障重试是为了提高成功率，但在高并发下，它也可能变成事故放大器。库存服务已经慢了，上游超时后马上重试，相当于给已经吃紧的服务再加一倍压力。如果网关、订单服务、库存客户端每一层都重试，流量会被层层放大。更稳的做法是：只对幂等操作重试。控制重试次数。使用退避策略。不在每一层都重试。对明显过载的下游先熔断。重试不是越多越可靠。很多时候，少重试、慢一点重试，甚至不重试，反而更保护系统。 故障放大 服务层事故经常不是单点失败，而是等待、重试和资源占用互相放大。 超时太长 线程和连接一直被占着，正常请求也进不来。 重试太猛 下游已经慢了，上游又叠了一层重复流量。 没有隔离 非核心链路跑满资源，把核心交易一起拖住。 五、无状态和隔离让故障有边界高并发服务想要扩容，最好保持无状态。无状态意味着请求不依赖某台固定机器，服务实例可以水平扩展，故障实例也能快速摘除。会话、缓存、任务状态这些东西，尽量放到外部存储或专门组件里。服务隔离也很重要。核心下单链路和后台报表，不应该共用同一批线程池、连接池和队列。否则一个低优先级任务跑满资源，可能把核心交易接口也拖慢。隔离不是洁癖，而是为了让故障有边界。六、可以带走的判断清单入口流量超过系统容量时，要限流。下游持续失败或变慢时，要熔断。非核心功能要提前设计降级开关。所有外部调用都要有合理超时。重试必须有次数、退避和幂等前提。核心链路和非核心链路要做资源隔离。服务治理的目标不是让每个调用都成功，而是防止一个局部问题拖垮整个系统。本系列：上一篇：分布式一致性：系统越拆，正确性越贵下一篇：热点和突发流量：真正难的是不均匀系列目录：高并发和海量数据，不只是背几个架构名词