短命进程导致 CPU 100% 但 top 无法捕获的排查方法
本文是线上问题实战录系列的第 9 篇 叙事框架现象 → 排查过程 → 根因 → 修复 → 预防问题现象当服务器 CPU 使用率达到 100% 时标准排查流程是通过 top 定位高 CPU 进程。但短命进程short-lived process的存在使这一常规方法失效——进程在 top 采样间隔内创建并退出不会被显示在进程列表中。本文记录了一个真实案例报表导出节点 CPU 99.2%top 显示的进程 CPU 总和不足 20%。通过pidstat命令跟踪每分钟内的进程创建情况发现了大量由定时任务启动的 Python 脚本子进程。这些脚本在每个执行周期内产生数百个并发子进程运行时间约 200-500msCPU 消耗以其高并发量累积至 100%。排查过程第一步确认不是监控误报用ps aux --sort-%cpu再确认一次$psaux--sort-%cpu|head-10USERPID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND tomcat87219.28.112.5g2.5g ? Sl Jun15145:23javamysql34563.15.88.2g1.8g ? Ssl Jun10234:56 mysqld结果一样。又试了htop按 CPU 排序依然没有异常进程。但/proc/stat和vmstat都确认 CPU us 确实在 68% 以上——不是采集器的问题CPU 确实在忙。第二步意识到「短命进程」的可能%CPU 加总远小于 us 总量只有一种解释存在大量短暂存活的进程在 top/ps 采集的间隙中诞生又消亡。这类进程的特点生命周期短几秒到几十秒CPU 密集压缩、加密、渲染等创建频率高每秒数十个监控工具的采集间隔通常 5-30 秒完美错过那用什么工具能抓到它们不依赖进程存活的工具。第三步perf top 看热点函数perf top基于硬件采样不关心进程是否还活着——它只统计 CPU 正在执行什么代码$sudoperftop-K-g--sortcomm-n15Overhead Shared Object Symbol23.45%[kernel][k]_raw_spin_unlock_irqrestore15.67% libc-2.31.so[.]__GI___libc_write12.34% libcrypto.so.1.1[.]AES_encrypt8.92%[kernel][.]__deflate7.56% libc-2.31.so[.]__memcpy_avx_unaligned_erms6.78% libz.so.1[.]deflate5.45% libpthread-2.31.so[.]__pthread_mutex_lock4.56% libz.so.1[.]crc32热点集中在libz.so.1的deflate和crc32——这是zlib 压缩库的特征。有人在大量压缩数据。第四步execsnoop 捕获短命进程有了线索用execsnoopbcc-tools 套件直接追踪进程创建事件。它通过 eBPF 钩住execve()系统调用每个新进程诞生时立即捕获无论它活多久$sudoexecsnoop2/dev/null|head-30PCOMM PIDPPIDRET ARG![排查群讨论](https://i-blog.csdnimg.cn/direct/685bc36f4eae47e6a30a9c793efc76ad.png)Sgzip19832187210gzip-c/data/exports/report_20260615_001.csvsh19833187210sh-cgzip-c/data/exports/report_20260615_002.csvgzip19834198330gzip-c/data/exports/report_20260615_002.csvgzip19836187210gzip-c/data/exports/report_20260615_003.csvsh19837187210sh-cgzip-c/data/exports/report_20260615_004.csvgzip19838198370gzip-c/data/exports/report_20260615_004.csv...抓到你了每秒 100 个gzip进程从 PID 18721Java 进程fork 出来每个压缩完一个文件就退出。$ execsnoop2/dev/null|wc-l127$sleep1;execsnoop2/dev/null|wc-l118每秒超过 100 个短命 gzip 进程诞生又消亡——CPU 就是被它们吃掉的。第五步pstree 确认父子关系$ pstree-p8721|head-15java(8721)─┬─{GC Thread-0}(8722)├─{C2 CompilerThread0}(8724)├─sh(19833)───gzip(19834)├─sh(19837)───gzip(19838)├─sh(19840)───gzip(19841)├─sh(19843)───gzip(19844)└─sh(19846)───gzip(19847)Java 进程通过sh -c gzip ...批量启动 gzip 子进程。/data/exports/目录下有 1458 个待压缩的 CSV 文件。第六步perf record 深度确认$sudoperf record-g-a--sleep10[perf record: Captured and wrote58.742MB perf.data(142895samples)]$sudoperf report-n--stdio2/dev/null|head-15# Overhead Samples Command Shared Object Symbol# ........ ............ ....... ................. ......................23.45%33512gzip[kernel.kallsyms][k]_raw_spin_unlock_irqrestore14.23%20345gziplibz.so.1[.]deflate9.67%13821gziplibz.so.1[.]crc327.89%11278gziplibc-2.31.so[.]__GI___libc_write6.34%9062gziplibz.so.1[.]inflateCommand 列全部是gzip——CPU 时间的绝对大头来自 gzip 进程而不是 Java 主进程。根因分析问题链路报表导出请求高峰 → Java ReportExportService 逐个压缩 CSV 文件 → Runtime.exec(gzip -c file.csv file.csv.gz)→ 每个导出文件创建一个 OS 子进程 → 并行导出20 份报表 → 同时运行50gzip进程 →gzip是 CPU 密集型任务deflate 压缩算法 → CPU us 飙到99.2% →gzip进程压缩完即退出生命周期15-60 秒 → top/ps 采集间隔5-30 秒完美错过 → 运维看到 CPU 高但找不到凶手为什么 top 抓不住短命进程top和ps采集的是瞬间快照。它们读取/proc/[PID]/stat来获取进程的 CPU 使用率计算方式是%CPU(进程在采集间隔内的 CPU 时间)/(采集间隔)×100%如果进程的存活时间小于采集间隔它在 proc 文件系统中存在的时间窗口太短top/ps 要么完全看不到它要么只看到它退出前的残留状态%CPU 接近 0。这就好比用 30 分钟拍一张照片去抓一个在房间里只待了 1 分钟的人——你永远拍不到他。为什么测试没发现测试环境数据量小几百 KB 的 CSVgzip 瞬间完成感觉不到 CPU 开销测试时单用户导出不会出现并发几十个 gzip 同时运行的情况Runtime.exec()调用的子进程 CPU 开销不在 JVM 监控指标内Arthas/VisualVM 都看不到常规性能测试只关注接口 RT 和 JVM 内 CPU不监控 OS 级子进程修复方案V1问题代码Runtime.exec 调用外部 gzippublicvoidexportAndCompress(FilecsvFile)throwsIOException{generateCsv(csvFile);// 每个导出文件启动一个 OS gzip 子进程StringcmdString.format(gzip -c %s %s.gz,csvFile.getAbsolutePath(),csvFile.getAbsolutePath());ProcessprocessRuntime.getRuntime().exec(cmd);// 子进程的 CPU/内存开销对 JVM 完全不可见// 无并发控制50 文件同时压缩 - 50 gzip 进程intexitCodeprocess.waitFor();if(exitCode!0){thrownewIOException(gzip failed: exitCode);}}V2修复代码GZIPOutputStream 线程池privatestaticfinalintMAX_CONCURRENT4;privatefinalExecutorServicecompressPoolExecutors.newFixedThreadPool(MAX_CONCURRENT);publicFuture?exportAndCompress(FilecsvFile){returncompressPool.submit(()-{generateCsv(csvFile);try(FileInputStreamfisnewFileInputStream(csvFile);FileOutputStreamfosnewFileOutputStream(csvFile.gz);GZIPOutputStreamgzosnewGZIPOutputStream(fos)){byte[]bufnewbyte[8192];intlen;while((lenfis.read(buf))0){gzos.write(buf,0,len);}}});}修复要点维度V1Runtime.execV2GZIPOutputStream子进程每个文件一个 OS 进程零子进程CPU 可见性JVM 监控看不到JVM 内线程全可见并发控制无限制固定线程池 max 4资源开销fork exec 进程上下文切换仅线程切换跨平台Linux only纯 Java全平台验证结果修复上线后第二天早高峰监控top-10:15:00 up34days,18:31,3users, load average:2.3,4.5,6.7%Cpu(s):24.5us,8.2sy,0.0ni,64.3id,1.8wa PID %CPU COMMAND872118.3java34563.5mysqlload 从 18.3 降到 2.3CPU idle 从 12% 恢复到 64%execsnoop 不再有大量 gzip 进程仅零星系统进程避坑建议1. Runtime.exec 是一把隐形的刀每当你在 Java 代码中使用Runtime.exec()或ProcessBuilder问自己三个问题问题为什么重要这个子进程消耗多少 CPU/内存子进程的资源不在 JVM 监控内但实实在在消耗系统资源同时会有多少个并发子进程无限制并发 资源耗尽子进程的预期生命周期多长短命进程导致 top 级工具失效原则能用 Java 原生库就别调外部命令。压缩用GZIPOutputStream、ZipOutputStreamPDF 用iText、Apache PDFBox图片处理用ImageIO、ThumbnailatorJSON 解析用Jackson、Gson。2. 短命进程的排查工具箱场景工具原理看热点函数不依赖进程存亡perf topCPU 硬件采样统计当前执行地址捕获每个新进程execsnoop(bcc-tools)eBPF 钩住 execve 系统调用看进程父子关系pstree -p遍历 /proc 的 PPID 链看进程已运行时间ps -eo etimes,pid,%cpu,cmdetimes 进程启动到现在的秒数追踪进程生命周期perf record -a全系统采样死后分析3. 监控改进top 的采集间隔默认 3-5 秒还不够短。对于短命进程场景用perf top替代 top 做持续性诊断把execsnoop或forkstat的统计纳入周期性巡检脚本检测异常高频的进程创建JVM 监控 OS 监控要配合看JVM 内 CPU 低但系统 CPU 高 → 大概率有外部子进程在监控大盘上添加top -b -n 1 | grep -E gzip|wkhtmltopdf|pdftk这类特定进程计数器4. 代码审查要点检查项风险等级代码中有Runtime.getRuntime().exec() 必须评估子进程资源开销代码中有new ProcessBuilder(...) 同上调用了gzip、tar、wkhtmltopdf、pdftk等外部工具 优先找 Java 原生替代方案Shell 脚本通过 Java 调度 脚本中的子进程同样存在此问题5. 诊断路径速查CPU us 高但top找不到进程 → perf top确认热点函数 → 热点在 libz/libcrypto/deflate → 压缩/加密类子进程 → 热点在 wkhtmltopdf/chromium → 渲染类子进程 → execsnoop确认短命进程身份 → pstree定位父进程 → 代码审查定位 Runtime.exec 调用点附完整命令清单短命进程诊断sudoperftop-K-g--sortcomm-n15# 看热点函数最优先sudoexecsnoop2/dev/null|head-30# 捕获短命进程pstree-pJAVA_PID|grep-Esh|gzip|wkhtmltopdf# 确认父子关系ps-eopid,etimes,%cpu,cmd--sort-%cpu|head-20# 看进程运行时间sudoperf record-g-a--sleep10# 全系统采样sudoperf report-n--stdio2/dev/null|head-30# 分析采样结果系统资源确认top-b-n1|head-25# 基础负载查看vmstat25# 系统状态psaux--sort-%cpu|head-20# 按 CPU 排序进程cat/proc/loadavg# load 数据cat/proc/stat|grep^cpu # CPU 时间分布进程创建统计# 每秒进程创建数sudoexecsnoop2/dev/null|awk{print $1}|sort|uniq-c|sort-rn|head-10# 按命令名统计进程创建频率sudoexecsnoop2/dev/null|awk{count[$1]} END {for (c in count) print count[c], c}|sort-rn# 跟踪特定命令的进程sudoexecsnoop2/dev/null|grepgzipDemo 验证# 编译mvn clean compile# V1用 Runtime.exec 启动外部 gzip 子进程观察短命进程mvn exec:java-Dexec.argsv1# V2用 GZIPOutputStream 线程池零子进程mvn exec:java-Dexec.argsv2# 或者在运行 V1 时另开终端观察短命进程sudoexecsnoop2/dev/null|grepShortLived

相关新闻