news 2026/10/21 3:17:28

Linux性能测试与内存泄漏排查:从命令组合到负载模拟

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux性能测试与内存泄漏排查:从命令组合到负载模拟

做Linux性能测试,最忌讳的就是一拿到机器就到处敲命令,top看一眼、free扫一行、再随手压一把负载,半小时过去,除了几个飘忽的数字什么都没留下来。我这些年跟核心命令、负载模拟和内存泄漏排查打交道,最深的体会是:性能测试靠的不是某个神级命令,而是一套能反复验证的流程。负载怎么造、命令怎么组合、内存泄漏怎么从"看着涨"一步步钉死到具体代码行,这些才是真正的干货。

这篇文章不打算罗列命令手册,我按实际干活的路子来写:先讲压测前该做哪些准备,再把排查瓶颈时的命令分组逻辑理清楚,然后说负载模拟工具怎么选、压力模型怎么定,最后用一整个章节复盘一次内存泄漏的完整排查过程。适合刚接触性能测试的后端开发、运维和测试同学,也适合那些已经会敲命令但总觉得排查思路乱成一团的人。

1. 上手压测前先做三件小事:基线、场景和观测面

很多人压测翻车,问题不出在工具上,而出在"没准备就开打"。拿到一台机器上来就加压,最后除了知道"它挂了"以外,什么结论都得不到。我先说三个最容易被忽略的准备工作。

1.1 留好空闲基线:没有对比就没有结论

性能测试的第一件事不是加压,而是先把机器在空闲状态下的数据采集一遍。没有基线,后面所有指标都成了无源之水。比如一台机器平时load average就在1.5左右,压测后涨到2.0,看起来似乎还好,但如果你不知道基线,这个数字根本没法判断。

我通常会在压测前启动一个后台采集脚本,至少收集以下几类数据:

  • CPU整体使用率和每核使用率:top、mpstat -P ALL 1
  • 内存占用和swap情况:free -h、vmstat 1 10
  • 系统负载和运行队列:uptime、vmstat的r列
  • 磁盘IO:iostat -x 1
  • 网络连接状态:ss -s

采集时间不用太长,10分钟足够。关键是这些数据要落盘,不能只看屏幕。你永远不知道压测到第3个小时会出现什么诡异问题,到那时候回头翻基线数据,往往能救命。

1.2 明确压测场景:你是想找瓶颈还是想找泄漏

负载模拟之前,我心里一定会先问一句:这次压测到底要验证什么?场景不同,方法论完全不同。

  • 容量测试:给定硬件配置,能支撑多少QPS或并发。这种场景适合短时间、阶梯式加压。
  • 稳定性测试(Soak Test):持续几小时甚至几天的中等负载,用来暴露内存泄漏、句柄泄漏、连接堆积这类慢性问题。
  • 极限测试(Stress Test)):把资源打满,看系统在极端压力下是优雅降级还是直接崩溃。

内存泄漏几乎只在稳定性测试中才会浮出水面。如果你每次都是压3分钟就收工,那内存泄漏永远测不出来。我在做稳定性压测时,持续时长一般不低于4小时,观察窗口拉长,RSS曲线的走势才看得出来。

1.3 想清楚观测面:命令是配合着用的,不是单挑

新手最喜欢的一件事就是盯着top看,仿佛top就是性能测试的全部。但top只是全局快照,它告诉你"谁在消耗资源",很少告诉你"为什么会消耗"。完整的观测面应该是分层的:

  • 全局层:load、CPU、内存、IO、网络整体水位,用uptime、vmstat、free、iostat、sar。
  • 进程层:哪个进程吃CPU、吃内存,用top、ps、pidstat。
  • 内核/栈层:为什么吃、卡在哪,用perf、gdb、valgrind、/proc文件系统。

这三个层次是从粗到细的漏斗。我见过太多人一上来就valgrind,结果卡得生不如死,其实前面两步就能把范围缩到很小。顺序搞对,效率翻倍。

2. 核心命令的漏斗式组合:从整机指标一路查到进程内部

这一章我按排查时的实际路径来讲命令,而不是按命令大全的方式。整套思路就是一句话:先看整机,再锁进程,最后进内部。每一步都以前一步的结果为指引。

2.1 全局视角:load、CPU队列、内存和swap

排查的开端永远是uptime和vmstat。uptime看load average三个数值,注意这三个数字分别对应1分钟、5分钟、15分钟的平均负载,如果1分钟远高于15分钟,说明负载是刚刚被压起来的;如果三个都很高,说明系统已经持续过载很久了。

