技术2026 · 07 / 096 分钟♡ 96评论 18

为什么我把「等一等」写进了架构

限流、退避、背压——三个都在做「等一等」,但等的理由完全不同。搞混它们会让系统在压力下表现得像坏了一样。

想不通就摊开纸画,画到第三版才对

我以前觉得,「等待」是系统里的坏味道。能不等就不等,能并发就并发。

后来在一条批量链路上栽了跟头:并发从 4 提到 16,吞吐没涨,失败率翻了三倍。我盯着监控看了半天,才承认一件事——我不是在提速,我是在制造拥塞。

从那之后我认真地把「等」这件事拆开了看。它至少有三种,理由完全不同。

一、限流:为了不被对面拒绝

对面的服务有配额,你超了它就拒绝你。这时候「等」是为了把请求摊平到时间轴上

关键在于:限流要限在最贴近出口的那一层。我一开始限在任务调度层——每次只放 8 个任务进来。但一个任务内部可能发出 3 次请求,实际并发是 24,照样被拒。

正确做法是在真正发请求的那个函数上加令牌桶,不管上面放进来多少任务,出口的速率是恒定的。

二、退避:为了不在对方虚弱时踩上一脚

对面已经返回 429 或者 5xx,说明它现在扛不住。这时候立刻重试,是在给一个已经喘不过气的服务继续加压。

指数退避的意义不是「等久一点成功率高一点」,而是在系统性故障期间,让整体请求量自动衰减

一个容易忽略的细节:退避一定要加随机抖动。如果 16 个并发任务同时收到 429,同时退避 1 秒,那 1 秒后它们又会同时发起——你只是把一次拥塞推迟了 1 秒,还让它变得更整齐、更猛。加上抖动之后,这 16 个请求会散开,拥塞就化掉了。

delay = base * (2 ** attempt) * (0.5 + random.random())

就这一行的差别,实测把批量任务的整体失败率压掉了一半以上。

三、背压:为了不让上游生产得比下游快

这是我最晚才真正理解的一个。

前两种「等」都是对外的——因为对面不行。背压是对内的:当下游处理不过来时,上游必须慢下来。

如果不做背压,会发生什么?队列无限增长。表面上一切正常,所有任务都「已提交」,但队列里排着上千条,最后一条要等几个小时。更糟的是内存被队列吃满,或者中间产物把磁盘写爆。

背压的实现其实很朴素:用有界队列,满了就阻塞生产者。难的不是实现,是接受它——你得允许「上游停下来等一等」,而不是想办法让上游一直有事做。

我第一次给队列设上界的时候,心里是抗拒的:这不就是故意让生产者闲着吗?后来才明白,生产者闲着不是浪费,队列堆积才是浪费——那些排在队尾几小时才被处理的任务,产出的结果很可能已经过期了。

三者搞混的典型症状

如果你的系统在压力下有这些表现,很可能是这三层混了:

写进架构,而不是写进补丁

这三层现在是我每条链路的标准配置,不是出问题之后才加的补丁:

位置参数
限流紧贴出口的调用函数令牌桶,速率按对面配额的 80% 设
退避API 层的重试循环指数 + 抖动,与业务层重试配额隔离
背压任务队列有界,容量 ≈ 并发数 × 3

背压那个容量我试过很多值。太小会让生产者频繁阻塞、上下文切换开销上来;太大就失去了背压的意义。并发数的 2–4 倍是个比较舒服的区间。


现在回头看,「能不等就不等」这个直觉本身没错,错在我把它当成了铁律。

系统在顺利的时候,等待确实是浪费。但系统在承压的时候,主动等待是唯一能让它不崩的东西。而一个系统一生中承压的时刻,远比我们设计时想象的多。

← 更早一篇
把提示词当代码管:版本、评测集与回归

关注公众号「十年追随」

新文章第一时间推给你。留言我都会回,公众号里也能收到回复通知。 不发广告,一周最多两条。

十年追随 公众号二维码
评论Comments18 条 · 匿名也能评,昵称头像会记住
评论服务(Waline)待部署 —— 香港 VPS 起来后填 SERVICES.waline 即生效。
想说话可以先去 留言板,那边我每条都回。
关注公众号「十年追随」更新第一时间推给你 · 留言也能收到我的回复
看二维码