自适应并发控制怎么做?
简化版
自适应并发控制不是按固定 QPS 拦请求,而是根据延迟、排队和成功率动态估计当前实例能安全承载的在途请求数。 常用做法是用 Little 定律给初值,再用 AIMD 或梯度算法逐步调节;一旦最小 RTT 抬升或排队增长,就减小上限,并为探测、抖动和最小容量留边界。
详细版
实现时先按调用方、接口或依赖建立独立 limiter,记录 inFlight、滑动窗口 RTT、历史最小 RTT、超时率和拒绝率。请求进入前比较 inFlight < limit,完成时再用本次样本更新上限,不能把被本机排队污染的等待时间忽略掉。
一个实用基线是 L ≈ λ × W:稳定吞吐为每秒 800 次、无排队延迟为 50ms,则在途量约为 40。上限可以从 40 附近启动,延迟稳定时缓慢加一;当前 RTT 明显高于基线、超时率上升时按比例下降。限额必须有上下界、冷却时间和最小采样量,防止少量慢请求令参数振荡。
它保护的是“同时占用多少资源”,与令牌桶限制“单位时间放多少请求”不同。实践中常把两者叠加:入口 QPS 限流挡突发和配额滥用,下游并发限制防线程、连接和内存被慢调用占满。拒绝后应快速失败或有限排队,不能把压力挪进一个无界队列。
完整版教学
一、固定阈值为什么会失效
服务容量会随缓存命中率、数据库计划、实例规格和网络状态变化。固定并发 100 在 RTT 为 20ms 时可能只利用一小部分容量,在 RTT 变为 500ms 时却可能把连接池全部占满。自适应控制的目标不是追求瞬时吞吐最大,而是把系统维持在吞吐平台区、排队拐点之前。
记忆钩子:先限制“正在处理的数量”,再观察“完成得是否变慢”;并发上限是反馈控制器,不是容量常量。
二、Little 定律给出可解释的起点
稳定系统满足下面的近似关系,其中 L 是平均在途量,λ 是完成吞吐,W 是平均处理时间。
L = λ × W
800 request/s × 0.050 s = 40 requests
如果实例在无排队时每秒完成 800 次、RTT 为 50ms,40 个在途请求足以支撑当前吞吐。把上限直接设成 200 不会凭空产生 5 倍吞吐,只会让多出来的 160 个请求等待线程、连接或 CPU;但该公式要求窗口相对稳定,不能拿流量陡升的一秒钟样本直接下结论。
三、如何识别排队拐点
控制器通常维护一段时间内的 minRtt 作为近似服务时间,再比较当前平滑 RTT。假设历史最小值为 40ms,当前窗口升到 64ms,额外的 24ms 多半是排队或资源争用;若吞吐没有同步提高,继续增加并发已经没有收益。只看平均延迟会掩盖长尾,因此还应观察 P90/P99、超时率和活动线程数。
queueRatio = currentRtt / minRtt
queueRatio <= 1.15 -> 小步探测容量
queueRatio >= 1.50 -> 快速收缩上限
四、AIMD 与梯度控制怎样调节
最容易落地的是加性增、乘性减:健康窗口把 limit 从 40 调到 41,拥塞时乘以 0.8 降到 32。它上升慢、退让快,能避免所有实例同时猛烈探测。梯度算法则根据 minRtt/currentRtt 计算方向,响应更细腻,但对样本清洗和参数更敏感。
| 方法 | 增长方式 | 拥塞响应 | 适用特点 |
|---|---|---|---|
| 固定上限 | 不增长 | 人工修改 | 容量稳定、实现简单 |
| AIMD | 每窗口加固定值 | 按比例减小 | 稳健、容易解释 |
| RTT 梯度 | 按延迟梯度变化 | 连续收缩 | 负载频繁变化 |
| 探测窗口 | 周期性临时放宽 | 探测失败回退 | 需要发现新增容量 |
五、请求生命周期必须计数准确
令牌要在真正占用受保护资源之前获取,并在成功、异常、取消和超时的所有路径释放。只在正常返回时释放会造成“逻辑泄漏”,上限最终降成零;在异步请求刚提交就释放,又会低估真实在途量。对于流式接口,应明确统计到首字节、完整响应还是下游资源释放为止。
Permit p = limiter.tryAcquire(routeKey);
if (p == null) throw new OverloadedException();
long start = System.nanoTime();
try {
return downstream.call();
} finally {
p.complete(System.nanoTime() - start);
}
六、限流维度、排队和冷启动
全实例共用一个上限会让慢接口拖累快接口,至少应按资源相同的接口组或下游依赖隔离。队列只能是有界且等待时间受 deadline 限制,例如上限 60、等待队列 20、最多等 30ms;队列满后立即拒绝,比占住线程等待 2 秒更安全。新实例没有 minRtt,可用保守初值和小流量预热,不能照搬老实例在不同硬件上的瞬时参数。
七、数字推演与故障响应
某接口初始 limit=50,连续三个窗口的 RTT 分别为 42、44、43ms,完成吞吐仍在增加,可逐步调到 53。随后数据库抖动使 RTT 升到 95ms、超时率达到 3%,按 0.7 收缩得到 floor(53×0.7)=37。若 37 下 RTT 回到 48ms,应先经历两个冷却窗口再增长,避免 37 与 53 之间来回摆动。
监控至少包含当前 limit、in-flight、排队数、拒绝原因、基线 RTT、当前 RTT 与调节次数。发布新算法时先影子计算“如果启用会给出什么上限”,与固定阈值并行对比,再按实例灰度;否则错误参数可能同时把整组实例限死。
八、常见误区与追问
- 误区:自适应并发等同于自适应 QPS。 一个约束在途资源,一个约束时间窗口流量,慢请求场景下差别尤其明显。
- 误区:延迟升高就一定是本实例过载。 网络抖动、下游限流也会抬高 RTT,需要结合吞吐和资源指标判断。
- 误区:拒绝前放进无界队列更友好。 无界等待会消耗内存并让请求超过 deadline,最终形成更大的超时洪峰。
- 误区:所有接口共用同一个控制器。 不同资源池和延迟分布互相污染,容易让健康接口被误伤。
- 追问:低流量时最小 RTT 不可靠吗? 设最小采样量、延长基线窗口,并限制单次调节幅度。
- 追问:实例扩容后上限怎样变化? 每实例独立学习,新实例保守冷启动,负载均衡逐步引流。
- 追问:被拒请求是否计入延迟样本? 不计入服务 RTT,但要单独计拒绝率;排队等待则应计入用户感知延迟。
九、加强记忆
把自适应并发串成“估、探、退、隔、观”:用吞吐乘无排队 RTT 估初值,小步探测未知容量,看到排队和超时就快速退让,按接口或依赖隔离控制器,同时观察 limit、in-flight、RTT 和拒绝率。面试时再补上有界队列、冷启动和全路径释放 permit,答案就从算法名落到了可运行的治理闭环。