有状态AI服务的会话感知负载均衡
Google官方解决AI Agent长连接的负载均衡工程问题。通过会话追踪与混合路由算法,防止后端单点瓶颈,直接可用于生产优化。
Google官方解决AI Agent长连接的负载均衡工程问题。通过会话追踪与混合路由算法,防止后端单点瓶颈,直接可用于生产优化。
构建实时 AI agent 与我们习惯的标准 web API 工作方式截然不同。对于典型的 API,有一个可预测的生命周期:客户端发送请求,服务器处理它,返回响应,然后继续。这种短暂的模型很好用,因为它易于跟踪——你可以通过延迟、QPS 和 CPU 使用率等熟悉的指标来衡量性能。
实时 AI 系统改变了这种模式。后端不仅需要处理隔离的请求,还必须管理一个连续的、实时的双向数据流。你要处理音频块、转录文本、模型输出和合成语音不断地同时双向流动。
当用户中断时,事情变得更加复杂。服务器必须立即停止当前的语音生成,转向更新上下文,可能触发新工具,并开始起草不同的响应;所有这些都必须在不断开连接的情况下完成。
这迫使我们重新思考基础设施。在实时 AI 的世界里,我们不再仅仅优化单个请求;我们管理的是活跃对话的复杂性。
传统的负载均衡策略通常优先考虑请求吞吐量和当前 CPU 利用率。这些方法假设每个传入请求消耗可预测数量的资源,且完成任务会释放容量。但对于长期存在、有状态的 AI 流,这些假设就不适用了。
考虑两个后端任务:任务 A 处理 100 个短请求,每个在 50 毫秒内完成。任务 B 仅接受 5 个请求,但每个转化成一个 20 分钟的会话。如果你只按请求到达率判断,任务 B 看起来请求率更低,但实际上它可能承载着显著更重、承诺更多的工作量。
QPS 追踪的是到达量,但无法反映服务器已在管理的实时对话数量。
同样,CPU 利用率可能具有欺骗性。例如,语音运行时可能托管 20 个静默会话;因为没有活跃的语音处理或模型推理发生,服务器看起来利用率不足。但一旦这 20 个用户同时开始说话,CPU 使用率会突然激增。
虽然 CPU 指标反映了即时的处理负载,但活跃会话数揭示了后端已承诺处理的工作。对于实时 AI,你需要平衡这两个信号。
实时 agent 通常依赖双向流协议,如 gRPC 或 WebSocket。虽然具体实现不同,但它们都面临同样的基础设施障碍:维护一个开放的、长期运行的连接,其中数据不断在两个方向上流动。
虽然网络观察者看到的是一个简单的连接,但应用程序将其视为一个复杂的、有状态的会话。在单个实时 agent 会话内,你可能有音频缓冲、部分转录、活跃工具调用、模型上下文和用户特定的指标,所有这些都存在于运行时内存中。
标准负载均衡器难以应对这种情况。它们看到流,但无法区分真正活跃的用户对话、空闲监听器或重试和健康检查等后台噪声。由于基础设施缺乏对这些内部状态的可见性,你不能仅依赖通用连接指标。相反,你需要应用级报告。后端服务本身是唯一具有足够上下文来准确跟踪会话何时真正活跃与何时已失败、完成或被取消的组件。
一个简单的模式是在流式会话生命周期开始和结束的地方追踪活跃会话。
suspend fun handleAudioSession(audioStream: Flow<AudioFrame>) {
activeSessions.incrementAndGet()
try {
withTimeout(20.minutes) {
audioStream.collect { frame ->
processAndRespond(frame)
}
}
} finally {
activeSessions.decrementAndGet()
}
}
finally 块的作用超越了基本的清理。它是保持活跃会话计数准确到足以支持路由决策的关键。
如果计数器未能递减,你的后端在会话完成后很久可能看起来过载。反之,双重递减可能报告虚假容量,吸引过多流量。生产实现还需要仔细管理超时、取消或断开连接事件同时触发而清理同一会话的边界情况。
本质上,幽灵会话充当误导的路由信号,而不是简单的内存泄漏。
即使有精确的计数器,你也必须考虑同步。由于负载均衡器通常按间隔拉取指标,而会话流动式开始和停止,你的服务需要报告活跃会话的一致快照,以防止负载均衡器基于过时数据做出决策。
一旦后端可以报告活跃会话,负载均衡模型就变得更加代表实际工作量。一个朴素的容量模型可能只是使用静态槽位:
remaining_capacity = max_sessions - active_sessions
如果你可以容纳 100 个会话且正在持有 80 个,你还有 20 个槽位。但这很脆弱。它假设每个会话消耗相同数量的 CPU,这在生成式 AI 中很少成立。
这就是为什么活跃会话计数不应替换基于利用率的均衡;它们必须合并成混合模型。利用率(CPU/内存)捕捉当前资源压力,而会话计数捕捉已承诺的未来负载。
合并这些信号的一种方法是使用反馈循环估计每个后端的有效容量。但首先,它们必须将两个信号规范化为共同的测量单位。因为负载均衡器固有地以速率思考,它们将静态会话计数转换为连续流。例如,如果一个后端在 10 秒的报告窗口内持有 90 个活跃会话,一个实现可能将其视为 9 "假设 QPS"。通过将静态会话转换为标准速率,路由层可以无缝地将会话压力添加到 QPS 等传统信号中。
估计有效容量的一个简化方法是问:这个后端在达到目标利用率之前还能接受多少额外工作?一个简化的容量估计公式可能是这样的:
让我们分解一下:
目标利用率:你想安全运行的上限(例如 80% CPU)。
平均利用率:上一个报告窗口内的平滑 CPU 消耗(例如,过去 10 秒)。
单位会话成本:在上一个报告窗口内活跃流的动态计算平均 CPU 成本。
安全缩放器:衰减乘数(例如 2.0)。因为 CPU 利用率通常随并发流数非线性扩展,这充当惩罚因子以防止负载均衡器一次性向看起来空闲的后端倾倒太多新会话。
一旦负载均衡器计算出 Additional_Session_Rate,它将其乘以报告间隔,并添加到当前活跃会话中以找到真正的有效容量。
这种混合方法帮助解决实时 AI 工作负载的核心路由问题。例如,一个有 10 个活跃会话和 90% CPU 的后端将有非常高的单位会话成本,将其 Additional_Session_Rate 驱动至零,导致它收不到新流量。同时,一个有 80 个活跃会话但仅 40% CPU 的后端可能看起来有空间,但安全缩放器确保它只逐步获得新会话,防止突然激增。
确切的算法取决于你的代理和工作负载,但原理是一致的:实时 AI 负载均衡器必须理解当前状态的权重和已承诺会话的体积。
使用标准的一次性负载测试验证这些系统往往会错过真实的故障模式。主要发送短请求突发的测试主要测量请求吞吐量,这不会复制长期存在的 AI 会话的行为。
有效的基准应该变化:
指标还必须捕捉流式行为。除了平均延迟和 QPS,追踪诸如后端间的活跃会话分布、过载分配率、p95 和 p99 启动延迟、首次流时间、丢弃会话和强制断开后的计数器行为等指标。
目标是比较随时间的路由行为。
基于请求的均衡通常会创建不均匀的流量,其中一些后端积累长期会话而其他后端闲置。会话感知均衡更均匀地分布活跃对话,防止任何单个后端被延迟工作压倒。
即使简单的会话追踪器也位于关键路径上。每个流的开始和结束都会触及它,所以如果你管理大规模并发,重要的是确保这种追踪不会成为服务的瓶颈。
对于 JVM 服务,这通常意味着使用适当的微基准测试框架来考虑 JIT 优化、JVM 预热和死代码消除,所有这些都可以轻易扭曲天真的结果。
通常最好专注于竞争测试而不仅仅是单线程延迟。当多个线程或协程同时hammering同一个计数器会发生什么?以 Java 为例,AtomicInteger 可能对许多工作负载完全可以,但在高并发时,它会遭受严重的缓存行竞争(bouncing),因为多个线程不断尝试更新同一内存地址。在高吞吐量场景中,像分片计数器或 LongAdder 风格的聚合这样的设计可能变得更合适以保持吞吐量。
会话追踪的成功或失败基于可靠性,不仅仅是实现复杂性。用于负载均衡的每个信号都必须准确、低开销,并针对现实的流量模式进行验证。
实时 AI 将负载均衡的负担从网络问题转变为应用级问题。
QPS 告诉你到达,CPU 揭示当前压力,活跃会话计数识别你的真正的、已承诺的并发。
对于像语音、视频或世界模型这样的交互式 AI 工作负载,最有效的路由策略综合这些信号。当后端通信其真实的会话状态时,负载均衡器可以做出更聪明的决策,防止任何单个实例成为瓶颈。
随着 AI agent 进入生产,基础设施需要跟上。扩展这些系统需要将我们的焦点从平衡离散请求转变为平衡连续的、实时的对话。