线程池到底设多大?4 台机器实测:两个八股和我自己的系数,都翻了

24 阅读14分钟

「高并发实测」系列第 6 篇。

环境声明:本文所有实验在 1 台 i5-1155G7 笔记本(Windows/WSL2)+ 3 台 2~4 核云服务器上完成,全部是本地/自建环境,非生产容量。绝对值不代表生产表现,可迁移的是结论的形状,不是数字。

覆盖范围澄清:本文主结论来自 C 版实验(GCC 11.4/13.3,-O2);Java(JDK 8)只有早期对照样本和一份探索性新协议样本,均不用于主结论(原因见第八节)。

实验形态声明:所有实验都是"每批新建线程、跑完一批固定任务"的一次性批次测量,不等同于常驻线程池的稳态行为——凡涉及常驻池的推断都单独标注(见第七、八节)。

〇、一分钟带走版

问题本文结论(含边界)证据强度
线程池设多大?在本文这组 CPU 负载和一次性批次实验条件下,给到逻辑核数进入收益递减区;加到 2×核数不掉也不涨(两台 Linux 各自观察到;"起点=逻辑核"只有两个数据点,不足以成规律)修订后 2 机 + 修订前 2 机方向一致(各自协议下)
「核数 + 1」是峰值吗不是。它既不是起点也不是峰值,三台机器都没支持过三机复现
「过订阅必掉速」对吗本文测试的 2 台 Linux 云机和负载下,过订阅到所测范围(≤24×核)未观察到稳定回落;不意味着其他负载/环境不掉速两台 Linux 各自复现(修订前协议)
我之前写的「逻辑核 ×1.5 = 峰值」降级了。同机复跑两个种子,冠军一个在 12 一个在 8——现有样本不足以稳定排序;在这台 8 核机上两组种子的可观测加速比约 3.4~4.7x,跨机器不可比bootstrap 区间跨 1(方法见第五节)
I/O(阻塞等待模拟)设多大经验估算式:线程数 ≈ 核数 × (1 + 等待/计算);Tomcat 标定来自此前系列实测:2 核 2432,4 核 4864(不是本文合成负载推出的,推导关系见第七节)两台云机各自标定 + 估算式复核

一句话:八股问"峰值在几",实测回答"收益递减区从几开始"——这两个问题的答案经常差很远,而名次本身在会话级漂移面前不可排序。

〇.5、环境与数据总表

#机器CPU / 核系统 / 内核实现 / 编译器协议版本任务规模线程池形态
1笔记本i5-1155G7 / 4C8T(8 逻辑核)Windows + WSL2(内核版本未单独记录)C / GCC 13.3.0 -O2修订后(7 测量轮+1 预热 × 2 种子)512 任务 × 1e7 内层每批新建
2云服务器 AXeon Gold 6148 @2.40GHz / 2 vCPUTencentOS / 5.15.0-198-genericC / GCC 11.4.0 -O2修订后(同协议,seed 20261019)同上每批新建
3云服务器 B4C(型号未记录)TencentOS(内核版本未记录)C / GCC(版本未记录)修订前(3 轮,64 任务)64 任务每批新建
4云服务器 A'2CTencentOSC修订前64 任务每批新建
5笔记本同 1WindowsJava / JDK 1.8.0_481修订前(3 轮)+ 单次 tierA 探索样本64 任务每批新建

⚠️ 可比性边界:#1/#2 是修订后同协议,可并排比形状;#3/#4/#5 是修订前口径(3 轮、64 任务),只作方向性参考,绝对值与修订后不可互比。云机未记录 CPU 频率/电源策略与亲和性设置;49 列 CSV 每行自带环境元数据(在线核 / sched_getaffinity / cgroup 配额 / 内核 / 编译器 / 源码版本)。

一、实验设计

负载:纯整数混合运算(xorshift64 + 累加)。准确说法:任务体不做 I/O、不分配对象、不使用应用级锁;线程创建、任务分发和结果归并的开销仍包含在测量内——它测的是"计算负载在不同线程数下的整体执行表现",不是孤立的"调度器"。