vmstat 1 5是每个性能测试者都应该烂熟于心的命令。它的输出里我最关心四列:

  • r:运行队列中的进程数。超过CPU核数,说明CPU已经饱和。
  • b:不可中断睡眠的进程数。这个数字一直不为0,往往伴随IO瓶颈。
  • si/so:swap换入换出。只要这两个数字长期非零,内存大概率吃紧,性能会断崖式下跌。
  • us/sy:用户态和内核态CPU占比。sy过高时,要怀疑系统调用频繁、锁竞争或者上下文切换过多。

内存层面,free -h里的available比used更有参考价值。used很大不代表内存不够,因为Linux会尽量把空闲内存用作page cache,而available才是"在不触发swap的情况下还能分配多少内存"的真实答案。

2.2 进程视角:谁在消耗资源,一查便知

全局指标不正常时,下一步就是定位到具体进程。top按CPU排序是第一选择,但我更喜欢在排查时用这两条命令:

  • 按内存排序找可疑进程:ps aux --sort=-rss | head -20
  • 持续观察单进程CPU和内存:pidstat -u -r -p <PID> 1

pidstat的-r参数会输出进程的RSS和VSZ,这是观察内存泄漏最趁手的工具之一。RSS(驻留内存)是进程实际占用的物理内存,VSZ(虚拟内存)是进程声明的地址空间大小。内存泄漏观察的核心指标是RSS,它持续单边上涨且不回落,基本就是泄漏的信号。

如果怀疑上下文切换过高,pidstat -w 1可以看到进程每秒的自愿和非自愿上下文切换次数,非自愿切换暴涨通常意味着CPU资源被争抢,配合mpstat -P ALL 1还能确认是不是只有部分核被打满。

2.3 内存纵深:从/proc文件系统看进程内部构造

当定位到某个进程内存异常后,top已经满足不了需求了,需要钻到/proc文件系统里看细节。我最常用的三件套:

  • cat /proc/<PID>/status:一次性看到VmRSS、VmSize、VmPeak等关键内存字段。VmPeak是进程历史最高虚拟内存,如果VmPeak一直小于VmSize,说明地址空间在膨胀。
  • pmap -x <PID>:把进程的地址空间映射摊开来看,哪一段在涨一目了然。[heap]不断增长,十有八九是堆内存泄漏。
  • cat /proc/<PID>/smaps:比pmap更细,能看到每一段映射的RSS、PSS和私有内存大小,适合排查第三方库造成的隐性内存增长。

这些信息结合起来,基本就能回答"泄漏到底发生在堆里、mmap区还是共享库里"。这一步做完,你已经有足够的理由去开valgrind或者ASan了。

2.4 别让IO和网络背锅

内存泄漏排查过程中有个很常见的误判:RSS上涨,结果free显示cache也在涨,判断给搞混了。区分要点在于,page cache是内核回收机制的一部分,内存压力大时会自动回吐,而进程RSS只增不减才是真正的报警信号。可以用iostat -x 1看%util和await来排除IO瓶颈,用ss -s看socket连接数量来排除连接泄漏。连接泄漏和内存泄漏经常同时出现,尤其是一些长连接服务,排查的时候我会把socket数量也纳入监控。

3. 负载模拟不是堆参数:工具选型与压力模型设计

负载模拟这件事,看起来简单,实际坑很多。很多人上来就是一条stress命令把CPU打满,结果压测机先挂了,被测服务还没出汗。负载模拟的价值在于"可控、可重复、可观测",而不是越猛越好。

3.1 造负载工具怎么选:各有所长,按需取用

市面上的负载工具不少,我按场景给大家分个类,都是自己用过的:

工具主要用途特点注意点
stress-ngCPU/内存/IO/系统调用全维度压力场景丰富,支持几百种stress参数多,需要读文档
lookbusy精确控制CPU占用百分比适合模拟固定负载需要编译安装
sysbench压测CPU/内存/线程/数据库自带测试模式,结果清晰对服务级压测支持弱
ab、wrkHTTP接口压测直接压业务接口,适合容量测试只覆盖网络和业务层
自写脚本模拟真实业务读写模式最灵活,能还原用户行为开发和维护成本高

压力测试一个完整的服务时,wrk这类HTTP压测工具是首选,因为它直接打业务接口,能反映真实容量。但如果要单独验证"系统在CPU打满时服务是否还稳定",stress-ng则更合适,因为它能把某一类资源精确打满,方便做单资源瓶颈的对照实验。

