线上服务器突然变慢,我花了2小时才定位到一个定时任务——聊聊Linux系统排障那些事
排障别慌,先建立一个排查框架
很多人遇到线上故障第一反应就是top一把看看,发现load高就开始查CPU,结果查了半天发现CPU其实没啥问题,真正的瓶颈在IO。
我现在养成的习惯是按一个固定的顺序走:
整体负载 → 瓶颈类型(CPU/内存/磁盘/网络)→ 定位进程 → 分析根因 → 解决问题
从宏观到微观,从现象到本质。听起来像废话,但真出事的时候脑子容易乱,有个框架不至于跑偏。
第一步:uptime看负载,top看全貌
上来先敲一个uptime:
$ uptime
15:32:10 up 67 days, 4:21, 3 users, load average: 23.45, 18.72, 12.36
load average三个数字分别是1分钟、5分钟、15分钟的平均负载。怎么判断高不高?跟CPU核心数比:
$ nproc
4
- load < 核心数:正常
- load ≈ 核心数 × 2:比较忙了
- load > 核心数 × 2:过载,需要排查
我这台4核的机器load到了23,明显过载。但load高不代表CPU高——load衡量的是「等待运行的任务队列长度」,IO等待的进程也会把load拉上去。
接下来top看一眼全貌:
$ top
top - 15:32:15 up 67 days, 4:21, 3 users, load average: 23.45, 18.72, 12.36
Tasks: 218 total, 2 running, 216 sleeping, 0 stopped, 0 zombie
%Cpu(s): 12.3 us, 5.6 sy, 0.0 ni, 38.2 id, 42.8 wa, 0.0 hi, 1.1 si, 0.0 st
MiB Mem : 7821.3 total, 312.5 free, 5843.2 used, 1665.6 buff/cache
MiB Swap: 2048.0 total, 1536.0 free, 512.0 used. 1423.4 avail Mem
重点关注%Cpu(s)这一行:
| 指标 | 含义 | 异常阈值 |
|---|---|---|
| us | 用户态CPU | >70% 有CPU密集型任务 |
| sy | 内核态CPU | >30% 大量系统调用 |
| wa | IO等待 | >20% 磁盘IO有瓶颈 |
| id | 空闲 | 越低越忙 |
| st | 被虚拟化偷走的 | >10% 宿主机超卖 |
我那次的情况,wa高达42.8%,id还有38%,us才12%。典型的IO瓶颈,不是CPU的问题。但我一开始没注意wa这个指标,盯着us看了半天,浪费了不少时间。
踩坑提醒:很多人一看load高就以为是CPU的事,但load高 + wa高的组合说明是IO问题。先分清瓶颈类型再往下查,方向错了后面全白费。
CPU排查:找到那个吃CPU的进程
虽然我那次不是CPU问题,但CPU排查用得最多,放在前面说。
判断是不是CPU瓶颈很简单:us + sy > 90%,id < 10%,基本就是了。
找到CPU高的进程:
# top默认就是按CPU排序
$ top -o %CPU
# 或者用ps
$ ps aux --sort=-%cpu | head -20
找到进程之后,关键是弄清楚它在干什么。
方法一:strace跟踪系统调用
$ strace -p <PID> -c -t
# 按Ctrl+C结束后会显示统计
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- --------
85.23 1.234567 123 10000 write
8.45 0.123456 12 10000 read
4.32 0.054321 5 10000 open
如果write占了85%的时间,那基本可以断定这个进程在疯狂写数据。配合lsof -p <PID>看看它在写哪个文件。
方法二:perf分析CPU热点
# 采样30秒
$ perf record -p <PID> -g -- sleep 30
# 查看结果
$ perf report
perf能看到函数级别的CPU消耗,对Java/Go/C++程序定位热点代码特别有用。
方法三:Java进程专用
# 先找到CPU最高的线程
$ top -Hp <PID>
# 把线程ID转成16进制
$ printf "0x%x" <Thread_PID>
# 在jstack中搜索
$ jstack <PID> | grep -A 30 "0x<hex>"
这套组合拳在排查Java应用的时候特别好使,能直接看到某个线程在执行什么代码。
内存排查:别被free -h骗了
内存问题排查,第一个命令就是free -h:
$ free -h
total used free shared buff/cache available
Mem: 7.6Gi 5.8Gi 234Mi 123Mi 1.6Gi 1.4Gi
Swap: 2.0Gi 512Mi 1.5Gi
新手容易犯的错误:看到free只剩234M就觉得内存不够了。不对,要看available。Linux会把空闲内存拿去做缓存(buff/cache),这部分在需要的时候是可以释放的。available才是真正可用的内存。
但如果Swap开始被使用了,说明物理内存确实不够了。Swap的读写速度比内存慢几十倍,一旦开始频繁换入换出,性能断崖式下降。
找内存大户:
# 按内存排序
$ ps aux --sort=-%mem | head -20
# 看某个进程的实际内存占用
$ cat /proc/<PID>/status | grep -E "VmRSS|VmSize"
VmSize: 4523412 kB # 虚拟内存(不重要)
VmRSS: 3321456 kB # 实际物理内存(看这个)
VmRSS才是进程真正占用的物理内存,VmSize包含了很多映射但没实际分配的空间,看着大但不代表真的用了那么多。
内存泄漏怎么抓
如果发现某个进程的内存在持续增长:
# 每5分钟记录一次内存
$ while true; do
echo "$(date '+%Y-%m-%d %H:%M:%S') $(ps -p <PID> -o rss= | awk '{print $1/1024"MB"}')" >> /tmp/mem_monitor.log
sleep 300
done
跑个一两天,把数据画个图看趋势。如果是一条稳步上升的线,八成就是内存泄漏。
Java应用可以直接dump堆内存分析:
# dump堆内存
$ jmap -dump:format=b,file=/tmp/heap.hprof <PID>
# 用MAT或VisualVM分析
OOM Killer翻旧账
进程莫名其妙挂了?可能是被OOM Killer干掉的:
$ dmesg | grep -i "out of memory"
$ dmesg | grep -i "killed process"
如果看到类似这样的日志:
[Thu Sep 3 02:34:56 2026] Out of memory: Killed process 12345 (java) total-vm:4523412kB, anon-rss:3321456kB
那就是内存不够被系统杀掉了。要么加内存,要么优化应用的内存使用,要么调整OOM score让系统优先杀其他不重要的进程。
# 降低某个进程被OOM Kill的优先级(-1000到1000,越低越不容易被杀)
$ echo -500 > /proc/<PID>/oom_score_adj
磁盘IO排查:那次翻车的元凶
回到我那次的故障。wa高达42%,确定是IO瓶颈后,用iostat看磁盘状态:
$ iostat -xz 2
Device r/s w/s rkB/s wkB/s await %util
sda 12.00 380.00 96.00 52480.00 45.23 99.80
%util都99.8%了,磁盘基本被打满。wkB/s有51MB/s的写入,明显有东西在疯狂写数据。
用iotop找到罪魁祸首:
$ iotop -oP
Total DISK READ: 0.00 B/s | Total DISK WRITE: 51.23 M/s
PID PRIO USER DISK READ DISK WRITE COMMAND
12345 be/4 mysql 0.00 B/s 48.56 M/s mysqld
6789 be/4 root 0.00 B/s 2.67 M/s rsync
MySQL在疯狂写?但业务逻辑不应该有这么大的写入量。接着查磁盘空间:
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 100G 98G 2.0G 98% /
98%了!赶紧看哪里占的空间:
$ du -sh /* 2>/dev/null | sort -rh | head -10
62G /var
18G /home
8.2G /usr
...
$ du -sh /var/* 2>/dev/null | sort -rh | head -5
58G /var/log
2.1G /var/lib
...
$ ls -lhS /var/log/*.log | head -5
-rw-r--r-- 1 root root 54G Sep 3 15:30 /var/log/app-debug.log
找到了!一个debug级别的日志文件涨到了54G。这个定时任务每分钟跑一次数据同步脚本,脚本里把日志级别设成了DEBUG,每次执行都写一堆调试信息。磁盘快满的时候,MySQL的binlog和redo log写入变慢,触发了大量的磁盘等待,load就这么飙上去了。
紧急处理:
# 清空日志文件(注意不是rm,因为进程还在写,rm了inode不释放)
$ > /var/log/app-debug.log
# 确认空间释放
$ df -h
注意这里用>重定向而不是rm。如果直接rm,进程还持有文件句柄,磁盘空间不会释放,你会发现删了文件但df -h显示的使用率没变。这时候要么重启那个进程,要么用lsof | grep deleted找到还在占用的进程。
# 如果误用了rm,可以找到还在占用的进程
$ lsof | grep deleted | grep "app-debug"
java 12345 root 5w REG 8,1 54000000000 /var/log/app-debug.log (deleted)
# 通过/proc清空文件内容释放空间
$ > /proc/12345/fd/5
事后加固
光解决当前问题还不够,得防止再犯。
配置logrotate:
$ cat /etc/logrotate.d/app-debug
/var/log/app-debug.log {
daily
rotate 7
compress
missingok
notifempty
size 1G
copytruncate
}
这里用copytruncate而不是默认的create方式,是因为很多应用不支持日志文件被重命名后重新打开(没有做SIGHUP处理)。copytruncate的方式是先复制一份再清空原文件,应用无感知。
磁盘监控告警:
#!/bin/bash
# disk_alert.sh - 磁盘使用率告警
THRESHOLD=85
df -h | awk 'NR>1 {gsub(/%/,"",$5); if($5 > '$THRESHOLD') print $6": "$5"%"}' | while read line; do
# 发告警,比如调企业微信机器人
curl -s "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"磁盘告警: '"$line"'"}}'
done
扔到crontab里每小时跑一次就行。
inode满了也是个坑
有一种情况比较隐蔽:df -h显示磁盘空间还有,但是写文件报No space left on device。
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 100G 60G 40G 60% /
$ touch /tmp/test
touch: cannot touch '/tmp/test': No space left on device
这时候查inode:
$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 6553600 6553600 0 100% /
inode满了!通常是某个目录下有海量小文件。
# 找inode消耗大户
$ find / -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -10
3245678 /var/spool/postfix/maildrop
12345 /tmp/session
邮件队列里堆了三百多万个小文件...定时任务的cron报错邮件没人收,全堆在这儿了。
# 批量删除(文件太多rm会报参数过长)
$ find /var/spool/postfix/maildrop -type f -delete
# 或者用xargs分批删
$ find /var/spool/postfix/maildrop -type f | xargs -n 1000 rm -f
网络排查:TIME_WAIT和连接数
网络问题相对好判断,通常表现为连接超时、响应慢、丢包。
查看连接状态:
$ ss -s
Total: 2345
TCP: 1890 (estab 567, closed 423, orphaned 12, timewait 888)
$ ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
888 TIME-WAIT
567 ESTAB
234 CLOSE-WAIT
123 FIN-WAIT-2
78 LISTEN
TIME_WAIT多到888个,这对于端口回收是有压力的。系统默认可用端口范围:
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
大概28000个端口。TIME_WAIT默认60秒才释放,如果短连接并发高,端口很容易耗尽。
优化内核参数:
# 开启TIME_WAIT快速回收
$ sysctl -w net.ipv4.tcp_tw_reuse=1
# 扩大端口范围
$ sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# 持久化写入/etc/sysctl.conf
$ cat >> /etc/sysctl.conf << 'EOF'
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
EOF
$ sysctl -p
CLOSE_WAIT暴增是另一个常见问题。CLOSE_WAIT说明对端已经关闭连接了,但本地没有调用close(),通常是代码bug——HTTP请求完成后没有正确关闭连接。
# 查CLOSE_WAIT最多的进程
$ ss -tanp | grep CLOSE-WAIT | awk '{print $NF}' | sort | uniq -c | sort -rn
156 users:(("java",pid=12345,fd=234))
23 users:(("python",pid=6789,fd=56))
这种基本只能从代码层面修复,加finally块确保连接释放。
网络延迟排查:
# mtr比traceroute好用,持续探测
$ mtr -r -c 100 目标IP
# 抓包分析特定端口
$ tcpdump -i eth0 port 3306 -c 1000 -w /tmp/mysql.pcap
# 然后用Wireshark分析
一键排查脚本
每次出故障都敲一堆命令太累了,我写了个脚本扔到所有服务器上:
#!/bin/bash
# quick-check.sh - 服务器快速体检
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m'
echo "========== 系统负载 =========="
load=$(uptime | awk -F'load average:' '{print $2}' | awk -F, '{print $1}' | tr -d ' ')
cores=$(nproc)
echo "Load(1min): $load / CPU cores: $cores"
if (( $(echo "$load > $cores * 2" | bc -l) )); then
echo -e "${RED}[WARNING] 系统过载!${NC}"
fi
echo -e "\n========== CPU使用 =========="
top -bn1 | head -5
echo -e "\n========== 内存使用 =========="
free -h
swap_used=$(free | awk '/Swap/ {print $3}')
if [ "$swap_used" -gt 0 ]; then
echo -e "${YELLOW}[NOTICE] Swap正在使用,物理内存可能不足${NC}"
fi
echo -e "\n========== 磁盘空间 =========="
df -h | grep -v tmpfs | awk '{if(NR>1 && int($5)>85) print "\033[0;31m"$0"\033[0m"; else print $0}'
echo -e "\n========== 磁盘IO =========="
which iostat &>/dev/null && iostat -xz 1 2 | tail -10 || echo "iostat未安装,yum install sysstat"
echo -e "\n========== CPU TOP10 =========="
ps aux --sort=-%cpu | head -11
echo -e "\n========== 内存 TOP10 =========="
ps aux --sort=-%mem | head -11
echo -e "\n========== 网络连接统计 =========="
ss -s
echo -e "\n========== 最近系统日志(可能有OOM等关键信息) =========="
dmesg -T 2>/dev/null | tail -20 || dmesg | tail -20
echo -e "\n========== 排查完成 =========="
扔到/usr/local/bin/下,chmod +x,出问题直接quick-check.sh一把梭。
排障时间线和关键阈值速查
| 阶段 | 命令 | 关键看点 | |
|---|---|---|---|
| 看负载 | uptime | load > 核心数×2 就该查了 | |
| 分方向 | top | wa>20%查IO,us+sy>90%查CPU | |
| CPU定位 | top -o %CPU / ps aux --sort=-%cpu | 找到吃CPU的进程 | |
| CPU分析 | strace -p PID -c / perf record | 看系统调用/函数热点 | |
| 内存概览 | free -h | 看available,不是free | |
| 内存定位 | ps aux --sort=-%mem | 找内存大户 | |
| 内存泄漏 | cat /proc/PID/status | 监控VmRSS持续增长 | |
| OOM检查 | `dmesg | grep "killed process"` | |
| IO概览 | iostat -xz 2 | %util>80%磁盘繁忙 | |
| IO定位 | iotop -oP | 找IO大户 | |
| 磁盘空间 | df -h / df -i | 别忘了查inode | |
| 大文件 | `du -sh /* | sort -rh` | |
| 网络连接 | ss -s / ss -tan | TIME_WAIT和CLOSE_WAIT | |
| 网络延迟 | mtr 目标IP | 看哪一跳丢包 | |
| 抓包 | tcpdump -i eth0 port XX | 终极排查手段 |
几个容易踩的坑
- rm大文件空间没释放:进程还持有文件句柄,要么重启进程,要么用
> /proc/PID/fd/N清空 - top看到的内存使用包含buff/cache:真实可用看available
- load高不等于CPU高:IO等待也会拉高load
- 在高负载服务器上跑perf/strace:会让本来就慢的服务更慢,先确认不会导致雪崩
- 直接改sysctl参数不持久化:重启就丢了,改完记得写/etc/sysctl.conf
- SSH连不上时忘记用带外管理:云服务器可以用VNC控制台,物理机用IPMI/iLO
写在最后
系统排障这个事,说到底就是对操作系统底层机制要熟。CPU调度、内存管理、文件系统、网络协议栈,这些基础功扎实了,排查起来才能又快又准。
我那次花两个小时,现在回头看,如果当时先看了wa这个指标,直接走IO排查路径,可能20分钟就搞定了。框架和经验就是这么攒出来的,每次踩坑都是学费。
我是「运维躬行录」,专注分享云计算和运维的生产实践经验。如果这篇文章对你有帮助,帮忙点个赞、转发一下。 关注公众号:耕云躬行录 个人博客:躬行笔记