用好这 5 个 Linux 命令,排查效率提升 10 倍

引言

"上周三晚上,支付服务突然不响应,登录到机器上 screen 早已刷不动。值班同学的第一反应是重启——被拦下来了。30 分钟后我们还原出了完整的事故链条:top -H 看到一个线程 CPU 100%(不是进程,是 GC 线程),jstack 确认是 Young GC 风暴,ss -s 发现 ESTAB 连接只有 47 条但 SYN-RECV 堆了 4000+,lsof 揪出一个 Tomcat 容器拿着已被删除的 42GB 日志文件不放手——磁盘早满了,GC 日志都写不出来,一连串症状相互掩盖。如果按'感觉像什么就查什么'的乱枪打法,这 30 分钟可能变成 3 小时。"

排障和破案一样:新手靠猜,高手靠证据链。而证据链的生产工具,就是那几个看起来平平无奇的 Linux 命令。这篇文章按"从系统到进程、从现象到根因"的排查路径,精选 5 个我日常使用频率最高、也最容易拉开效率差距的命令:ss(网络连接)、lsof(谁占着端口/文件)、strace(进程卡在哪次系统调用)、top -H(哪个线程在烧 CPU)、jstack(Java 线程在等什么)。每个命令只讲三件事:实战场景(什么症状该想起它)、核心用法(可直接抄的命令行)、输出解读(每一列/每个状态什么意思)。文末给一张"症状 → 命令"的速查决策卡,让它成为你 next 值班手册的第一页。

先说清楚这 5 个命令在排障地图上的位置,避免"有锤子看什么都是钉子":

【现象】服务不响应 / 慢 / CPU 高 / 磁盘满 / 连接异常
   │
   ├─ 网络层问题? ────── ss        连接状态/队列溢出/端口监听
   ├─ "资源被谁占着"? ── lsof      端口冲突/文件被删仍占磁盘/socket 泄漏
   ├─ 进程"卡住"在哪? ── strace    卡在哪个系统调用(connect/read/futex...)
   ├─ CPU 谁在烧? ───── top -H     定位到线程级(进程内哪个线程)
   └─ Java 线程在干嘛?─ jstack     把线程号翻译成代码行(与 top -H 联动)

一、ss:看网络连接,比 netstat 快一个量级

1.1 为什么弃用 netstat

netstat 遍历 /proc 文件系统逐个读取,连接数一多就慢得离谱;ss 直接通过 netlink 套接字从内核取数据,十万级连接也是毫秒级出结果。CentOS 7+/Ubuntu 16+ 默认预装(iproute2 包),没有理由再用 netstat。

1.2 实战场景一:服务不响应,先看连接健康度

# 总览:各状态连接计数(排障第一步,10 秒看清全貌)
ss -s

Total: 8231
TCP:   8231 (estab 47, closed 0, orphaned 0, synrecv 0, timewait 8102/0)
       ports 0

Transport Total     IP        IPv6
*         8231      -         -
UDP       12        12        0
TCP       8231      8231      0

解读:这个输出里藏着本次排查的第一个线索——ESTAB 只有 47 条,但 TIME-WAIT 高达 8102。TIME-WAIT 是主动关闭方 2MSL 的滞留态,短连接风暴(每个请求新建连接)的典型指纹。配合 ESTAB 数与业务 QPS 对不上,方向就有了:下游调用是不是没用连接池?

# 各状态明细计数
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn

   8102 TIME-WAIT
     47 ESTAB
     12 LISTEN
      4 SYN-RECV

危险信号速记:

状态异常含义排查方向
SYN-RECV 大量堆积半连接队列溢出,疑似 SYN Flood 或对端不回 ACKss -n state syn-recv;内核 somaxconn / syncookies
SYN-SENT 持续偏高本端疯狂发起连接但握手不完成下游服务挂了/网络不通/连接池不复用
TIME-WAIT 数十万短连接风暴应用侧连接池化(呼应《连接池泄漏排查》)
CLOSE-WAIT 偏高不降对端已关、本端没 close,代码泄漏本端应用 bug,用 lsof/线程 dump 定位
ESTAB 突然归零服务进程挂了或端口没了下一节 lsof 查端口

