Chatbot后台管理实战:高并发场景下的架构设计与性能优化
- 背景与痛点
去年“618”大促,公司自研客服机器人峰值 QPS 飙到 8k,单体 SpringBoot 直接被打穿:
- 会话状态存在内存 HashMap,节点一挂全部丢失,用户被迫重复登录
- 消息落库走同步 MySQL,RT 均值 480 ms,队列堆积 20w+,客服界面卡成 PPT
- 横向扩容后,Nginx 轮询导致同一会话落到不同节点,重复拉取历史消息,数据库 CPU 飙红
痛定思痛,决定把“聊天”这件事拆成“高并发读”“高并发写”“状态一致性”三个子问题,用微服务+消息中间件+缓存逐一击破。
- 技术选型
在 Chatbot 场景里,延迟敏感、消息有序、会话幂等是硬指标,对主流中间件做了 4 维度对比:
| 指标 | Redis | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|---|
| 延迟 | <1 ms | 5 ms | 2 ms | 3 ms |
| 顺序写 | 单分区内有序 | 分区有序 | 队列有序 | 队列有序 |
| 堆积能力 | 内存受限 | 磁盘百兆级 | 内存+磁盘 | 磁盘 |
| 幂等 | 需 LUA | 生产端幂等 | 需业务去重 | 内置幂等 |
结论:
- 会话热数据用 Redis,TTL 自动过期,省掉 DB 往返
- 消息流用 Kafka,分区键=sessionId,保证同一用户顺序消费,吞吐可线性扩展
- 后台任务(如 NLP 意图识别)对延迟不敏感,扔到 RabbitMQ 即可
- 核心实现
整体采用 Spring Cloud 2022.x + JDK17,代码仓库拆成 4 个微服务:
gateway(入口)、session(状态)、im(消息)、bot(机器人逻辑)。
下面给出最核心两段代码,可直接复制到 IDEA 跑通。
3.1 会话状态管理(session 服务)
@RestController @RequestMapping("/session") @RequiredArgsConstructor public class SessionController { private final StringRedisTemplate redis; // 自动注入 private static final String KEY_PREFIX = "chat:session:"; /** * 创建或刷新会话 * TTL=30min,滑动过期 */ @PostMapping("/{userId}") public SessionDTO create(@PathVariable Long userId, @RequestBody Map<String,Object> profile){ String key = KEY_PREFIX + userId; SessionDTO dto = new SessionDTO(userId, profile); redis.opsForHash().putAll(key, BeanUtil.beanToMap(dto)); redis.expire(key, Duration.ofMinutes(30)); return dto; } /** * 查询会话,miss 返回 404,前端引导重新登录 */ @GetMapping("/{userId}") public SessionDTO get(@PathVariable Long userId){ Map<Object,Object> map = redis.opsForHash() .entries(KEY_PREFIX + userId); if (map.isEmpty()) throw new ResponseStatusException(NOT_FOUND); return BeanUtil.mapToBean(map, SessionDTO.class, true); } }要点:
- 使用 Hash 结构,单 key 下可存 200+ 字段,hgetAll 一次 IO
- TTL 每次写操作都续期,防止“聊到一半被踢”
- 大促前把 maxmemory-policy 设为 allkeys-lru,预留 20% 内存给新会话
3.2 消息异步生产与消费(im 服务)
@Service public class MsgService { private final KafkaTemplate<String, String> kafka; private final String TOPIC = "chat.msg"; /** * 网关层直接调用,只负责把消息丢进 Kafka,RT <10ms */ public void send(ChatMessage msg){ // 同一用户顺序写,分区键=sessionId kafka.send(TOPIC, msg.getSessionId().toString(), JSON.toJSONString(msg)); } } @Component @KafkaListener(topics = "chat.msg", groupId = "im-consumer") public class MsgConsumer { private final BotService botService; private final SimpMessagingTemplate ws; // 推送 WebSocket /** * 单线程顺序消费,保证机器人答复顺序不乱 */ @KafkaHandler public void handle(String json, Acknowledgment ack){ ChatMessage msg = JSON.parseObject(json, ChatMessage.class); // 1. 调用机器人 String reply = botService.reply(msg); // 2. 回写 Kafka(可再走一个 topic,省略) // 3. 推送给前端 ws.convertAndSend("/topic/" + msg.getSessionId(), reply); ack.acknowledge(); // 手工提交位移,防止崩溃时丢消息 } }要点:
- 生产端开启幂等配置
props.put("enable.idempotence", true),避免重试时重复 - 消费端设置
max.pool.interval.ms=5min,防止 FullGC 导致 rebalance - 单分区吞吐 10MB/s,按 1k 消息体算 ≈10k QPS,压测时 3 个分区即可扛 5w 峰值
- 性能测试
环境:4C8G * 3 节点,Redis 6.2 集群 3 主 3 从,Kafka 3 节点。
工具: Gatling 模拟 50k 并发长连接,持续 15min。
指标对比如下:
| 版本 | 平均 RT | P99 RT | QPS | 错误率 | 会话丢失 |
|---|---|---|---|---|---|
| 单体旧系统 | 480 ms | 2.3 s | 4k | 6% | 2.1% |
| 微服务新架构 | 28 ms | 110 ms | 28k | 0.2% | 0 |
提升 7 倍吞吐、延迟降 17 倍,会话零丢失,机器 CPU 只到 55%,仍有 40% 余量。
- 生产环境建议
- 连接池:Redis 默认 Lettuce 连接数 64,高并发下被打满,调大到 256 并开启共享本地连接
- 异常处理:消费端捕获所有 Exception,写入
chat.msg.dlq主题,再配 Alertmanager 告警,防止“静默丢消息” - 监控:
- Redis 监控 hit 率,<90% 立即扩容
- Kafka 监控
records-lag-max,>5k 即增加分区或 consumer - 自定义指标:会话数、消息延迟、bot 推理耗时,接入 Grafana+Prometheus
- 灰度:按 userId 尾号灰度 5%,观察 30min 无异常再全量
- 压测脚本要模拟“慢客户端”,否则 WebSocket 背压会把内存打爆,开启
setSendBufferSizeLimit(64*1024)限制
- 延伸思考:Serverless 是否适合 Chatbot?
把 gateway+bot 做成函数计算,看似省机器,但聊天场景长连接、状态多,冷启动 2s 会直接击穿 SLA。折中方案:
- 入口 gateway 保持常驻 Pod,维持 WebSocket
- 无状态的 bot 推理服务丢给 Knative,根据 CPU 200ms 指标弹性伸缩,峰值可缩到 0,节省 40% 成本
- 会话仍放 Redis,Serverless 实例通过 VPC 访问,延迟增加 <5ms,可接受
实测同样 5w QPS,全常驻需 90 核,Serverless 混合架构仅 55 核,冷启动命中率 98%,整体成本下降 35%。
结语
整套方案已在生产稳定运行 8 个月,大促零故障。若你也想亲手搭一个可落地的实时对话系统,不妨从 0 开始体验从0打造个人豆包实时通话AI动手实验,实验把 ASR、LLM、TTS 串成完整闭环,代码全部开源,本地 Docker 一键启动。跟着做一遍,对“高并发+实时”四个字会有更立体的体感。