背景痛点:拼多多客服到底难在哪?
做电商客服的同学都懂,拼多多流量像“过山车”:平时风平浪静,秒杀/百亿补贴一开,QPS(每秒查询率)瞬间翻30倍。我们第一次接入时,直接把单机版NLP服务打爆——GPU利用率飙到98%,用户消息平均延迟从200ms涨到4s,差评雪片一样飞来。
更麻烦的是语音。拼多多用户遍布全国,方言模型要同时支持粤语、四川话、闽南语,而官方ASR接口只给通用普通话。冷启动一次方言模型要90s,刚好撞上流量洪峰,结果就是“用户说完5s,后台还在load模型”。
技术选型:轮询、长连接还是消息队列?
我们把常见三种接入方式放在4核8G的容器里跑了一遍,压测数据如下(8KB文本对话,RTT≈25ms):
| 方案 | 峰值QPS | P99延迟 | 额外痛点 |
|---|---|---|---|
| 纯HTTP轮询 | 1.2k | 380ms | 容易被拼多多的“API限频”拍死 |
| WebSocket长连接 | 8k | 95ms | 需要自己做心跳、断线重连 |
| 消息队列(RocketMQ) | 12k | 65ms | 链路长,排查问题费劲 |
结论:WebSocket适合“用户→AI”实时聊;队列适合“AI→拼多多”异步回调;两者混用才能扛住大促。于是最终架构:
- 用户侧:WS网关集群,支持TLS 1.3、自定义帧压缩
- 平台侧:队列做削峰填谷,再回写HTTP接口
核心实现
1. Spring Cloud Gateway + 令牌桶限流
拼多多对“查询订单”接口给出X-RateLimit-Order=200次/60s的硬限制,我们得先做一层自我保护。下面代码基于Redis令牌桶,桶容量=200,填充速率=3/s,瞬时突发允许200,后续平滑。
/** * 拼多多令牌桶限流过滤器 * @author yourname */ @Component public class PddRateLimitFilter extends AbstractGatewayFilterFactory<PddRateLimitFilter.Config> { private final RedisScript<Long> script; public PddRateLimitFilter() { super(Config.class); DefaultRedisScript<Long> s = new DefaultRedisScript<>(); s.setScriptText( "local key=KEYS[1] local capacity=tonumber(ARGV[1]) local refill=tonumber(ARGV[2]) " + "local now=tonumber(ARGV[3]) local interval=1000/refill " + "local bucket=redis.call('hmget',key,'tokens','last') " + "local tokens,last=tonumber(bucket[1]),tonumber(bucket[2]) " + "if tokens==nil then tokens=capacity last=now end " + "tokens=math.min(capacity,tokens+(now-last)/interval) " + "if tokens<1 then return -1 end " + "redis.call('hmset',key,'tokens',tokens-1,'last',now) " + "redis.call('expire',key,60) return tokens"); script = s; } @Override public GatewayFilter apply(Config c) { return (exchange, chain) -> RedisReactiveUtils.eval(script, List.of("pdd:bucket:" + c.key), List.of(String.valueOf(c.capacity), String.valueOf(c.refill), String.valueOf(System.currentTimeMillis()))) .filter(remain -> remain >= 0) .switchIfEmpty(Mono.error(new RateLimitException("Exceed PDD rate limit"))) .then(chain.filter(exchange)); } public static class Config { private String key; // 业务维度,如店铺ID private int capacity = 200; private int refill = 3; // getter/setter省略 } }2. Nacos动态配置热更新
拼多多的API_SECRET每季度会轮换一次,如果把密钥写死,凌晨两点起夜改配置的滋味谁试谁知道。用Nacos +@RefreshScope一行代码搞定:
# bootstrap-prod.yml pdd: api: client-id: ENC(xxx) secret: ENC(yyy) # 秒杀期临时上调 timeout: 1500ms@Configuration @RefreshScope public class PddApiProperties { @Setter private Duration timeout = Duration.ofSeconds(2); // getter省略 }改完Nacos,30s内全部节点自动生效,无需重启。
3. 对话状态机(PlantUML)
AI客服内部用有限状态机(FSM)保证“同一用户并发消息”不乱。下图是简化版,真实线上比它多一倍状态。
性能测试:JMeter压测 & K8s弹性扩容
- 场景:模拟1w并发用户,每秒发1条语音转文字,持续5min
- 指标:99线延迟 420ms → 95ms(开启队列削峰后)
- 资源:Gateway Pod CPU 85% → HPA自动扩容到3副本,CPU降到47%
HPA模板(已生产验证):
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: ai-gateway-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: ai-gateway minReplicas: 2 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 behavior: scaleUp: stabilizationWindowSeconds: 15 policies: - type: Percent value: 100 periodSeconds: 15避坑指南
1. 拼多多签名时钟漂移
官方要求timestamp与服务器时间误差≤30s,但容器宿主机没做NTP同步,差出90s,直接403。解决:
- 业务容器启动脚本里加
ntpdate -s time1.aliyun.com - 签名前统一取
System.currentTimeMillis() / 1000,拒绝本地时间
2. 多租户Redis缓存污染
早期把“店铺A的FAQ”与“店铺B的FAQ”丢进同一个hashKey,结果A改配置把B的热数据顶掉。后来加二级前缀:
String cacheKey = "pdd:faq:" + tenantId + ":" + md5(question);同时把Redis内存淘汰策略从allkeys-lru改成volatile-lfu,降低热数据被误杀概率。
3. 方言模型冷启动
90s的加载时间扛不住秒杀洪峰,采用“预加载 + 本地缓存”双保险:
- 提前把粤语、四川话模型打到推理Pod的
emptyDir - 启动脚本里用
warmup.sh跑一遍空白音频,TensorRT生成engine文件 - 线上真正流量来时,平均首包延迟从3.8s降到280ms
代码规范小结
- 所有Java代码静态扫描通过
p3c-pmd,高危项清零 - 方法级JavaDoc必须写“@param、@return、@throws”,不写TODO占位
- 日志用占位符,禁止字符串拼接:
log.info("orderId={}, resp={}", orderId, resp);
互动环节:你的降级方案?
拼多多大促期间,平台会临时下调“查询订单”接口频率到50次/60s。假设你的令牌桶刚刚被压成负值,但用户还在疯狂问“我的快递到哪了”,你会:
- 直接返回“客服繁忙,稍后再试”?
- 走静态缓存,30s内不刷新物流?
- 把请求异步进队,由后台定时兜底?
欢迎在评论区聊聊你的降级策略,一起把AI客服做得既稳又快!