Administrator
发布于 2023-08-26 / 2059 阅读
41

一次核心链路的性能优化:P99 从 800ms 到 120ms

背景:大促前的压测暴露了长尾

八月底大促压测,核心的下单查询接口平均 RT 只有 120ms,看着挺好,但 P99 飙到 800ms,监控里偶发还有 1.5 秒的尖刺。平均好看掩盖了长尾,而用户体验恰恰被那 1% 的慢请求毁掉。我接了这活,目标是把 P99 压到 150ms 以内。

全链路耗时分析:火焰图定位

先用 Arthas 抓了线上火焰图,又用 SkyWalking 拉单条慢 trace,把一次请求拆开:

阶段耗时性质
参数校验2ms本地
查用户基本信息35ms串行 RPC
查订单列表180ms串行 DB
查优惠券120ms串行 RPC
查库存90ms串行 RPC
拼装返回5ms本地

总耗时 432ms(慢请求里 DB 和 RPC 会排队到更高)。关键发现:这五个查询彼此无依赖,却全串行执行,纯属浪费。

串行改并行:CompletableFuture 编排

把无依赖的查询并行化,用 CompletableFuture 统一汇聚:

CompletableFuture<User>   f1 = supplyAsync(() -> userService.get(uid), pool);
CompletableFuture<List<Order>> f2 = supplyAsync(() -> orderService.list(uid), pool);
CompletableFuture<Coupon> f3 = supplyAsync(() -> couponService.get(uid), pool);
CompletableFuture<Stock>  f4 = supplyAsync(() -> stockService.get(sku), pool);

CompletableFuture.allOf(f1, f2, f3, f4).join();
// 取结果拼装

注意要指定自定义线程池,别用 ForkJoinPool.commonPool()——那会和业务线程抢,且默认并行度受 CPU 数限制。改完后理论耗时从「求和」变「取最大」,432ms 降到约 180ms。

缓存改造:热点先行

用户基本信息和库存是读多写少的热点,加了一层 Redis:

  • 用户信息缓存 60 秒,命中率 92%,砍掉 35ms RPC。
  • 库存用本地 Caffeine 缓存 5 秒,避免每次打 Redis,又不过期太久。

缓存要处理穿透和抖动:库存接口在缓存失败时降级回查 DB,不把缓存 miss 变成雪崩。

批量化:减少往返

订单列表原本是按用户逐页拉,再在内存里按 sku 聚合。改成一次 IN 查询 + 数据库侧 GROUP BY,SQL 往返从 12 次降到 1 次,这部分从 180ms 降到 40ms。

结果对比

指标优化前优化后
平均 RT120ms95ms
P99800ms120ms
P999 尖刺1500ms210ms

并行化贡献最大(约 60% 的 P99 下降),缓存和批量化补齐长尾。大促当天该接口零超时告警。

写在后面

现在回头看,《一次核心链路的性能优化:P99 从 800ms 到 120ms》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。

参考