1.3 实战场景二:定位具体连接与监听

# 谁在监听 8080(-l 只看监听,-p 显示进程,-n 不做域名解析)
ss -tlnp | grep 8080
LISTEN 0  511  0.0.0.0:8080  0.0.0.0:*  users:(("java",pid=2213,fd=96))

# 看某进程的全部连接(与 lsof 联动,按 pid 过滤)
ss -tnp | grep "pid=2213" | head

# 找与某下游的连接质量:Recv-Q/Send-Q 非零 = 接收/发送积压
ss -tn dst 10.0.0.20:6379

Recv-Q Send-Q  Local Address:Port   Peer Address:Port
0      1523    10.0.0.11:43122     10.0.0.20:6379      # Send-Q 1523:Redis 端处理不过来

# 半连接队列是否溢出过(计数器看历史)
netstat -s | grep -i listen
#   162 times the listen queue of a socket overflowed   ← 出现过溢出,调 backlog
#   162 SYNs to LISTEN sockets dropped

关键列解读:Recv-Q(还没被应用 read 走的数据)长期非零说明应用读不过来;Send-Q(已写入内核还没被对端 ACK 的数据)非零说明对端慢或网络拥塞;LISTEN 行第一列 511 是 accept 队列 backlog,溢出计数器非零就要调大(tomcat acceptCount / nginx backlog / 内核 somaxconn 三者取最小值生效)。


二、lsof:一切"被占用"问题的终极答案

2.1 实战场景一:端口被占,起不来服务

# 最常见的启动失败:Address already in use
lsof -i:8080
COMMAND   PID   USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
java    2213   app    96u  IPv4  812345      0t0  TCP *:http-alt (LISTEN)

# 一条命令看到:谁(java pid=2213)、哪个文件描述符(fd=96)、什么状态

解读与延伸:FD 列的 96u 中数字是文件描述符编号,u 表示读写模式(r 读/w 写/u 读写);TYPE=IPv4/IPv6/unix/REG/DIR。端口冲突的常规处置:确认是否残留进程(ps -p 2213)、是否被其他服务抢占、或者 TIME-WAIT 滞留(此时 ss -tan | grep 8080 能看到)。

2.2 实战场景二:磁盘满了,但 du 找不到大文件

这是 lsof 最"救命"的场景:文件被 rm 删除,但进程还持有文件描述符,空间不释放。

