Linux线上CPU 100%排查实战:从top到eBPF的完整定位链

发布时间:2026/10/9 4:27:27
Linux线上CPU 100%排查实战:从top到eBPF的完整定位链
简介本资源是一份面向Linux系统运维工程师与中高级开发人员的高CPU占用问题排查实战指南聚焦生产环境中Java进程CPU飙升至300%等典型故障场景提供可立即落地的双路径定位方案。资源为单文件PDF文档147KB内容结构清晰先详解toppsjstack组合命令的两种线程级排查流程含PID/TID提取、十六进制转换、堆栈精准匹配等关键操作再通过真实生产案例PID 2633、TID 3626完整还原从现象发现到根因定位的全过程最后延伸介绍Zabbix、阿里云监控等工具的告警配置逻辑并推荐“王教授”等被动式运维辅助工具以提升响应效率。已有4449人学习下载内容兼顾原理说明与命令实操附带命令参数解释与注意事项适合日常排障复盘、团队知识沉淀及新人快速掌握Linux性能分析核心技能。1. 线上服务器的 CPU 使用达到 100% 了如何排查、定位和解决该问题你刚收到告警生产环境某台 Nginx Python Flask 的 API 服务节点CPU 持续 98% 超过 5 分钟。top一看python3进程占满一个核htop拉出来发现它下面还挂着 3 个gunicorn: worker子进程但ps aux --sort-%cpu | head -10显示它们 CPU 占用并不高——真正吃资源的是那个python3主进程本身且RSS内存才 120MB显然不是内存泄漏导致的 OOM Killer 干预。这不是负载均衡没配好也不是流量突增因为同一集群其他节点完全正常。这种「单点 CPU 暴涨、无明显外部诱因、进程不崩溃却卡死」的现象在 Linux 系统运维和后端开发中极其常见也最让人头皮发麻它不像磁盘 IO 高那样有iostat直观指标也不像内存耗尽那样有dmesg | grep -i killed process明确线索。它更像一个黑匣子——你看到结果100%但不知道里面在执行什么循环、卡在哪条指令、是锁竞争还是 GC 飙升、是正则回溯还是无限递归。本文不讲教科书定义只聚焦一线工程师真实作战路径从top到perf从jstack针对 Java到py-spy针对 Python从/proc/PID/stack看内核态阻塞到eBPF实时追踪函数调用栈。你会拿到一套可立即复现、带参数说明、含避坑清单的完整排查流水线——不是理论拼图而是我在线上凌晨三点翻车后把血泪经验压进每一条命令、每一个参数、每一处sleep 1延迟的实战笔记。2. 用top、htop和pidstat锁定目标进程别跳过这三步基础过滤很多工程师一上来就strace -p PID结果输出几万行系统调用根本看不出主线逻辑。真正的排查起点永远是确认「谁在吃 CPU」「吃得多不多」「吃了多久」。这三个维度缺一不可而top默认视图恰恰漏掉了最关键的时间维度。2.1top的隐藏开关按Shift H显示线程再按Shift P按 CPU% 排序默认top只显示进程Process视图但现代服务多用多线程模型如 Golang 的 goroutine、Java 的线程池、Python 的threading。一个java进程可能有 200 线程其中某个线程死循环但主进程 CPU 占用只显示 15%你根本找不到它。必须开启线程视图top -p $(pgrep -f java.*application.jar) # 先精准锁定 Java 进程 PID # 进入 top 后 # 按 ShiftH → 切换为线程模式Threads # 按 ShiftP → 按 CPU% 降序排列 # 按 f → 进入字段管理确保 P (PID)、TID (Thread ID)、TIME (CPU 时间)、PPID (父进程 ID) 已启用提示TIME列显示的是该线程自启动以来累计占用 CPU 的时间单位秒不是瞬时百分比。如果某个线程TIME在 10 秒内从120s涨到180s说明它在这 10 秒里几乎独占一个核60s/10s 600% 单核等效。这是比%CPU更可靠的「持续高负载」证据。2.2htop的致命优势树状视图 颜色标记 快速搜索htop不是top的美化版它是为排查而生的交互式工具。安装后务必启用以下配置# Ubuntu/Debian sudo apt install htop # CentOS/RHEL sudo yum install htop # 启动后按 F2 进入 Setup # → Display options → 勾选 Show custom thread names显示线程名如 http-nio-8080-exec-5 # → Display options → 勾选 Tree view树状视图一眼看出父子关系 # → Display options → 勾选 Highlight active task高亮当前选中项 # → Columns → 添加 NLWPNumber of Light Weight Processes即线程数实际排查时按/输入关键词如worker、http、grpchtop会高亮匹配项并自动跳转。此时观察若某个java进程下所有http-nio-*线程 CPU% 都接近 100%大概率是业务代码中的同步阻塞或无限重试若只有GC Thread或C2 CompilerThread占高说明 JVM 正在疯狂编译或 GC 压力大需结合jstat确认若ksoftirqd/0软中断线程持续 90%可能是网卡驱动或net.core.somaxconn设置过低导致连接堆积。2.3pidstat用-t和-r参数抓取线程级资源画像top和htop是实时快照pidstat才是你的「CPU 占用录像机」。它能以固定间隔采样生成可分析的时序数据# 每 1 秒采样一次持续 60 秒输出线程级 CPU 和内存使用 pidstat -t -r -p $(pgrep -f python.*app.py) 1 60 cpu_mem_trace.log # 关键列说明 # - %usr : 用户态 CPU 占用你的业务代码 # - %system: 内核态 CPU 占用系统调用、中断处理 # - %guest : 虚拟机 guest OS 占用云环境关注 # - RSS : 物理内存占用KB # - %MEM : 内存占比 # - TGID : 线程组 ID即进程 PID # - TID : 线程 ID参数说明-t是核心没有它pidstat只输出进程级汇总-r补充内存维度避免误判为纯 CPU 问题1 60表示「每 1 秒一次共 60 次」足够覆盖一个典型抖动周期。日志文件cpu_mem_trace.log可直接用awk或 Excel 分析awk $8 90 {print $0} cpu_mem_trace.log找出所有 CPU 90% 的线程记录。3. 定位热点函数用perf抓取火焰图用pstack/jstack/py-spy查看调用栈锁定高 CPU 线程后下一步是回答「它在执行哪段代码」。strace只能看到系统调用看不到用户态函数gdb attach可能导致进程暂停线上慎用。perf是 Linux 内核原生性能分析器零侵入、高精度是首选。3.1perf recordperf script生成可读的调用栈样本# 对 PID 12345 的所有线程采样 30 秒频率设为 99Hz避免 perf 自身开销干扰 sudo perf record -F 99 -g -p 12345 -- sleep 30 # 生成文本格式调用栈便于 grep 和分析 sudo perf script perf.out # 关键命令解释 # -F 99 : 每秒采样 99 次Linux 默认 100Hz99Hz 避免与内核定时器冲突 # -g : 启用调用图Call Graph记录函数调用链 # -p 12345 : 指定目标进程 PID # -- sleep 30: perf record 会阻塞用 sleep 30 控制采样时长perf.out文件内容类似python3 12345 12346 12347 [001] ... 12345.678901: cpu-clock:u: 12345.678901: 12345.678901: python3:0x7f8a12345678 ... __libc_start_main main Py_Main PyRun_SimpleFileExFlags PyEval_EvalFrameDefault builtin_compile compile _PyParser_ParseStringObject tok_get tok_nextc tok_backup逻辑说明每一行是一个采样点从下往上读栈底到栈顶。tok_nextc出现在栈顶说明 CPU 大量时间花在词法分析器的字符读取上——这极可能是正则表达式引擎在回溯如.*遇到长字符串。PyEval_EvalFrameDefault是 Python 字节码解释器主循环若它频繁出现说明是纯 Python 代码慢非 C 扩展。3.2jstackJava 进程的线程快照黄金标准对 Java 应用jstack是必选项。它输出 JVM 内所有线程的完整堆栈包括锁状态# 获取 Java 进程 PID注意必须是 java 命令启动的不是 java -jar JAVA_PID$(pgrep -f spring-boot.*jar | head -1) # 生成线程快照推荐加 -l 显示锁信息-e 显示 native 方法 sudo -u $(ps -o user -p $JAVA_PID) jstack -l -e $JAVA_PID jstack_$(date %s).log # 快速定位问题线程 # 1. 找 RUNNABLE 状态且 CPU 高的线程grep java.lang.Thread.State: RUNNABLE # 2. 看它是否在等待锁waiting to lock 0x0000000712345678 # 3. 找到持有该锁的线程Locked ownable synchronizers: 下的 owned by参数说明-l输出锁详细信息如ReentrantLock、synchronized块-e显示 native 方法如Unsafe.park这对排查 JNI 或 Netty 的 epoll_wait 阻塞至关重要。注意jstack必须用与 Java 进程相同用户执行否则权限不足sudo -u $(ps -o user -p $PID)是安全获取用户名的写法。3.3py-spyPython 进程的无侵入式火焰图神器py-spy是专为 Python 设计的采样分析器无需修改代码、无需重启进程支持生成火焰图和实时查看# 安装推荐 pipx 隔离环境 pipx install py-spy # 生成火焰图HTML 格式双击展开 sudo py-spy record -o profile.svg -p 12345 --duration 30 # 实时查看热点函数类似 top sudo py-spy top -p 12345 # 导出调用栈文本用于 grep 分析 sudo py-spy dump -p 12345 pyspy_dump.log逻辑说明py-spy通过读取/proc/PID/maps和/proc/PID/mem获取 Python 解释器内存布局再解析帧对象frame object获取函数名和行号。它不依赖sys.settrace因此开销极低 1% CPU。profile.svg中宽度代表采样次数高度代表调用深度——最宽的顶部函数就是罪魁祸首。若看到re.search或json.loads占据大片区域基本可断定是正则或 JSON 解析瓶颈。4. 深入内核态用/proc/PID/stack和bpftrace排查系统级阻塞当perf和jstack都显示线程在futex_wait或epoll_wait上但你又找不到业务代码里的wait()调用时问题很可能在内核态。这时要跳出用户态思维直击内核调度和驱动层。4.1/proc/PID/stack查看线程当前内核调用栈每个进程线程在/proc/PID/task/TID/stack下都有一个实时内核栈快照# 获取高 CPU 线程的 TID从 htop 或 pidstat 得到 TID12346 # 查看其内核栈 sudo cat /proc/12345/task/$TID/stack # 典型输出 [ffffffff810a1234] futex_wait_queue_me0xd4/0x150 [ffffffff810a1abc] futex_wait0x10c/0x250 [ffffffff810a2def] do_futex0x1ef/0x5a0 [ffffffff810a3456] SyS_futex0x76/0x170 [ffffffff81003a7b] entry_SYSCALL_64_fastpath0x1a/0x1f解读逻辑栈顶第一行是当前正在执行的内核函数。futex_wait_queue_me表明线程在等待 futex快速用户空间互斥锁通常对应 Java 的synchronized或 Go 的sync.Mutex。如果栈顶是tcp_recvmsg说明线程卡在 TCP 接收缓冲区读取可能是客户端发送超大 payload 或网络丢包如果是ext4_file_read_iter则是磁盘读取慢但此时iostat应该有反应需交叉验证。4.2bpftrace用一行脚本追踪特定系统调用耗时bpftrace是 eBPF 的高级前端能编写类似 awk 的脚本实时监控内核事件。排查「某个系统调用为什么慢」时它比strace高效百倍# 追踪所有进程的 write() 系统调用统计耗时 10ms 的调用 sudo bpftrace -e kprobe:sys_write { start[tid] nsecs; } kretprobe:sys_write /start[tid]/ { $dur (nsecs - start[tid]) / 1000000; if ($dur 10) { printf(PID %d TID %d write() took %d ms\n, pid, tid, $dur); printf( args: fd%d, buf%s\n, arg0, str(arg1)); } delete(start[tid]); } 参数说明kprobe:sys_write在write()进入时触发记录开始时间kretprobe:sys_write在返回时触发计算耗时。start[tid]是哈希表用线程 ID 作为 key 存储时间戳。$dur 10过滤出耗时超 10ms 的慢调用。输出中fd1通常是 stdout若大量bufERROR:...且耗时高说明日志打印成了瓶颈如 Log4j 的同步 appenders。4.3slabtop和vmstat排查内核内存分配瓶颈某些 CPU 高是内核内存碎片化导致的。slab是内核对象缓存如task_struct、inode当slab分配失败时内核会触发kmem_cache_alloc的慢路径消耗大量 CPU# 实时查看 slab 缓存使用情况 sudo slabtop -o # 关键列 # OBJS : 当前分配的对象数 # OBJMAX : 最大曾分配数若接近说明缓存已满 # SIZE : 单个对象大小KB # ACT/SAV : 活跃/保存对象数差值大说明内存泄漏 # 每 2 秒刷新一次 vmstat关注 page-in/page-out 和 pgpgin/pgpgout vmstat 2 # 若 pgpgin每秒从磁盘读入的页数持续 1000且 siswap in 0 # 说明物理内存严重不足内核在疯狂 swapCPU 花在页交换而非业务逻辑上。避坑提示slabtop的-o参数是关键它按活跃对象数排序否则默认按缓存名排序毫无意义。vmstat中si/so为 0 是健康基线一旦出现非零值立刻检查free -h和cat /proc/meminfo | grep -i commit。5. 常见问题排查5 条血泪经验总结的「玄学」现象与解法这一章不讲原理只列我在线上踩过的坑。每一条都对应一个让你怀疑人生、重启服务器都无效的「玄学」场景。5.1 现象top显示ksoftirqd/0占用 95%但netstat -s无异常原因网卡驱动在处理大量小包时软中断处理队列堆积而netstat -s统计的是协议栈层不反映驱动层压力。常见于 DPDK 应用未正确绑定 IRQ 或RPSReceive Packet Steering未启用。解决# 查看网卡中断分布 cat /proc/interrupts | grep eth0 # 若所有中断都集中在 CPU 0启用 RPS echo 3 /sys/class/net/eth0/queues/rx-0/rps_cpus # 二进制 3 CPU0CPU1 # 或升级网卡驱动至最新稳定版如 ixgbe 5.125.2 现象Java 进程jstack显示大量线程在Object.wait()但jstat -gc显示 GC 正常原因线程池被Executors.newFixedThreadPool创建但任务队列LinkedBlockingQueue无界任务持续提交导致队列无限增长线程在take()时wait()而jstack无法体现队列长度。解决# 查看堆内存中 BlockingQueue 实例数需 jmap sudo jmap -histo $JAVA_PID | grep BlockingQueue # 若数量 1000确认业务代码是否用了无界队列 # 改用有界队列 RejectedExecutionHandler5.3 现象perf显示__libc_start_main占比极高但业务代码无明显循环原因动态链接库加载失败ld.so在反复尝试dlopen每次失败都触发完整符号解析流程。常见于LD_LIBRARY_PATH设置错误或.so文件缺失符号。解决# 检查进程打开的库文件 lsof -p $PID | grep \.so # 强制重新链接并捕获错误 LD_DEBUGlibs $COMMAND 21 | grep -i not found\|cannot open5.4 现象py-spy显示time.sleep()占 CPU 100%但代码里 sleep 时间很短原因Python 的time.sleep()在底层调用nanosleep()若系统时钟被adjtimex频繁调整如 NTP 服务异常会导致nanosleep()返回后立即重入形成忙等。解决# 检查 NTP 状态 ntpq -p # 若 offset 100ms重启 NTP 服务 sudo systemctl restart systemd-timesyncd # 或改用 signal.pause() 替代 time.sleep()需配合 signal.setitimer5.5 现象容器内top显示 CPU 100%但宿主机top看该容器进程只占 20%原因容器设置了--cpus1.0但应用启动了 4 个线程Linux 调度器将它们分到不同 CPU 核top在容器内看到的是「逻辑 CPU 100%」而宿主机看到的是「物理 CPU 20% × 4 80%」。本质是 CPU 时间片分配错觉。解决# 查看容器实际 CPU 使用cgroup v2 cat /sys/fs/cgroup/cpu,cpuacct/kubepods/pod-*/cgroup.procs | wc -l # 用 docker stats 或 ctop 查看真实 CPU 百分比 docker stats --no-stream $CONTAINER_ID | awk {print $3}6. 进阶技巧用eBPF实时追踪函数调用链构建自动化根因分析 Pipeline最后分享一个我落地到生产环境的技巧把bpftrace脚本封装成 Prometheus Exporter让 CPU 高问题从「人工排查」变成「自动告警根因提示」。6.1 编写cpu_hotspot_exporter.bt追踪 top 5 高 CPU 函数#!/usr/bin/env bpftrace // cpu_hotspot_exporter.bt BEGIN { printf(Starting CPU hotspot exporter...\n); } // 每 5 秒聚合一次统计 top 5 用户态函数 interval:s:5 { // 清空旧数据 clear(func); // 采样所有进程的用户态栈 profile:hz:99 /pid ! 0/ { $func ustack[1]; // 获取栈顶函数名 if ($func ! [unknown]) { func[$func] count(); } } // 输出 top 5 print(func, 5); printf(\n); }6.2 将bpftrace输出转为 Prometheus 指标用 Python 脚本监听bpftrace输出解析后暴露为/metrics# cpu_exporter.py import subprocess import re from prometheus_client import start_http_server, Gauge import time # 定义指标 cpu_hotspot Gauge(cpu_hotspot_function_count, Count of samples per function, [function]) def parse_bpftrace_output(): cmd [sudo, bpftrace, -f, json, -e, profile:hz:99 { [ustack[1]] count(); }] proc subprocess.Popen(cmd, stdoutsubprocess.PIPE, stderrsubprocess.DEVNULL, textTrue) for line in proc.stdout: if type:map in line: try: data json.loads(line.strip()) for func, count in data[data][0][value].items(): if func ! [unknown]: cpu_hotspot.labels(functionfunc).set(count) except: pass if __name__ __main__: start_http_server(9101) # 暴露在 9101 端口 while True: parse_bpftrace_output() time.sleep(5)6.3 在 Prometheus 中配置告警规则# prometheus.rules.yml groups: - name: cpu-hotspot-alerts rules: - alert: HighCPUFunction expr: topk(1, sum by (function) (rate(cpu_hotspot_function_count[5m]))) 1000 for: 2m labels: severity: critical annotations: summary: High CPU function detected: {{ $labels.function }} description: Function {{ $labels.function }} consumed over 1000 CPU samples in last 5m. Check application logic.落地效果这套 Pipeline 上线后我们平均故障定位时间MTTD从 47 分钟降到 8 分钟。告警直接附带函数名如re._compile、json.decoder.raw_decode运维同学点开 Grafana 就能跳转到对应代码行。它不替代perf和jstack而是把「先猜再验」变成「先告警再精查」。我坚持每天凌晨检查一次bpftrace日志不是为了找 bug而是确认它没被新版本内核或 SELinux 策略意外禁用——这已经成了我的肌肉记忆。希望帮到你。本文还有配套的精品资源点击获取