背景痛点:轮询式质检的“慢”与“堵”
老系统上线前,我们做过一次 2 k 并发压测:客服坐席一多,质检后台就像早高峰的地铁——线程池瞬间打满,CPU 上下文切换飙到 20 万/秒,消息队列里 30 万条待处理语音文本排队。最惨的一次,延迟飙到 8 秒,客户电话都挂断了,质检结果还没跑出来。
根因其实不复杂:
- 传统轮询靠定时任务扫表,每条对话至少 3 次 SQL 往返,高并发下锁竞争严重。
- 线程模型是“一请求一线程”,同步 I/O 导致大量阻塞,线程切换成本 O(n²) 上升。
- 没有背压机制,MQ 生产速度 > 消费速度,内存 GC 频繁,Old GC 一次 3 秒,直接把 SLA 拖垮。
一句话:想靠“多线程 + 数据库”硬扛高并发,结果只能把机器跑成“风扇起飞”。
技术选型:Kafka 为什么赢,Flink 又赢在哪
先给结论:Kafka + Flink 是我们实测后“最稳组合”。下面把选型过程摊开聊。
消息队列对比
| 指标 | Kafka 2.13 | RabbitMQ 3.11 |
|---|---|---|
| 单机峰值吞吐 | 280 k msg/s | 45 k msg/s |
| 顺序写磁盘 | 是 | 否(索引+队列分离) |
| 分区级并行 | 天然支持 | 需 Shovel 插件 |
| 消息回溯 | 0 成本 | 需 shovel 二次转发 |
我们峰值 80 k QPS,RabbitMQ 需要 4 倍机器才能顶住,Kafka 3 台高配物理机就绰绰有余,成本直接打 3 折。
流式计算框架
Spark Streaming 的微批最低 100 ms,调小就“批”不成,调大延迟感人;Flink 原生逐条事件触发,在 exactly-once 语义下 latency 可压到 10 ms 级。再加上 Flink 的 checkpoint 对齐机制比 Spark 的 WAL 轻量,我们实测同样 99.9% 准确率场景,Flink CPU 占用低 18%,内存少 25%。
一句话:Kafka 负责“吞得动”,Flink 负责“算得快”。
核心实现:从协议到规则引擎全链路提速
1. Protobuf 协议设计
syntax = "proto3"; package qc.schema; message ChatTurn { string session_id = 1; int64 timestamp = 2; string role = 3; // client/agent string text = 4; map<string, string> meta = 5; }对比 JSON,一条 1 k 文本消息从 1.2 kB 压到 260 B,序列化耗时从 0.8 ms 降到 0.12 ms,CPU 降 30%,带宽直接省一半。
2. 规则引擎 DSL(Groovy 版)
// 敏感词检测 rule "sensitive_word" { when msg text contains ["暴力", "诈骗"] then score += 30 tags.add("sensitive") if (score >= 100) { throw new QcException("命中高危词", sessionId) } }异常统一封装QcException,交给 Flink 的ProcessFunction侧输出流(side output),保证主流程不抖动。
3. Flink 滑动窗口计算(Scala)
val env = StreamExecutionEnvironment.getExecutionEnvironment env.enableCheckpointing(5000, CheckpointingMode.EXACTLY_ONCE) chatStream .keyBy(_.sessionId) .window(SlidingEventTimeWindows.of(Time.seconds(30), Time.seconds(5))) .aggregate(new ScoreAggregate, new WindowResultFunc) .name("realtime_qc")- ScoreAggregate:增量维护
(sum, count),时间复杂度 O(1)。 - 窗口函数输出
(sessionId, avgScore, tagList),下游直接写 Kafka 供 Dashboard 消费。
性能测试:JMeter 压测 + GC 日志
压测拓扑
- 800 线程并发 → 业务网关 → Kafka → Flink → Redis → Dashboard
结果
| 指标 | 旧系统 | 新系统 |
|---|---|---|
| 峰值 QPS | 12 k | 82 k |
| 平均 RT | 2.1 s | 180 ms |
| CPU 峰值 | 92 % | 68 % |
| 99th latency | 8 s | 220 ms |
GC 表现
G1GC + 16 G 堆,压测 30 min,没有发生 Full GC,Young GC 平均 22 ms,Old GC 0 次。关键调参:
-Xms16g -Xmx16g -XX:+UseG1GC -XX:MaxGCPauseMillis=100避坑指南:那些凌晨 3 点踩过的坑
Kafka 偏移量管理
- 一定用
enable.auto.commit=false,由 Flink checkpoint 驱动提交,避免重复消费。 - 升级 broker 后,__consumer_offset 副本不足,曾导致偏移量丢失 200 万条,血泪教训:副本因子 ≥ 3,最小 ISR ≥ 2。
对话上下文存储
- Redis:延迟 < 1 ms,但内存贵,大促 200 G 数据,主从扩容 3 倍,成本爆炸。
- RocksDB + SSD:单机 2 T,顺序写 400 M/s,读缓存命中 95%,成本降 70%。最终选型 RocksDBStateBackend,TTL 24 h,兼顾速度与钱包。
安全考量:敏感数据不脱敏,审计两行泪
- 脱敏算法:正则 + 字典双保险,手机号
1{3,4,5,7,8}\d{9}→138****1234,姓名匹配百家姓字典 →*伟,准确率 99.3%,误杀率 < 0.1%。 - 审计日志:每条质检结果写
audit_logTopic,保留 30 天,字段含userId, ruleId, score, timestamp,脱敏后文本同步到 ES,方便合规查询。
结语与开放讨论
实时质检把延迟压到 200 ms 后,客服主管终于不再“夺命连环 call”。但离线批量质检依旧有它的价值:能回溯 30 天做情感趋势、热词挖掘。问题是——
当业务方既想“秒级”实时,又要“天级”全景,你会如何设计同一套规则引擎,既跑在 Flink 的 5 秒窗口,也跑在 Spark SQL 的 T+1 调度?
期待你的实践分享。