规模:512 个任务(早期 64 任务单轮过短,调度抖动占比过大),单任务约 23~34ms。

对照:主线程同步串行为 1.00x 基线(每个种子内各自归一化——A/B 种子的串行基线不同:13898 vs 14251 ms,加速比都在各自种子内计算)。

协议:

项做法为什么
轮次每配置 7 测量轮 + 1 预热轮(CPU);I/O 用 5 轮3 轮(修订前协议)中位数对全样本最大偏离 30.9%,修订后 7 轮同一口径降到 3.1%——但 5 轮等其他档位未比较,7 轮是本次的操作性折中,不是最优解
顺序配置执行顺序按种子随机化(Fisher-Yates),可复现对抗时间漂移
正确性门槛每轮校验"完成任务数==预期"且"校验值==参照值",不合格轮排除并置退出码 1防止把错误结果当性能数据
结果聚合每任务写独立槽位、主线程按序归并消除修订前共享累加器的数据竞争(TSan 正控抓到 2 处)
落盘49 列稳定 schema CSV,全部原始轮次保留;verify_csv.py 可从原始轮次重算中位/p10/p90/MAD/吞吐/加速比任何汇总值可复核

一个反直觉的协议结论:随机化顺序只能把漂移摊到各配置上,不能消除它——这不是白噪声,加轮次平均不掉一个 10 分钟级的频率斜坡(实测:这台 i5-1155G7 在 10 分钟量级内,同一配置前后半中位数可偏 6%~34%)。

二、修订后两机数据

机器 #1(8 逻辑核 WSL2,C 版,7 轮 × 2 个种子):

线程A 种子中位 msA 加速比B 种子中位 msB 加速比
sync138981.00x142511.00x
283691.66x92221.55x
450752.74x52672.71x
839213.54x30534.67x
1233834.11x34684.11x
1640983.39x32744.35x
3240333.45x35314.04x
4836803.78x36943.86x

机器 #2(2 逻辑核云服,同协议,完整五个带内配置):

线程中位 ms加速比MAD/中位
sync118151.00x0.21%
1118351.00x0.22%
26828.71.73x0.23%
36827.31.73x0.19%
46838.11.73x0.35%
66819.21.73x0.31%
86801.01.74x0.36%

注意机器 #2 这张表:2/3/4/6/8 五个配置的中位极差只有 0.546%,且同一次运行的正控检验(sync vs 8)区间紧到 ±0.4%——检验本身有分辨力,是差距真的小。

三、八股一:「核数 + 1」

「线程数 = 核数 + 1」大概是流传最广的线程池口诀。三台机器的数据都没支持过它作为"峰值":

  • 机器 #1:拐点在 8(逻辑核数),12/16 与 8 的名次在两种子间互换(见第五节)——"核数+1 = 9"没有特殊性
  • 机器 #2:2/3/4/6/8 全平坦,带内极差 0.546%——"+1"没有特殊性
  • 修订前两台云机(各自协议下):448 / 248 平坦——同样没有

这条八股错在把"起点"当成了"峰值":核数是收益递减区的起点,不是需要精调的峰。可辩护的工程结论是 README 里那句降级后的版本:CPU 密集任务给到核数就够;继续加到 2×核数不掉也不涨,不必按任何系数精调。

fig1-冠军互换.png

四、八股二:「过订阅必掉速」

"线程数超过核数,上下文切换会吃掉收益"——这条在本文测试的两台 Linux 云机上、所测过订阅范围内(≤24×核)不成立:

  • 机器 #2:2/4/8 线程 1.73/1.73/1.74x;修订前口径的同一台机 pool 2~48 全部 1.75x(512 任务稳态复测)
  • 修订前 4 核机:pool 448 全部 3.383.47x
  • 一种可能解释(README 原判断,未实测切换计数):Linux CFS 公平调度 + 动态领任务 + 任务粒度粗(23ms+)⇒ 多余线程只是在等时间片。⚠️ 本文没有 perf/切换计数数据,不给出切换开销的定量数字;两台云机内核分别为 5.15.0-198-generic 与未记录,"Linux"并非同一种调度器形态

