运维知识
悠悠
2026年9月7日

线上服务器突然变慢,我花了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% 大量系统调用
waIO等待>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一把梭。

排障时间线和关键阈值速查

阶段命令关键看点
看负载uptimeload > 核心数×2 就该查了
分方向topwa>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检查`dmesggrep "killed process"`
IO概览iostat -xz 2%util>80%磁盘繁忙
IO定位iotop -oP找IO大户
磁盘空间df -h / df -i别忘了查inode
大文件`du -sh /*sort -rh`
网络连接ss -s / ss -tanTIME_WAIT和CLOSE_WAIT
网络延迟mtr 目标IP看哪一跳丢包
抓包tcpdump -i eth0 port XX终极排查手段

几个容易踩的坑

  1. rm大文件空间没释放:进程还持有文件句柄,要么重启进程,要么用> /proc/PID/fd/N清空
  2. top看到的内存使用包含buff/cache:真实可用看available
  3. load高不等于CPU高:IO等待也会拉高load
  4. 在高负载服务器上跑perf/strace:会让本来就慢的服务更慢,先确认不会导致雪崩
  5. 直接改sysctl参数不持久化:重启就丢了,改完记得写/etc/sysctl.conf
  6. SSH连不上时忘记用带外管理:云服务器可以用VNC控制台,物理机用IPMI/iLO

写在最后

系统排障这个事,说到底就是对操作系统底层机制要熟。CPU调度、内存管理、文件系统、网络协议栈,这些基础功扎实了,排查起来才能又快又准。

我那次花两个小时,现在回头看,如果当时先看了wa这个指标,直接走IO排查路径,可能20分钟就搞定了。框架和经验就是这么攒出来的,每次踩坑都是学费。

我是「运维躬行录」,专注分享云计算和运维的生产实践经验。如果这篇文章对你有帮助,帮忙点个赞、转发一下。 关注公众号:耕云躬行录 个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码