1. 线上问题排查的核心思路与命令体系拆解
线上出问题的时候,最怕的不是问题本身有多复杂,而是手忙脚乱不知道从哪里下手。我经历过好几次半夜被叫起来处理故障的场景,最开始那几次真的是两眼一抹黑,登录到机器上到处乱敲,结果越搞越乱。后来踩坑踩多了,慢慢总结出一套自己的排查路径和常用命令组合,基本上能在几分钟内定位到大致方向。
这套东西说白了就一句话:先看全局,再钻细节,从外到内逐层收敛。听起来像废话,但真正在紧张的环境下能按这个节奏走的人不多。大多数人一上来就盯着某个自己怀疑的点猛查,结果方向错了,白白浪费时间。
这篇文章适合谁看?如果你是刚接触线上运维或者后端开发的,还没形成自己的排查套路,那这些内容可以直接拿去用。如果你已经有一定经验,也可以看看我在每个环节用的具体命令和参数,说不定有你没注意到的细节。我尽量把每个命令为什么这么用、什么场景下用、输出怎么看都讲清楚,不堆砌命令列表,而是按排查流程串起来。
整个排查体系我分成四个层面:系统资源层、进程与线程层、网络连接层、应用日志层。这四个层面基本覆盖了绝大多数线上问题的排查需求。下面逐个展开,把每个层面常用的命令、参数、输出解读和实操经验都过一遍。
2. 系统资源层:CPU、内存、磁盘、负载的快速体检
2.1 用 top 和 uptime 建立第一印象
登录到出问题的机器上,我第一个敲的命令永远是uptime。这个命令输出极简,但信息量很大:
uptime # 输出示例: 14:32:05 up 127 days, 3:22, 4 users, load average: 8.52, 6.31, 4.97重点看load average后面三个数,分别代表1分钟、5分钟、15分钟的平均负载。怎么判断高不高?用负载值除以CPU核心数。比如这台机器是4核,1分钟负载8.52,那就是8.52/4≈2.13,意味着平均每个核心上有2.13个任务在排队,已经明显过载了。
更关键的是看趋势:如果1分钟值远高于15分钟值(比如8.52 vs 4.97),说明负载在快速上升,问题正在恶化;反过来如果1分钟值在下降,说明高峰可能已经过去了。这个趋势判断在决定要不要紧急扩容时特别重要。
紧接着就是top,这个大家都会用,但我重点说几个容易被忽略的操作:
- 按
1可以展开看每个CPU核心的使用率,能快速判断是单核跑满还是多核均衡 - 按
Shift+M按内存排序,Shift+P按CPU排序,Shift+T按运行时间排序 top -H -p <pid>可以看某个进程下所有线程的资源占用,排查多线程问题时必用
注意:top 默认3秒刷新一次,在排查快速变化的问题时可以按
d然后输入1改成1秒刷新,但别改太快,否则输出刷屏根本看不清。
2.2 内存排查:free、vmstat 和 OOM 的蛛丝马迹
内存问题最隐蔽,因为它不像CPU那样直接体现为响应变慢,很多时候是突然进程被杀或者服务崩溃。free -h是最常用的:
free -h # total used free shared buff/cache available # Mem: 31Gi 28Gi 1.2Gi 512Mi 1.8Gi 1.5Gi这里最容易看错的是free那一列。Linux会把大量空闲内存拿去做缓存(buff/cache),所以free很小不代表内存不够。真正要看的是available这一列,它才是应用程序真正可以用的内存量。上面这个例子 available 只有1.5Gi,说明内存确实紧张了。
如果怀疑有进程被OOM Killer干掉了,用dmesg查:
dmesg -T | grep -i "oom\|killed process"这个命令能直接告诉你哪个进程在什么时间被系统杀掉了,以及当时的内存状况。我遇到过好几次服务莫名其妙重启,最后都是通过这个命令确认是内存泄漏导致被OOM。
vmstat 1也是内存排查利器,重点看si和so两列(swap in/out)。如果这两个值持续不为0,说明系统在频繁使用交换分区,性能会急剧下降。正常情况这两个值应该一直是0。
2.3 磁盘与IO:df、iostat 和 du 的组合拳
磁盘满了是线上最常见的问题之一,df -h一眼就能看出来:
df -h # Filesystem Size Used Avail Use% Mounted on # /dev/vda1 99G 94G 1.2G 99% /Use% 超过90%就要警惕了,超过95%基本就要立即处理。但光看 df 不够,还得知道是什么占满了。我常用的组合是:
# 从根目录逐层往下找大目录 du -h --max-depth=1 / 2>/dev/null | sort -rh | head -20这个命令会列出根目录下每个一级目录的大小并降序排列,然后你对着最大的那个目录再执行一次,逐层定位到具体的大文件。这个过程听起来笨,但比任何工具都可靠。
如果磁盘没满但IO很高,用iostat -x 1看:
iostat -x 1 # Device r/s w/s rkB/s wkB/s await %util # vda 120.5 340.2 4820 13608 15.3 98.7重点看%util,接近100%说明磁盘IO已经饱和。await是平均等待时间(毫秒),正常应该在个位数,超过20就要关注了。如果%util高但r/s和w/s都不大,可能是磁盘硬件本身有问题。
实操心得:有一次线上数据库响应极慢,CPU和内存都正常,最后用 iostat 发现 %util 持续100%,但读写次数很低,换了一块盘就好了。硬件故障有时候比软件问题更隐蔽。
3. 进程与线程层:定位资源消耗的元凶
3.1 ps 命令的高阶用法
ps大家都会用,但很多人只用ps aux | grep xxx。其实排查线上问题时,几个组合用法效率更高:
# 按CPU使用率降序排列,取前10 ps aux --sort=-%cpu | head -11 # 按内存使用率降序排列 ps aux --sort=-%mem | head -11 # 查看进程的启动时间和运行时长 ps -eo pid,lstart,etime,cmd | grep <进程名>etime显示进程已经运行了多久,如果发现某个进程刚启动不久,结合服务日志就能判断是不是刚重启过。lstart显示精确的启动时间,在排查“服务什么时候挂的”这类问题时特别有用。
还有一个很实用的技巧:查看进程的父子关系树。
ps -ef --forest | grep -A 5 <进程名>这个能帮你理清进程之间的调用关系,尤其是排查脚本启动的服务时,能快速找到真正的父进程。
3.2 线程级排查:top -H 和 jstack 配合
当发现某个Java进程CPU很高时,光看进程级别不够,得钻到线程级别:
# 第一步:找到CPU最高的进程 top -c # 假设PID是12345 # 第二步:查看该进程下所有线程的CPU占用 top -H -p 12345 # 第三步:找到CPU最高的线程,假设TID是12378 # 把线程ID转成16进制 printf "%x\n" 12378 # 输出:305a # 第四步:用jstack导出线程栈,搜索这个16进制ID jstack 12345 | grep -A 30 "305a"这一套组合拳下来,基本能定位到具体是哪段代码在消耗CPU。我排查过好几次死循环导致的CPU飙高,都是靠这个方法找到的。
对于非Java的应用,可以用gdb或者perf来分析,但原理是一样的:先找到线程,再看线程在干什么。
3.3 文件句柄与进程限制
有时候服务报“Too many open files”,问题出在文件句柄数上:
# 查看系统级限制 cat /proc/sys/fs/file-max # 查看进程级限制 cat /proc/<pid>/limits | grep "open files" # 查看当前进程已打开的文件句柄数 ls /proc/<pid>/fd | wc -l # 查看具体打开了哪些文件 ls -l /proc/<pid>/fd | head -20如果发现句柄数接近限制值,要么调大限制(修改/etc/security/limits.conf),要么排查是不是有句柄泄漏。句柄泄漏通常是因为打开了文件或网络连接没有正确关闭,用lsof -p <pid>可以看到具体的连接状态。
注意:修改 limits.conf 后需要重新登录或者重启服务才能生效,别改完就以为好了。
4. 网络连接层:从连通性到连接状态的全面诊断
4.1 连通性排查:ping、telnet 和 curl 的正确打开方式
网络问题排查的第一步永远是确认连通性。但很多人只会ping,其实不同场景要用不同工具:
# 基础连通性 ping -c 4 <目标IP> # 检查特定端口是否可达 telnet <目标IP> <端口> # 或者用 nc nc -zv <目标IP> <端口> # 检查HTTP服务是否正常 curl -v -o /dev/null -s -w "HTTP状态码: %{http_code}\n连接时间: %{time_connect}s\n总时间: %{time_total}s\n" http://<目标地址>curl的-w参数特别有用,可以输出详细的耗时分解。time_connect是TCP连接建立的时间,time_total是完整请求的时间。如果time_connect很长,说明网络延迟高或者对方响应慢;如果time_connect正常但time_total很长,说明是服务处理慢。
4.2 连接状态分析:ss 和 netstat 的实战对比
netstat是老牌命令,但现在更推荐用ss,因为它更快且信息更全:
# 查看所有TCP连接状态统计 ss -s # 查看特定端口的连接 ss -tnp | grep :8080 # 统计各状态的连接数 ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn重点关注的连接状态:
| 状态 | 含义 | 异常判断 |
|---|---|---|
| ESTABLISHED | 已建立的连接 | 数量突增可能是流量暴涨或连接泄漏 |
| TIME_WAIT | 等待关闭的连接 | 大量出现说明短连接频繁,考虑连接池 |
| CLOSE_WAIT | 等待应用关闭 | 持续增多说明应用没有正确关闭连接 |
| SYN_RECV | 收到SYN但未完成握手 | 大量出现可能是SYN Flood攻击 |
CLOSE_WAIT 是我遇到最多的问题。如果这个状态的连接数持续增长,基本可以确定是应用程序的bug——收到了对方的FIN但没有调用close()关闭连接。这种情况光靠调系统参数没用,必须改代码。
TIME_WAIT 过多则是另一个极端,通常是短连接太多。解决办法是使用连接池,或者调整内核参数:
# 查看当前TIME_WAIT相关参数 sysctl net.ipv4.tcp_tw_reuse sysctl net.ipv4.tcp_max_tw_buckets实操心得:tcp_tw_reuse 可以安全开启,它允许将TIME_WAIT状态的连接重新用于新的TCP连接。但 tcp_tw_recycle 在NAT环境下会导致问题,新版本内核已经移除了这个参数,不建议使用。
4.3 抓包分析:tcpdump 的精准用法
当连通性和连接状态都正常但服务就是不通时,就得上抓包了:
# 抓取指定网卡、指定端口、指定IP的包 tcpdump -i eth0 -nn port 8080 and host 192.168.1.100 -w /tmp/capture.pcap # 实时查看HTTP请求 tcpdump -i eth0 -nn -A port 80 | grep -E "GET|POST|HTTP" # 抓取特定数量的包后自动停止 tcpdump -i eth0 -nn -c 100 port 443抓包文件可以用tcpdump -r读取,也可以下载到本地用Wireshark分析。我一般会先抓100个包看看有没有明显的异常,比如TCP重传、RST包等。
# 检查是否有TCP重传 tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-rst != 0'这个命令专门抓RST包,如果频繁出现RST,说明连接被异常重置,可能是防火墙拦截或者服务端主动拒绝。
5. 应用日志层:从日志中快速提取关键信息
5.1 日志查看的基本功
日志排查的核心就三个操作:找、筛、追。
# 实时追踪日志 tail -f /var/log/app/application.log # 查看最后100行 tail -n 100 /var/log/app/application.log # 搜索关键字 grep "ERROR" /var/log/app/application.log # 搜索并显示上下文 grep -C 5 "Exception" /var/log/app/application.log # 统计错误出现次数 grep -c "ERROR" /var/log/app/application.loggrep -C 5这个参数特别重要,它显示匹配行的前后5行,能帮你看到异常发生时的上下文。很多时候错误信息本身不够,需要看前面的调用链和后面的堆栈。
5.2 大日志文件的处理技巧
线上日志动辄几个G,直接cat或者grep会非常慢。几个提速技巧:
# 用tail查看文件末尾,比cat快得多 tail -n 10000 application.log | grep "ERROR" # 用sed查看指定时间范围的日志 sed -n '/2024-01-15 14:00:00/,/2024-01-15 14:05:00/p' application.log # 用awk做更复杂的过滤 awk '/2024-01-15 14:00/,/2024-01-15 14:05/' application.log | grep "ERROR" # 统计每分钟的日志量 awk '{print substr($2,1,5)}' application.log | uniq -c最后一个命令特别有用,能快速看出哪个时间段日志量突增,通常对应着问题发生的时间点。
5.3 日志中的关键信号
看日志不能只看ERROR,有些信号藏在WARN和INFO里:
- 频繁的GC日志:说明内存压力大,可能需要调整堆大小
- 连接池等待:说明连接池配置不够,需要调大
- 重试日志:说明下游服务不稳定
- 超时日志:说明网络或者下游处理慢
我一般会先用grep -c统计各类日志的数量,找出异常多的那一类,再深入看具体内容。这个方法比从头翻日志快得多。
注意:有些日志框架会异步写日志,服务崩溃时最后几条日志可能丢失。如果发现日志突然中断,不要以为服务是正常停止的,很可能是崩溃了。
6. 常见问题速查与排查路径总结
6.1 典型问题排查路径速查表
| 问题现象 | 第一步 | 第二步 | 第三步 |
|---|---|---|---|
| 服务响应慢 | uptime看负载 | top看CPU/内存 | iostat看磁盘IO |
| 服务无响应 | ss看连接状态 | 检查CLOSE_WAIT | jstack看线程栈 |
| 服务频繁重启 | dmesg看OOM | 检查内存泄漏 | 看启动日志 |
| 网络不通 | ping测连通 | telnet测端口 | tcpdump抓包 |
| 磁盘告警 | df -h看使用率 | du逐层定位 | 清理或扩容 |
| CPU飙高 | top -H看线程 | jstack定位代码 | 分析死循环 |
6.2 我踩过的几个坑
第一个坑:只看平均值不看分布。有一次CPU使用率显示只有60%,看起来不高,但服务就是慢。后来用mpstat -P ALL 1发现是单核跑满,其他核空闲。原因是应用没有做好多核并行,所有请求都压在一个核上。所以看CPU一定要用top按1展开看每个核的情况。
第二个坑:忽略TIME_WAIT的影响。有一次压测时QPS上不去,查了半天发现是TIME_WAIT连接太多导致端口耗尽。后来开启了tcp_tw_reuse并调整了端口范围才解决。这个问题的隐蔽性在于,它不会导致报错,只是性能上不去。
第三个坑:日志级别设置不当。生产环境开了DEBUG级别日志,结果磁盘IO被日志写满,影响了正常业务。后来改成INFO级别,并且对日志做了异步和限流处理。这个教训告诉我,日志虽好,但不能贪多。
第四个坑:排查时改了配置忘了改回来。有一次为了排查问题临时调大了某个参数,问题解决后忘了恢复,结果过了一段时间引发了另一个问题。现在我养成了习惯,所有临时改动都记在备忘录里,排查结束后逐一确认恢复。
6.3 建立自己的排查清单
最后分享一个我自己的做法:把常用的排查命令整理成一个脚本,登录机器后直接跑一遍,输出一份系统体检报告。这样即使半夜被叫起来脑子不清醒,也能按部就班地收集信息。
#!/bin/bash echo "===== 系统负载 =====" uptime echo "===== CPU TOP5 =====" ps aux --sort=-%cpu | head -6 echo "===== 内存 TOP5 =====" ps aux --sort=-%mem | head -6 echo "===== 磁盘使用 =====" df -h | grep -v tmpfs echo "===== 连接状态统计 =====" ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn echo "===== 最近OOM记录 =====" dmesg -T | grep -i "oom\|killed" | tail -5这个脚本跑一遍大概10秒钟,但能覆盖80%的常见问题。我把它放在每台服务器的/usr/local/bin/下,需要的时候直接执行,省去了逐个敲命令的时间。
排查线上问题这件事,工具和命令只是基础,更重要的是形成自己的排查思路和节奏。遇到问题不要慌,按照“系统资源→进程线程→网络连接→应用日志”这个顺序逐层排查,大部分问题都能在几分钟内定位到方向。剩下的就是经验和细节的积累了,踩的坑越多,下次排查就越快。