3.2 我自己常用的压力模型:双维度齐压

稳定性测试时,我一般不会只压CPU,而是CPU和内存一起上,这样才能暴露资源竞争问题。举个例子:

# 压满4个CPU核,同时分配2个虚拟内存stress进程,每个吃512MB stress-ng --cpu 4 --vm 2 --vm-bytes 512M --timeout 600s --metrics-brief

这条命令的含义是:4个CPU压力线程把核占满,2个虚拟内存进程各占512MB,持续10分钟,结束时打印统计摘要。它模拟的是一个"CPU密集且内存占用偏高"的混合场景,很适合观察系统在双重压力下的表现。

如果你需要更精确的CPU占用率,比如"让CPU占用维持到60%而不是100%",stress-ng不太好控制,lookbusy这时候更顺手:

# 一个进程占用2个CPU核,利用率为60% ./lookbusy -c 2 -q 60

3.3 压测时最容易踩的坑:把压测机压死

负载模拟有个很现实的教训:加压之前,先确认压测机和被测机不是同一台。我曾经见过有人用测试机跑stress-ng --cpu $(nproc),结果把用来采集数据的压测机直接压到卡死,监控数据全丢。

更隐蔽的坑是压力进程本身变成瓶颈。有些同学压测时用Python写了循环来模拟请求,结果Python解释器先把CPU吃完了,被测服务倒是很闲。所以HTTP压测尽量用wrk这类C实现的高性能工具,避免压测端自己先成为瓶颈。

此外,压力要阶梯式往上加。比如并发从50到100再到200,每档跑5分钟,观察指标变化趋势。一次性上满并发,只能看到"挂了"这一个结果,看不到资源水位是怎么一步步逼近极限的,对调优没有帮助。

4. 内存泄漏排查三连击:判泄漏、缩范围、挖根因

内存泄漏排查是个经典命题,我把它拆成三步:判定是不是真的泄漏,缩小泄漏发生的范围,挖到具体的根因代码。这三步,一步都不能跳。

4.1 判定泄漏:别把page cache和RSS混为一谈

第一步最难,因为"内存涨了"不一定是泄漏。我见过最典型的误判是:进程RSS没怎么动,但是free显示used在涨,一查全是cache。这里必须分清两个概念:

  • page cache / buffer cache:内核用来缓存文件数据的,属于可回收内存,内存不够时内核会自动回吐,压测结束后会逐步降下来。
  • 进程RSS:进程真实占用的物理内存,除非进程自己释放,否则降不下来。

所以判定泄漏的正确姿势是盯pidstat -r输出的RSS列,或者盯/proc/<PID>/status里的VmRSS。如果VmRSS在稳定负载下呈现单边上涨趋势,且压测结束后没有回落,这就是泄漏的强信号。另外,vmstat里的si列如果频繁出现非零值,说明系统已经在靠swap硬撑了,这往往是泄漏导致的次生灾害。

为了判定更准确,我会连续采集至少30个采样点,画一个RSS随时间的趋势。数据量不大,用shell就能算:

for i in $(seq 1 30); do grep VmRSS /proc/<PID>/status | awk '{print $2}' sleep 10 done

取到的数字如果一路向上不带回头,基本可以确认泄漏存在。

4.2 缩小范围:pmap一出手,就知道有没有

确认泄漏之后,别急着开valgrind,那玩意儿一开整个进程慢十倍,不是排查的第一步。先用pmap把范围缩到"哪一段内存区域在涨"。

# 连续拍两次pmap,对比哪段内存增长最凶 pmap -x <PID> > pmap_1.txt sleep 300 pmap -x <PID> > pmap_2.txt diff pmap_1.txt pmap_2.txt

如果两次对比中[heap]段明显变大,说明是堆内存泄漏,问题大概率出在业务代码的动态分配上。如果是某个.so映射段在涨,那就要去看对应的第三方库是不是有静态缓存。如果是anon段在涨但不在heap里,则要考虑mmap分配的对象。

pmap还有个兄弟叫smaps,能按页看到更细的RSS分布,排查共享库导致的泄漏时我会用grep -A5 heap /proc/<PID>/smaps看堆段详情。但日常场景pmap够用了。

4.3 挖根因:valgrind、ASan和gdb的配合

范围缩小到heap之后,就该上重武器了。这里我按推荐顺序讲:

第一推荐:AddressSanitizer(ASan)。用-fsanitize=address -fno-omit-frame-pointer重新编译目标程序,然后在测试环境跑同样的负载,ASan会在每一次非法内存访问和泄漏发生时直接打印出分配点的调用栈。它比valgrind快得多,适合集成到CI里。