df -h
/dev/vdb        200G  198G  2.0G   99%  /data        # 满了
du -sh /data/* | sort -rh | head
# 总和只有 120G ← 还有 78G 去哪了?

# 找"已删除但仍被打开"的文件(开篇事故的真凶)
lsof +L1 | awk '$7 > 1048576 {print $1,$2,$7/1073741824"GB",$10}' | sort -k3 -rn
java      2213  42.6GB  /data/logs/gc.log (deleted)
tomcat    1877  31.2GB  /data/logs/catalina.out (deleted)

处置:空间不能重启释放时,让进程把 fd 关掉——最温和的方式是 truncate 而不是 rm(echo > /proc/2213/fd/96 之前先 ls -l /proc/2213/fd/96 确认目标),根治是让日志轮转走 copytruncate 或重定向到新版文件名。

2.3 实战场景三:socket/资源泄漏的量化证据

# 每个进程的 fd 数量 top10——fd 泄漏的量化排查
for pid in $(ps -eo pid,comm --no-headers | awk '{print $1}'); do
  n=$(ls /proc/$pid/fd 2>/dev/null | wc -l)
  [ -n "$n" ] && [ "$n" -gt 500 ] && echo "$n $pid $(cat /proc/$pid/comm)"
done | sort -rn

# 或直接看某进程 fd 都是什么类型
lsof -p 2213 | awk '{print $5}' | sort | uniq -c | sort -rn
  1871  IPv4        ← 1871 个网络连接:连接池泄漏的实锤
   312  REG
    45  unix

# 配合细分:1871 个连接都连去了哪
lsof -p 2213 -i | awk '{print $9}' | sort | uniq -c | sort -rn | head
  1024 10.0.0.20:6379      ← Redis 连接没还池子(呼应《连接池泄漏排查》)
   847 10.0.0.30:3306

解读:lsof 的价值不止"查占用",更在于给出泄漏的量化证据——单进程 fd 数、按对端聚合的连接分布,都是给开发定位代码缺陷的"铁证清单"。对应到 JVM 侧,jstack 里那些 WAITING 在 SocketInputStream.read 上的线程数,会与 lsof 的连接数互相印证。


三、strace:进程到底卡在哪一次系统调用

3.1 适用边界:先想清楚再上

strace 逐次拦截系统调用,有 2~5 倍量级的性能开销,生产环境不要对高 QPS 进程长时间挂载。它的正确姿势是三种轻量用法:

# 用法一:挂到已卡住的进程(无 -c,只看"现在卡在哪",开销极低——不输出时不拦截)
strace -p 2213
strace: Process 2213 attached
futex(0x7f2b3c000b48, FUTEX_WAIT, 2215, NULL     ← 卡在锁等待

# 用法二:统计模式(先 -c 概览,性能开销小,看"谁调用最多/最慢")
strace -cp 2213
% time  seconds  usecs/call  calls  syscall
 87.33  12.041321     1204     10003  futex          ← 锁竞争激烈
  6.71   0.925412       91     10122  read
  3.20   0.441200      1100       401  connect        ← ★ 新建连接且耗时1ms+

解读:-c 汇总里两个信号最值钱——futex 占比高 = 应用内锁竞争(jstack 看 BLOCKED 线程);connect 大量出现 = 没走连接池,每次新建 TCP(与 ss 的 TIME-WAIT 风暴互相印证)。三条线索指向同一个结论时,根因就锁死了。

3.2 实战场景:一次"偶发超时"的完整定位

# 现象:调下游偶发 3s 超时。跟踪一次真实请求的完整系统调用
strace -tt -T -e trace=network -p 2213 2>&1 | grep -A2 "connect"

12:04:31.201102 connect(96, {sa_family=AF_INET, sin_port=htons(8080),
             sin_addr=inet_addr("10.0.0.30")}, 16) = 0 <0.000089>   # 建连正常(89us)
12:04:31.201902 sendto(96, "POST /api/pay ...", 2048, ...) = 2048 <0.000041>
12:04:34.208811 recvfrom(96, 0x7f2b..., 8192, ...) = -1 EAGAIN   # ★ 3 秒后 EAGAIN

解读:-tt 带微秒时间戳、-T 显示每次调用耗时(尖括号内)。这条记录说明建连只花 0.09ms、请求也发出去了,但 recvfrom 等了整整 3 秒——慢在对端处理,不在本机。排障最有价值的时刻,就是 strace 帮你把"我们超时了"修正为"他们慢了",从而把皮球踢回正确的团队。

常用参数速记:-p PID 挂载;-f 跟踪子进程/线程;-e trace=%network/%file/futex 按类过滤;-tt -T 时间戳+耗时;-c 统计模式;-o file.out 落盘。高危红线:-f 跟踪多线程 Java 进程时输出量巨大,务必先过滤 syscall 类型,避免拖垮业务。


四、top -H:把 CPU 高定位到线程

4.1 实战场景:进程 CPU 100%,第一步做什么

top -H -p 2213          # -H 显示线程(在 top 交互态按 H 也可切换)

  PID USER  %CPU  COMMAND
 2215 app   98.7  java          ← 线程 2215 在烧 CPU
 2216 app    0.3  java

解读:进程视角(不带 -H)只能看到"java 占 98%",线程视角(-H)才能看到"是 tid=2215 这一具线程"。此时按 P(CPU 排序)、H(线程)、1(展开各核)三个键是肌肉记忆。记录下最耗 CPU 的几个 tid——它们就是下一节 jstack 的输入。

4.2 与 JVM 的联动:tid → 16 进制 → jstack 定位

这是 Linux 排障里最经典的一段联动,值得背下来:

# ① top -H 得到热点线程 tid=2215
printf "%x\n" 2215            # ② 转十六进制 → 8a7
8a7

# ③ jstack 里用十六进制 tid 找到线程(nid 字段)
jstack 2213 | grep -A 20 "nid=0x8a7"

"GC task thread#4 (ParallelGC)" #12 daemon prio=5 os_prio=0 tid=0x... nid=0x8a7 runnable
   java.lang.Thread.State: RUNNABLE

解读:jstack 的 nid(native id)就是操作系统的 tid(十六进制)。本例 nid=0x8a7 对应 GC 线程——CPU 高的根因是 GC 风暴,方向从"业务代码死循环"修正为"内存分配过快",下一步看 GC 日志和内存(呼应《堆外内存暴增排查》的整套工具链)。如果 nid 对应的是业务线程,栈顶行就是元凶代码;如果是 BLOCKED,说明不是算力问题而是锁问题,回到 strace 的 futex 线索。

4.3 补充:不需要 top 的快速替代

# 一次拿到全部热点线程(按 CPU 排序前 10)
ps -eo tid,pid,pcpu,comm --sort=-pcpu | head
# 或者对某进程:
ps -Lo tid,pcpu,comm -p 2213 --sort=-pcpu | head

容器环境注意:top/ps 看到的是宿主机视角,容器里请用 cat /sys/fs/cgroup/cpu.stat(cgroup v2)核对配额与 throttling——很多"CPU 100%"在容器里其实是 cpu.max 配额被限流,不是真的算力耗尽。


五、jstack:Java 线程的"案发现场照片"

5.1 基本用法与三看原则

jstack 2213 > jstack_$(date +%H%M%S).txt
# 生产安全:jstack 对业务进程是秒级暂停,可放心执行
# 建议间隔 3~5 秒连拍 3 张,对比线程状态是否迁移

三看原则:

一看状态分布:grep -c "RUNNABLE\|BLOCKED\|WAITING" jstack*.txt
  BLOCKED 多 = 锁竞争;WAITING 多 = 线程池空转等任务或资源
二看相同栈帧聚合:同样的栈出现 20 次 = 一群线程堵在同一处
三看连续两拍的差异:状态/栈完全不变的线程 = 永久卡死(真泄漏)

5.2 三个必会解读样例

样例一:死锁(jstack 会直接告诉你)

Found one Java-level deadlock:
"Thread-A":
  waiting to lock monitor 0x... (object 0x..., a java.lang.Object),
  which is held by "Thread-B"
"Thread-B":
  waiting to lock monitor 0x... (object 0x..., a java.lang.Object),
  which is held by "Thread-A"

jstack 末尾自动输出死锁报告及双方持锁信息,直接定位到两个类的加锁顺序问题。

样例二:一群线程堵在同一个锁(BLOCKED)

"qtp123-45" #34 prio=5 ... BLOCKED on monitor entry
  - waiting to lock <0x000000076ab62208> (a com.xxx.OrderService)
  at com.xxx.OrderService.createOrder(OrderService.java:88)
"qtp123-46" #35 ... BLOCKED, same stack
(连续 30 行相同栈 = synchronized 粒度过粗或临界区里有慢调用)

样例三:连接池耗尽(WAITING 在获取连接上)

"http-nio-8080-exec-58" #58 ... WAITING (on object monitor)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await
  - waiting on <0x000000076c71b7a8> (a com.zaxxer.hikari.util.ConcurrentBag)
  at com.zaxxer.hikari.util.ConcurrentBag.borrow(..., 10000, ...)
  at com.zaxxer.hikari.cp.HikariPool.getConnection(HikariPool.java:197)
  at com.xxx.UserMapper.selectById(...)      ← 业务栈直接给出是哪张表的查询

几十个 Tomcat 线程全在 HikariPool.getConnection 上 WAITING——这就是《连接池泄漏排查》的案发现场:结合业务栈去查那条 SQL/那个调用链漏 close 的位置。

5.3 联动全景:5 命令在一次事故里的接力

服务无响应
 → top -H            发现 GC 线程 98% CPU(不是业务死循环)
 → jstack 连拍       确认 GC 风暴 + 80 线程 WAITING 在 HikariPool
 → ss -s             TIME-WAIT 8102:短连接风暴佐证连接池未生效
 → lsof +L1          找到已删除仍占 42GB 的 gc.log(磁盘满 → GC 日志写不出)
 → strace -cp        futex 高占比:锁竞争是果不是因
 → 证据链闭合:磁盘满 → 日志异常 → GC 配置失效 → 频繁 GC → 响应超时
    → 连接池配置丢失 → 每请求新建连接 → TIME-WAIT 风暴
   修复顺序:清 fd 释放磁盘 → 恢复 GC 配置 → 修复连接池 → 复盘归档

六、常见问题

6.1 strace 会不会把生产进程搞挂?到底多慢?

不会搞挂(ptrace 挂载失败时进程无感知),但有性能税:高频 syscall 的进程被 strace 后 QPS 可能掉 30%~80%。安全姿势:优先 -c 统计模式(开销小);卡死进程用无参数 strace -p 只看"当前卡点"(无输出几乎零开销);绝不对高 QPS 进程开 -f 全量跟踪。替代方案:perf trace / bpftrace 开销更低,但学习成本高,日常 strace 足够。

6.2 容器里这些命令都能用吗?

镜像里通常没装(最小化镜像原则)。两个办法:宿主机上 nsenter -t <容器主PID> -n ss -tnp(用宿主机工具进入容器网络命名空间);或为业务镜像加一个 sidecar debug 工具镜像。注意 lsof/top 看到的是宿主机全局视角,端口/进程要按命名空间换算;cgroup 层面的 CPU/内存限额要从 /sys/fs/cgroup 读,宿主机 top 会骗你。

6.3 jstack 和 jmap 有什么区别?什么时候只能 jmap?

jstack 只抓线程栈(秒级暂停、安全);jmap 抓堆快照/直方图(大堆可能暂停数秒到分钟,谨慎)。CPU 高先 jstack(查线程)、内存涨先 jmap -histo:live(类直方图,轻量)、深挖对象引用链才 jmap -dump:live(重量级,配合 MAT 分析,《堆外内存暴增排查》有完整流程)。另有 jcmd(JDK 推荐,Thread.print/GC.class_histogram 一个工具覆盖两者)。

6.4 TIME-WAIT 多到底要不要治?怎么治?

TIME-WAIT 本身是协议正确性要求(确保最后 ACK 可达),几千条无需紧张。要治的是两种情况:① 本端作为服务端出现大量 TIME-WAIT(应由客户端主动关闭,服务端 TIME-WAIT 多说明关闭方向反了);② 达到数万导致端口耗尽。工程手段按优先级:应用侧连接池化(根治)> 长连接复用 > tcp_tw_reuse(仅客户端方向有效)> 扩端口范围。永远不要用 tcp_tw_recycle(NAT 环境下丢连接,内核 4.12 已移除)。

6.5 排障时这 5 个命令的先后顺序是什么?会不会查反了?

推荐"由表及里"的固定路径,避免乱枪:① top -H 定位是 CPU/内存/IO 哪类问题 → ② ss -s 看网络健康度(连接数与业务量是否匹配)→ ③ lsof 查资源占用(端口/fd/被删文件)→ ④ Java 进程再 jstack 连拍翻译线程 → ⑤ 卡住类问题 strace 看系统调用。多数事故在第 ②③ 步就能锁定方向,strace 是最后的"显微镜"。这套顺序的价值在于:每一步都在缩小假设空间,而不是按直觉跳来跳去。

6.6 这些输出怎么沉淀成团队资产?

三件事:① Runbook 化——把"症状 → 命令 → 解读"写成值分手册(本文速查卡是底稿);② 证据归档——事故时 top/jstack/ss 输出统一落盘到时序目录(/data/ops/20261018-2213/),复盘时链路可重现;③ 巡检自动化——把 TIME-WAIT 阈值、fd 数、磁盘 fd 残留(lsof +L1)做成巡检脚本,问题在故障前暴露。命令人人会用,但"证据链纪律"才是 10 倍效率的真正来源。


七、总结

症状 → 命令速查卡

【网络类】
  服务不响应/连接异常        ss -s → ss -tlnp → ss -tn dst IP:PORT
  状态解读                   TIME-WAIT高=短连接风暴  CLOSE-WAIT高=本端忘close
                             SYN-RECV堆积=半连接溢出  Recv-Q/Send-Q非零=积压
  端口冲突                   ss -tlnp | grep 端口(netstat 慢,换 ss)

【资源占用类】
  端口被谁占                 lsof -i:8080
  磁盘满但du找不到           lsof +L1(已删除仍被占用的文件)
  fd/连接泄漏                lsof -p PID 按TYPE/对端聚合统计

【卡顿类】
  进程卡在哪次系统调用       strace -p PID(看当前卡点,零输出零开销)
                             strace -cp PID(统计:futex高=锁竞争 connect多=没连接池)
  偶发慢请求定位责任方       strace -tt -T:connect快+recvfrom慢=对端慢

【CPU类】
  进程CPU高→定位线程         top -H -p PID → 记录tid
  tid翻译成Java线程          printf %x tid → jstack | grep nid=0x...
  结论分流                   nid是GC线程=内存问题  是业务RUNNABLE=热点代码
                             是BLOCKED=锁问题(回strace futex印证)

【Java线程类】
  线程在等什么               jstack PID(间隔3~5s连拍3张)
  三看                       状态分布 / 相同栈聚合 / 两拍差异
  经典指纹                   HikariPool.getConnection WAITING=连接池耗尽
                             "Found one Java-level deadlock"=死锁自报告

给团队的建议

项建议
路径排障按"top -H → ss → lsof → jstack → strace"固定顺序,缩小假设空间而非乱枪
纪律事故现场证据(top/jstack/ss 输出)统一落盘归档,复盘可重现
容器镜像预装 iproute2/psmisc 或备 sidecar debug 镜像;cgroup 配额与宿主机视角分开看
联动tid→十六进制→nid 的三步翻译、lsof+L1、jstack 连拍对比,写进值分手册
自动化TIME-WAIT/fd 数/磁盘残留文件做巡检告警,故障前暴露
演练半年一次"拔网线/塞满磁盘/锁竞争"故障注入,练证据链而非背命令

一句话

这 5 个命令组合成一条"由表及里"的证据链:ss 用 netlink 直读内核让网络全景毫秒级呈现,ESTAB 与业务量的偏差、TIME-WAIT 与 CLOSE-WAIT 的比例、Recv-Q/Send-Q 的积压、listen 队列的溢出计数,每一个数字都在陈述"流量与连接的关系是否正常";lsof 把"谁占用"变成可量化的事实——端口冲突的一行输出、lsof +L1 揪出的被删大文件、按对端聚合的连接分布,是 fd 与磁盘类泄漏的终极证据;strace 的价值不在于每次都上,而在于两个低开销姿势(无参数看当前卡点、-c 统计模式)就能回答"进程卡在哪次系统调用",connect 大量出现证明没走连接池、recvfrom 三秒返回证明慢在对端,它常常负责把球踢回正确的团队;top -H 把"进程 CPU 100%"翻译成"tid=2215 在烧",再由 printf %x 与 jstack 的 nid 完成从操作系统到 JVM 的接力,GC 线程高意味着内存问题、业务线程 RUNNABLE 指向热点代码、BLOCKED 指向锁竞争;jstack 连拍三张加三看原则(状态分布、相同栈聚合、两拍差异),把"HikariPool.getConnection 上 WAITING 着 80 个线程"这样的案发现场变成开发可直接定位的代码行。命令本身半小时就能学会,真正的 10 倍效率来自三条纪律:固定"由表及里"的排查路径缩小假设空间、事故证据统一落盘让复盘可重现、把每次事故的指纹沉淀进值分手册——工具是锤子,证据链思维才是排障的手艺。

互动话题:你排障时最离不开的一条命令是什么?有没有被 strace/lsof 一次"点破真相"的经历(比如 lsof 找回被删的大文件、strace 证明是隔壁团队的锅)?评论区分享你的"一击必杀"命令组合,我会把高赞组合补进速查卡。


标题:用好这 5 个 Linux 命令,排查效率提升 10 倍
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/10/20/1792330904343.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消