代价的另一个维度倒是实测的:线程创建是每批次一次性的绝对成本(total_ns − run_ns 双边界实测)——48 线程的创建阶段 17.0~26.6ms 且不稳定。按此量级推算(非实测),单批次只跑 100ms 有效工作的业务用 48 线程新建池,创建开销占 1727%(相对有效工作时长)。常驻复用池正是为此(属后续工作,本轮未实现——所以"常驻池设多大"本文没有数据)。

fig2-平坦带.png

五、我自己的系数,被复验降级

10-19 我用 4C8T 笔记本(Windows/Java,64 任务,修订前协议)跑出峰值在池=12 = 逻辑核 8 × 1.5,写进了实践公式。

10-20 按修订协议重跑(WSL2/C,512 任务,7 轮 × 2 种子):

  • A 种子冠军是 12(比 A 的 8 快 13.7%);B 种子冠军是 8(比 B 的 12 快 12.0%)——冠军互换,而"冠军领先幅度"(12~14%)小于同一配置跨种子的摆动(8 线程 A/B 差 22.1%、16 线程差 20.1%)
  • bootstrap 检验(方法交代:对两份 CSV 的逐轮 total_ns,两组各自独立重抽后比中位数——非配对,重采样 seed 固定 20261020,脚本 analyze_ranking.py;P(X 更快) = 重采样中 X 更快的比例,不是现实世界的概率;区间为比值的 90% bootstrap 分位区间):A 里 P(8 更快)=36.8%、区间 0.8471.399 跨 1;B 里 P=94.7%、区间 0.7711.008 擦边;12 vs 16 的胜负方向在两种子间翻转(A: P(12 更快)=84.0%,B: 26.2%)
  • 同一次运行的正控(sync vs 8)区间 1.729~1.744(±0.4%)——检验有分辨力,是 8/12/16 之间的真实差距小于本机噪声
  • 第二台 2 逻辑核机器上,2 × 1.5 = 3 这个预测连对应物都没有(五个配置极差 0.546%,四个 90% 区间全部跨 1)

结论只能改写成:「×1.5 峰值」降级为"落在平台带内";在这台 8 核机上,8/12/16 的名次证据不足以稳定排序——"统计上没分出胜负"不等于"已证明性能等效",平台带仍是工作假设(跨机校验值逐字相同——0x532a35978503dc34 跨 GCC 版本、跨 8↔2 核、跨线程数 bit 级一致——保证的是正确性,不是名次)。

顺带撤回 10-19 的两条:「WSL 池=16 过订阅回落 19%」——修订后 16 vs 8:A 差 4.5%、B 差 7.2%(且方向相反:A 中 16 慢、B 中 16 快),均小于 16 自身跨种子的 20.1% 摆动 ⇒ 未复现;「池=16 的差异」若按 16 vs 12 读主表,A 慢 21.1%、B 反而快 5.6%——连方向都不稳定,这正是"名次不可排序"的又一实例。

教训:单会话样本给出的"峰值名次",在会话级漂移面前不可排序。能发的结论是形状(收益递减区从哪开始),不是名次。

六、方法论:比结论更值钱的三条

1. 会话级漂移大于待分辨的差异。 这台 i5-1155G7 在 10 分钟量级内,同一配置前后半中位数可偏 6%~34%,而待分辨的配置差异只有 ≤20%。随机化顺序只能把漂移摊到各配置上,不能消除它。直接后果:A/B 两个种子的冠军互换。

2. A/B 对比必须防"顺序混淆"。 修订前后配对测试最初固定"旧版先跑",16 次配对 14 次偏向"新版慢 2235%"——差点写成性能回归。把顺序也随机化、再做同进程三臂交错探针后,方向翻转、|t| 跌到噪声地板以下:那个 2235% 来自两个独立进程的块级状态差(新进程多做的参照串行把 CPU 频率/温度推向低速),不是代码开销。反汇编确认的"指令形态差异"实测代价 0.983x——形态不同 ≠ 实测更慢。

