高可用服务并发增加后先守住哪些边界
“并发上来后先守住哪条线”首先要落到可观察、可回滚的工程动作上。本文从配置、调用链和运行指标三个层面梳理判断方法,重点说明应先收集什么证据、怎样做小范围验证,以及何时应停止扩张改动。
文中数值仅用于说明机制,不能直接照搬。
队列陡增:Agent 长链条调用带来的连锁雪崩
在处理复杂 Agent 工作流时,常见的架构演进路径是将长任务拆分为多个子 Task,放入 Kafka 或 RabbitMQ 中进行异步并行处理。
在一次线上大促推广中,智能客服 Agent 迎来了前所未有的流量峰值。主 Agent 接收到用户复杂的售后退换货请求后,将任务拆解为“订单状态校验”、“物流轨迹抓取”、“智能风控审核”与“退款金额预测”4 个并行子任务。
随着上游请求数突破 50,000 QPS,后台任务队列的积压曲线开始直线陡峭上升:
排查现场发现,上游 Gateway 没有针对子任务的放大系数上限与全局队列背压控制。由于 Agent 的某些推理子任务耗时较长(单个 LLM 推理需 1.2 秒),Worker 消费速度远跟不上生产者投递速度。Worker 节点的 JVM 内存迅速被积压的 Task 对象挤爆,GC 停顿时间飙升至 10 秒以上,进而触发了上游心跳超时与 Worker 节点的批量下线,最终演变成全盘雪崩。
动态令牌桶:给高耗时推理算力加装刹车片
守住高并发防线的第一步,是在 Agent 入口网关层与算力调度层接入基于耗时权重的动态令牌桶(Dynamic Token Bucket)。
传统令牌桶限制的是每秒 Request 数量(QPS),但这在 Agent 场景下行不通——一个消耗 200 个 Token 的简单 Agent 查询和一个消耗 8,000 个 Token 并且触发 5 次 Tool Calling 的复杂 Agent 查询,对后端的算力消耗相差数十倍。
必须将令牌桶的“令牌”定义从“请求数”重构为“预计 Token 消耗量 / 预计算力 CPU-Time”:
// 生产级动态算力令牌桶控制实现 type AgentCapacityLimiter struct { tokenBucket chan struct{} maxTokenTokens int64 // 允许的总体 Token 算力水位 currentTokens int64 mu sync.Mutex } func (l *AgentCapacityLimiter) AllowTask(estimatedCost int64) bool { l.mu.Lock() defer l.mu.Unlock() // 结合动态内存水位与 LLM KV Cache 利用率做背压反馈 if atomic.LoadInt64(&l.currentTokens)+estimatedCost > l.maxTokenTokens { return false // 算力超载,直接触发背压拒绝 } atomic.AddInt64(&l.currentTokens, estimatedCost) return true } func (l *AgentCapacityLimiter) ReleaseTask(actualCost int64) { l.mu.Lock() defer l.mu.Unlock() atomic.AddInt64(&l.currentTokens, -actualCost) }网关根据 Prompt 长度与任务拆解深度动态计算estimatedCost。当系统总体算力水位达到 待项目确认的阈值 警戒线时,动态令牌桶停止向复杂 Agent 任务发放许可,将其自动降级为“单步简易回复”或“静态 FAQ 匹配”,从而在入口处强行切断流量突增的源头。
内存水位告警后的降级兜底路径
高可用架构的第二道防线,是在任务队列与 Worker 节点之间建立基于 Reactive 响应式的背压反馈机制(Backpressure)。
当 Worker 节点的 CPU 利用率突破 90%,或者 JVM 堆内存水位突破 80% 时,Worker 不能再被动接收 MQ 压过来的数据,而是必须主动向上游 Gateway 发送 Backpressure 信号。
下表详细定义了在亿级流量下,针对 Agent 不同阶段的降级防线守则:
| 流量/内存水位阶段 | 触发条件 | 守牢的底线原则 | 自动化降级动作 |
|---|---|---|---|
| 绿线 (正常运行) | CPU < 60%, 内存 < 65% | 保障完整多 Agent 链条 | 开启最大拆解深度(允许最多 5 级 Tool Calling) |
| 黄线 (预警阶段) | CPU > 75%, MQ 积压 > 10 万 | 保证核心 Agent 任务响应 | 限制子任务拆解深度至 2 级,禁用非核心检索 |
| 红线 (背压熔断) | 内存 > 85%, Worker 出现 GC 告警 | 严防系统 OOM 崩溃 | 激活 Reactive 背压,入口网关丢弃 20% 复杂 Agent 请求 |
| 黑线 (灾难兜底) | 算力节点挂掉 30% 以上 | 保证基础可用性 | 切换为本地规则引擎 / 静态缓存回复,断开大模型依赖 |
在代码落地层面,利用 Go channel 的非阻塞写入或 Java Reactor 框架的onBackpressureDrop()属性,可以极其简洁地写出背压防护逻辑:
// Spring WebFlux / Reactor 体系下的背压控制链 public Flux<AgentStepResult> executeAgentWorkflow(Flux<AgentTask> inputTasks) { return inputTasks .onBackpressureDrop(droppedTask -> { // 当消费端处理不及,直接丢弃新入队任务,并触发兜底降级告警 Metrics.counter("agent.backpressure.dropped").increment(); notifyFallbackChannel(droppedTask); }) .publishOn(Schedulers.boundedElastic(), 128) // 严格限定线程并发上限 128 .flatMap(task -> processSubTaskAsync(task), 16); // 限制单节点最大并行子任务数为 16 }容量估算与背压落地的三条铁律
面对 Agent 增强后的亿级流量系统,守住系统不失效的防线核心在于把控流量放大的源头与建立算力反馈机制:
- 算力估算必须包含“任务拆解放大系数”:估算系统 QPS 容量时,不能拿简单的 1 写入 1 读取来算,必须乘上
平均子 Task 数 × 平均 LLM 推理耗时的放大因子; - 拒绝无界队列(Unbounded Queue):无论是内存中的 Channel、线程池 BlockingQueue,还是中间件 MQ,必须配置严格的
Capacity上限,一旦溢出立即触发入口背压拒绝; - 建立多级梯次降级方案:大模型算力资源极其昂贵且容易成为瓶颈,必须具备从“多 Agent 深度推理”到“单 Agent 极简回答”再到“规则 FAQ 兜底”的秒级降级切换能力。
当并发如潮水般涌来时,先守住了算力水位与背压防线,系统才能在激增的流量冲击下稳如磐石。