【背景】 做了 15 年后端,线程池 sizing 是我见过「文档抄得最勤、推导做得最少」的一件事。上周排查一个异步导出接口 P99 劣化,翻出来的配置五花八门:
- A 服务:coreSize = CPU 核数 × 2,注释写着「经典公式」;
- B 服务:coreSize = 200,注释抄自某博客「I/O 密集型用 2N 或更多」;
- C 服务:直接照搬 HikariCP 的 CPU × 10 思路给了 Tomcat 工作线程;
- D 服务:coreSize 和 maxPoolSize 都写死 8,队列无界。 同一个集群、同一批机器(8C16G),同一个下游 RPC(P99 约 30ms),四种写法,没有一个是从自己的负载倒推出来的。
【常见分歧 / 几种做法】
- 核数派:CPU 密集型用 N+1,I/O 密集型用 2N 甚至 10N。问题是「I/O 密集」是定性描述,乘 2 还是乘 10 全凭手感,没人拿数据说话。
- 抄 HikariCP 派:官方给连接池的 CPU×10 建议被大量平移到线程池上,但连接池争的是 DB 这个外部资源,线程池争的是「谁在等谁」,两者约束对象根本不同。
- Little's Law 派:L = λ × W,线程数 ≈ QPS × 单请求耗时。听着优雅,但 W 取 P50 还是 P99?下游抖一下 W 翻 5 倍,公式立刻失效。
- 玄学派:「先跑起来,压测卡住了再加」——实际上大多数线上服务从来没被单独压过线程池这一层。
我自己的翻车经历:一个 8C 容器里 coreSize=16 的池子,下游 RPC 正常时 P99 20ms 很健康;下游抖到 200ms 时,16 个线程全部阻塞,请求堆在无界队列里,P99 直接 4s+,而且 CPU 才用了 30%——典型的「线程全在等 I/O、CPU 空转、队列爆炸」三件套。事后用 Little's Law 反推:稳态 500 QPS × 0.03s ≈ 15 个并发,coreSize 16 在稳态刚好够;但按 P99 下游耗时算 500 × 0.2 = 100,缺口 6 倍。问题不在公式,在于 W 取错了分位数,也没给队列上限兜底。
【我的倾向与理由】
- 线程池大小应该从「QPS × 下游耗时分位数」倒推,而不是从 CPU 核数正推;CPU 核数只是上限约束,不是起点。
- W 必须按你要扛住的那个分位数取(要保 P99 就按下游 P99 算),并且叠加一个下游劣化系数(我们内部经验是 ×3 留余量),而不是拍脑袋的 ×2 / ×10。
- 队列必须有界,且队列长度本身要进监控——无界队列等于把背压问题推迟到内存打爆那天。
- 线程数 ≠ 连接数,两者要分开算:线程数按 Little's Law,下游连接数按下游能承受的并发上限,取较小者。
【想听大家在什么场景下怎么选】
- 你们的线程池参数是推导出来的还是抄的?抄的时候抄的是谁的?
- Little's Law 里的 W 你们取哪个分位数?下游耗时波动大的服务(比如依赖第三方 API)怎么定「安全 W」?
- 遇到「下游劣化 + 无界队列」这种组合,你们的兜底是限流、超时、还是有界队列 + 拒绝策略?拒绝策略选 CallerRuns 还是直接抛,线上踩过什么坑?
- 虚拟线程(Loom)普及后,「线程数 = 并发数」这个等式如果还成立,上面这些公式是不是可以整体扔掉?
我先把我的 Little's Law 推导模板和那次 P99 4s 的火焰图留待后续沉淀成经验文,欢迎大家在帖里先拍砖。
0赞
0收藏
0楼层