3. 跨机校验值逐字相同,是正确性门槛最硬的一次验收。 0x532a35978503dc34 跨 GCC 13.3.0↔11.4.0、跨 8↔2 核、跨线程数与执行顺序,bit 级一致。性能可以有噪声,正确性没有。

七、I/O:阻塞等待模拟 + 标定

先说清负载性质:等待段是 50ms nanosleep(阻塞等待模拟),加 ~10ms 计算(等待:计算 = 5:1)——这是合成负载,不是真实磁盘/网络/数据库 I/O;真实 I/O 的延迟分布、连接池、限流都会改变结果。

8 核 WSL 实测吞吐:1/2/4/8/16 线程近线性(17→241/s),32 起进平坦带(428→319/s 不再上升),直到 128 线程。

  • 理论天花板(8 核 ÷ 10ms = 800/s)按中位数只达成 53%,但最好的一轮在 128 线程达到 95%——过订阅的代价不在均值,在离散度:128 线程下,单轮总耗时的 p10(最快的 10% 分位)约为中位数的 1/2.4(指标=单轮总耗时,p10 是耗时低分位;样本为该配置全部有效轮次)
  • 修订前 I/O 表(96 线程 774/s)与修订后形状不同、绝对值差 1.8x——nanosleep 粒度受 Windows 主机状态影响,这条差异需要服务器复测才能定形,不下结论

Tomcat 标定的来源要说清楚:2 核 2432 / 4 核 4864 来自此前系列对真实 Tomcat 请求的实测标定(本系列第 2 篇的 I/O 背景),不是上面 50ms+10ms 合成负载推出的——两条线的等待/计算比不同。两者的关系是:实测平台起点落在"理论最优(核×6)~ 天花板"之间,且 Little's Law(L=λW)在三个环境全部命中复核。"线程数 ≈ 核数 × (1 + 等待/计算)"是经验估算式(Little's Law 的应用型变体,不是它本身),适用前提是你的负载有相对稳定的等待比;不确定就用自己的负载标定。

关于「Tomcat 默认 200 偏大」:这句话的依据是栈内存账(200 × 1MB 线程栈)与上述标定区间,不是本文合成负载的压测结论——生产 maxThreads 应结合请求阻塞特征、下游连接池容量和压测确定,2432/4864 只是特定环境的初始标定建议。

fig3-IO曲线.png

八、局限(发前认账)

  • 一次性批次 ≠ 常驻线程池:所有数字来自每批新建线程的成本结构,常驻池的稳态行为未测——"常驻池设多大"本文没有数据
  • 会话级漂移未被协议消除,只能被识别和绕开(形状结论);要给带内名次,需要绑核 + 固定调频 + 专用机
  • I/O 曲线是阻塞等待模拟,绝对位置受 Windows 定时器分辨率与电源状态影响,需服务器复测定形
  • Java 侧新协议样本只有 1 份单会话数据(池=8 的 7.75x 明显失真——串行基线与池内基线至少一侧失真),未用于任何结论;修订前 Java 表在修订后协议下未重跑,属历史数据
  • "无 HT"在 2C 机上是推断(lscpu 未暴露 core/socket 数),实测到的准确说法是"核数起即平坦";4 核机未留下 lscpu 记录
  • 云机未记录频率/电源策略与 CPU 亲和性;49 列 CSV 的环境元数据列是补充,不是完整环境复刻

系列目录

篇主题
1Redis String 三种编码实测:juejin.cn/post/7681171487518097442
2把最重的库改回同步,QPS 反而涨了 38%:juejin.cn/post/7683387359011110975
3Agent 工程化:可观测/舱壁/熔断:juejin.cn/post/7684615074136981544
4RAG 评测三层反转:precision@4=0.55 是尺子错:juejin.cn/post/7694123609883869227
5AI Agent 落地全景:juejin.cn/post/7697500325121998848
6本文:线程池设多大,4 台机器实测