一个看起来简单的功能差点让团队破产:实时AI助手通过WebSocket流式返回模型响应。开发环境没问题,预发布环境也没问题。一上线,模型调用量暴涨,账单飙高,用户却看到重复回复和过期状态。
导火索是一次移动客户端重连事故。客户端在重连时疯狂重试,引发 duplicate model calls 洪水。症状很明显:重连风暴期间模型调用率飙升2到5倍,后端代理反复处理同一事件,尾部延迟恶化,模型队列被挤爆。起初以为是客户端bug,但根因横跨客户端、Socket集群和编排层三层。
早期的三个假设全错了。第一,以为Socket层"最多一次"投递就够用了。第二,选了Redis PUB/SUB,因为看起来低延迟、接入简单。第三,让工作节点自己做去重,靠内存缓存压掉重复。这些选择在节点重启或网络分区时全部失效:Redis PUB/SUB在风暴期间节点重启会丢消息;内存去重缓存不跨工作节点共享,自动扩缩容时重复请求漏网;客户端激进退避重连时,服务端照单全收并立即触发重放逻辑,系统被瞬间冲垮。
架构必须换底。核心思路是让消息层成为事件持久化、顺序性和背压的单一真相源,同时给昂贵的模型调用加道闸门。四个关键动作:切到支持消费者组语义的持久有序流;实现模型执行的槽位预留模式;让WebSocket集群无状态,定向投递交给编排层;监控消费者滞后和模型队列深度,而非只看CPU内存。
真正管用的三招。第一,幂等事件加不透明ID。每个请求带event_id和causal_id,事件处理器按event_id存结果标记,重放时直接返回不执行副作用。去重存储设TTL控制体积。第二,模型调用预占槽位。消费者拉取事件后,先向有界调度器预占计算槽位:reserve_compute_slot → 确认 → 执行 → 收尾。预占失败就NACK加指数退避,避免突发流量冲垮模型集群,同时按租户限流保GPU队列健康。第三,持久流加消费者组。编排层从临时PUB/SUB迁到持久流,消费者能从最后提交位点恢复,故障时安全重放。
热门跟贴