简介:lmbench-3.0 是一款由 Larry McVoy 编写的多平台开源性能基准测试工具,面向系统管理员、内核与驱动开发者以及硬件评测工程师,用于评估系统综合性能,尤其在内存带宽与内存延时测试方面表现突出。资源包共 225 个文件,约 508KB,以 63 个 C 源码文件为核心,配合 10 个头文件、5 个 Makefile 及 configure 等构建脚本,另有 36 个 .8、16 个 .tbl、6 个 .ms 等手册与文档文件,以及 results、output、stats 等测试结果与统计脚本,目录结构清晰,便于按模块查阅与二次编译。已有 3658 人学习下载。通过该工具,读者可完成内存拷贝、填充等带宽测试与读写查找延时测量,并借助多线程与可配置参数适配不同硬件场景,获取吞吐量、响应时间等关键指标,为性能诊断、算法对比与产品验证提供可靠数据支撑。
1. lmbench-3.0:当微基准测试从“跑个分”变成“定位瓶颈”
你大概率遇到过这种场景:服务上线后延迟毛刺不断,监控大盘上 CPU、内存、IO 都“看起来正常”,但 P99 就是压不下去。这时候拿 lmbench-3.0 跑一轮,往往能在几分钟内把问题从“玄学”拉回到具体的系统调用、内存带宽或上下文切换开销上。lmbench 是一套经典的微基准测试工具集,专注测量操作系统和硬件的底层性能指标——内存延迟、带宽、进程创建、上下文切换、信号处理、管道与网络吞吐等。3.0 版本在可移植性和测试粒度上做了不少调整,让它更适合在现代 Linux 发行版上直接编译运行。它适合谁?做性能优化的后端工程师、内核调优的运维、选型阶段需要对比不同云主机/物理机底层能力的架构师。它不解决业务逻辑问题,但能告诉你“这台机器的地基到底稳不稳”。
2. lmbench-3.0 的测试矩阵与选型逻辑:为什么不是随便跑跑
2.1 它到底测什么:从 lat_ctx 到 bw_mem 的指标地图
lmbench-3.0 的测试项按“延迟”和“带宽”两条线组织。延迟类包括lat_ctx(上下文切换)、lat_syscall(系统调用)、lat_pipe(管道往返)、lat_unix(Unix 域套接字)、lat_tcp/lat_udp(网络往返)、lat_mem_rd(内存读延迟)。带宽类包括bw_mem(内存读写带宽)、bw_pipe(管道带宽)、bw_tcp(TCP 吞吐)、bw_file_rd(文件读带宽)。还有lat_proc测进程创建开销,lat_sig测信号处理延迟。
这些指标的价值在于“拆解”。比如一个 HTTP 服务延迟高,你可以先跑lat_syscall看read/write是否异常,再跑lat_ctx看进程切换是否被调度器拖慢,最后用lat_mem_rd确认是不是内存访问模式导致缓存命中率崩了。lmbench-3.0 相比早期版本,在lat_mem_rd的步长控制和bw_mem的并发线程支持上更灵活,能更细地画出内存层次结构曲线。
选型理由很直接:市面上压测工具很多,但大多在应用层。lmbench 是少数能直接触碰系统调用边界和硬件内存子系统的工具。它不依赖特定运行时,编译出来就是一堆独立可执行文件,适合在裸机、容器、虚拟机里快速部署。如果你需要的是“这台机器跑我的业务能扛多少 QPS”,那该用 wrk 或 JMeter;如果你需要的是“为什么这台机器跑同样的业务就是比那台慢”,lmbench-3.0 才是对的工具。
2.2 编译与部署:从源码到可执行文件的最小路径
lmbench-3.0 通常以源码包形式分发。常见做法是下载后解压,进入src目录执行make。但现代系统上直接make大概率翻车,因为默认配置可能找不到rpc头文件或bison。我一般会先装依赖:
# Debian/Ubuntu 系 sudo apt-get install -y build-essential libtirpc-dev bison # RHEL/CentOS 系 sudo yum install -y gcc make glibc-devel rpcgen bison然后进入源码目录,先跑make config生成config文件。这一步会交互式询问一些参数,比如是否使用gettimeofday还是clock_gettime、内存测试的默认大小等。对于大多数 x86_64 Linux,直接一路回车用默认值即可。但有一个关键点:如果系统是 glibc 2.30 以上,rpc相关头文件被移到了libtirpc,需要在CFLAGS里加-I/usr/include/tirpc,链接时加-ltirpc。我通常直接改src/Makefile里的CFLAGS和LDLIBS:
# 在 Makefile 顶部附近找到 CFLAGS 和 LDLIBS,追加 CFLAGS += -I/usr/include/tirpc LDLIBS += -ltirpc改完执行make,如果看到一堆.o文件和最终的可执行文件生成,就成功了。编译产物包括lat_ctx、lat_mem_rd、bw_mem、lat_syscall等,都在src目录下。不需要make install,直接在当前目录运行即可。
注意:不要在容器里用默认的
shm大小跑lat_ctx,容器默认/dev/shm只有 64MB,而 lmbench 的上下文切换测试需要分配足够大的共享内存段,否则会报shmget失败。解决办法是启动容器时加--shm-size=1g,或者修改测试参数减小lat_ctx的并发进程数。
2.3 参数怎么设:让结果可复现而不是“看运气”
lmbench-3.0 的每个测试项都有独立参数。以最常用的lat_ctx为例:
# 测试 2 到 64 个进程之间的上下文切换延迟,每个规模重复 5 次 ./lat_ctx -s 512 -P 5 2 4 8 16 32 64-s 512表示每个进程使用的共享内存段大小为 512KB,这个值要大于 CPU 的 L2 缓存,否则测出来的是缓存内切换,不能反映真实调度开销。-P 5表示每个规模重复 5 次取最优值。后面的数字列表是并发进程数。输出会给出每个规模下的平均延迟(微秒)。
再比如lat_mem_rd:
# 测试内存读延迟,步长从 16 字节到 64MB,每次跳跃 1MB ./lat_mem_rd -t 16 64 1-t 16是初始测试大小(KB),64是最大测试大小(MB),1是步长(MB)。输出是一张“大小-延迟”表,能清晰看到 L1、L2、L3 和主存的延迟台阶。关键参数是-t和步长,步长太小会导致测试时间过长,太大则可能跳过缓存拐点。我一般先用-t 16 64 4快速扫一遍,找到拐点附近再缩小步长精测。
bw_mem的典型用法:
# 测试 512MB 内存的读写带宽,分别测 rd、wr、rdwr、cp、fwr、frd ./bw_mem -N 5 512 rd wr rdwr cp fwr frd-N 5表示重复 5 次取最优,512是内存大小(MB)。后面的rd、wr等是测试模式。cp是内存拷贝,fwr是写文件,frd是读文件。注意fwr和frd会实际写磁盘,如果不想污染磁盘,可以指定-P参数输出到/dev/null或临时文件系统。
提示:所有测试都建议在系统空闲时跑,并且关闭 CPU 频率调节(
cpupower frequency-set -g performance),否则结果波动会大到让你怀疑人生。另外,lat_ctx和lat_proc对 CPU 亲和性敏感,可以用taskset绑核减少迁移干扰。
3. 用 lmbench-3.0 定位真实性能问题:三个可复现的排查场景
3.1 场景一:上下文切换延迟异常高,先查调度器和 CPU 隔离
假设你在一台 8 核云主机上跑lat_ctx,发现 2 进程切换延迟 1.2us,但 16 进程时飙到 8us 以上。正常情况 16 进程切换应该在 2-3us 量级。这时候不要急着怀疑硬件,先按下面步骤排查:
第一步,确认 CPU 频率是否被锁在低频。用cpupower frequency-info看当前策略,如果是powersave,切到performance再跑一次。
第二步,检查是否有其他进程在抢 CPU。用top -H看是否有高负载线程,或者直接taskset -c 0-3 ./lat_ctx -s 512 -P 5 2 4 8 16把测试绑到前 4 个核,减少跨 NUMA 调度。
第三步,如果绑核后仍然高,检查内核调度参数。/proc/sys/kernel/sched_min_granularity_ns和sched_wakeup_granularity_ns如果被调得过大,会导致切换延迟增加。常见做法是临时改小:
echo 1000000 | sudo tee /proc/sys/kernel/sched_min_granularity_ns echo 1000000 | sudo tee /proc/sys/kernel/sched_wakeup_granularity_ns然后重新跑lat_ctx。如果延迟明显下降,说明是调度器粒度问题。但注意这些参数是全局的,生产环境调整需要评估。
第四步,如果以上都无效,用perf stat -e context-switches,cpu-migrations ./lat_ctx ...看切换次数和迁移次数。如果迁移次数很高,说明 CPU 亲和性没生效,或者 NUMA 平衡在捣乱。可以临时关闭 NUMA 平衡:echo 0 | sudo tee /proc/sys/kernel/numa_balancing。
3.2 场景二:内存带宽跑不满,检查 NUMA 和内存通道
bw_mem测出来带宽只有理论值的一半,这种情况在双路服务器上尤其常见。原因通常是内存分配跨了 NUMA 节点,或者测试线程没有绑到对应节点的 CPU 上。
先确认 NUMA 拓扑:numactl --hardware。假设有两个节点,每个节点 4 个核。如果你在节点 0 的核上跑bw_mem,但内存分配到了节点 1,带宽就会腰斩。解决办法是用numactl --cpunodebind=0 --membind=0把测试绑到节点 0:
numactl --cpunodebind=0 --membind=0 ./bw_mem -N 5 1024 rd wr rdwr cp如果绑核后带宽翻倍,说明之前就是 NUMA 问题。另外,bw_mem默认单线程,现代 CPU 需要多线程才能跑满内存带宽。可以用-P参数指定线程数:
# 使用 4 个线程测试内存带宽 ./bw_mem -P 4 -N 5 1024 rd wr但注意-P是进程数,不是线程数。lmbench 的bw_mem多进程模式下每个进程独立分配内存,会加剧 NUMA 问题。更可靠的做法是用numactl绑核后,手动起多个bw_mem实例分别测不同节点。
还有一个容易被忽略的点:透明大页(THP)。如果 THP 开启,bw_mem的测试结果会偏高且不稳定,因为大页减少了 TLB miss。要得到可复现的结果,建议临时关闭 THP:
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled跑完再改回always或madvise。
3.3 场景三:系统调用延迟毛刺,用 lat_syscall 逐项排除
lat_syscall可以测null、read、write、stat、fstat、open、close等调用的延迟。如果你发现read延迟偶尔跳到几十微秒,而null调用正常,那问题大概率在文件系统或块设备层。
先跑一轮基线:
./lat_syscall -N 10 null read write stat fstat open close输出会给出每个调用的平均延迟和标准差。如果read的标准差远大于平均值,说明有毛刺。这时候用strace -c -f ./lat_syscall read看实际系统调用次数和耗时分布。但strace本身会引入开销,更精确的方法是用perf trace或bpftrace挂个脚本统计sys_read的延迟直方图。
常见原因是文件被换出到 swap,或者磁盘 IO 调度器在合并请求。检查free -h看 swap 使用情况,如果 swap 有大量占用,用swapoff -a临时关闭再测。另外,/sys/block/<dev>/queue/scheduler如果是mq-deadline或bfq,可以临时切到none(NVMe 设备)再测:
echo none | sudo tee /sys/block/nvme0n1/queue/scheduler如果切换后read延迟毛刺消失,说明是 IO 调度器的问题。生产环境是否要改,取决于你的 IO 模式是延迟敏感还是吞吐敏感。
4. lmbench-3.0 避坑指南:五条血泪经验
4.1 坑一:编译报错 “rpc/rpc.h: No such file or directory”
现象:执行make时提示找不到rpc/rpc.h,或者链接时提示undefined reference to clnt_create。
原因:glibc 2.30 之后把 Sun RPC 相关实现移到了libtirpc,头文件路径也变了。lmbench-3.0 的源码默认按老路径找。
解决:安装libtirpc-dev(Debian/Ubuntu)或libtirpc-devel(RHEL/CentOS),然后在Makefile的CFLAGS加-I/usr/include/tirpc,LDLIBS加-ltirpc。如果还报错,检查config文件里RPCDIR是否指向了正确路径。
4.2 坑二:lat_ctx 结果波动巨大,同一台机器跑三次三个样
现象:lat_ctx输出在 1.5us 到 6us 之间随机跳,标准差比平均值还大。
原因:CPU 频率调节、其他进程干扰、NUMA 迁移、THP 都在影响结果。lmbench 的测试本身很敏感,不是工具不准,是环境没控制住。
解决:跑之前做四件事——锁 CPU 频率到performance、用taskset绑核、关闭 THP、确保系统空闲。如果是在云主机上,还要注意宿主机是否超卖,这种情况无解,只能换时段多跑几次取稳定值。
4.3 坑三:bw_mem 测出来的带宽比理论值高出一大截
现象:DDR4 3200 理论带宽 51.2GB/s,bw_mem报出 80GB/s。
原因:THP 开启导致 TLB miss 大幅减少,或者测试大小小于 L3 缓存,测的其实是缓存带宽而不是内存带宽。
解决:确认测试大小远大于 L3(比如 1GB 以上),关闭 THP,用numactl绑到单节点。如果还是偏高,检查 CPU 是否开启了数据压缩或内存侧缓存(比如某些 Intel 处理器的 LLC 压缩),这些特性会让实际带宽超过理论值。
4.4 坑四:lat_syscall 的 open/close 延迟异常高
现象:open和close延迟达到几十微秒,而read/write正常。
原因:文件路径在 NFS 或 FUSE 挂载点上,或者目录项缓存(dcache)被频繁回收。lmbench 默认在当前目录创建临时文件,如果当前目录是网络文件系统,延迟自然高。
解决:把 lmbench 编译产物和测试目录放到本地文件系统,比如/tmp或/dev/shm。如果必须测网络文件系统,那结果反映的就是真实情况,但要在报告里注明。
4.5 坑五:容器里跑 lat_ctx 报 shmget 失败
现象:lat_ctx: shmget failed: Invalid argument。
原因:容器默认/dev/shm太小,lmbench 需要分配大于该值的共享内存段。
解决:启动容器时加--shm-size=1g,或者修改lat_ctx的-s参数减小共享内存大小。但-s太小会导致测试不准,建议至少 256KB。如果无法改容器配置,可以在宿主机上跑,或者用--ipc=host共享宿主机的 IPC 命名空间。
5. 从单次测试到持续基线:把 lmbench-3.0 变成性能回归的“后悔药”
单次跑 lmbench 只能看到快照,真正有价值的是建立基线并持续对比。我一般会写一个包装脚本,把关键指标抽出来存成 CSV,每次硬件变更、内核升级、BIOS 调参后跑一轮,用 diff 看变化。下面是一个最小化的采集脚本:
#!/bin/bash # lmbench_baseline.sh - 采集关键指标并追加到 CSV OUTFILE="lmbench_baseline.csv" TIMESTAMP=$(date +%Y%m%d_%H%M%S) # 确保 CPU 频率锁定 cpupower frequency-set -g performance > /dev/null 2>&1 # 采集上下文切换延迟(2/8/16 进程) CTX_2=$(./lat_ctx -s 512 -P 5 2 2>&1 | awk '/^2 /{print $2}') CTX_8=$(./lat_ctx -s 512 -P 5 8 2>&1 | awk '/^8 /{print $2}') CTX_16=$(./lat_ctx -s 512 -P 5 16 2>&1 | awk '/^16 /{print $2}') # 采集内存读延迟(16KB 和 64MB) MEM_L1=$(./lat_mem_rd -t 16 16 1 2>&1 | awk '/^0.015625/{print $2}') MEM_MAIN=$(./lat_mem_rd -t 64 64 1 2>&1 | awk '/^64.000000/{print $2}') # 采集内存带宽(1GB,单线程读) BW_RD=$(./bw_mem -N 5 1024 rd 2>&1 | awk '/^1024.00/{print $2}') # 写入 CSV if [ ! -f "$OUTFILE" ]; then echo "timestamp,ctx_2us,ctx_8us,ctx_16us,mem_l1_ns,mem_main_ns,bw_rd_mbs" > "$OUTFILE" fi echo "$TIMESTAMP,$CTX_2,$CTX_8,$CTX_16,$MEM_L1,$MEM_MAIN,$BW_RD" >> "$OUTFILE" echo "Baseline saved to $OUTFILE"这个脚本的关键点:lat_ctx的输出格式是“进程数 延迟”,用awk按第一列匹配。lat_mem_rd的输出第一列是大小(MB),需要根据实际输出调整匹配模式。bw_mem的输出第一列是大小,第二列是带宽。实际使用时,建议先手动跑一次,看输出格式再改awk的匹配条件。
采集到 CSV 后,可以用gnuplot或简单的 Python 脚本画趋势图。我习惯用 pandas 读 CSV,然后算每次相对基线的变化率:
import pandas as pd df = pd.read_csv("lmbench_baseline.csv") baseline = df.iloc[0] # 第一次采集作为基线 latest = df.iloc[-1] for col in ["ctx_2us", "ctx_8us", "ctx_16us", "mem_l1_ns", "mem_main_ns", "bw_rd_mbs"]: change = (latest[col] - baseline[col]) / baseline[col] * 100 print(f"{col}: {baseline[col]:.2f} -> {latest[col]:.2f} ({change:+.1f}%)")如果某次内核升级后ctx_16us涨了 30%,你就知道该去查调度器变更了。如果bw_rd_mbs掉了 20%,可能是 BIOS 里内存频率被重置了。这套方法不能防止问题发生,但能在问题扩散前给你一个明确的信号。
我自己的习惯是:每次上架新机器、每次内核大版本升级、每次调整 BIOS 性能相关选项后,都跑一轮基线采集,把 CSV 提交到内部 Git 仓库。时间久了,这份 CSV 就是最可靠的“后悔药”——当有人说“这台机器感觉比那台慢”时,直接翻历史数据,比任何口头描述都管用。希望帮到你。
本文还有配套的精品资源,点击获取