🏆 本文收录于 《滚雪球学 Ubuntu》 专栏。 本专栏面向有一定计算机基础,但尚未系统学习 Linux / Ubuntu 的读者,采用“滚雪球式学习法”:先装好、再会用、再理解、再优化、再实战,带你从第一次进入 Ubuntu 桌面 / 终端开始,逐步掌握 Ubuntu 的日常使用、命令操作、软件安装、系统配置、开发环境搭建与服务器部署能力。 🎯 适合人群: Linux 初学者、Ubuntu 新手、计算机专业学生、后端 / 运维 / 测试入门同学、想搭建 Linux 开发环境的开发者、准备学习服务器部署的同学,以及想从 Windows 过渡到 Ubuntu 工作流的用户。
从“会打开终端”到“能管理系统”,从“复制命令”到“理解 Linux 思维”,这一次,我们不零散学,而是一路滚雪球! 🎉 限时福利:当前专栏活动中,一次订阅,终身阅读,后续更新章节全部免费解锁 👉 立即查看 👈️
演示环境说明:
- 开发工具:Visual Studio Code / JetBrains 系列 IDE / Vim / Nano
- Ubuntu版本:Ubuntu 22.04 LTS / Ubuntu 24.04 LTS,推荐 Ubuntu 24.04 LTS
- Linux内核版本:Linux Kernel 6.x 或更高
- 终端环境:Bash / Zsh
- 包管理工具:APT / Snap / Flatpak
- 远程连接工具:OpenSSH / Windows Terminal / MobaXterm / Termius
- 虚拟化环境:VMware Workstation / VirtualBox / WSL2 / 云服务器
- 容器环境:Docker / Docker Compose
- 操作系统:Ubuntu Desktop / Ubuntu Server
全文目录:
-
- 一、前言:服务器为什么会"生病"
- 二、本文要解决什么问题
- 三、排查工作的整体思路与方法论
- 四、基础环境准备与工具核查
-
- 4.1 确认系统版本
- 4.2 安装排查工具包
- 4.3 核查工具是否安装成功
- 4.4 快速系统状态扫描命令
- 五、案例一:CPU 飙升 100%——谁在消耗我的算力
-
- 5.1 场景描述
- 5.2 排查步骤
- 5.3 根因分析与解决
- 5.4 命令解析
- 六、案例二:内存耗尽与 OOM Killer 介入
-
- 6.1 场景描述
- 6.2 排查步骤
- 6.3 根因分析与解决
- 6.4 关于 OOM Score 的补充说明
- 七、案例三:Load Average 异常但 CPU 不高
-
- 7.1 场景描述
- 7.2 理解 Load Average 的本质
- 7.3 排查步骤
- 7.4 根因分析与解决
- 7.5 一个关键认知
- 八、案例四:磁盘 I/O 异常导致系统响应缓慢
-
- 8.1 场景描述
- 8.2 排查步骤
- 8.3 补充:真正的磁盘 I/O 性能测试
- 九、案例五:僵尸进程堆积引发的资源泄漏
-
- 9.1 场景描述
- 9.2 理解为什么僵尸进程过多会有问题
- 9.3 排查步骤
- 9.4 根因修复:在 Python 代码层面处理
- 十、案例六:网络资源异常与连接数打满
-
- 10.1 场景描述
- 10.2 排查步骤
- 10.3 命令解析
- 十一、案例七:综合性故障——多资源交叉异常的系统化排查
-
- 11.1 场景描述
- 11.2 建立系统化排查流程
- 11.3 实际排查脚本
- 11.4 lsof 的高级用法
- 十二、核心命令与工具解析
-
- 12.1 资源监控命令全景图
- 12.2 top 的高级用法
- 12.3 ps 命令深度解析
- 12.4 vmstat 内存和系统性能综合分析
- 十三、常见问题与踩坑总结
-
- Q1:服务器响应慢,我第一步应该看什么?
- Q2:CPU 使用率高,直接 kill 掉进程对吗?
- Q3:journalctl 怎么看才高效?
- Q4:OOM Kill 日志怎么看?
- Q5:磁盘明明有空间,但写文件失败,是为什么?
- Q6:TIME_WAIT 太多怎么办?
- Q7:僵尸进程能直接 kill 吗?
- Q8:系统 Load 高但什么进程都看不出异常,怎么办?
- Q9:用 `strace` 跟踪进程后,进程变更慢了,正常吗?
- Q10:怎么判断是应用问题还是系统问题?
- Q11:`dstat` 和 `vmstat` 有什么区别,用哪个更好?
- Q12:排查完问题,有没有必要写记录?
- 十四、最佳实践建议
-
- 14.1 建立监控体系,不要等问题上门
- 14.2 定期执行系统健康检查
- 14.3 资源排查的优先级原则
- 14.4 使用 cgroup 限制进程资源
- 十五、扩展知识点
-
- 15.1 Linux 进程状态机
- 15.2 /proc 文件系统的实用性
- 15.3 perf 工具入门
- 15.4 容器环境中的资源排查差异
- 十六、完整排查流程回顾
-
- 排查速查卡
- 十七、总结
-
- 本节学到了什么
- 下一节可以继续学习什么?
- 🧧 学习福利 · 限时开放 🧧
- 🫵 Who am I?
一、前言:服务器为什么会"生病"
我第一次遇到服务器资源异常是在一个深夜。那是一台跑着 Nginx + PHP + MySQL 的小型 VPS,用户突然反馈网站打不开。我 SSH 登进去,发现终端响应极慢,敲个 ls 要等好几秒。top 打开后,CPU 使用率接近 100%,load average 显示三个数字都超过了 10——而这台机器只有 2 个核心。
当时我完全不知道该从哪里下手。一通乱操作之后,重启了服务器,网站暂时恢复了,但根本原因没找到。两个小时后,同样的问题再次出现。
那一晚之后,我花了很长时间系统地学习 Linux 资源排查这件事。我发现很多初学者都会犯同样的错误:遇到服务器异常,第一反应是重启,而不是排查原因。重启是最后手段,不是第一手段。
本节是第十章的收尾,也是整个"资源管理"部分的实战综合。前面九节我们分别讲了进程与线程的概念、ps/top/htop 的使用、前后台任务管理、信号控制、优先级调整、CPU 分析、内存分析、Load Average 理解,以及僵尸进程与孤儿进程——这一节,我们要把这些知识点串起来,通过真实的异常场景,建立一套完整的资源排查方法论和操作体系。
服务器"生病"的原因千变万化,但排查的逻辑是有规律可循的。掌握这套逻辑,比背一百条命令更有价值。
二、本文要解决什么问题
这篇文章试图回答以下几个在实际运维中真实存在的问题:
这些问题都会在本节的案例中得到解答。每个案例都来自于真实场景的抽象,可以直接映射到你在工作中遇到的情况。
三、排查工作的整体思路与方法论
在进入具体案例之前,我需要先讲一件比命令更重要的事:排查的思路。
很多人遇到问题之后,打开 top 看一眼,看到某个进程 CPU 高,就直接 kill 掉它。这个操作有时候能解决问题,但更多时候只是把症状压下去了,根本原因没有解决,过一会儿问题还会复现。
正确的排查流程应该是这样的:
相关示意图绘制如下,仅供大家保存查阅:
这张图描述了一个标准的故障排查闭环。我用文字再解释一遍每个环节的意义:
第一步:确认异常现象。 不要一上来就猜原因。先看清楚:是响应慢还是完全不可用?是偶发还是持续?是最近才有还是一直都有?是某个服务崩了还是整个系统卡死?这些信息决定了你后面排查的优先级。
第二步:分类异常类型。 CPU、内存、I/O、网络、Load——这五个维度是服务器资源的核心维度。先用快速的命令(比如 top、free、iostat、ss)做一个全貌扫描,看哪个维度异常最明显。
第三步:定位异常进程。 确定是哪个(或哪几个)进程在消耗资源。这一步要精确到 PID,不能只说"是 Java 进程"。
第四步:分析进程行为。 该进程为什么会有这种行为?是代码 bug?是请求量暴增?是配置不当?是数据量突然变大?
第五步:定位根本原因。 是应用层的问题,还是系统层的问题?是偶发性的还是必然性的?
第六步:修复或缓解。 如果能立即修复就修复;如果不能,先临时缓解(比如重启进程、限制资源使用),然后安排时间彻底解决。
第七步:记录和完善监控。 这一步非常容易被忽略,但它是最能避免同类问题再次出现的步骤。
我每次排查问题都会在内部 Wiki 上留下记录:时间、现象、排查过程、根本原因、修复方案、预防措施。时间长了,这份记录会成为一个非常有价值的知识库。
四、基础环境准备与工具核查
在做资源排查之前,我们需要确保几个核心工具都已经安装好。Ubuntu 22.04 / 24.04 默认安装了大部分工具,但有几个需要手动安装。
4.1 确认系统版本
# 查看 Ubuntu 版本信息
lsb_release -a
# 查看内核版本
uname -r
# 查看系统架构
uname -m
输出示例:
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.4 LTS
Release: 22.04
Codename: jammy
4.2 安装排查工具包
# 更新软件源索引(每次安装软件前建议先执行)
sudo apt update
# 安装核心排查工具
sudo apt install -y \\
sysstat \\ # 包含 iostat、mpstat、pidstat、sar
htop \\ # 交互式进程查看器,比 top 更直观
iotop \\ # 按进程查看磁盘 I/O
nethogs \\ # 按进程查看网络使用情况
lsof \\ # 查看进程打开的文件和网络连接
strace \\ # 跟踪系统调用(高级排查工具)
perf \\ # 性能分析工具
dstat \\ # 系统资源统计工具,可实时显示多个维度
net-tools \\ # 包含 netstat(虽然老旧,但某些场景仍有用)
iproute2 # 包含 ss、ip 等现代网络工具
# 启用 sysstat 数据收集服务(用于 sar 历史数据查询)
sudo systemctl enable sysstat
sudo systemctl start sysstat
4.3 核查工具是否安装成功
# 检查各工具是否可用
which htop iotop iostat pidstat dstat nethogs lsof ss
# 验证 sysstat 服务状态
systemctl status sysstat
4.4 快速系统状态扫描命令
在正式排查之前,我有一套"五秒扫描"的习惯命令,能让我在进入系统后迅速了解整体状况:
# 第一眼:看 CPU、内存、Load 综合情况
top -bn1 | head -20
# 第二眼:看内存使用摘要
free -h
# 第三眼:看磁盘使用情况
df -h
# 第四眼:看 I/O 状态(需要 sysstat)
iostat -x 1 3
# 第五眼:看网络连接数
ss -s
这五条命令,我打开终端后基本上能在 30 秒内完成,对系统整体状态有个大致判断,再决定下一步深入哪个方向。
五、案例一:CPU 飙升 100%——谁在消耗我的算力
5.1 场景描述
某天下午,你的值班手机收到告警:线上 API 服务响应时间从平均 200ms 升至 8000ms,同时 CPU 使用率告警触发,显示 CPU 利用率达到 98%。这台服务器是 4 核 8G 的 Ubuntu 22.04 LTS。
你需要在尽量不影响线上服务的前提下,定位原因。
5.2 排查步骤
Step 1:登录服务器,快速查看整体 CPU 状态
# 查看实时 CPU 使用情况,刷新间隔 1 秒,只打印一次
top -bn1 | head -25
输出示例:
top – 14:32:17 up 12 days, 3:15, 1 user, load average: 3.89, 3.72, 3.41
Tasks: 187 total, 3 running, 184 sleeping, 0 stopped, 0 zombie
%Cpu(s): 96.2 us, 2.1 sy, 0.0 ni, 0.8 id, 0.5 wa, 0.0 hi, 0.3 si, 0.1 st
MiB Mem : 7800.0 total, 412.3 free, 5986.7 used, 1400.9 buff/cache
MiB Swap: 2048.0 total, 1024.5 free, 1023.5 used. 1600.2 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8731 www-data 20 0 1823456 512340 21456 R 91.2 6.4 12:34.21 php-fpm8.1
8732 www-data 20 0 1654321 498231 21456 R 88.3 6.2 11:22.43 php-fpm8.1
8410 mysql 20 0 2345678 1024512 56789 S 4.1 12.8 89:12.34 mysqld
1023 root 20 0 812345 123456 12345 S 1.2 1.5 23:45.12 nginx
这一眼能看出:
- %Cpu(s): 96.2 us —— 96% 的 CPU 时间消耗在用户态(us),说明是应用程序在消耗,而不是内核(sy)或 I/O 等待(wa)
- 有两个 php-fpm8.1 进程分别占用了 91% 和 88% 的 CPU
- Load Average 约为 3.89,对于 4 核机器来说,接近满负荷
这已经很清楚了:是 PHP-FPM 工作进程在消耗 CPU。
Step 2:确认是哪种 CPU 消耗
# 用 mpstat 查看各 CPU 核心的详细使用情况
# -P ALL 表示显示所有核心,1 表示每秒采样,3 表示采样 3 次
mpstat -P ALL 1 3
输出示例:
Linux 5.15.0-101-generic (web-server) 06/10/2026 _x86_64_ (4 CPU)
14:32:20 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
14:32:21 all 94.31 0.00 2.25 1.23 0.00 0.21 0.00 0.00 0.00 1.98
14:32:21 0 96.00 0.00 2.00 1.00 0.00 0.00 0.00 0.00 0.00 1.00
14:32:21 1 95.00 0.00 2.50 0.50 0.00 0.00 0.00 0.00 0.00 2.00
14:32:21 2 92.00 0.00 2.00 2.00 0.00 0.50 0.00 0.00 0.00 3.50
14:32:21 3 94.00 0.00 2.50 1.50 0.00 0.00 0.00 0.00 0.00 2.00
四个核心都在 90%+ 的用户态使用率,说明是应用代码在全力运转,不是系统调用或 I/O 瓶颈。
Step 3:定位具体进程和线程
# 使用 pidstat 查看 CPU 消耗最高的进程,每秒采样一次,显示 5 次
pidstat -u 1 5 | head -30
# 如果想看某个特定进程内部的线程消耗情况(假设 PID 是 8731)
pidstat -t -p 8731 1 3
输出示例(pidstat -t -p 8731):
Linux 5.15.0-101-generic (web-server) 06/10/2026
14:32:25 UID TGID TID %usr %system %guest %wait %CPU CPU Command
14:32:26 1000 8731 – 91.00 1.00 0.00 0.00 92.00 0 php-fpm8.1
14:32:26 1000 – 8731 91.00 1.00 0.00 0.00 92.00 0 |__php-fpm8.1
PHP-FPM 是单线程的工作进程模型,-t 参数在这里显示该进程本身就是主要的 CPU 消耗者。
Step 4:查看该进程在做什么(strace 跟踪系统调用)
# strace 跟踪进程 8731 的系统调用,-p 指定 PID,-c 统计各系统调用耗时
# 注意:strace 本身有性能开销,生产环境谨慎使用
sudo strace -p 8731 -c -T 10
如果你看到大量的 read、write、recvfrom、sendto 等系统调用密集出现,说明进程在频繁做网络或文件 I/O;如果主要是 CPU 计算相关的调用,可能是代码有死循环或者计算量过大。
Step 5:查看 PHP-FPM 的请求日志,找到具体的慢请求
# 查看 PHP-FPM 慢日志(需要提前在 php-fpm pool 配置中开启)
sudo tail -f /var/log/php8.1-fpm.log
# 如果配置了 slowlog,查看慢请求日志
sudo tail -100 /var/log/php/slow.log
Step 6:通过 /proc 文件系统进一步了解进程状态
# 查看进程 8731 的状态文件
cat /proc/8731/status
# 查看进程的命令行参数
cat /proc/8731/cmdline | tr '\\0' ' '
# 查看进程打开的文件描述符数量
ls /proc/8731/fd | wc -l
# 查看进程的内存映射
cat /proc/8731/maps | head -20
/proc 文件系统是 Linux 内核对外暴露的进程信息接口,很多高级排查工具的底层数据都来自这里。直接读取 /proc 不需要安装任何额外软件,即使在最精简的系统上也可用。
5.3 根因分析与解决
通过以上步骤,我们发现 PHP-FPM 进程 CPU 消耗极高。结合慢日志,发现有一个接口在处理某个特殊请求时陷入了死循环——是一个 while 循环中的退出条件在特定数据下永远不会触发。
临时缓解方案:
# 临时杀掉失控的 PHP-FPM 进程(它会被 master 进程自动重启)
sudo kill -9 8731 8732
# 或者重启整个 PHP-FPM 服务
sudo systemctl restart php8.1-fpm
# 确认服务已恢复
systemctl status php8.1-fpm
根本修复方案: 修复代码中的死循环逻辑,上线修复版本,并添加接口超时限制。
5.4 命令解析
这组排查命令覆盖了"从宏观到微观"的定位逻辑:
- top 给出 CPU 消耗的全貌,帮助快速锁定异常进程
- mpstat 帮助我们判断 CPU 消耗的性质(用户态/内核态/I/O 等待),这一步很关键,不同性质对应不同的根因
- pidstat 可以以固定时间间隔打印进程级别的 CPU 消耗,比 top 更适合脚本采集
- strace 是高级工具,适用于你已经定位到具体进程但不知道它在做什么的情况,注意它有额外开销
- /proc 文件系统是一切的底层数据源,很多工具其实是它的友好封装
一个常见误区: 看到某个进程 CPU 高,很多人的第一反应是这个进程有"bug",但实际上 CPU 高不一定是 bug,也可能是请求量暴增、数据量变大、或者某个定时任务恰好在运行。要结合业务情况综合判断。
六、案例二:内存耗尽与 OOM Killer 介入
6.1 场景描述
你的 Java 应用服务在凌晨 3 点突然停止响应,运维同学发现进程消失了,重启后恢复正常,但没有人知道发生了什么。这种情况在很多团队里是"重启大法"的经典场景,但它可能预示着严重的内存泄漏。
6.2 排查步骤
Step 1:先确认进程是否真的消失,以及是否有遗留痕迹
# 确认 Java 进程当前状态
pgrep -la java
# 查看进程是否在运行
systemctl status your-app.service
如果进程不存在,说明它确实被终止了。
Step 2:查看 OOM Killer 日志
这是排查内存耗尽场景最关键的一步。当系统内存耗尽时,Linux 内核的 OOM(Out of Memory)Killer 机制会介入,选择一个进程强制终止以释放内存。这个过程会在系统日志中留下明确记录。
# 使用 journalctl 查找 OOM 相关日志
sudo journalctl -k | grep -i "oom"
# 查看内核日志中的 OOM Kill 事件
sudo journalctl -k | grep -i "killed process"
# 查看今天的系统日志中的 OOM 信息
sudo journalctl –since today | grep -i "out of memory"
# 也可以直接查看内核环形缓冲区(对刚发生的事件更及时)
sudo dmesg | grep -i "oom\\|killed"
典型的 OOM Kill 日志输出:
Jun 10 03:14:22 web-server kernel: java invoked oom-killer: gfp_mask=0x6000c0, order=0, oom_score_adj=0
Jun 10 03:14:22 web-server kernel: [<ffffffff8119f3d5>] oom_kill_process+0x245/0x3c0
Jun 10 03:14:22 web-server kernel: Out of memory: Kill process 9823 (java) score 892 or sacrifice child
Jun 10 03:14:22 web-server kernel: Killed process 9823 (java) total-vm:8543212kB, anon-rss:7012344kB, file-rss:21456kB
这条日志非常有价值。它告诉我们:
- java 进程触发了 OOM Killer
- PID 是 9823
- 它的总虚拟内存达到 8.5GB,实际物理内存占用(RSS)达到 7GB
- oom_score 是 892(满分 1000),说明内核认为这个进程是"最应该被杀"的
Step 3:查看 OOM 发生时的内存状态
OOM 日志中通常还包含内存快照,可以看到当时各类内存的使用情况:
# 查看 OOM 完整日志,包含内存快照
sudo journalctl -k –since "2026-06-10 03:00:00" –until "2026-06-10 03:30:00" | grep -A 50 "oom-killer"
Step 4:查看当前内存状态
# 综合内存使用情况
free -h
# 查看内存详细分类
cat /proc/meminfo | head -30
# 查看消耗内存最多的进程(按 RSS 排序)
ps aux –sort=-%mem | head -15
输出解析(free -h):
total used free shared buff/cache available
Mem: 7.6Gi 6.8Gi 124Mi 234Mi 724Mi 412Mi
Swap: 2.0Gi 1.9Gi 85Mi
当 Swap 几乎用满时,说明内存压力已经极大,系统很可能正在频繁进行 swap in/swap out(内存页面置换),这会导致严重的性能下降。
Step 5:追踪历史内存使用趋势
# 用 sar 查看过去 24 小时的内存使用历史(需要提前安装并启用 sysstat)
sar -r 1 –start-time "03:00:00" –end-time "03:30:00"
# 或者查看今天的内存统计(每隔 10 分钟一条数据)
sar -r -f /var/log/sysstat/sa$(date +%d)
如果有监控平台(如 Prometheus + Grafana),能看到内存使用的趋势图更直观。这里提前埋下一个最佳实践的伏笔:生产环境一定要有监控,不能只依靠登机排查。
Step 6:判断是内存泄漏还是正常增长超限
# 查看 Java 进程的 JVM 内存配置(如果应用还在运行)
# 注意:以下命令需要 JDK 工具包
jcmd $(pgrep java) VM.flags
# 查看 JVM 内存使用详情
jcmd $(pgrep java) GC.heap_info
# 如果是 Spring Boot 应用,可以通过 Actuator 接口查看内存(如果配置了)
curl http://localhost:8080/actuator/metrics/jvm.memory.used
6.3 根因分析与解决
通过 OOM 日志,我们确认了 Java 进程占用了约 7GB 内存,而服务器只有 8GB。结合应用代码分析,发现应用在处理某个批量数据接口时,将大量数据加载到内存中没有及时释放,导致内存逐渐耗尽。
临时缓解:
# 调整 Java 应用的 JVM 内存上限(修改启动参数或 systemd 服务配置)
# 在 systemd service 文件中修改 ExecStart:
sudo systemctl edit your-app.service
在编辑器中修改或添加:
[Service]
# 限制 Java 进程最大堆内存为 4GB,元空间 512MB
Environment="JAVA_OPTS=-Xmx4g -Xms2g -XX:MaxMetaspaceSize=512m"
# 重新加载 systemd 配置
sudo systemctl daemon-reload
sudo systemctl restart your-app.service
根本修复: 修复代码中的内存泄漏,优化批量接口使用流式处理而不是全量加载。
6.4 关于 OOM Score 的补充说明
Linux 内核通过 oom_score 决定 OOM 时优先杀哪个进程。分数越高,越优先被杀。可以通过 oom_score_adj 文件调整单个进程被 OOM Kill 的倾向:
# 查看某进程的 OOM score
cat /proc/$(pgrep java)/oom_score
# 查看当前的调整值(范围 -1000 到 1000)
cat /proc/$(pgrep java)/oom_score_adj
# 让某个重要进程不那么容易被 OOM Kill(值越低越难被杀,-1000 表示永不被杀)
# 注意:这需要 root 权限,并且可能导致 OOM 时系统整体更糟糕
echo -500 | sudo tee /proc/$(pgrep nginx)/oom_score_adj
⚠️ 警告: 轻易调整 oom_score_adj 会干扰内核的 OOM 决策机制,可能在内存耗尽时导致更严重的问题。建议仅在确实理解影响的情况下使用,并且更应该从根本上解决内存问题,而不是逃避 OOM Kill。
七、案例三:Load Average 异常但 CPU 不高
7.1 场景描述
监控告警显示服务器的 Load Average(1分钟)达到 16.5,但 CPU 使用率只有 23%,内存也还有 2GB 空闲。服务响应变慢,但没有完全不可用。
这种情况初学者会觉得很奇怪:CPU 又不高,内存又够用,Load 为什么这么高?
7.2 理解 Load Average 的本质
在第八节我们讲过,Load Average 统计的是可运行状态(R)和不可中断睡眠状态(D)的进程数量。CPU 不高但 Load 高,通常意味着大量进程处于 D 状态,也就是正在等待 I/O 操作完成。
相关示意图绘制如下,仅供大家保存查阅:
Load Average 的本质:它反映的是系统的"需求量",而不是"CPU 使用量"。当大量进程在等待磁盘、网络等 I/O 时,它们都在占用调度队列,但不消耗 CPU。
7.3 排查步骤
Step 1:确认进程状态分布
# 查看处于 D 状态(不可中断睡眠)的进程数量
ps aux | awk '$8=="D" {print $0}'
# 快速统计各种状态的进程数量
ps aux | awk 'NR>1 {print $8}' | sort | uniq -c | sort -rn
Step 2:确认是否是 I/O 等待导致
# 查看 CPU 等待时间(wa 列)
top -bn1 | grep "Cpu(s)"
如果 wa(I/O wait)列的值很高(比如超过 20%),那几乎可以确定是磁盘或网络 I/O 问题。
# 查看磁盘 I/O 状态
iostat -x 1 5
输出示例:
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sda 0.20 289.40 5.30 82340.60 0.00 89.20 0.00 23.57 1.20 245.30 70.89 26.50 284.40 3.30 96.20
注意这几个关键指标:
- w/s:每秒写入请求数 289.40,非常高
- wkB/s:每秒写入数据量 82MB,很大
- w_await:写入操作平均等待时间 245ms,这非常高(正常应该在 10ms 以内)
- %util:磁盘利用率 96.2%,接近饱和
Step 3:定位是哪个进程在做大量 I/O
# iotop 实时查看各进程的 I/O 使用(需要 root 权限)
sudo iotop -o -d 1
# 如果不想交互式查看,-b 批处理模式 -n 采样次数
sudo iotop -bo -n 5 -d 1
输出示例:
Total DISK READ: 0.00 B/s | Total DISK WRITE: 87.45 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
15234 be/4 mysql 0.00 B/s 85.32 M/s 0.00 % 94.23 % mysqld
8821 be/4 www-data 0.00 B/s 1.89 M/s 0.00 % 3.45 % php-fpm8.1
锁定了!MySQL 进程每秒在写入 85MB 的数据,占用了绝大部分磁盘写入带宽。
Step 4:分析 MySQL 在做什么
# 登录 MySQL,查看当前正在执行的查询
mysql -u root -p -e "SHOW FULL PROCESSLIST\\G"
# 查看 MySQL 的 InnoDB 状态(包含 I/O 信息)
mysql -u root -p -e "SHOW ENGINE INNODB STATUS\\G" | grep -A 30 "FILE I/O"
# 查看慢查询日志(如果开启了的话)
sudo tail -50 /var/log/mysql/mysql-slow.log
Step 5:查看是否有大量写入来自某个特定表或操作
通过 MySQL processlist,我们发现有一个批量导入操作正在进行,有人在往一个大表里 INSERT 数百万条记录,没有做任何批量控制,导致 MySQL 的 redo log 写入量巨大,把磁盘 I/O 打满了。
7.4 根因分析与解决
根本原因: 批量数据写入操作没有做分批控制,单次提交量过大,导致 MySQL I/O 持续高负荷,磁盘 I/O 利用率接近 100%,大量进程等待 I/O,Load Average 飙高。
临时缓解:
# 登录 MySQL,找到批量导入的 session 并 kill
mysql -u root -p -e "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO FROM information_schema.PROCESSLIST WHERE TIME > 30 ORDER BY TIME DESC"
# kill 掉问题 session(替换 XXXX 为实际的 ID)
mysql -u root -p -e "KILL XXXX"
根本修复: 对批量导入逻辑做分批控制,每批次 1000-5000 条,批次之间适当 sleep,避免持续打满 I/O。
7.5 一个关键认知
这个案例说明了一件很重要的事:当 Load 高但 CPU 不高时,排查重点应该转向 I/O。不要因为 CPU 看起来不高就认为系统没什么问题。Load Average 是系统压力的综合指标,I/O 等待同样是一种压力。
八、案例四:磁盘 I/O 异常导致系统响应缓慢
8.1 场景描述
运维同学反馈,服务器上某个 Nginx + Node.js 的应用最近日志写入非常缓慢,Node.js 的 API 响应时间不稳定,有时候很快,有时候要等好几秒。CPU 和内存看起来都正常,但系统整体感觉很"卡"。
8.2 排查步骤
Step 1:确认磁盘使用情况
# 查看各磁盘分区的使用率
df -h
# 查看磁盘 inode 使用情况(inode 耗尽也会导致"磁盘满"的症状)
df -i
⚠️ 注意: 很多人只关注磁盘容量(df -h),忽略了 inode 使用率(df -i)。inode 耗尽会导致无法创建新文件,即使磁盘还有空间。日志系统尤其容易出现这个问题,因为会产生大量小文件。
输出示例(df -i):
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 26214400 26200123 14277 100% /
IUse% 100% —— inode 已经耗尽!这直接解释了日志写入缓慢的问题:系统根本无法创建新文件了。
Step 2:找到是哪个目录占用了最多 inode
# 统计根目录各子目录的 inode 使用情况
# 这个命令可能执行稍慢,请耐心等待
find / -xdev -printf '%h\\n' 2>/dev/null | sort | uniq -c | sort -rn | head -20
# 更具体地,查看某个目录下的文件数量(比如怀疑 /tmp 或日志目录)
find /var/log -type f | wc -l
find /tmp -type f | wc -l
Step 3:处理 inode 耗尽问题
# 查看哪些日志文件最多
ls -la /var/log/ | sort -k5 -rn | head -20
# 查找并删除过老的日志文件(谨慎操作,先确认文件类型)
find /var/log -name "*.gz" -mtime +30 -type f -ls
# 确认后再删除(将 -ls 替换为 -delete)
# ⚠️ 警告:-delete 操作不可恢复,请确认 find 命令筛选条件正确后再执行
find /var/log -name "*.gz" -mtime +30 -type f -delete
# 如果是 Node.js 应用产生的大量小临时文件
find /tmp -name "*.tmp" -mtime +7 -type f -delete
Step 4:配置 logrotate 防止日志堆积
这个问题从根本上说是日志没有配置好轮转。Ubuntu 系统自带 logrotate,应该为所有产生日志的应用配置轮转策略:
# 查看现有的 logrotate 配置
ls /etc/logrotate.d/
# 为 Node.js 应用创建 logrotate 配置
sudo nano /etc/logrotate.d/myapp
配置文件内容:
# Node.js 应用日志轮转配置
/var/log/myapp/*.log {
# 每天轮转
daily
# 保留 14 天的日志
rotate 14
# 日志文件不存在时不报错
missingok
# 日志为空时不轮转
notifempty
# 压缩旧日志
compress
# 延迟一个周期后再压缩(保留一份未压缩的最新旧日志)
delaycompress
# 轮转后创建新日志文件,权限 644,属主 node,属组 node
create 644 node node
# 轮转后执行命令(通知 Node.js 重新打开日志文件)
postrotate
kill -USR1 $(cat /var/run/myapp.pid 2>/dev/null) 2>/dev/null || true
endscript
}
# 测试 logrotate 配置是否正确(-d 表示 dry-run,不实际执行)
sudo logrotate -d /etc/logrotate.d/myapp
# 手动执行一次轮转(强制执行,即使不满足触发条件)
sudo logrotate -f /etc/logrotate.d/myapp
8.3 补充:真正的磁盘 I/O 性能测试
有时候磁盘 I/O 慢是硬件层面的问题,可以用以下方法测试磁盘真实读写性能:
# 测试顺序写入性能(使用 dd 命令,写入 1GB 数据)
# ⚠️ 注意:会在当前目录产生 1GB 临时文件,确保有足够空间
dd if=/dev/zero of=/tmp/test_write bs=1M count=1024 conv=fdatasync
# 测试顺序读取性能
dd if=/tmp/test_write of=/dev/null bs=1M
# 测试完后清理临时文件
rm /tmp/test_write
# 更专业的测试工具(如果安装了 fio)
sudo apt install -y fio
fio –name=randwrite –ioengine=libaio –iodepth=1 –rw=randwrite \\
–bs=4k –direct=1 –size=256m –numjobs=4 –runtime=60 \\
–filename=/tmp/fio_test.bin –group_reporting
rm /tmp/fio_test.bin
九、案例五:僵尸进程堆积引发的资源泄漏
9.1 场景描述
在第九节,我们已经详细讲过了僵尸进程和孤儿进程的概念。本案例我们看一个真实场景:某台服务器运行着一个 Python 多进程爬虫应用,运行了几天后,发现系统中僵尸进程(Zombie)的数量持续增长,已经超过了 500 个。虽然 CPU 和内存看起来还好,但系统开始偶尔出现"无法创建新进程"的错误。
9.2 理解为什么僵尸进程过多会有问题
僵尸进程本身不占用 CPU 和内存(子进程已经结束,资源已经释放),但它占用进程表中的一个 PID 槽位。Linux 系统允许的最大 PID 数量是有限的:
# 查看系统允许的最大 PID 数量
cat /proc/sys/kernel/pid_max
# Ubuntu 22.04 默认通常是 4194304(4M)
虽然 4M 个 PID 听起来很多,但如果一个应用持续泄漏僵尸进程,最终系统真的会耗尽 PID 空间,导致无法创建任何新进程。在容器环境中这个问题更容易出现,因为容器的 PID 命名空间通常有更小的限制。
9.3 排查步骤
Step 1:确认僵尸进程数量和分布
# 查看系统中所有僵尸进程
ps aux | awk '$8=="Z" {print $0}'
# 统计僵尸进程数量
ps aux | awk '$8=="Z"' | wc -l
# 查看僵尸进程的父进程 ID(PPID)
ps aux | awk '$8=="Z" {print "PID:"$2, "PPID: "}'
ps -eo pid,ppid,stat,comm | awk '$3~/Z/ {print $0}'
Step 2:找到产生僵尸进程的"元凶"父进程
# 获取所有僵尸进程的 PPID
ps -eo pid,ppid,stat,comm | awk '$3~/Z/ {print $2}' | sort | uniq -c | sort -rn
# 假设 PPID 是 12345,查看这个父进程的信息
ps -p 12345 -o pid,ppid,cmd,stat
# 查看父进程是什么
cat /proc/12345/cmdline | tr '\\0' ' '
Step 3:分析父进程为什么没有回收子进程
在 Python 多进程场景中,父进程必须调用 wait() 或 waitpid() 来回收子进程的退出状态,否则子进程会变成僵尸。常见原因:
- 父进程代码中没有调用 wait(),只是启动子进程而不管理其生命周期
- 父进程陷入死循环,无法执行 wait()
- 父进程使用了某些不当的子进程管理模式
# 用 strace 跟踪父进程的系统调用,看是否有 wait 系列调用
sudo strace -p 12345 -e wait4,waitpid -f 2>&1 | head -50
Step 4:临时处理——向父进程发送 SIGCHLD 信号
# 向父进程发送 SIGCHLD 信号,提示它去回收子进程
# 很多编写正确的父进程会在收到 SIGCHLD 后自动调用 wait()
sudo kill -SIGCHLD 12345
# 发送后检查僵尸进程数量是否减少
ps aux | awk '$8=="Z"' | wc -l
Step 5:如果父进程无法修复,只能终止它
# 终止父进程后,其僵尸子进程会被 init 进程(PID 1)接管并回收
sudo kill -9 12345
# 确认僵尸进程已被清理
ps aux | awk '$8=="Z"' | wc -l
9.4 根因修复:在 Python 代码层面处理
僵尸进程问题的根本解决方案是在代码层面正确管理子进程。正确的 Python 多进程写法示例(仅供参考,不是 Ubuntu 命令):
# 正确的子进程管理方式(示意,在代码审查中使用)
# 方式一:使用 multiprocessing.Pool,框架自动管理子进程
from multiprocessing import Pool
with Pool(4) as pool:
results = pool.map(worker_function, task_list)
# with 块结束后,Pool 会自动 join 所有子进程
# 方式二:手动管理时,必须调用 join()
import multiprocessing
p = multiprocessing.Process(target=worker_function, args=(task,))
p.start()
p.join() # 必须调用 join(),否则产生僵尸进程
十、案例六:网络资源异常与连接数打满
10.1 场景描述
线上 Nginx 服务突然大量报错,错误日志里出现 (24: Too many open files) 和 connect() failed (111: Connection refused)。同时,部分用户反馈接口超时,但服务器 CPU 和内存都还好。
10.2 排查步骤
Step 1:查看当前连接数状态
# 使用 ss 命令查看 TCP 连接状态摘要
ss -s
# 查看各种状态的连接数量
ss -ant | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
典型输出(连接数打满场景):
State Recv-Q Send-Q Local Address:Port Peer Address:Port
Total: 65521
TCP: 65512
ESTABLISHED: 45231
TIME_WAIT: 18234
LISTEN: 47
CLOSE_WAIT: 1892
TIME_WAIT 状态有 18234 个,而且总连接数接近 65521——这就是 Linux 默认的文件描述符限制(65535)。
Step 2:查看文件描述符限制
# 查看系统级别的文件描述符限制
cat /proc/sys/fs/file-max
# 查看当前已使用的文件描述符数量
cat /proc/sys/fs/file-nr
# 查看 Nginx 进程的文件描述符限制和使用情况
# 找到 Nginx 的 PID
pgrep nginx | head -1
# 查看该进程的资源限制
cat /proc/$(pgrep nginx | head -1)/limits | grep "open files"
# 查看进程当前打开的文件描述符数量
ls /proc/$(pgrep nginx | head -1)/fd | wc -l
Step 3:分析 TIME_WAIT 过多的原因
TIME_WAIT 是 TCP 四次挥手关闭连接后的正常状态,它存在是为了确保网络中残留的数据包不会干扰新连接。每个 TIME_WAIT 状态需要占用一个端口和文件描述符,保持时间默认是 2MSL(约 60-120 秒)。
短时间内大量 TIME_WAIT 出现,通常意味着:
- 服务在短时间内处理了大量短连接(HTTP/1.0 或未开启 Keep-Alive 的 HTTP/1.1)
- 客户端与服务端的连接频繁建立和断开
Step 4:针对 Nginx 的调优
# 编辑 Nginx 主配置文件
sudo nano /etc/nginx/nginx.conf
在 http 块中添加/修改以下配置:
http {
# 开启 Keep-Alive,减少连接的频繁建立和销毁
keepalive_timeout 65;
# 每个 Keep-Alive 连接允许的最大请求数
keepalive_requests 1000;
# 工作进程可以打开的最大文件描述符数(配合 worker_rlimit_nofile)
# 这个值在 main 块中配置
}
在 main 块(nginx.conf 最顶层)中:
# Nginx 主配置 main 块
worker_processes auto; # 工作进程数,auto 表示等于 CPU 核心数
worker_rlimit_nofile 65535; # 每个工作进程可以打开的最大文件描述符数
events {
worker_connections 10240; # 每个工作进程的最大连接数
use epoll; # 使用 epoll 事件模型(Linux 最高效的 I/O 多路复用)
multi_accept on; # 允许工作进程一次接受多个新连接
}
Step 5:调整系统级文件描述符限制
# 查看系统限制配置文件
cat /etc/security/limits.conf | grep -v "^#" | grep -v "^$"
# 编辑系统限制配置,增加文件描述符上限
# ⚠️ 修改此文件需要谨慎,不当配置可能影响所有用户的资源限制
sudo nano /etc/security/limits.conf
在文件末尾添加:
# 为 www-data 用户(Nginx 运行用户)增加文件描述符上限
www-data soft nofile 65535
www-data hard nofile 65535
# 为 root 用户设置
root soft nofile 65535
root hard nofile 65535
# 修改内核 TCP 参数,允许 TIME_WAIT 状态的端口复用
# ⚠️ 修改内核参数需要谨慎,请理解参数含义后再修改
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
sudo sysctl -w net.ipv4.tcp_fin_timeout=30
sudo sysctl -w net.core.somaxconn=65535
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=65535
# 将参数持久化(重启后依然生效)
sudo nano /etc/sysctl.conf
在 /etc/sysctl.conf 中添加:
# TCP 参数调优
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=30
net.core.somaxconn=65535
net.ipv4.tcp_max_syn_backlog=65535
# 让 sysctl 参数立即生效
sudo sysctl -p
# 验证参数是否生效
sysctl net.ipv4.tcp_tw_reuse
sysctl net.core.somaxconn
# 测试并重新加载 Nginx 配置
sudo nginx -t
sudo systemctl reload nginx
10.3 命令解析
ss -s 是 Ubuntu 现代系统中查看网络连接摘要的推荐方式,它比老旧的 netstat 更快、更准确(netstat 来自 net-tools 包,在 Ubuntu 中已经是可选安装,而 ss 来自 iproute2,默认安装)。
TIME_WAIT 状态过多是很多刚上线的高并发服务会遇到的问题,正确的处理思路是:首先开启 Keep-Alive 减少连接创建频率,然后才考虑调整内核 TCP 参数。不要一上来就调内核参数,那是在治标,Keep-Alive 才是治本。
十一、案例七:综合性故障——多资源交叉异常的系统化排查
11.1 场景描述
这是最复杂的一类场景:服务器同时出现多个异常信号——CPU 使用率波动很大(时高时低),内存持续增长,Load Average 偏高,磁盘 I/O 也有异常,服务间歇性不可用。这种情况没有单一的"银弹"解决方案,需要系统化地一步一步排查。
11.2 建立系统化排查流程
相关示意图绘制如下,仅供大家保存查阅:
11.3 实际排查脚本
我把上面的流程整理成了一个可以直接运行的排查脚本,方便在紧急情况下快速收集信息:
#!/bin/bash
# 系统资源快速诊断脚本
# 用法:bash system_diag.sh > /tmp/diag_$(date +%Y%m%d_%H%M%S).txt 2>&1
# 用途:在故障发生时快速收集系统状态信息,便于事后分析
echo "===== 系统诊断报告 ====="
echo "时间: $(date)"
echo "主机名: $(hostname)"
echo "系统版本: $(lsb_release -d | cut -f2)"
echo ""
echo "===== 1. 系统基本信息 ====="
uptime
echo ""
echo "===== 2. CPU 使用情况 ====="
# 打印一次 top 输出的前 20 行
top -bn1 | head -20
echo ""
echo "===== 3. 内存使用情况 ====="
free -h
echo ""
echo "— /proc/meminfo 关键字段 —"
grep -E "MemTotal|MemFree|MemAvailable|SwapTotal|SwapFree|Cached|Buffers|Dirty" /proc/meminfo
echo ""
echo "===== 4. 磁盘使用情况 ====="
df -h
echo ""
echo "— inode 使用情况 —"
df -i
echo ""
echo "===== 5. 磁盘 I/O 状态 ====="
iostat -x 1 3
echo ""
echo "===== 6. 网络连接状态 ====="
ss -s
echo ""
echo "— 各状态连接数统计 —"
ss -ant | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
echo ""
echo "===== 7. 进程状态统计 ====="
ps aux | awk 'NR>1 {print $8}' | sort | uniq -c | sort -rn
echo ""
echo "===== 8. CPU 消耗 TOP 10 进程 ====="
ps aux –sort=-%cpu | head -11
echo ""
echo "===== 9. 内存消耗 TOP 10 进程 ====="
ps aux –sort=-%mem | head -11
echo ""
echo "===== 10. 僵尸进程列表 ====="
zombie_count=$(ps aux | awk '$8=="Z"' | wc -l)
echo "僵尸进程总数: $zombie_count"
if [ "$zombie_count" -gt 0 ]; then
ps -eo pid,ppid,stat,comm | awk '$3~/Z/ {print $0}'
fi
echo ""
echo "===== 11. 最近系统日志(最后 50 行)====="
sudo journalctl -k –no-pager -n 50 2>/dev/null
echo ""
echo "===== 12. OOM Kill 历史记录 ====="
sudo journalctl -k | grep -i "oom\\|killed" | tail -20 2>/dev/null
echo ""
echo "===== 诊断报告结束 ====="
# 保存脚本
sudo nano /usr/local/bin/sys_diag.sh
# 粘贴上面的内容,然后 Ctrl+O 保存,Ctrl+X 退出
# 添加执行权限
sudo chmod +x /usr/local/bin/sys_diag.sh
# 使用方式:故障时执行,输出重定向到文件保存
sys_diag.sh > /tmp/diag_$(date +%Y%m%d_%H%M%S).txt 2>&1
# 查看生成的报告
cat /tmp/diag_*.txt
11.4 lsof 的高级用法
lsof(List Open Files)是一个极其有用的排查工具,在综合性故障排查中经常用到:
# 查看某个进程打开的所有文件(替换 PID)
sudo lsof -p 12345
# 查看哪些进程打开了某个特定文件
sudo lsof /var/log/nginx/error.log
# 查看哪些进程在监听网络端口
sudo lsof -i -P -n | grep LISTEN
# 查看某个进程的网络连接
sudo lsof -i -p 12345
# 查看哪些进程打开了已被删除但还未释放的文件(排查磁盘空间异常释放问题)
sudo lsof +L1
lsof +L1 的价值: 这个命令能找到"已删除但还被进程占用"的文件。在实际运维中,经常遇到这种情况:用 rm 删除了大日志文件,但 df -h 显示磁盘空间没有释放。这是因为虽然文件在文件系统目录中被删除了,但进程还持有文件描述符,真正的磁盘空间要等到进程关闭文件描述符后才会释放。重启对应服务或进程,磁盘空间就会释放。
十二、核心命令与工具解析
在前面的案例中我们用到了大量命令,这里做一个系统性的整理和深度解析。
12.1 资源监控命令全景图
相关示意图绘制如下,仅供大家保存查阅:
12.2 top 的高级用法
很多人只会用 top 看一眼 CPU 和内存,其实它有很多有用的交互按键:
# 进入 top 后的常用按键
# 按 1:展开显示每个 CPU 核心的使用情况
# 按 M:按内存使用量排序
# 按 P:按 CPU 使用量排序(默认)
# 按 T:按进程运行总时长排序
# 按 k:输入 PID 后发送信号给进程
# 按 r:修改进程的 nice 值(需要 root)
# 按 f:进入字段管理界面,可以添加/移除显示的列
# 按 W:将当前 top 配置保存到 ~/.toprc
# 按 i:切换是否显示空闲进程
# 按 H:切换是否显示线程
# 非交互模式下的高级用法
# 监控特定进程(只显示 PID 为 1234 的进程)
top -p 1234
# 监控多个特定进程
top -p 1234,5678,9012
# 批处理模式输出(适合脚本调用或日志记录)
top -b -n 1 -o %CPU | head -20
# 按进程排序并过滤(结合管道)
top -bn1 | awk 'NR>7 && $9>10.0 {print $0}' # 显示 CPU 超过 10% 的进程
12.3 ps 命令深度解析
# 最常用的几种 ps 格式
# 1. BSD 风格(最常见)
ps aux
# a: 显示所有用户的进程
# u: 以用户友好格式显示
# x: 显示没有控制终端的进程
# 2. System V 风格,自定义字段
ps -eo pid,ppid,user,pcpu,pmem,stat,start,etime,cmd
# -e: 显示所有进程
# -o: 自定义输出字段
# etime: 进程已运行时长
# 3. 查看进程树,直观展示父子关系
ps -elf –forest
# 也可以使用专门的工具
pstree -p | head -30
# 4. 实时监控某个进程的资源使用(类似 watch + ps)
watch -n 1 'ps -p 1234 -o pid,pcpu,pmem,vsz,rss,stat'
12.4 vmstat 内存和系统性能综合分析
vmstat 是一个被很多人忽略但非常有价值的工具:
# 每秒输出一次系统统计,共输出 10 次
vmstat 1 10
输出字段解析:
procs ———–memory———- —swap– —–io—- -system– ——cpu—–
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 0 512000 412316 102456 1423456 0 0 128 8234 234 567 8 2 89 1 0
关键字段含义:
- r:处于可运行状态(R)的进程数(持续大于 CPU 核数说明 CPU 压力大)
- b:处于不可中断睡眠(D)的进程数(大于 0 说明有 I/O 等待)
- swpd:已使用的 swap 空间(持续增长说明内存压力大)
- si/so:每秒从磁盘换入/换出内存的量(非零说明在频繁 swap,性能大打折扣)
- bi/bo:块设备每秒读入/写出的数据量(单位:块/秒)
- in:每秒中断数
- cs:每秒上下文切换次数(极高的 cs 说明进程切换频繁,可能有锁竞争问题)
十三、常见问题与踩坑总结
Q1:服务器响应慢,我第一步应该看什么?
不要打开 top 盯着看——至少不要只看 top。正确的第一步是用 uptime 看 Load Average,然后用 top 扫一眼 CPU/内存整体,再用 iostat -x 1 3 看磁盘,用 ss -s 看网络。这四个命令 30 秒内就能完成,能让你对问题类型有基本判断,然后再深入。
Q2:CPU 使用率高,直接 kill 掉进程对吗?
不一定对。你得先知道这个进程是什么,它高 CPU 是正常业务行为(比如批量任务)还是异常行为(死循环、无限递归)。盲目 kill 可能造成数据损坏或业务中断。定位清楚原因后,再决定是 kill、还是降低优先级、还是修复代码。
Q3:journalctl 怎么看才高效?
# 看系统内核日志(OOM、硬件错误等)
sudo journalctl -k
# 看某个服务的日志
sudo journalctl -u nginx -n 100 –no-pager
# 看某个时间段的日志
sudo journalctl –since "2026-06-10 14:00:00" –until "2026-06-10 14:30:00"
# 实时跟踪日志(类似 tail -f)
sudo journalctl -f
# 只看 ERROR 级别以上的日志
sudo journalctl -p err
Q4:OOM Kill 日志怎么看?
sudo journalctl -k | grep -i "oom\\|killed process" | tail -20
如果找到了 OOM Kill 日志,重点看三件事:哪个进程被杀了?它占用了多少内存?那个时间点系统内存的整体状态是什么?
Q5:磁盘明明有空间,但写文件失败,是为什么?
八成是 inode 耗尽。用 df -i 确认,如果某个分区的 IUse% 是 100%,就是 inode 问题。找到产生大量小文件的目录清理掉,然后配置好 logrotate。
Q6:TIME_WAIT 太多怎么办?
先确认这是真正的问题(服务真的因为连接数不够用而报错),然后按顺序处理:
不要一上来就调内核参数,那像是治感冒吃感冒药,Keep-Alive 才是根治。
Q7:僵尸进程能直接 kill 吗?
不能。僵尸进程已经结束了,它只是一个等待被回收的"空壳",占用了 PID 但没有运行中的代码,你无法 kill 一个不存在的进程。正确做法是让它的父进程调用 wait() 来回收它,或者 kill 掉父进程(子进程会被 init 接管并自动回收)。
Q8:系统 Load 高但什么进程都看不出异常,怎么办?
这种情况通常是 I/O 等待。试试 ps aux | awk '$8=="D"',看看有多少进程处于 D 状态。如果很多,再用 iostat -x 1 5 看具体是哪个磁盘在忙,然后用 iotop 定位进程。
Q9:用 strace 跟踪进程后,进程变更慢了,正常吗?
正常。strace 通过 ptrace 系统调用拦截所有系统调用,本身有较大开销,可能使被跟踪进程变慢 2-10 倍。生产环境要谨慎使用,确认方向后尽快脱离跟踪。
Q10:怎么判断是应用问题还是系统问题?
- 系统日志有 OOM、硬件错误、内核 panic → 系统层问题
- 特定进程 CPU/内存异常高 → 大概率是应用层问题
- 多个服务同时异常 → 可能是底层依赖(数据库、中间件)或系统资源耗尽
- 重启服务就好,但过一段时间复发 → 应用层资源泄漏(内存泄漏、文件描述符泄漏)
Q11:dstat 和 vmstat 有什么区别,用哪个更好?
dstat 比 vmstat 输出更直观,支持彩色显示,并且可以同时显示 CPU、内存、I/O、网络多个维度,非常适合日常监控。vmstat 更轻量,输出格式更标准,适合脚本处理。我日常倾向于用 dstat,写脚本时用 vmstat 或 sar。
# dstat 的一个很好的用法:同时监控 CPU、内存、I/O、网络
dstat -cdngy 1
# c: CPU
# d: 磁盘 I/O
# n: 网络
# g: 分页
# y: 系统(中断和上下文切换)
Q12:排查完问题,有没有必要写记录?
非常有必要。我自己有一个习惯:每次处理完一个系统问题,不管大小,都会在团队 Wiki 或者个人笔记里记录:时间、现象描述、排查思路、命令输出、根本原因、解决方案、预防措施。这些记录日积月累,遇到类似问题时能节省大量时间,也是团队知识沉淀的重要来源。
十四、最佳实践建议
14.1 建立监控体系,不要等问题上门
在生产环境中,被动等待告警是不够的。应该建立主动监控:
# 安装简单的系统监控工具
sudo apt install -y prometheus-node-exporter
# 启用服务
sudo systemctl enable prometheus-node-exporter
sudo systemctl start prometheus-node-exporter
# 它会在 9100 端口暴露系统指标,可以被 Prometheus 采集
curl http://localhost:9100/metrics | head -30
对于没有完整监控平台的小团队,至少应该配置以下基础告警:
# 安装 monit(轻量级进程和资源监控工具)
sudo apt install -y monit
# 编辑 monit 配置
sudo nano /etc/monit/monitrc
# monit 基础监控配置示例
# 监控 CPU 使用率
check system localhost
if loadavg (1min) > 10 then alert
if loadavg (5min) > 8 then alert
if cpu usage > 95% for 3 cycles then alert
if memory usage > 90% then alert
if swap usage > 50% then alert
# 监控 nginx 进程
check process nginx with pidfile /run/nginx.pid
start program = "/bin/systemctl start nginx"
stop program = "/bin/systemctl stop nginx"
if cpu > 80% for 5 cycles then restart
if mem > 200MB then alert
14.2 定期执行系统健康检查
我习惯每天花 5 分钟看一眼服务器的基本状态:
# 编写每日健康检查脚本
sudo nano /etc/cron.daily/system_health_check
#!/bin/bash
# 每日系统健康检查脚本
REPORT_FILE="/var/log/health_check/$(date +%Y%m%d).log"
mkdir -p /var/log/health_check
{
echo "=== 系统健康检查 $(date) ==="
# CPU 负载
echo "Load Average: $(cat /proc/loadavg)"
# 内存
free -h | grep -E "Mem|Swap"
# 磁盘使用率(告警阈值 85%)
df -h | awk 'NR>1 && $5+0>85 {print "WARNING: 磁盘使用率告警: "$0}'
# inode 使用率(告警阈值 80%)
df -i | awk 'NR>1 && $5+0>80 {print "WARNING: inode 使用率告警: "$0}'
# 僵尸进程
zombie=$(ps aux | awk '$8=="Z"' | wc -l)
[ "$zombie" -gt 10 ] && echo "WARNING: 僵尸进程数量: $zombie"
echo "=== 检查完成 ==="
} >> "$REPORT_FILE" 2>&1
# 添加执行权限
sudo chmod +x /etc/cron.daily/system_health_check
14.3 资源排查的优先级原则
当服务器出现问题时,处理优先级建议如下:
不要为了排查问题而让故障持续更长时间。
14.4 使用 cgroup 限制进程资源
在 Ubuntu 22.04 / 24.04 中,systemd 集成了 cgroup v2,可以为服务设置资源使用上限,防止单个服务把整台机器拖垮:
# 为某个 systemd 服务限制 CPU 和内存资源
sudo systemctl edit myapp.service
在编辑器中输入:
[Service]
# 限制 CPU 使用配额:CPUQuota=50% 表示最多使用 50% 的单核 CPU
CPUQuota=80%
# 限制内存使用:超过后进程会被 OOM Kill
MemoryMax=4G
# 限制进程最多打开的文件描述符数
LimitNOFILE=65535
# 限制进程数量(适合有大量 fork 操作的服务)
TasksMax=1000
# 重新加载并重启服务
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
# 查看资源限制是否生效
systemctl show myapp.service | grep -E "CPUQuota|MemoryMax|LimitNOFILE"
这种方式的好处是:即使应用出现 bug 导致资源消耗异常,也不会影响到其他服务。这在多租户或多服务共用一台机器的场景下尤为重要。
十五、扩展知识点
15.1 Linux 进程状态机
理解进程状态是排查资源异常的基础。Linux 中进程有以下几种状态:
| R | Running | 正在运行或等待 CPU 时间片 | 计入 |
| S | Sleeping | 可中断睡眠,等待事件(I/O、信号等) | 不计入 |
| D | Disk Sleep | 不可中断睡眠,等待 I/O 完成 | 计入 |
| Z | Zombie | 进程已结束,等待父进程回收 | 不计入 |
| T | Stopped | 进程被暂停(收到 SIGSTOP 或 SIGTSTP) | 不计入 |
| I | Idle | 内核线程空闲状态 | 不计入 |
D 状态是运维排查中最容易被误解的状态。 当你看到大量 D 状态进程时,说明系统有严重的 I/O 等待问题,但 CPU 可能并不高,这就是前面案例三的场景。
15.2 /proc 文件系统的实用性
/proc 是一个虚拟文件系统,由 Linux 内核维护,包含了系统运行时的所有信息。很多排查工具(top、ps、free 等)的底层数据都来自 /proc:
# /proc 中常用的文件
# 系统整体 CPU 统计
cat /proc/stat
# 系统整体内存信息
cat /proc/meminfo
# 系统负载
cat /proc/loadavg
# 进程相关(以 PID 1234 为例)
cat /proc/1234/status # 进程状态
cat /proc/1234/cmdline # 命令行参数
cat /proc/1234/environ # 环境变量
cat /proc/1234/maps # 内存映射
cat /proc/1234/net/tcp # 网络连接
ls /proc/1234/fd/ # 打开的文件描述符
cat /proc/1234/io # I/O 统计
# 网络相关
cat /proc/net/dev # 网络接口统计
cat /proc/net/sockstat # socket 统计
15.3 perf 工具入门
perf 是 Linux 内核自带的性能分析工具,可以帮助你找到 CPU 热点代码。在排查某个进程 CPU 持续高负载时,perf 比 strace 开销小,信息量更大:
# 安装 perf(需要匹配内核版本)
sudo apt install -y linux-tools-$(uname -r) linux-tools-common
# 采样某个进程 60 秒的 CPU 热点(替换 PID)
sudo perf record -g -p 1234 — sleep 60
# 生成分析报告
sudo perf report –stdio | head -50
# 实时查看系统级 CPU 事件
sudo perf top
15.4 容器环境中的资源排查差异
如果你在 Docker 容器中运行应用,资源排查会有一些差异:
# 查看容器的资源使用情况
docker stats
# 查看特定容器的详细资源使用
docker stats my-container –no-stream
# 查看容器的 cgroup 资源限制
docker inspect my-container | grep -i "memory\\|cpu"
# 进入容器内部查看(注意:容器内可能没有完整的工具)
docker exec -it my-container top
# 在宿主机上查看容器进程(容器进程在宿主机上也是普通进程)
ps aux | grep container-name
一个重要的认知: 在容器中运行的进程,在宿主机上仍然是普通的 Linux 进程,只是被 namespace 和 cgroup 隔离了。你可以在宿主机上用所有常规工具来排查容器内的进程,只需要知道对应的 PID 即可。
十六、完整排查流程回顾
经过本节七个案例的学习,让我们整理一个完整的资源异常排查 SOP(标准操作程序):
相关示意图绘制如下,仅供大家保存查阅:
排查速查卡
| CPU 持续高 | top -bn1 | pidstat -u 1 5 | 死循环、请求量暴增、密集计算 |
| 内存持续增长 | ps aux –sort=-%mem | journalctl -k | grep oom | 内存泄漏、缓存未清理 |
| Load 高 CPU 不高 | ps aux | awk '$8=="D"' | iostat -x 1 5 | I/O 等待、大量 D 状态进程 |
| 磁盘 I/O 高 | iostat -x 1 5 | sudo iotop -bo | 批量写入、日志轰炸、数据库压力 |
| 磁盘满但有空间 | df -i | find / -xdev -printf '%h\\n' | sort | uniq -c | sort -rn | inode 耗尽、大量小文件 |
| 进程突然消失 | journalctl -k | grep killed | dmesg | grep oom | OOM Kill |
| 连接超时 | ss -s | ss -ant | uniq -c | 连接数打满、TIME_WAIT 过多 |
| 服务无法创建文件 | df -h && df -i | lsof +L1 | 磁盘满、inode 满、文件描述符耗尽 |
十七、总结
这节课我们走过了七个真实的资源异常场景,每个场景都代表了运维工作中常见的一类问题。让我用几句话总结本节最重要的几个认知:
第一,排查要有方法论,不能靠直觉和运气。 宏观扫描 → 分类异常 → 定位进程 → 分析行为 → 找到根因 → 修复验证 → 记录沉淀。这个闭环要形成习惯。
第二,重启只是缓兵之计,不是解决方案。 每一次服务器重启,都应该有一个问题记录——"这次重启是因为什么?根本原因是什么?下次如何预防?"如果永远找不到原因,同样的问题就会反复出现。
第三,CPU 高、内存高、Load 高是三件不同的事。 CPU 高代表计算压力,内存高代表空间压力,Load 高代表调度压力(包含了等待 I/O 的情况)。很多初学者把这三个混为一谈,是排查走弯路的根源。
第四,工具只是手段,理解原理才是目的。 top、iostat、ss 这些命令,你只有理解了它们背后展示的是什么(CPU 统计、I/O 统计、TCP 状态),才能真正读懂输出,而不是看着一堆数字不知所措。
第五,监控和预防比排查更重要。 最好的故障排查是——问题还没影响业务,监控就已经告警了。在这节课的案例中,很多问题如果有完善的监控,都可以在早期就被发现和处理,而不是等到"突然不可用"了才去救火。
本节学到了什么
回顾本节,你应该已经掌握了:
- ✅ 资源排查的整体思路和闭环方法论
- ✅ CPU 飙高的定位方法(top → mpstat → pidstat → strace)
- ✅ OOM Killer 的工作原理和日志查看方法
- ✅ Load Average 高但 CPU 不高的深层原因(I/O 等待与 D 状态进程)
- ✅ 磁盘 I/O 异常的排查(iostat → iotop),以及 inode 耗尽问题
- ✅ 僵尸进程的处理方式(向父进程发信号或 kill 父进程)
- ✅ 网络连接数问题的排查(ss → 连接状态分析 → Keep-Alive 调优)
- ✅ 综合性故障的系统化排查脚本
- ✅ cgroup v2 通过 systemd 限制服务资源的方法
- ✅ 常见资源异常场景的速查卡
下一节可以继续学习什么?
本节是第十章的最后一节。从第十一章开始,我们将进入存储与文件系统的领域:
- 第十一章第1节:Linux 文件系统基础——ext4、XFS、ZFS、Btrfs 对比与选择
- 第十一章第2节:磁盘分区与 LVM 逻辑卷管理实战
- 第十一章第3节:挂载点管理、/etc/fstab 配置与开机自动挂载
- 第十一章第4节:文件权限深入——ACL、SUID、SGID、Sticky Bit
- 第十一章第5节:磁盘空间不足的系统化排查与扩容实战
如果你觉得本节内容对你有帮助,建议你:
运维排查能力是在一次次真实故障中练出来的,知识和实践缺一不可。加油,每一次排查都是一次成长!
…
ok,同学们,本节课就上到这儿,下课~
🧧 学习福利 · 限时开放 🧧
当然,无论你是计算机专业在读学生,还是刚开始接触 Linux 的入门者,亦或是希望提升开发环境搭建、服务器管理、系统运维能力的开发者,都强烈建议系统学习 Ubuntu 全体系专栏:👉 「滚雪球学 Ubuntu」;涵盖 Ubuntu 从入门到进阶的完整学习内容。
该专栏以“循序渐进 + 实战驱动 + 场景落地”为核心理念,从基础认知到系统安装,从桌面操作到命令行实践,从系统管理到网络配置,再到安全加固、开发环境搭建、性能优化、故障排查、服务器部署与云平台应用,逐层展开,帮助你快速建立完整的 Ubuntu / Linux 技术体系,真正把 Ubuntu 用明白、用熟练、用稳定。
📌 学习承诺: 通过该专栏,你将能够:
- 快速掌握 Ubuntu 系统安装、配置与日常使用能力
- 熟练使用 Linux 常用命令、终端工具与系统管理技巧
- 搭建稳定可复用的开发环境、测试环境与服务器环境
- 掌握网络配置、SSH 远程访问、防火墙、安全加固与备份恢复
- 建立从“入门使用”到“独立运维”的完整 Ubuntu 实战能力
就像“滚雪球”一样,先打牢基础,再持续叠加系统管理、开发环境、网络安全、性能优化与服务器部署能力,让知识不断积累、经验持续放大,最终形成属于自己的 Linux 技术护城河 🚀
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注技术号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新 BAT 互联网公司面试题、4000G PDF 编程电子书、简历模板、技术文章 Markdown 文档等海量资料。
ps:本文涉及的 Ubuntu 常用命令、系统配置、环境搭建步骤、服务部署思路与排障方法,均已在文章中结合实战场景进行整理,同学们可以直接对照学习、边看边练。原创整理不易,如果觉得内容有帮助,欢迎点赞、收藏、评论支持一波,非常感谢❗
🫵 Who am I?
我是 bug菌,一名长期深耕后端开发、Linux 系统实践与服务器运维领域的一线技术创作者,曾担任独角兽企业后端技术经理、研发架构师等职位,长期专注于 Java 后端、Linux / Ubuntu 系统管理、服务器部署、分布式架构、微服务治理、高并发系统、工程效能与研发管理等方向。
目前活跃于多个主流技术社区,包括:
CSDN|稀土掘金|InfoQ|51CTO|华为云开发者社区|阿里云开发者社区|腾讯云开发者社区|开源中国|博客园|墨天轮 等平台。
曾获得:
- CSDN 博客之星 Top30
- 华为云多年度十佳博主 & 卓越贡献奖
- 掘金多年度人气作者 Top40
- CSDN、掘金、InfoQ、51CTO 等平台签约作者 / 优质作者
截至目前,全网技术内容累计影响读者众多,全网粉丝已超过 30w+。
如果你也关注 Linux、Ubuntu、Java 后端、服务器运维、架构设计、技术成长、职场进阶与研发管理,欢迎关注我的技术内容合集入口:👉 点击查看 👈️
硬核技术号 「猿圈奇妙屋」 期待你的加入。
这里不仅分享技术干货,也记录一线研发人的成长、踩坑、思考与进阶路径。
愿我们一起打怪升级,把 Ubuntu 用成生产力工具,把 Linux 练成核心竞争力,在技术路上持续进阶。
– End –





