架构设计
收录于 多租户采集演进
ACK 解决不了资源隔离:多租户消费者真正要补的四层治理
细粒度 ACK 能解决消费进度问题,但线程、连接、内存和重试预算仍然会在进程内部继续互相争抢。
多租户消费者往前走以后,ACK 很容易被讲大。
场景通常是这样的:一个消费者进程里同时扛着多批租户,某个租户的下游开始抖,重试越来越多,处理时间一路拉长。你会发现,快租户的 ACK 明明还在正常前进,但整个进程还是越来越难看,线程池被吃满,连接数抬头,内存队列堆起来,值班的人依然会觉得“慢租户还是把别人一起拖住了”。
这时候很多人第一反应会是:既然 ACK 已经按消息拆开了,为什么问题还在。
原因很简单。
ACK 解决的是 Broker 侧的消费进度,不是进程内部的资源隔离。
ACK 真正解决了什么
先把这件事说准,后面才不会把它越讲越大。
细粒度 ACK 有价值的地方,是把“谁该前进、谁该重试”这层进度管理拆开了。
- 成功消息可以立即 ACK。
- 失败消息可以单独 NACK、延迟重试或者进入死信。
- 一条坏消息不再天然卡住整段消费进度。
这件事非常重要,尤其在多租户链路里。因为一旦没有这层能力,某个租户的一条坏消息就可能把整批消费都挂住,后面的租户也跟着一起等。
所以我并不觉得 ACK 不重要。恰恰相反,它是多租户消费往前走时必须补上的能力。只是它解决的是“进度怎么走”,不是“资源怎么分”。
资源争抢发生在 Broker 之外
慢租户为什么明明没有卡住 ACK,还是会拖累别人?因为争抢大多发生在消费进程内部,不在 Broker 那层。
同一个消费者里,租户之间通常还在共用这些东西:
- 线程池。
- 下游连接池。
- 进程内队列和缓存。
- 重试预算。
ACK 可以把进度轨迹拆开,却不会自动把这些资源也拆开。
某个租户虽然只让自己的消息不断重试,但它在重试过程中消耗的线程、连接、内存和时间片,还是会继续跟别的租户共用。
所以多租户系统里经常会出现一种很别扭的状态:
Broker 侧看,ACK 没有被某个租户卡死;
进程侧看,整体处理还是越来越慢。
这个落差排查起来很磨人。
把这个层级关系画出来,就不容易再把 ACK 的边界搞混:
flowchart TB
subgraph broker["Broker 侧 — ACK 已覆盖"]
A["消费进度管理"] --> B["ACK / NACK / 死信"]
end
subgraph process["消费进程内部 — ACK 未覆盖"]
C["并发隔离:线程池"]
D["连接隔离:下游连接池"]
E["内存隔离:队列与缓存"]
F["重试隔离:重试预算"]
end
B -->|"进度拆开 ≠ 资源拆开"| C
C --> D --> E --> F
第一层要补的:并发隔离
进程内部最先该补的,通常是并发隔离。
如果所有租户共享同一个大线程池,慢租户重试一多,很容易把整个进程的执行槽位都占掉。快租户虽然消息处理很简单,也得排队等。
并发隔离要做的,不在于把线程池名字起得更复杂,关键是明确控制“某个租户最多能同时占多少执行槽位”。
常见做法有两种:
- 按租户直接限制并发上限。
- 先按租户等级分层,再按等级给不同并发预算。
这一步一旦补上,很多“ACK 明明没卡住,系统还是越来越慢”的问题会立刻好看很多。因为慢租户终于不再能无限度把整个进程的执行能力一起占满。
第二层:连接隔离
很多拖垮并不是从线程开始的,而是从连接开始的。
数据库连接池、HTTP 连接池、RPC 连接池——看起来都在下游。可对多租户消费者来说,它们就是共用资源。某个租户持续重试、持续超时,连接很快就会被它长时间占住。ACK 轨迹是分开的,连接不是。
所以连接隔离要做的,是把“谁能占多少连接”也显式拉出来,而不是默认全局共享。
如果不想一上来就做到每租户一套资源,至少也要按租户等级拆池,或者给高风险租户单独的连接预算。不然很多问题最后看起来像“下游波动”,说到底是消费者内部没有把连接竞争隔离开。
第三层:内存隔离
再往后,很多系统会卡在内存和队列。
某个租户一旦处理慢、重试多、backlog 又在涨,最容易发生的就是进程内队列不断堆积。只要这些队列没有租户级边界,内存压力就会跟着扩散,最后连本来没问题的租户也会一起受影响。
内存这一层不能只靠“机器大一点”解决,而要把队列和缓存的边界做出来。
至少要回答这些问题:
- 单租户队列最多允许堆多少。
- 触顶之后是背压、丢弃、降级还是延迟重试。
- 哪些租户允许短时占更多缓冲,哪些不允许。
如果这些问题一直不回答,很多系统最后会把实际的租户问题,拖成一场整进程的内存问题。
第四层:重试隔离
最后一层最容易被低估,就是重试预算。
很多人觉得 ACK 已经让失败消息可以单独重投,所以问题已经被解决了一大半。可一旦没有重试隔离,失败消息虽然不会卡住别人,却会不断把资源继续吞下去。
需要补的是另一句更具体的话:
某个租户到底允许重试多少次,重试窗口有多长,退避策略是什么,什么时候该直接进死信,什么时候该熔断。
如果没有这层预算,系统就会出现一种很糟的状态:
ACK 没卡死,消费看起来还在前进,但某个租户其实已经在后台持续烧线程、烧连接、烧内存。
哪些场景里 ACK 已经够了,哪些场景里远远不够
也别把这件事说得过头。不是所有场景都需要把四层治理一次补满。
如果租户数量还不多,下游耗时差异也不大,失败模式相对均匀,单进程里实际共享的竞争并不严重,那 ACK 带来的进度拆分已经能解决大部分问题。这个阶段不一定非要急着做很重的隔离体系。
但只要出现下面这些信号,就别再把希望只压在 ACK 上:
- 某些租户持续高流量。
- 不同租户下游差异很大。
- 重试轨迹开始拉长。
- 某个租户抖一下,整体线程、连接或内存都会跟着难看。
到了这个阶段,再继续把“ACK 已经够了”当成默认判断,问题只会被藏进进程内部。
这篇最后该留下什么
回头看,这篇要留下来的就一句话:
ACK 是进度治理能力,不是隔离能力。
它很重要,但它解决的是 Broker 侧那层“谁能继续前进”。决定慢租户会不会把快租户一起拖难受的,还是进程内部那几层更土、更现实的东西:并发、连接、内存和重试预算。
如果这几层不补,ACK 会把问题拆得更细,但不会自动把问题拆没。