背景痛点:大促 0 点那一刻,客服接口先崩了
去年 618,我们给某头部电商做智能客服升级。�型上线当天,0 点 30 分并发直接冲到 42 万 QPS,老接口平均 RT 从 120 ms 飙到 2.8 s,Hystrix 熔断像雪崩一样把整条链路打挂。复盘发现几个典型坑:
- 问答模型单次推理 150 ms,但 REST 短连接三次握手+JSON 序列化就要 40 ms,网络耗时吃掉大半
- 每个商品页同时弹出「问大家」入口,同一用户 5 s 内能发 12 次重复问题,后端没有做请求合并,直接把线程池打满
- 模型容器是 CPU 版,大促前没做预热,第一个请求推理要 3 s,刚好撞在熔断阈值上
- 会话保持用 Redis setnx 做分布式锁,过期时间 10 s,结果 GC 抖动导致锁误删,用户上一句问「退货地址」、下一句被分到另一个实例,答非所问
痛定思痛,今年 618 我们把整套链路推翻重来,目标只有一个:峰值 100 万 QPS、P99 RT < 200 ms、错误率 < 0.5%。
技术选型:REST 还是 gRPC?先跑个分再说话
在「协议层」上我们做了 3 组基准,环境:同机房容器,4C8G,Netty 4.1.x,payload 1 KB。
| 指标 | REST(JSON) | gRPC(proto) | GraphQL |
|---|---|---|---|
| QPS(单连接) | 9 200 | 28 700 | 10 100 |
| 平均 RT(ms) | 10.8 | 3.1 | 9.7 |
| CPU 占用 | 38% | 25% | 41% |
| 序列化耗时(μs) | 1 200 | 180 | 950 |
gRPC 性能碾压,但网关层、前端 CDN 仍然只能走 HTTP。为了兼顾「对内高效」「对外通用」,最终采用「Spring Cloud Gateway + Dubbo3 三协议」的混合架构:
- 对外网关继续暴露 REST,方便 H5、小程序直接调用
- 网关到客服微服务走 Dubbo2-triple 协议(基于 gRPC),天然支持流式背压
- AI 模型推理服务单独拆成「dubbo-ai」模块,用 Dubbo 泛化调用,避免 Pojo 来回拷贝
这样既能复用现有注册中心(Nacos),又能享受 gRPC 的 Netty 零拷贝红利。
核心实现:代码级落地
1. Spring Boot 问答端点(JWT + 异步)
@RestController @RequestMapping("/api/bot") @RequiredArgsConstructor public class BotController { private final ChatService chatService; private final JwtValidator jwtValidator; @PostMapping("/chat") public CompletableFuture<AnswerDTO> chat(@Valid @RequestBody QueryDTO query, @RequestHeader("Authorization") String bearer) { // 1. 鉴权 String uid = jwtValidator.parse(bearer); // 2. 异步提交 return CompletableFuture.supplyAsync( () -> chatService.ask(uid, query.getQuestion(), query.getSessionId()), ForkJoinPool.commonPool()); } }QueryDTO 用@NotBlank把「问题为空」挡在入口;CompletableFuture直接把 Tomcat 线程还给容器,业务逻辑扔到ForkJoinPool,实测 RT 降低 18%。
2. Dubbo 泛化调用对接 AI 模型
消费方只依赖接口名,不引用 provider 的 API 包,升级模型版本时无需重启:
dubbo: consumer: check: false generic: true # 泛化开关 reference: interface: com.ai.service.ChatModelService protocol: tri # triple 协议 cluster: failfast调用代码:
GenericService genericService = (GenericService) SpringContext.getBean("chatModelService"); Object result = genericService.$invoke("predict", new String[]{"java.lang.String"}, new Object[]{question});Nacos 侧把模型服务做成「临时实例」,利用 Dubbo3 的「应用级注册」30 s 心跳,模型扩缩容时上游无感。
性能优化:把 42 万 QPS 压到 98 万
1. 请求合并(Batching)
同一毫秒内的相似问法聚成一批,调用一次模型、返回 Map<question, answer>:
@Aspect @Component public class BatchAspect { private final ConcurrentHashMap<String, CompletableFuture<String>> pendings = new ConcurrentHashMap<>(); @Around("@annotation(BatchPredict)") public Object batch(ProceedingJoinPoint pjp) throws Throwable { String q = (String) pjp.getArgs()[0]; CompletableFuture<String> f = new CompletableFuture<>(); (); CompletableFuture<String> prev = pendings.putIfAbsent(q, f); if (prev != null) return prev.get(800, TimeUnit.MILLISECONDS); try { List<String> qs = new ArrayList<>(pendings.keySet()); Map<String,String> ans = (Map<String,String>)pjp.proceed(new Object[]{qs}); ans.forEach((k,v)->pendings.remove(k).complete(v)); return f.get(800, TimeUnit.MILLISECONDS); }finally{ pendings.clear(); } } }阈值 5 ms 或 20 条,whichever comes first。压测显示 QPS 提升 2.7 倍,模型 GPU 利用率从 35% 涨到 68%。
2. 结果缓存(Caffeine)
热门问题(「发货时间」「退货包运费吗」)命中率高达 42%,用 Caffeine 堆内缓存:
Cache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(50_000) .expireAfterWrite(30, TimeUnit.MINUTES) .recordStats().build();开启recordStats()后通过 Micrometer 打到 Prometheus,面板看到缓存命中时 RT 从 150 ms 直降到 3 ms。
优化前后对比(wrk, 200 并发,30 s):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| QPS | 42 k | 98 k |
| 平均 RT | 268 ms | 92 ms |
| P99 | 1.2 s | 198 ms |
| CPU | 78% | 55% |
避坑指南:三个隐形炸弹
1. 分布式锁别乱用
会话保持要求「同一会话必须打到同一实例」。以前用 Redis setnx,过期 10 s,GC 抖动把锁误删,用户会话串台。改成分段锁:
- 网关层按
uid%桶数做一致性 Hash,保证同一用户落到同一 Pod,无需全局锁 - 仅当 Pod 宕机时,在 Nacos 下线事件里用 Redisson 的
RLock重新迁移会话,降低 99% 的锁冲突
2. 模型冷启动
CPU 版模型首次推理要加载词典 + 参数,3 s 直接触发熔断。解法:
- 容器启动完先跑一条「空问题」预热,把参数驻留到内存
- 配合 K8s 的
readinessProbe,只有预热成功才注册到 Nacos - 大促前 1 小时通过压测脚本批量「假请求」再热身一次,确保 0 真实冷启动
3. 敏感词过滤钩子
监管要求对话实时过滤。把 DFA 敏感词树做成Dubbo Filter,在 provider 侧统一拦截:
@Activate(group = CommonConstants.PROVIDER) public class SensitiveFilter implements Filter { public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException { String q = (String) inv.getArguments()[0]; if (DFAMatch.hit(q)) { return AsyncResult.toAsyncResult(new RpcResult("内容涉嫌违规,已隐藏")); } return invoker.invoke(inv); } }这样无论哪个版本模型上线,自动继承合规检查,无需业务方改代码。
延伸思考:如果搬到 Serverless,会怎样?
Serverless 的卖点是「按调用计费 + 极致弹性」。我们做过一张成本模型:
- 日常低谷 2 万 QPS,Serverless 单价 0.15 元/万次,一天 43 元
- 大促峰值 100 万 QPS 持续 4 小时,调用费瞬间 2.4 万元,相当于 8 台 32C128G 包月节点
性能方面:
- 冷启动 800 ms(GPU 镜像 3.8 GB),对 200 ms 目标 RT 不可接受
- 通过 Provisioned Concurrency 预置 2000 实例,冷启动降到 50 ms,但费用又翻 3 倍
结论:流量平稳且低峰明显适合 Serverless;像电商大促这种「瞬间 50 倍」的脉冲,混合云(包月+弹性)仍然是成本与性能的最优解。未来如果 GPU 冷启动能压到 100 ms 以内,再考虑把「模型推理」单独拆到函数计算,客服逻辑继续保留常驻池,真正做到「快慢分离、弹性计价」。
踩完坑回头看,高并发场景下 AI 接口的优化就是「先压测、再合并、后缓存」,把耗时从「模型」转移到「网络+序列化」上,最后用弹性扩缩把峰值吃掉。代码、协议、容量三板斧抡完,百万 QPS 也能稳稳扛住。祝你在下一个大促少掉几根头发。