两核两 GiB 的构建机,最优并发不是核数,是一条准入公式

一、先说清楚这份数据能抄什么
有一件事必须先声明:本文引用的全部数字来自一份公开实测,它是在一台 2 核 2 GiB 的小规格构建机上、对某压缩库这类约 75 个编译单元的混合负载做出的。实测方自己也写了适用声明——这组结果不代表重模板工程,不代表链接密集型工程,更不代表大规格机器。所以我们抄的是它的方法形状,不是它的具体数字,也不由它推出任何采购结论;这里不谈任何云产品与机型规格,只谈调度。
它给出的档位表是这样的:
| 并行度 | 一组负载耗时 | 另一组负载耗时 | 同时观察到的现象 |
|---|---|---|---|
| 一 | 约 58 秒 | 约 61 秒 | 基线 |
| 二 | 约 29 秒 | 约 31 秒 | 接近两倍收益,约 1.99 倍 |
| 三 | 约 29.5 秒 | 约 32.5 秒 | 不再下降,略有回退 |
| 四 | 约 31 秒 | 约 33.4 秒 | 无增益,常驻内存涨到约 388 MiB,上下文切换增多 |
这张表值得抄的地方不在于”二是答案”,在于它的形状:一跳到二有近乎翻倍的收益,二跳到三就平了,三跳到四开始变慢,同时内存与上下文切换在第四档抬头。也就是说,最优点在两个核的机器上并不落在两个核上,而落在一条由内存余量与调度开销共同决定的边界上。按核数取默认值这件事,恰好是在把调度问题当成算力问题。
二、为什么到二之后就不再涨
并行度提高之后,瓶颈会从算力转移到两处:内存带宽与调度。前面几档确实吃满了处理器,加一路就快一路;到了某一档之后,多出来的进程并不是在干活,而是在等同一批资源——等内存带宽、等锁、等页缓存,同时互相把对方的缓存行挤掉。耗时不再下降,开销却一路增加,图上表现为平段之后微微上翘。
这类曲线在小规格机器上尤其陡峭,因为它的余量本来就薄。同一份代码在大机器上可能到第四档还在涨,在 2 GiB 的机器上第三档就开始回退。结论必须各自测,不能互相借用。
我们给客户做构建环节治理时,第一个动作就是要求把这张表补全:一、二、三、四档各跑一遍,每档同时记录耗时与常驻内存峰值。只记耗时的表一定会误导人,因为决策者会一路加档,一直加到机器开始动用交换分区的那一档才停手——而那已经是故障区。
三、耗时与内存要一起记,交换分区是硬边界
两个字段一起看才知道代价。耗时是收益,内存峰值是风险,两者的比值决定档位是不是可以留。单看任何一个都不成立:耗时相近的两档,内存峰值可能差一倍;内存充裕的档位,可能是靠交换分区把另一部分换出去换来的。
这份实测里有一条被明确写成硬约束:不依赖交换分区。这个措辞值得单独解释。一旦构建过程触发交换,耗时曲线就不再是线性变差,而是非线性劣化,并且表现为偶发长尾——同样一份改动,有时 30 秒完成,有时 4 分钟。最难归因的故障就是这种:看代码没问题,看编译器没问题,看依赖没问题,因为它取决于当时那台机器上还驻留了什么。把”不允许发生交换”写成准入条件,比事后从长尾里找原因便宜得多。
于是调度目标不是”尽量把机器塞满”,是”在内存余量之内安排任务,剩下的排队等”。
四、缓存收益不是一个倍数,是三个状态
另一组数据更值得抄,因为它拆得更细。实测方的结论是这样的:
- 热缓存且命中:清空输出目录后重跑,约 0.83 秒,相对基线约 34 倍。
- 冷缓存做全量构建:约 31.5 秒,反而慢于完全不加缓存的约 28.7 秒,因为多付了一次写缓存的开销。
- 首次增量改动:约 1.28 秒降到约 1.23 秒,五组改动里只有两组变快,原因是新改出来的对象还没进过缓存。
三条状态对应三条结论。冷启动阶段不要指望缓存,那一档它一定比不用还慢;缓存目录必须持久化并纳入容量管理,否则每一次容器重建都会把热态打回冷态;增量收益只在连续多次构建之后才成立,一次性的构建任务基本拿不到。实测方给这台机器建议的缓存容量是半吉上下,这个数值同样只属于那台机器,我们从中取走的只是”容量要显式设定、要被监控、不能无限增长”这一条,而不是任何具体档位。
这里还有一个更普遍的读数纪律:不要报平均提速。把一次冷、一次热放在一起求平均,得到的数字正好抹掉了最该被看到的那一次劣化——也就是冷缓存比不用缓存还慢这件事。要报分布,把每一次构建的耗时按”冷、热、增量首改”三类分开列,让冷态的退化留在图上。我们在交付里遇到”平均构建时长下降”这类说法时,都会要求补一张分状态的表,因为平均值几乎总是把最差状态藏起来。
缓存目录还有一条容易被忽略的规则:隔离维度要选对。不同分支、不同编译配置混在同一目录会互相污染,症状是命中率虚高但产物正确性不稳。合理分维是按构建类型与依赖版本切,不要按分支切——分支是无限增长的维度,按它切等于让缓存目录随时间膨胀到被清理策略整体删空。
五、把并发决定写成一条准入公式
实测方给出的调度规则可以照抄结构,数值必须自己重测。公式是两条:
- 预留项取”384 MiB”与”有效内存的两成”二者中的较大值。
- 单个任务的估算占用取”该任务串行运行的历史峰值内存乘 1.25,再加 64 MiB”。
- 余量不足时任务等待,而不是强行调度;全程不依赖交换分区。
两个系数各自防的东西不同。1.25 这一档是防历史峰值被低估:并行执行时同时驻留的对象比串行多,用串行单次的峰值直接当并行占用一定会偏小,系数是给这个偏差留的余量。取大值的预留项是防挤占:小机器上系统与旁路进程的固定开销占比很高,只按比例预留会在总内存很小的时候算出一个不够用的数字,所以要有一个绝对下限兜住。
第三句”不足则等待”是整套规则里最有工程价值的部分。它把排队变成一个显式状态,指标上看得到等待时长,也就知道容量到底缺不缺。强行调度的后果是所有任务一起变慢,而从监控上看不出是谁的问题——排队是可控的劣化,压垮是失控的劣化,两者的运维成本差一个量级。
这条公式在大规格机器上仍然适用,但预留项要加一个维度。小机器上瓶颈是内存与调度,大机器上瓶颈常常变成共享缓存与网络盘,这时候只算内存会明显乐观,必须把 IO 相关的等待也纳入预留判断。同一份公式换机器要先重估参数,不能直接搬。
六、上线前的四项验证与六个观测字段
这类调度改动上线前,我们只认四项验证,缺一项就不能算配好了:
- 并发档位表:一、二、三、四档各跑一遍,每档同时记耗时与内存峰值;
- 冷缓存与热缓存各跑一次,把冷态的退化显式测出来;
- 峰值内存实测,不采信估算值,用实测数据回填公式的系数;
- 连续多次构建的耗时趋势,看热态是否真的稳定保持,还是在第几次之后开始回退。
跑起来之后,每次构建至少落这六个字段:并发档位、缓存命中计数、峰值内存、等待时长、是否发生交换、负载类型与构建标识。有了这六项,”这台机器还能不能再加一档”才是一个能回答的问题;没有这些字段,讨论只会回到按核数取默认值。
最后的常见误判也要写清:构建慢通常不被归到档位上,而是被归给编译器、归给依赖、归给代码规模。而实际原因往往只有两种——档位在错误的那一档上,或者缓存处于冷态却被反复重建,每一轮都在付写缓存的钱。这两种都能被上面那张表和那六个字段直接识别。洹拓这边做这类改造时,会把”先出档位表,再改并发”作为硬性顺序:没有表就改配置,等于把一次可解释的调整变成一次不可解释的尝试,出问题之后谁也无法回滚到那个已知能用的档位。