gcc -fsanitize=address -fno-omit-frame-pointer -g -o app app.c

第二推荐:valgrind。它适合"已经定位到大概模块,但不确定泄漏点"的场景。用--leak-check=full能报告"definitely lost"和"indirectly lost"两类泄漏,后者往往是被容器或资源管理器持有的对象。

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./app

注意valgrind会让程序慢10倍以上,所以一定要用最小化复现用例去跑,比如缩短压测时长、减小数据量,只要能在几分钟内触发一次泄漏就行。等确认了泄漏调用栈,再用gdb attach到线上进程或者分析core dump,定位到具体的业务代码逻辑。

4.4 修复后的验证:不见兔子不撒鹰

修复之后不能光看"程序不崩了",得用同等的压力场景重新跑一遍,确认RSS曲线走平。这里有一个经验值:修复后的稳定性测试持续时长至少要跟原来发现泄漏的时长一样,甚至更长,因为有些泄漏是慢性的,前两小时看不出来。

我的验证标准很简单:RSS在测试期间随业务负载正常波动,压测结束后20分钟内回落到接近初始水平,就认为修复有效。

5. 完整复盘:一个模拟服务的内存泄漏排查全记录

理论讲多了容易飘,这一章我完整记录一次真实的排查过程,场景和数据做了脱敏,但思路完全是实战思路。这个案例我留了很久,每次带新人都会拿出来讲。

5.1 事发场景:长稳压测第6小时,服务开始掉点

某接口网关进程(就叫它A服务吧),压测计划是200并发持续12小时。前5小时一切正常,从第6小时开始监控面板上RSS从400MB一路爬升,到第7小时已经摸到1.6GB,容器健康检查开始告警。初步判断是内存泄漏,但从哪里漏的,毫无头绪。

我当时没有立刻重启服务,因为重启虽然能止损,但也会把最有价值的现场丢掉。正确做法是保留现场,继续采集数据,同时准备排查。

5.2 逐步排查链路:每一步都指向下一个动作

第一步:确认系统级内存压力。执行free -h和vmstat 1 5,发现si偶发非零,说明系统已经出现部分swap,内存压力不是错觉。再看free的available,已经从压测开始时的7GB跌到不到1.5GB。到这里,"内存紧张"实锤。

第二步:锁定进程。执行ps aux --sort=-rss | head -10,A服务排在第一,RSS约1.6GB,远超同环境下的其他进程。此时还没法确定是A服务泄漏,还是它的某个子模块,但嫌疑已经高度集中。

第三步:用pidstat -r -p <A服务PID> 1连续观察10秒,RSS在1.6GB附近缓慢爬升,每分钟大约涨20MB。这个速率如果持续下去,再过几个小时就会OOM。

第四步:pmap -x <PID>采集两次,间隔10分钟。对比发现[heap]段从900MB涨到1.1GB,其他映射段基本稳定。于是问题收敛为"堆内存持续增长",也就是A服务业务代码或基础库里有对象分配后没有被释放。

第五步:用valgrind跑最小化复现。因为直接对压测中的进程挂valgrind不现实,我复制了一份代码分支,减小输入数据量并缩短运行时间,几十秒内就能重复触发泄漏路径。结果指向某内存池组件,它在每次请求完成后没有归还临时对象。

5.3 修复和验证:压测要回归才叫完事

修复手段这里不细说,核心就是补上对象归还逻辑。但验证这段值得多讲几句:我重新跑了12小时长稳测试,采集RSS趋势,前2小时有些正常波动,之后一路走平,压测结束后很快回落,swap也不再出现。这才敢说泄漏修复了。

那次之后我定了条规矩:任何涉及长连接、内存池或自定义缓存的改动,合并前必须通过至少8小时的稳定性压测,并且把RSS趋势图作为发布准入条件之一。

5.4 复盘中最值钱的三条经验

  • 现场比止损更重要:只要容器没挂,就先别重启,保留完整的进程现场,排查线索都在那里面。
  • 压测日志里打内存点:A服务后来加了一个每5分钟打印一次RSS和heap大小的日志,下次再出问题,不需要抓现场也能还原涨势。
  • 小流量复现永远比大流量硬扛高效:valgrind慢得吓人,但只要你把数据量降下来,泄漏路径还是在,速度反而比大流量压测快得多。

6. 最后补一个我平时会用的排查小脚本

