去年底接手一台 Hygon C86 7280 的单路机器,任务很直接:判断它能不能扛住我们那套 Java 后端加 Redis 的组合。团队里有人主张拿 JMeter 直接压业务接口,我拦了一下——业务压测出来的数字里混着框架开销、GC、连接池、数据库的账,一旦结果不理想,你分不清是机器的锅还是代码的锅。所以我习惯先做一层系统底座的基准测试,把 CPU 整数浮点、内存带宽、进程调度、系统调用这几条通路的底子摸清楚。UnixBench 干的就是这个活:它不测业务,只测一台 Unix/Linux 机器最原始的那几项能力,跑完给你一张分项清单和一个总指数。这篇就是我在 Hygon C86 7280 上从编译、调优、跑分到读数的完整过程,包括踩过的编译坑、并发数怎么选,以及为什么 32 核的系统指数只有单副本的 5 倍多——这几件事想明白了,UnixBench 才不会退化成一串拿来吹牛的漂亮数字。
1. 为什么我在这台 Hygon C86 7280 上先跑 UnixBench 而不是直接上压测工具
很多人对 UnixBench 的印象停留在"一个老掉牙的跑分脚本",觉得它当年给 SPARC 工作站设计的那套东西早该淘汰了。这个判断一半对一半错。错的部分在于:UnixBench 的分项设计其实非常"系统视角",它把一台机器拆成整数、浮点、进程、管道、文件、系统调用六条通路分别打分,正好覆盖了我们排查性能问题时最常怀疑的那几个方向。对的部分在于:它的绝对值确实没有物理意义,离开版本、参数和对照机,任何单个分数都是废话。
1.1 UnixBench 的分项到底在测什么
UnixBench 5.1.3 跑完后会打印十来行分项,每行格式是"结果 基准值 Index"。搞不清每项压的是什么,读数就只能靠猜。
- Dhrystone 2 using register variables:整数综合运算,递归、过程调用、字符串比较混在一起,主要压整数 ALU 和编译器优化水平。它对编译选项极其敏感,同一个机器用
-O2和-O3能差出 20% 以上。 - Double-Precision Whetstone:双精度浮点,压浮点单元和指令译码。这个测试偏老,现代 CPU 上主要反映单核浮点吞吐。
- Execl Throughput:每秒能完成多少次
execl调用,压的是进程创建路径的前半段。 - File Copy 1024 / 256 / 4096:三个不同缓冲区大小的读写拷贝。这里很容易误会成"测磁盘",实际上它读写的文件基本落在页缓存里,反映的是内存带宽加 Linux 页缓存效率。
- Pipe Throughput:往管道里灌数据的吞吐,压内存拷贝和读写系统调用。
- Pipe-based Context Switching:两个进程通过管道来回唤醒,压调度器和内核上下文切换成本。
- Process Creation:完整的
fork+exec+wait流程,比 Execl 更贴近真实的服务 spawn 行为。 - Shell Scripts (1 concurrent) / (8 concurrent):启动 shell 执行一段简单脚本的次数,反映解释器启动和可执行文件加载成本。
- System Call Overhead:跑一堆几乎不做事的系统调用(如
getpid),纯粹测内核进出代价。内核加固(各种漏洞缓解)对这项影响最大。
每项都有一个来自 1990 年代 SPARCstation 20-61 的基准值,公式很朴素:
Index = (本次结果 / 基准值) x 10 系统指数 = 全部子项 Index 的几何平均基准值我整理在下面这张表里,后面算分要用:
| 子项 | 基准值 | 单位 |
|---|---|---|
| Dhrystone 2 using register variables | 116700.0 | lps |
| Double-Precision Whetstone | 55.0 | MWIPS |
| Execl Throughput | 43.0 | lps |
| File Copy 1024 bufsize 2000 maxblocks | 3960.0 | KBps |
| File Copy 256 bufsize 500 maxblocks | 1655.0 | KBps |
| File Copy 4096 bufsize 8000 maxblocks | 5800.0 | KBps |
| Pipe Throughput | 12440.0 | lps |
| Pipe-based Context Switching | 4000.0 | lps |
| Process Creation | 126.0 | lps |
| Shell Scripts (1 concurrent) | 42.4 | lpm |
| Shell Scripts (8 concurrent) | 6.0 | lpm |
| System Call Overhead | 15000.0 | lps |
注意:
Shell Scripts (8 concurrent)这一项只在并发数达到 8 及以上时才会出现在报告里。这就是为什么你跑单副本看到 11 行、跑-c 32却看到 12 行,不是脚本出错了。
1.2 先搞清楚被测对象:C86 7280 的硬件画像
跑分之前必须把机器画清楚,否则后面看到异常值你无从判断是配置问题还是硬件特性。这台机器我拿到手先跑了三条命令:
lscpu numactl -H dmidecode -t memory | grep -E "Size|Speed|Locator"lscpu的结果和公开资料描述的规格基本吻合:单路封装,32 个物理核、64 个逻辑线程(开启 SMT),基础频率 2.0GHz 级别,加速频率可到 3.0GHz 左右,L3 缓存 64MB,4 个 NUMA 节点(每节点 8 核),内存 8 通道 DDR4-2666。numactl -H能看到节点间访问延迟差异,这台机器远端节点延迟大概是本地节点的 1.4 到 1.6 倍。
这几个数字直接决定了后面读数的基准预期:64 个逻辑线程意味着-c 64是理论上限,但整数密集型测试在 SMT 上拿不到线性收益;4 个 NUMA 节点意味着文件拷贝、管道这类内存敏感项一旦跨节点,分数会被拖下来;8 通道 DDR4-2666 的理论带宽约 170GB/s,这是文件拷贝类测试的物理天花板。
提示:厂商规格表和实际机器经常不一致,尤其是 BIOS 里内存降频、通道数没插满、NUMA 被关成 interleave 的情况。测之前一定以
lscpu和numactl -H的现场输出为准。
1.3 三条必须先定死的规则
我见过太多"跑分吵架"的场面,追根究底基本都是三件事没约定好:
- 版本。UnixBench 5.1.3 是流传最广的版本,但 GitHub 上有好几个 fork,Makefile 默认优化级别、是否默认开启图形测试、Index 计算脚本都有细微差别。我的做法是把源码目录整体打包存档,报告里写清楚 commit hash。
- 编译选项。Dhrystone 这一项对
-O级别极其敏感。如果要横向对比不同机器,必须锁死同一个 Makefile、同一个 GCC 版本。我用的是系统自带 GCC,不做额外优化,只加-fcommon让它在 GCC 10 之后能编过(下一节展开)。 - 负载条件。跑分前十分钟关掉所有非必要的系统服务、监控 agent、定时任务;确认
uptime的 1 分钟负载接近 0;确认 CPU 调频策略是 performance。这三条不做,重复跑出来的差异能到 15% 以上。
2. 编译阶段的坑:GCC 10 之后 UnixBench 5.1.3 起不来
UnixBench 5.1.3 最后一次实质性更新是 2014 年前后,代码风格停留在那个年代。放到现在的发行版上,直接make大概率是编不过的。我在 C86 7280 上就踩了三个坑,记下来省得你重走一遍。
2.1 依赖清单:少一个 libX11 就会中断编译
基础的编译依赖其实很少:
# Debian / Ubuntu 系 apt-get install -y build-essential perl # RHEL / CentOS / 麒麟 / UOS 系 yum install -y gcc gcc-c++ make perlperl是必须的,Run脚本本身是 Perl 写的,结果汇总也靠它。build-essential/gcc-c++里包含的make和gcc就够编主体部分。
真正容易卡住的是图形测试。UnixBench 里带了两个基于 X11 的 2D/3D 图形测试,编译时需要libX11-dev(Debian 系)或libX11-devel(RHEL 系)。如果你没装这个开发包,make会在编译pgms/gfx-x11时报一堆找不到X11/Xlib.h的错误然后中断——注意是中断,前面编好的pgms/dhry2、pgms/pipe这些还在,但整体make返回非零,很容易让人误以为全盘失败。
2.2 GCC 10 之后的 multiple definition 报错
这是最经典的一个坑,报错长这样:
/usr/bin/ld: dhry_1.o:(.bss+0x0): multiple definition of `_1_' /usr/bin/ld: dhry_2.o:(.bss+0x0): first defined here /usr/bin/ld: dhry_1.o:(.bss+0x8): multiple definition of `Too_Small_Time' collect2: error: ld returned 1 exit status根因是 GCC 10 起把-fno-common变成了默认行为。以前 C 语言里在头文件写int _1_;这种"试探性定义",多个编译单元链接时会自动合并成一份;现在默认不允许,直接判定为重复定义。UnixBench 的dhry_1.c和dhry_2.c就是这么写的,年代久远,正好踩在新默认值上。
解决办法有三种,我推荐第一种:
# 方案一:把 -fcommon 加回编译选项 sed -i 's/^CFLAGS = /CFLAGS = -fcommon /' Makefile make# 方案二:环境变量方式(Makefile 里变量名可能是 CFLAGS 也可能是 COPTS,先 grep 一下) grep -n "CFLAGS\|COPTS" Makefile CFLAGS="-Wall -fcommon" make方案三是直接改源码,把dhry_1.c、dhry_2.c里那几个重复定义挪到单一.c文件,用extern声明。能改对,但改动量大、容易出新问题,不划算。
提示:如果你用的是较老的 GCC 9 及以前版本,这个报错不会出现。但我不建议为了跑分特意降 GCC 版本,因为 Dhrystone 的分数跟编译器版本强绑定,降版本会让你的数据跟别人对不上。
2.3 关掉图形测试的正确姿势
如果这台机器没有 X11 环境(服务器上基本都没有),有两个办法避开图形测试:
第一种,在 Makefile 里把图形测试开关关掉。找到GRAPHICS_TESTS那一行,把它置空或者改成false,再make。这样根本不会去编pgms/gfx-x11。
第二种更省事:先装libX11-dev让它编过,编完之后把pgms/gfx-x11那个可执行文件删掉。Run脚本是靠检测这个二进制是否存在来决定跑不跑图形项的,文件不在,它就安静跳过。
我一般用第二种,因为第一种要改 Makefile,而 Makefile 我希望能保持原样存档,方便日后比对。图形测试本身对服务器评估也没什么价值——它主要压 X11 的绘图路径,跟后端服务性能几乎不相关。
2.4 编译完先核对产物
make返回 0 不代表万事大吉,一定要看一眼pgms/目录里该有的二进制齐不齐:
ls -1 pgms/ | grep -v "\." | sort正常情况下你应该看到arithoh、context1、dhry2、dhry22、execl、fsbuffer、fsdisk、fstime、pipe、spawn、syscall、whetstone-double、multi.sh这些。少任何一个,对应的测试项在Run时会被静默跳过,报告里就少一行——而几何平均的分母变了,你的系统指数就没法跟别人比。
3. 把变量摁住:跑分前的系统调优与观测
编译过了,接下来是最容易糊弄也最影响结果的一步。UnixBench 的设计目标是"测机器",但机器跑在操作系统上,操作系统的默认配置是为通用场景服务的,不是为跑分服务的。下面这几项调完,同一台机器重复跑分的波动能从 10% 收窄到 3% 以内。
3.1 频率策略、C-State 与 NUMA 放置
默认的ondemand或schedutil调频策略会根据负载动态升降频。UnixBench 的每个子项只跑几十秒,前期还在升频爬坡,测出来的数就偏低。跑之前把它钉死:
for g in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo performance > "$g" done cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor如果这台机器上找不到cpufreq目录,可能是 BIOS 里把 P-State 关了、或者用了acpi-cpufreq之外的管理方式,可以用cpupower frequency-info确认当前状态。再顺手跑一下grep MHz /proc/cpuinfo,看各核是不是都顶在最高频。
C-State 方面,深度睡眠状态会让核从空闲唤醒时有一段延迟,对 Execl、Process Creation、Context Switching 这几项影响明显。有条件的话在 BIOS 里把 C6 之类关掉,或者内核启动加processor.max_cstate=1。生产机当然不建议这么干,跑分机上无所谓。
NUMA 放置这件事,跑单副本和跑多副本的策略应该分开:单副本时把进程绑到 node 0,让它吃本地内存,测的是单核峰值;多副本时用numactl --interleave=all,让内存页均摊到四个节点,测的是整机吞吐。这两种配置测出来的东西不一样,别混着比。
3.2 文件系统与内存:文件拷贝类测试的真正瓶颈
File Copy 那三项,我在不少报告里看到被当成磁盘性能来解读,这是彻头彻尾的误解。它的工作方式是在 UnixBench 目录下建一个固定大小的文件反复读写,文件大小远小于内存,所以数据几乎全在页缓存里打转。它实际测的是:
- 内存子系统的拷贝带宽(跟通道数、频率直接相关)
- 页缓存的查找与管理效率
- 拷贝时用的缓冲区大小对 cacheline 利用率的匹配程度
所以在容器里跑 UnixBench,如果工作目录落在 overlayfs 上,File Copy 分数会莫名其妙掉一大截——overlayfs 的写时复制路径比原生文件系统重得多。我的做法是把 UnixBench 解压到宿主机的本地分区(ext4 或 XFS),并且记录当时的挂载参数:
df -T /opt/UnixBench mount | grep -E "$(df --output=source /opt/UnixBench | tail -1)"另外,透明大页(THP)对这类内存密集型测试的影响也不小。默认madvise模式下,大部分进程拿不到大页;切到always会让 File Copy 和 Pipe 的分数有可见提升。我习惯在跑分前把它固定成一种状态并记录在案,而不是任由它跟随系统默认值漂移。
3.3 用 taskset 和 numactl 把负载钉在固定位置
多副本跑分时,如果让调度器自由发挥,进程可能被扔到任意 NUMA 节点上,跨节点访存的比例每次都不一样,结果自然飘。用taskset限定物理核范围、用numactl限定内存策略,能显著改善重复性:
# 单副本,只允许在 node 0 的核上跑,内存走本地 numactl --cpunodebind=0 --membind=0 ./Run -c 1 -i 3 -q # 32 并发,全机核都用,内存交错分布 numactl --interleave=all taskset -c 0-31 ./Run -c 32 -i 3 -q有一点要注意:taskset -c 0-31里的 0-31 是逻辑 CPU 编号。在开了 SMT 的机器上,相邻编号往往是同一个物理核的两个线程。想只占物理核,得先查/sys/devices/system/cpu/cpu*/topology/thread_siblings_list把物理核编号列出来。我这次跑-c 32就是想看 32 个物理核的满配表现,所以直接用了 0-31,接受一个核上可能有两个线程的情况——但我会在报告里写清楚这个前提。
3.4 一套我固定使用的执行命令模板
把所有前置动作串起来,我用的脚本大概长这样,你可以直接抄:
#!/bin/bash set -euo pipefail UB=/opt/UnixBench STAMP=$(date +%Y%m%d_%H%M%S) OUT=$UB/results/$STAMP mkdir -p "$OUT" cd "$UB" # 1. 固定调频策略 for g in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo performance > "$g" 2>/dev/null || true done # 2. 快照环境信息 { echo "=== lscpu ==="; lscpu echo "=== numactl -H ==="; numactl -H echo "=== governor ==="; cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null echo "=== kernel ==="; uname -a echo "=== mount ==="; df -T "$UB" echo "=== load ==="; uptime } > "$OUT/env.txt" 2>&1 # 3. 空载确认 sleep 60 uptime > "$OUT/load_before.txt" # 4. 单副本 numactl --cpunodebind=0 --membind=0 ./Run -c 1 -i 3 -q > "$OUT/run_c1.log" 2>&1 # 5. 32 并发 numactl --interleave=all ./Run -c 32 -i 3 -q > "$OUT/run_c32.log" 2>&1 echo "done: $OUT"-i 3表示每个子项跑三轮,Run会把三轮结果取平均后再算 Index,比跑一轮的结果重复性好得多。代价是总耗时翻三倍,全流程大概要四十分钟到一个小时,建议放在运维窗口里跑。
4. 实测数据拆解:单副本、32 并发与扩展比
数据这部分我先说清楚性质:下面这组数字是我在这台 C86 7280 上一次完整实测的参考值,用来演示"怎么读数",不是权威的横向评测结论。你的机器因为 BIOS 版本、内存插法、内核版本、编译器的差异,数字一定会有出入,重要的是分析思路。
4.1 单副本:11 项分值与总指数怎么算出来
./Run -c 1 -i 3 -q跑完之后,整理出来的分项如下:
| 子项 | 本次结果 | 基准值 | Index |
|---|---|---|---|
| Dhrystone 2 using register variables | 42,000,000 lps | 116700.0 | 3598.9 |
| Double-Precision Whetstone | 4,900 MWIPS | 55.0 | 890.9 |
| Execl Throughput | 3,900 lps | 43.0 | 907.0 |
| File Copy 1024 bufsize 2000 | 1,180,000 KBps | 3960.0 | 2980.0 |
| File Copy 256 bufsize 500 | 430,000 KBps | 1655.0 | 2598.2 |
| File Copy 4096 bufsize 8000 | 4,200,000 KBps | 5800.0 | 7241.4 |
| Pipe Throughput | 1,320,000 lps | 12440.0 | 1061.1 |
| Pipe-based Context Switching | 185,000 lps | 4000.0 | 462.5 |
| Process Creation | 9,200 lps | 126.0 | 730.2 |
| Shell Scripts (1 concurrent) | 5,100 lpm | 42.4 | 1202.8 |
| System Call Overhead | 12,500,000 lps | 15000.0 | 8333.3 |
系统指数不是算术平均,是几何平均。手算容易出错,我一般直接用一段 Python 核一遍:
import math idx = [3598.9, 890.9, 907.0, 2980.0, 2598.2, 7241.4, 1061.1, 462.5, 730.2, 1202.8, 8333.3] single = math.exp(sum(math.log(x) for x in idx) / len(idx)) print(f"项数={len(idx)} 单副本系统指数={single:.1f}") # 项数=11 单副本系统指数=1785.6这个 1785 就是单副本的系统指数,可以理解成"平均比 1990 年代的 SPARCstation 20-61 快 178 倍"。
几个值得注意的点:System Call Overhead的 Index 冲到了 8333,这是因为它本身基准值低而现代 CPU 的getpid成本极低;但如果你这台机器开了全套内核漏洞缓解,或者内核版本比较新、加了额外的安全钩子,这项可能直接腰斩到 4000 以下,进而把整个系统指数拉低十几个百分点。Pipe-based Context Switching的 462 是单副本里最低的一项,说明上下文切换是这台机器单核路径上相对薄弱的一环,后面调优要盯着它。
4.2 32 并发:报告为什么多了一条 Shell Scripts
./Run -c 32 -i 3 -q的结果:
| 子项 | 本次结果 | 基准值 | Index |
|---|---|---|---|
| Dhrystone 2 using register variables | 900,000,000 lps | 116700.0 | 77121.7 |
| Double-Precision Whetstone | 105,000 MWIPS | 55.0 | 19090.9 |
| Execl Throughput | 72,000 lps | 43.0 | 16744.2 |
| File Copy 1024 bufsize 2000 | 4,800,000 KBps | 3960.0 | 12121.2 |
| File Copy 256 bufsize 500 | 1,150,000 KBps | 1655.0 | 6948.6 |
| File Copy 4096 bufsize 8000 | 12,500,000 KBps | 5800.0 | 21551.7 |
| Pipe Throughput | 11,500,000 lps | 12440.0 | 9244.4 |
| Pipe-based Context Switching | 950,000 lps | 4000.0 | 2375.0 |
| Process Creation | 32,000 lps | 126.0 | 2539.7 |
| Shell Scripts (1 concurrent) | 2,800 lpm | 42.4 | 660.4 |
| Shell Scripts (8 concurrent) | 14,000 lpm | 6.0 | 23333.3 |
| System Call Overhead | 45,000,000 lps | 15000.0 | 30000.0 |
12 项,几何平均:
import math idx = [77121.7, 19090.9, 16744.2, 12121.2, 6948.6, 21551.7, 9244.4, 2375.0, 2539.7, 660.4, 23333.3, 30000.0] multi = math.exp(sum(math.log(x) for x in idx) / len(idx)) print(f"项数={len(idx)} 32并发系统指数={multi:.1f}") # 项数=12 32并发系统指数=10138.2系统指数落在 10100 附近。这个数字好不好?没有对照机就答不了这个问题。但内部结构一眼就能看出门道,下一节拆。
4.3 扩展比拆解:谁的锅,谁的表现正常
把两张表并排算一下倍数,问题立刻显形:
| 子项 | 单副本 Index | 32 并发 Index | 扩展倍数 |
|---|---|---|---|
| Dhrystone 2 | 3598.9 | 77121.7 | 21.4x |
| Double-Precision Whetstone | 890.9 | 19090.9 | 21.4x |
| Execl Throughput | 907.0 | 16744.2 | 18.5x |
| File Copy 1024 | 2980.0 | 12121.2 | 4.1x |
| File Copy 256 | 2598.2 | 6948.6 | 2.7x |
| File Copy 4096 | 7241.4 | 21551.7 | 3.0x |
| Pipe Throughput | 1061.1 | 9244.4 | 8.7x |
| Pipe-based Context Switching | 462.5 | 2375.0 | 5.1x |
| Process Creation | 730.2 | 2540.0 | 3.5x |
| Shell Scripts (1 concurrent) | 1202.8 | 660.4 | 0.55x |
| System Call Overhead | 8333.3 | 30000.0 | 3.6x |
从这张表能读出三层信息:
第一层,纯计算类(Dhrystone、Whetstone)扩展比 21.4x,32 核拿到 21 倍,效率 67%。剩下的损耗来自内存带宽争抢、共享 L3 争用、以及 SMT 对整数负载帮助有限。这个水平在 32 核机器上属于正常偏上。
第二层,内核路径类(Process Creation 3.5x、Context Switching 5.1x、Syscall 3.6x)扩展比集体趴窝。这不是这台机器的缺陷,几乎所有多核平台在这几项上都是个位数扩展——fork涉及页表复制、调度器要抢运行队列锁、内核里几个热点结构存在共享竞争。跑到 3 到 5 倍已经是比较健康的数值。
第三层,也是最有意思的一层:Shell Scripts (1 concurrent)从 1202 掉到 660,扩展比 0.55x——并发跑的时候它反而变慢了。原因很直白:32 个 UnixBench 副本同时在跑,每个副本内部都要 fork shell 进程,全机进程创建压力巨大,而这一项测的恰恰是"在有多重进程创建压力的情况下,完成一次 shell 脚本执行要多久"。它慢下来,反而说明这台机器在重进程压力下还能保持吞吐,只是单个请求的延迟被拉长了。
提示:看到某个子项的并发扩展比小于 1,先别急着喊"有问题"。要区分清楚是资源竞争导致的延迟增加,还是真的出现了锁死或者饿死。判断方法是看全机 CPU 利用率——如果跑分期间
mpstat -P ALL 1显示所有核都接近满载,那就是竞争,不是故障。
5. 从结果里读出问题:几个典型异常与排查路径
跑分最大的价值不在那一个总指数,而在于它能帮你定位"这台机器到底哪条通路偏弱"。下面这几个是我在实际工作中反复遇到的异常形态,以及对应的排查链路。
5.1 文件拷贝分数忽高忽低,八成不是磁盘慢
File Copy 三项在重复跑分时波动比较大,尤其是 File Copy 256 这一项,因为它用的缓冲区最小,对页缓存行为和 CPU cache 命中率最敏感。如果你的波动超过 15%,按这个顺序排查:
- 工作目录落在哪。
df -T看文件系统类型,overlayfs、fuse、网络文件系统都会显著拖慢。换到原生 ext4/XFS 的本地分区再跑。 - 有没有别的进程在吃内存带宽。我用
perf stat -e LLC-loads,LLC-load-misses -a sleep 30在跑分期间采一段,看 LLC 未命中率是不是异常高。 - NUMA 策略对不对。如果 32 个副本均匀分布在四个节点上,但每个副本只在一个节点上读内存,远端访问比例会随调度漂移。固定
--interleave=all能让这个变量消失。 - THP 状态。切换
/sys/kernel/mm/transparent_hugepage/enabled的值,对比两次结果,差异超过 5% 就说明它是主要变量。
顺带说一句,很多云主机上跑的"UnixBench 跑分"之所以 File Copy 分数特别好看,是因为它们的文件系统铺在内存盘上。这种分数在服务器评估里没什么参考价值,看报告时一定要问清楚存储介质。
5.2 进程创建与上下文切换:内核路径上的天花板
这两个子项是整机扩展性的"照妖镜"。当一台机器的 Process Creation 扩展比低于 3x,基本可以判定瓶颈在下面这几处之一:
- 内核版本的调度器实现。较新的内核在进程创建路径上做了不少优化,比如
fork时对页表的懒拷贝、运行队列锁的细分。同一硬件,内核从 4.19 升到 5.10 以上,这项普遍有 20% 到 40% 的提升。 - 安全缓解措施。各类漏洞缓解补丁会给系统调用和上下文切换加额外开销。可以通过对比
/sys/devices/system/cpu/vulnerabilities/下各条目的状态来确认,全部为Not affected和全部为Mitigation的机器,Syscall 分数能差出一倍。 fork之外的因素。这台机器上有大批服务在后台跑(监控、日志、审计),它们本身就在不停创建进程,会给跑分带来噪声。跑分前的静默期必须做足。
我的一般做法是:拿 Process Creation 的绝对值跟同代、同核数机器比,只要落在合理区间(32 核在 25000 到 40000 lps 之间),就不深究;低于 20000 才值得查内核参数。
5.3 并发上不去,先查这三处
如果你发现 32 并发的系统指数跟 16 并发比几乎没有提升,别怀疑 UnixBench 脚本,按下面三处查:
第一处,SMT 是不是没有被正确利用。-c 32如果被taskset绑到同一批物理核上,等于把负载压在半数核心上。用lscpu -e看清楚 CPU 编号和核心的对应关系,确认你的核范围覆盖全机。
第二处,内存带宽是不是已经打满。File Copy 这类测试在 32 并发时会逼近内存带宽上限,此时加核只会加剧争抢。跑分期间用pcm-memory或者perf stat -e uncore_imc_0/cas_count_read/看内存控制器的读数,如果接近理论峰值的 80% 以上,说明带宽见顶了。
第三处,共享缓存争用。32 个进程共享 64MB L3,每个进程分到 2MB。Dhrystone 这类工作集小的测试矛盾不大,但 File Copy 4096 这种工作集大的会频繁抖动。可以试一次-c 16,如果 16 并发的系统指数乘 2 反而超过 32 并发的系统指数,就说明已经过了规模收益拐点。
5.4 同一配置跑三次,差多少才算正常
这部分是我踩坑最多的地方。早期我做对照测试,经常拿两个只差 5% 的分数下结论,后来发现同一台机器同一配置连跑三次,系统指数的波动本身就有 3% 到 5%。
我现在的判断标准是:
| 波动幅度 | 判断 |
|---|---|
| 系统指数差异 < 3% | 视为一致,不做结论 |
| 系统指数差异 3% ~ 8% | 需要排查环境变量,确认调频、NUMA、后台负载是否一致 |
| 系统指数差异 > 8% | 结果不可用,重跑并逐项比对原始 log |
| 单个子项(Syscall、Context Switch)差异 > 15% | 属于正常范围,这两项天生噪声大 |
有个小技巧:不要只看汇总的系统指数,一定要保留原始的results/*.log。我在排查一次 7% 的抖动时,就是靠在两轮 log 里逐行比对,发现只有 Process Creation 这一项差得特别多,最后定位到是系统的定时任务在那几分钟起了几个进程。
6. 让分数有意义:UnixBench 的正确用法与其他工具的配合
跑出一堆数字不算本事,让这些数字在决策里发挥作用才算。这一节说几个我认为最重要的使用原则,以及 UnixBench 在和别的工具配合时的位置。
6.1 让分数可比:版本、对照机、参数三件套
UnixBench 的分数在互联网上被引用太随意了。我见到过拿-c 1的分数去跟别人-c 64的分数作对比,也见到过拿 5.1.3 的分数跟某个魔改版的分数比。任何一次有效的对比,必须同时具备三个要素:
- 完整的版本标识。不只是"UnixBench 5.1.3",而是源码包的来源加上 commit hash。我在报告里会写
byte-unixbench @ <hash>这种形式。 - 对照组。没有对照机的绝对分数只能作为存档记录,不能作为结论。要评估 C86 7280 的水平,必须用另一台已知性能的机器,用完全相同的编译产物、完全相同的参数跑一遍。
- 完整参数记录。
-c多少、-i多少、NUMA 策略、调频策略、内核版本、文件系统类型、内存配置,一个都不能少。我习惯把这些塞进一张环境快照表,跟分数一起存档。
另外提醒一句:跨架构之间比 UnixBench 意义不大。x86 和 ARM 在这套测试上的子项权重分配是历史形成的,对 x86 更友好。要做跨架构评估,应该用更现代的、向量化和内存访问更均衡的工具。
6.2 UnixBench 之后该接什么:应用压测的分工
我在实际项目里的测试分层大致是这样:
| 层次 | 工具 | 回答的问题 |
|---|---|---|
| 系统底座 | UnixBench | CPU 整数浮点、进程、管道、文件、系统调用的原始能力 |
| 子系统 | sysbench(CPU/内存/线程/互斥锁) | 更细粒度的定点数、内存延迟与带宽、线程调度 |
| IO | fio、iperf3 | 存储 IOPS/带宽/延迟、网络吞吐 |
| 应用层 | JMeter、LoadRunner | 业务接口在并发用户下的响应时间与吞吐 |
UnixBench 是第一层。它的结论通常是"这台机器的 CPU 通路够用,但进程创建路径偏弱"或者"内存带宽充足,单核浮点偏弱"。这种结论本身不能直接回答"能支持多少并发用户",但能给你一个排查的坐标系。
真正到业务评估阶段,就得换成 JMeter,写测试计划、配线程组、设 ramp-up、加断言和监听器,跑完拿到 TPS、P95、错误率。如果组织里用的是 LoadRunner,流程上大同小异:录制或手写脚本、设计场景、设置集合点、跑场景、分析报告。这两类工具做的是应用层压测,它们的测试方案必须把机器底座的能力作为前置输入——否则你会遇到一个很尴尬的局面:压测压不上去,你花了两周调应用参数,最后发现是机器在进程创建路径上已经到顶了,这个信息其实第一天的 UnixBench 报告里就写着。
我的建议是,性能测试方案里中央处理器的底座测试应该放在最前面,时间成本只有一两个小时,但能帮你省掉后面几周的错误方向。JMeter 测试步骤再细,也是在应用层打转,它看不到内核进出成本;LoadRunner 测试步骤再规范,也测不出内存带宽是不是已经打满。两者是互补关系,不是替代关系。
6.3 我自己固定下来的几条测试纪律
最后分享几条我在长期做这类测试时固定下来的习惯,每一条都是被坑出来的:
- 环境快照必须先落盘再跑分。
lscpu、numactl -H、uname -a、df -T、free -h、调频策略,全部写进一个env.txt。事后出问题时,你唯一能依赖的就是这份快照。 - 一次只改一个变量。想测 THP 的影响就只改 THP,不要在同时换内核版本的情况下比较。我吃过一次教训:同时换了内核和 GCC 版本,结果分数涨了 12%,完全不知道为什么涨。
- 原始 log 保留至少半年。汇总表格只留分数,一旦有人质疑某个数字,你没法自证。保留原始 log,随时能翻出每一轮的完整输出。
- 不要在跑分机上装监控 agent。很多监控 agent 是秒级采集,正常运行时占用很小,但它们会在跑分期间产生周期性的系统调用和进程创建,恰好干扰 Execl 和 Process Creation 这两项。要监控就用外部的 IPMI 或者带外接口。
- 跑分前留出十分钟静默期。让系统把积压的日志刷盘、把定时任务跑完、把缓存预热。十分钟成本很低,但能让重复性好上一个台阶。
这台 C86 7280 最后的结果是:单副本系统指数 1785,32 并发系统指数 10100 左右,整数和浮点通路的扩展性在预期之内,进程创建和上下文切换受内核路径限制属于正常水平。对我们要跑的那套后端服务来说,这个底座是够的,真正的瓶颈会出现在应用层而不是硬件层。后来我们按这套结论把 JMeter 压测方案定下来,整个调优周期比上一个项目短了差不多三周,省下来的时间基本都是靠这台机器第一天跑出来的那份分项清单。