新闻详情

新闻详情

首页 / 资讯中心 / 详情

聚合页从 1.8s 压到 320ms:CompletableFuture 用对了是提速器,线程池配错就是 OOM 倒计时

发布时间:2026/9/26 7:21:36来源:尧图网络
聚合页从 1.8s 压到 320ms:CompletableFuture 用对了是提速器,线程池配错就是 OOM 倒计时
title: 聚合页从 1.8s 压到 320msCompletableFuture 用对了是提速器线程池配错就是 OOM 倒计时date: 2026-09-25tags: [Java, CompletableFuture, 异步, 线程池, 源码]2024 年做电商大促详情页重构商品基础信息、库存、价格、优惠券、评价 5 个接口串行调用P99 稳定在 1.8s 左右。PM 要求压到 500ms 以内我们第一版用CompletableFuture把 5 个调用改成并行本地测试直接降到 280ms。结果上线当晚就出了两次 OOMdump 出来全是ForkJoinPool.commonPool-worker-*线程。排查到最后发现CompletableFuture.supplyAsync(() - ...)不加线程池默认用的就是ForkJoinPool.commonPool而这个池子的大小是 CPU 核数减一。这篇文章我把当时的完整复盘和CompletableFuture源码拆开聊清楚默认线程池的坑、异常吞掉的问题、以及allOf没有超时的代价。一、事故现场P99 降了服务却 OOM 了详情页接口的初始代码大概是这个样子public ProductDetail getDetail(Long skuId) { ProductBasic basic productClient.getBasic(skuId); // 120ms Stock stock stockClient.getStock(skuId); // 80ms Price price priceClient.getPrice(skuId); // 150ms ListCoupon coupons couponClient.getCoupons(skuId); // 200ms ListReview reviews reviewClient.getReviews(skuId); // 180ms return new ProductDetail(basic, stock, price, coupons, reviews); }5 个接口串行理想情况下 12080150200180 730ms加上网络抖动和重试P99 到 1.8s 不奇怪。第一版优化非常自然public ProductDetail getDetail(Long skuId) { CompletableFutureProductBasic basicCf CompletableFuture.supplyAsync( () - productClient.getBasic(skuId)); CompletableFutureStock stockCf CompletableFuture.supplyAsync( () - stockClient.getStock(skuId)); CompletableFuturePrice priceCf CompletableFuture.supplyAsync( () - priceClient.getPrice(skuId)); CompletableFutureListCoupon couponCf CompletableFuture.supplyAsync( () - couponClient.getCoupons(skuId)); CompletableFutureListReview reviewCf CompletableFuture.supplyAsync( () - reviewClient.getReviews(skuId)); CompletableFuture.allOf(basicCf, stockCf, priceCf, couponCf, reviewCf).join(); return new ProductDetail( basicCf.get(), stockCf.get(), priceCf.get(), couponCf.get(), reviewCf.get()); }本地 JMeter 压测QPS 200P99 从 1.8s 降到 280ms。但上线后 40 分钟容器开始报警堆内存占用从 60% 涨到 95%GC 频率从每分钟 2 次涨到每分钟 40 次。最后 OOM。dump 出来线程数 300其中 260 多个都是ForkJoinPool.commonPool-worker-*。问题定位非常快我们没有传自定义线程池CompletableFuture 默认用了 common pool。二、源码为什么默认是 ForkJoinPool.commonPool看CompletableFuture.supplyAsync(SupplierU supplier)的源码public static U CompletableFutureU supplyAsync(SupplierU supplier) { return asyncSupplyStage(asyncPool, supplier); }这里asyncPool是一个静态变量private static final Executor asyncPool useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();useCommonPool的判断逻辑是private static final boolean useCommonPool ForkJoinPool.getCommonPoolParallelism() 1;也就是说只要并行度大于 1默认就用ForkJoinPool.commonPool()。而这个 common pool 的大小是Runtime.getRuntime().availableProcessors() - 1在 8 核容器里只有 7 个线程。7 个线程要处理所有没指定线程池的supplyAsync调用一旦某个上游接口变慢或者阻塞线程很快就会被占满。后续请求进入 common pool 后没有线程可用任务队列不断堆积内存就爆了。三、最小复现common pool 被打满的过程我写了一个最小复现public class CommonPoolOOM { public static void main(String[] args) throws Exception { for (int i 0; i 1000; i) { final int idx i; CompletableFuture.runAsync(() - { System.out.println(task idx on Thread.currentThread().getName()); try { Thread.sleep(5000); // 模拟慢 IO } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } Thread.sleep(Integer.MAX_VALUE); } }在 8 核机器上运行很快就能看到前 7 个任务启动后面 993 个任务全部排队。如果这是详情页接口每个请求都触发 5 个这样的任务那 common pool 瞬间就会被塞满。正确的写法是传一个自定义线程池private final ThreadPoolExecutor detailExecutor new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(200), new ThreadFactoryBuilder().setNameFormat(detail-pool-%d).build(), new ThreadPoolExecutor.CallerRunsPolicy() ); CompletableFutureProductBasic basicCf CompletableFuture.supplyAsync( () - productClient.getBasic(skuId), detailExecutor);但这里有个细节要注意线程池的队列长度不能无限。我们一开始用了无界队列结果上游抖动时队列无限堆积最后一样 OOM。改成 200 长度并加CallerRunsPolicy后才稳定下来。四、第二个坑allOf 没有超时hang 住整条链路修复线程池后详情页 P99 稳定在 320ms 左右。但一周后又一个故障某个下游接口超时配置是 30 秒而CompletableFuture.allOf(...).join()没有超时结果详情页接口也跟着 hang 了 30 秒直接把 Tomcat 线程池耗尽。问题代码CompletableFuture.allOf(basicCf, stockCf, priceCf, couponCf, reviewCf).join();join()会一直等没有任何超时控制。正确做法是CompletableFutureVoid all CompletableFuture.allOf( basicCf, stockCf, priceCf, couponCf, reviewCf); try { all.get(800, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { // 降级用已返回的数据组装一个兜底详情页 log.warn(detail aggregate timeout, skuId{}, skuId); return fallbackDetail(skuId); }或者对每个子任务单独加超时CompletableFutureProductBasic basicCf CompletableFuture.supplyAsync( () - productClient.getBasic(skuId), detailExecutor) .orTimeout(500, TimeUnit.MILLISECONDS) .exceptionally(ex - ProductBasic.empty());orTimeout是 JDK 9 引入的如果还在用 JDK 8需要自己封装CompletableFuture加定时器或者用 Guava 的TimeLimiter。五、第三个坑exceptionally 挂错层级异常被吞掉我们一开始把exceptionally挂在allOf上CompletableFuture.allOf(...) .exceptionally(ex - { log.error(aggregate error, ex); return null; }) .join();但这样只能捕获allOf本身的异常无法捕获每个子任务的异常。子任务的异常会作为CompletionException被包装等调用.get()或.join()时才抛出来。如果你在allOf后面没有调用 get/join异常会被吞掉。正确的做法是对每个子任务单独处理异常CompletableFuturePrice priceCf CompletableFuture.supplyAsync( () - priceClient.getPrice(skuId), detailExecutor) .exceptionally(ex - { log.warn(price query failed, skuId{}, skuId, ex); return Price.empty(); });这样即使价格接口挂了详情页还能用其他字段兜底展示。六、源码CompletableFuture 的异步执行链路supplyAsync最终会调用asyncSupplyStagestatic U CompletableFutureU asyncSupplyStage(Executor e, SupplierU f) { if (f null) throw new NullPointerException(); CompletableFutureU d new CompletableFutureU(); e.execute(new AsyncSupplyU(d, f)); return d; }AsyncSupply是一个 Runnable它的run()方法会调用Supplier.get()并把结果通过complete写回 futurestatic final class AsyncSupplyT extends ForkJoinTaskT implements Runnable, AsynchronousCompletionTask { CompletableFutureT dep; SupplierT fn; AsyncSupply(CompletableFutureT dep, SupplierT fn) { this.dep dep; this.fn fn; } public void run() { CompletableFutureT d; SupplierT f; if ((d dep) ! null (f fn) ! null) { dep null; fn null; if (d.result null) { try { T t f.get(); d.completeValue(t); } catch (Throwable ex) { d.completeThrowable(ex); } } d.postComplete(); } } }关键点d.completeThrowable(ex)会把异常包装成AltResult存起来。这也就是为什么调用get()或join()时会抛出ExecutionException或CompletionException。allOf的实现是维护一个原子计数器每个子任务完成时计数器减一当所有子任务完成时才完成返回的 future。它的异常不是立即抛出的而是等你去取结果时才发现。七、我的取舍判断CompletableFuture 是个好东西但它不是银弹。我的建议是永远显式传线程池别让 common pool 背你的 IO 慢查询锅。IO 密集型任务的线程池核心线程数可以设为 2*CPU 或更高而不是 CPU 数减一。allOf/join必须有超时否则就是给链路埋雷。异常处理要下沉到每个子任务不要只挂在最外层。如果团队还在用 JDK 8建议引入orTimeout的替代方案或者用 Guava / resilience4j 做超时和降级。对于纯 CPU 计算型任务common pool 是可以用的但详情页这种强 IO 聚合场景用 common pool 等于慢性自杀。八、复盘真实数字优化前详情页 P991.8s第一版并行后 P99280ms本地上线后 OOM 影响40 分钟内 3 个容器重启修复后 P99320ms连续 7 天无 OOM线程池配置核心 8最大 16队列 200拒绝策略 CallerRunsPolicy单个任务超时500ms整体聚合超时800ms九、思考题你项目里的CompletableFuture.supplyAsync都传线程池了吗如果去掉线程池参数你的系统会不会也出现 common pool 被打满的问题
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

智慧安防数采网关与物联网网关的本质区别 2026/9/26 10:36:34

智慧安防数采网关与物联网网关的本质区别

1. 为什么“数采网关”和“物联网网关”在安防现场一混用就炸锅?干了十多年安防系统集成,从最早布同轴电缆接模拟摄像头,到后来上IP高清、做平台对接,再到如今推AI边缘分析和统一物联管理,我亲手调过37个大型园区、12个…

阅读更多 →
用 OpenClaw 配 TaoToken:小红书 AI 自动化发布配置与 Cookie 验证 2026/9/26 10:36:34

用 OpenClaw 配 TaoToken:小红书 AI 自动化发布配置与 Cookie 验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
GPT-6 究竟强在哪?TaoToken 统一 Key 实测各大 benchmarks 解读 2026/9/26 10:36:27

GPT-6 究竟强在哪?TaoToken 统一 Key 实测各大 benchmarks 解读

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
OpenClaw三种方式安装:手把手保姆级教程(含TaoToken配置) 2026/9/26 10:36:27

OpenClaw三种方式安装:手把手保姆级教程(含TaoToken配置)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
AgentScope 2.0:企业级Agent运行时与RAG服务化实践 2026/9/26 10:36:08

AgentScope 2.0:企业级Agent运行时与RAG服务化实践

1. 不是“又一个LLM框架”,而是Agent生命周期的操盘手最近在几个技术群里被反复问到:“AgentScope到底是不是下一个LangChain?”——我直接回了句:“别拿它跟LangChain比,它压根不在同一个设计维度上。”这话不是抬杠&…

阅读更多 →
Jev哑巴模型实战:TypeSafe AI类型安全调用与工程接入指南 2026/9/26 10:36:08

Jev哑巴模型实战:TypeSafe AI类型安全调用与工程接入指南

1. 从“哑巴模型”这个外号说起:Jev到底是个什么东西第一次看到“哑巴模型”这四个字,我以为是哪个团队做了个只会输出固定话术的玩具。直到身边几个做后端和工具链的朋友连续几天在群里刷“Jev”“TypeSafe AI”“system_one”,我才意识到这…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