说了这么多,最后分享一个我平时排查内存泄漏时直接落地的脚本。它做的事很简单:每隔一段时间自动采样指定进程的RSS和系统内存水位,把结果追加到CSV文件里,方便后面画趋势图。

#!/bin/bash # usage: ./mem_watch.sh <PID> <interval_seconds> <output.csv> PID=$1 INTERVAL=${2:-10} OUT=${3:-mem_trend.csv} echo "timestamp,rss_kb,vmrss_kb,mem_available_kb" > "$OUT" while kill -0 "$PID" 2>/dev/null; do TS=$(date +%Y-%m-%d_%H:%M:%S) RSS=$(grep VmRSS /proc/$PID/status 2>/dev/null | awk '{print $2}') AVAIL=$(grep MemAvailable /proc/meminfo | awk '{print $2}') echo "$TS,$RSS,$AVAIL" >> "$OUT" sleep "$INTERVAL" done echo "process $PID exited, sampling stopped"

这个脚本不依赖第三方工具,在绝大多数Linux发行版上都能直接用。采样时间间隔别设太短,不然CSV会非常大,10秒到30秒是比较合理的区间。拿到CSV之后,用Excel或者任何脚本语言都能快速出趋势图,RSS是不是单边上涨一目了然。

我自己的习惯是把这个脚本放在压测机上的/opt/scripts/mem_watch.sh,每次压测前启动,压测后直接归档CSV文件。排查问题的时候,这些历史数据比任何截图都有说服力。如果你还没建立这个习惯,我建议下一次压测就开始用起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 3:17:04

JPA映射MySQL JSON字段:从转换器到原生类型的最佳实践

最近在一个项目里处理“jpa 映射 mysql json 对象”这个需求&#xff0c;刚开始以为只是给实体加个字段的事&#xff0c;结果真做起来才发现&#xff0c;这里面的坑比想象中多不少。这个主题主要解决一个实际问题&#xff1a;MySQL 从 5.7 开始原生了 JSON 类型&#xff0c;但 …

作者头像 李华
网站建设 2026/10/21 3:17:02

ULN2003达林顿管阵列芯片介绍

概述ULN2003是单片集成高耐压、大电流达林顿管阵列&#xff0c;电路内部包含七个独立的达林顿管驱动单路。电路内部设计有续流二极管&#xff0c;可用于驱动继电器、步进电机等电感性负载。单个达林顿管集电极可输出500mA电流。将达林顿管并联可实现更高的输出电流能力。该电路…

作者头像 李华
网站建设 2026/10/21 3:16:36

Ultimate Vocal Remover 人声分离完整指南:4 步拿到干声与伴奏

Ultimate Vocal Remover 人声分离完整指南&#xff1a;4 步拿到干声与伴奏 【免费下载链接】ultimatevocalremovergui GUI for a Vocal Remover that uses Deep Neural Networks. 项目地址: https://gitcode.com/GitHub_Trending/ul/ultimatevocalremovergui 翻唱缺一版…

作者头像 李华
网站建设 2026/10/21 3:16:34

D3121地隔离放大器芯片介绍与应用

​一、产品概述D3121是一款专为消除地线噪声设计的双通道地隔离放大器&#xff0c;采用DIP8/SOP8/SIP8封装&#xff0c;广泛应用于车载音响系统等对电磁干扰敏感的场景。其核心优势在于&#xff1a;​低噪声&#xff1a;输出噪声电压仅3.5μVrms&#xff08;典型值&#xff09;…

作者头像 李华
网站建设 2026/10/21 3:15:47

VS Code高效开发指南:工作区、多光标与调试技巧

1. 第一次使用就该搞懂的工作区概念&#xff1a;文件夹与.code-workspace很多人上手VS Code&#xff0c;习惯就是点击"文件-打开文件夹"&#xff0c;把一个项目目录拖进窗口就开始写代码。这个流程对单仓库项目完全没问题&#xff0c;直到你开始同时维护多个仓库、前…

作者头像 李华
网站建设 2026/10/21 3:15:37

STM32 LCD1602字符屏极简驱动库:GPIO模拟时序详解

简介&#xff1a;面向STM32开发者的LCD1602字符液晶驱动基础库&#xff0c;压缩包内仅含2个文件&#xff08;1个C源文件与1个头文件&#xff09;&#xff0c;体积约1KB&#xff0c;轻量易集成。C文件封装了LCD1602初始化、字符显示、清屏等核心操作&#xff0c;头文件则提供函数…

作者头像 李华