背景痛点:传统客服系统在高并发下的“三高”症状
去年双十一,我们老客服系统凌晨三点“三连跪”:
- CPU飙到98%,线程池打满,日志疯狂报“RejectedExecutionException”
- 数据库连接池耗尽,用户消息卡在“转菊花”,客服同学只能手动刷新
- 最惨的是,扩容后RT(Response Time)不降反升,因为线程在抢锁,CPU空转
根因总结一句话:同步阻塞 + 无界队列 + 单点瓶颈。
传统架构把“用户输入→NLP→业务查询→回复”全部塞进同一个Tomcat线程,QPS≈200就触顶。想再快,只能加机器,结果横向扩容ROI极低。
技术选型:Rasa vs Botpress 扩展性横评
我们给两款主流开源框架做了两周POC,结论直接上表:
| 维度 | Rasa 3.x | Botpress 12.x | |----|--------|mers| | 开源协议 | Apache-2.0 | AGPL(商用需买商业授权) | | 水平扩展 | 无状态Worker,可任意扩容 | 有状态Session,需Sticky粘性 | | NLP可插拔 | 支持DIET、spaCy、transformers | 仅内建FastText,扩展需改core | | 多语言 | 好 | 一般 | | 社区活跃度 | 高 | 中 |
一句话总结:要并发、要魔改、要省钱,选Rasa;要开箱即用、业务简单、不怕被GPL传染,选Botpress。
我们团队对并发和定制深度要求高,最终锁定Rasa,并把对话状态外置到Redis,实现“无状态化”。
核心实现一:Kafka异步化,让请求先“排队”
设计思路
把“用户输入”与“机器人思考”解耦,前端只负责把消息丢进Kafka,返回202 Accepted;后端用Consumer按需扩容,处理能力理论上只跟分区数有关。
Python Producer(Flask接口)
# producer.py from kafka import KafkaProducer import json, time, os producer = KafkaProducer( bootstrap_servers=os.getenv("KAFKA_BROKERS", "kafka:9092"), value_serializer=lambda m: json.dumps(m).encode(), retries=3, # 关键性能指标:重试3次,降低丢消息 max_in_flight_requests_per_connection=5 ) def send_user_query(user_id, text): """线程安全:KafkaProducer内部带发送锁,可复用单例""" future = producer.send( topic="user.query", key=user_id.encode(), # 相同user_id进同一分区,保证顺序 value={"uid": user_id, "text": text, "ts": time.time()} ) # 异步非阻塞,不等待;如要阻塞确认可future.get(timeout=1) return {"code": 202, "msg": "已接收,正在排队"}Java Consumer(Spring-Kafka)
@KafkaListener(topics = "user.query", groupId = "rasa-worker") public void handle(String msg) { try { QueryDTO dto = objectMapper.readValue(msg, QueryDTO.class); String answer = rasaClient.predict(dto.getText()); // 可能耗时300ms redisTemplate.opsForHash().put("answer", dto.getUid(), answer); } catch (Exception ex) { // 异常处理:写入DLQ(死信队列),后续人工review kafkaTemplate.send("user.query.dlq", msg); } }要点注释:
- 异常捕获后一定落DLQ,避免消息丢失
- 手动提交offset,处理完才ack,防止Consumer重启时丢数据
- 线程安全:KafkaConsumer单线程消费,与Rasa Client无共享状态
核心实现二:K8s自动扩缩容,让“弹性”不止于口号
HPA(HorizontalPodAutoscaler)配置片段:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: rasa-worker-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: rasa-worker minReplicas: 3 maxReplicas: 120 metrics: - type: Pods pods: metric: name: kafka_consumer_lag target: type: AverageValue averageValue: "30" # 单Pod平均lag>30条即扩容配合Cluster-Autoscaler,节点也能自动弹,真正“秒级”横向扩容。
压测时我们观察到:从3 Pod→60 Pod约90s,QPS由2k提到1.2w,RT稳定在350ms以内。
性能优化:同步vs异步压测对比
| 模式 | 平均RT | P99 RT | CPU利用率 | 单台QPS |
|---|---|---|---|---|
| 同步阻塞 | 1.2s | 4.3s | 85% | 200 |
| 异步+Kafka | 0.35s | 0.8s | 55% | 2000 |
结论:异步化让单机吞吐提升10倍,CPU还更闲。
连接池调优小贴士
- DB连接池:maxPoolSize = (CPU核心 * 2) + 1,再多就是排队
- Redis Lettuce:共享连接、非阻塞,IO线程数=核心数,别跟业务线程混用
- Kafka Producer:buffer.memory=64MB,batch.size=32KB,延迟与吞吐平衡点
避坑指南:生产环境血泪总结
对话状态管理的幂等性
用户重试、Kafka重发都会导致同一条消息重复。我们在Redis里用SETNX user:{uid}:reply保证“只写一次”,返回失败即丢弃。敏感词过滤的内存泄漏
早期用DFA+正则,每建一次树就new一片HashMap,Full GC频繁。后改为:- 预编译成单例
SensitiveFilter - 用
WeakReference持有缓存,每次热更新整体替换,不增量patch - 上线后堆内存下降40%,GC停顿<50ms
- 预编译成单例
线程安全警钟
Rasa SDK默认单线程,但自定义Action里如果用了全局静态缓存,要自己加锁;否则并发高时会出现ConcurrentModificationException。
代码规范小结
- 所有外部调用包Hystrix/Circuit Breaker,超时300ms,失败率>5%直接熔断
- 日志统一用
%X{traceId}透传,方便在ELK里跨Pod追踪 - 关键路径打
@Timed注解,指标自动进Prometheus,命名规则:namespace_service_method_status
生产部署最佳实践
- 蓝绿发布:K8s双Deployment+Ingress切换,保证zero-downtime
- 配置走ConfigMap+Secret,敏感信息统一上Vault,避免明文写YAML
- 压测脚本放在Git,GitLab CI每晚定时跑nightly-test,性能曲线自动对比,异常发Slack告警
互动时间
思考题:当第三方NLP服务出现超时或熔断时,你的智能客服如何优雅降级,既不让对话戛然而止,又能给用户合理预期?欢迎留言聊聊你的策略与代码实现!