第 31 章 · 排障工具箱
一句话:按症状反查:手里只有一条报错或一个现象时,从这里找到先跑哪条命令、多半是什么原因、去哪一章看处理办法。 用它还是用别的:已经知道是哪个工具出的问题(Nginx、PostgreSQL、Docker),直接翻那一章的“注意事项”;只知道“端口被占”“连不上”“起不来”“满了”“慢”,先在这一章按层定位到具体工具,再跳过去。 核对版本:Ubuntu 24.04.3(WSL 2.6)、PowerShell 7.6,2026-10
31.1 排障的顺序
Section titled “31.1 排障的顺序”不管什么症状,按同一个顺序走,能省掉大半时间:
- 先复现。确认问题能稳定重现,记下时间、哪台机器、做了什么操作。只出现过一次、再也复现不了的,先去翻那个时间点的日志。
- 看报错原文。完整复制第一条报错,不要只看最后一行;Python 的调用栈(traceback)看最后一行,Java 看第一行和
Caused by:那几行,再往里找第一行自己写的代码。拿报错原文去搜,比用自己的话描述准得多。 - 看日志。服务端日志里的信息往往比客户端报错多一倍:systemd 管的服务用
journalctl -u <服务>(第 30 章),容器用docker logs <容器>(第 12 章),Nginx 看error.log(第 16 章),Windows 上的程序和服务看事件查看器(eventvwr.msc,命令行是Get-WinEvent)。 - 缩小范围。按“本机 → 网络 → 服务”一层层排除:先在服务所在的机器上用
curl localhost:<端口>试,通了再从外面试;不通就不用查网络。 - 一次只改一处。改完重新复现一次,没好就改回去再试下一处。同时改三处,好了也不知道是哪一处起的作用,坏了更不知道。
“连不上服务”是最常见、也最容易乱查的一类。下图把它拆成六层,从上一层通了才往下一层查,每层旁边是该跑的命令(Windows 和 Linux 各一条)和这一层不通时看到的现象。前三层在你自己的电脑上就能查,后三层要登上服务器。第二层的 ping 和不带端口的 Test-NetConnection 用的是 ICMP,云服务器常禁 ping,ping 不通不等于网络不通,直接试第三层。
31.2 端口被占用
Section titled “31.2 端口被占用”同一个地址和端口同一时间只能有一个程序在听。报这类错时只需回答一个问题:谁占着它。先查出进程号,再看它的完整启动命令,判断是自己忘了关的程序、另一个容器,还是系统服务,三种情况的处理完全不同。
尖括号里的内容换成自己的。Linux 上 ss -ltnp 不加 sudo 时,别人进程的那一列是空的;lsof -i 不加 sudo 干脆不列出别人的进程,查不到不代表端口空闲,要用 sudo lsof -i :8080。
| 症状或报错 | Windows(PowerShell) | Linux | 说明 |
|---|---|---|---|
启动时报 address already in use、EADDRINUSE、[WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次 |
Get-NetTCPConnection -LocalPort 8080 -State Listen |
ss -ltnp 'sport = :8080' |
Windows 看 OwningProcess 列,Linux 看最后一列的 pid= |
| 同上,换个命令看 | netstat -ano | findstr :8080 |
sudo lsof -i :8080 |
netstat 最后一列是 PID;lsof 第二列是 PID,不加 sudo 看不到别人的进程 |
| 这个 PID 是什么程序 | Get-Process -Id <PID> |
ps -fp <PID> |
看进程名和启动时间,判断是不是自己上次没关的 |
| 看它完整的启动命令 | (Get-CimInstance Win32_Process -Filter "ProcessId=<PID>").CommandLine |
ps -o cmd= -p <PID> |
同样是 python,命令行能看出是哪个项目 |
| 确认是自己的程序后结束它 | Stop-Process -Id <PID> |
kill <PID> |
必须先跑上面两行看清楚;系统服务用服务管理器停,不要直接结束 |
端口明明没人占,仍报 [WinError 10013](访问权限不允许) |
netsh interface ipv4 show excludedportrange protocol=tcp |
无 | 列出的范围被 Hyper-V、WSL 等系统组件保留,换一个范围外的端口 |
常见原因 → 处理:
- 上次启动的同一个程序还在后台(终端关了但进程没退、IDE 停止按钮没停干净、
nohup跑着)→ 按上表找到 PID,看命令行确认是自己的再结束。Linux 上先用kill,不要一上来kill -9,详见第 29 章 29.6。 - 另一个容器或本机装的服务占着:Docker 报
Bind for 0.0.0.0:8080 failed: port is already allocated→ 详见第 12 章 12.6;Compose 映射 3306 时报同样的错 → 详见第 13 章 13.6。 - 80、443 被另一个 Web 服务器占着:
bind() to 0.0.0.0:80 failed (98: Address already in use)→ 详见第 16 章 16.6;Caddy 报listen tcp :80: bind: address already in use→ 详见第 17 章 17.6。
31.3 连不上服务
Section titled “31.3 连不上服务”按 31.1 的图从上往下查。<主机> 可以是域名或 IP。
| 症状或报错 | Windows(PowerShell) | Linux | 说明 |
|---|---|---|---|
curl: (6) Could not resolve host、DNS 名称不存在 |
Resolve-DnsName <域名> |
getent hosts <域名>;dig +short <域名> |
解析不出 IP 就不用查后面几层;dig 要先装 dnsutils |
| 一直等,最后超时 | Test-NetConnection <主机> -Port <端口> |
nc -zv -w 3 <主机> <端口> |
超时多半是防火墙或云安全组把包丢了 |
Connection refused、[WinError 10061] 由于目标计算机积极拒绝,无法连接、curl: (7) Failed to connect |
同上,TcpTestSucceeded : False |
同上,nc: connect to … failed: Connection refused |
包到了那台机器,但那个端口没人在听 |
| 在服务器上看它听在哪个地址 | Get-NetTCPConnection -State Listen -LocalPort <端口> |
ss -ltnp |
地址是 127.0.0.1 只收本机,0.0.0.0 或 * 才收外面 |
| 防火墙有没有放行 | Get-NetFirewallRule -Enabled True -Direction Inbound -Action Allow |
sudo ufw status |
云服务器还要看控制台里的安全组 |
端口通了,网页返回 502、504 |
curl.exe -I <url> |
curl -I <url> |
反向代理连不上或等不到后端,去看代理的错误日志 |
| 端口通了,报认证失败 | Get-WinEvent -LogName Application -MaxEvents 20 |
journalctl -u <服务> -n 50 --no-pager |
用户名、口令、来源地址三样核对 |
常见原因 → 处理:
- 服务只监听
127.0.0.1:服务器上curl localhost正常,从外面连报Connection refused→ 详见第 29 章 29.6;本机装的 PostgreSQL 的listen_addresses→ 详见第 21 章 21.6。 - 容器里写了
localhost:应用日志报connect ECONNREFUSED 127.0.0.1:5432,数据库明明在跑 → 把地址改成 Compose 里的服务名,详见第 13 章 13.6。 - 反向代理后面的程序没起或端口不对:
502 Bad Gateway,错误日志有connect() failed (111: Connection refused)→ 详见第 16 章 16.6;本机用 curl 测出curl: (7)→ 详见第 18 章 18.6。 - 网络通了,认证没过:
password authentication failed→ 详见第 21 章 21.6;Access denied for user→ 详见第 22 章 22.6;NOAUTH Authentication required.→ 详见第 25 章 25.6;SSH 的Permission denied (publickey).→ 详见第 6 章 6.6。
31.4 进程起不来或自己退出
Section titled “31.4 进程起不来或自己退出”进程起不来,原因几乎都写在它退出前的最后几行输出里,难的是找到这几行去了哪:前台运行的程序在终端里,systemd 服务进了 journald,容器在 docker logs 里,Windows 服务进了事件日志。先找到这几行,再看退出码。
| 症状或报错 | Windows(PowerShell) | Linux | 说明 |
|---|---|---|---|
| 命令行程序一闪就退,没看清输出 | $LASTEXITCODE |
echo $? |
紧接着运行;0 是正常结束,非 0 是出错退出 |
| 服务现在是什么状态 | Get-Service <服务> |
systemctl status <服务> |
Linux 这条还会带出最近十行日志 |
| 看服务或程序的报错 | Get-WinEvent -FilterHashtable @{LogName='Application';Level=2} -MaxEvents 20 |
journalctl -u <服务> -n 50 --no-pager |
Windows 的 Level=2 是“错误”;图形界面在事件查看器的“Windows 日志” |
| 哪些服务启动失败了 | 事件查看器 → Windows 日志 → 系统 | systemctl --failed |
没有失败的服务时显示 0 loaded units listed. |
| 被系统因内存不足杀掉 | 无 | journalctl -k -g 'killed process' --case-sensitive=false;sudo dmesg -T | grep -i 'killed process' |
有 Out of memory: Killed process <PID> (<名字>) 就是被 OOM 杀了;Ubuntu 默认 kernel.dmesg_restrict=1,dmesg 要加 sudo(WSL 里不用) |
| 容器起来又停了 | docker ps -a 后 docker logs <容器> |
同左 | 状态列 Exited (1) 里的数字就是退出码 |
日志里什么都没有、程序就是一启动就退时,Linux 上可以 strace -f -o trace.txt ./app 记下它的系统调用,打开 trace.txt 从后往前找最后几个返回 -1 ENOENT(文件不存在)或 -1 EACCES(没权限)的调用,它找的那个路径往往就是原因。
常见原因 → 处理:
- 环境变量没带进去:终端里能跑,做成服务后报
KeyError: 'DATABASE_URL'→ 详见第 30 章 30.6;改了变量程序读到的还是旧值 → 详见第 15 章 15.6。 - 一启动就崩,被反复拉起:服务停在
failed,日志有Start request repeated too quickly.→ 先看再往上的那几行真正的报错,详见第 30 章 30.6。 - 容器主进程退出:
docker ps里没有,docker ps -a是Exited (1)→ 详见第 12 章 12.6。 - 权限或 PATH 不对:
Permission denied,或放进 cron 后报command not found→ 详见第 29 章 29.6。
31.5 磁盘满了
Section titled “31.5 磁盘满了”先确认是哪个分区满了,再一层层往下找是哪个目录、哪类文件。删之前确认没有进程还开着这个文件,否则删了空间也回不来。
| 症状或报错 | Windows(PowerShell) | Linux | 说明 |
|---|---|---|---|
No space left on device、“磁盘空间不足”、数据库写入失败 |
Get-PSDrive -PSProvider FileSystem |
df -h |
Windows 看 Free 列;Linux 看 / 那一行的 Use% |
df -h 还有空间,仍报 No space left on device |
无 | df -i |
IUse% 到 100% 是 inode 用完了,多是几百万个小文件 |
| 一个目录总共多大 | Get-ChildItem <目录> -Recurse -File | Measure-Object Length -Sum |
du -sh <目录> |
Windows 的 Sum 单位是字节 |
| 哪个子目录最大 | 对每个子目录跑上一行 | du -xh --max-depth=1 <目录> | sort -h |
最大的排在最后,一层层往下钻 |
| 找大文件 | Get-ChildItem <目录> -Recurse -File | Sort-Object Length -Descending | Select-Object -First 10 FullName,Length |
find <目录> -xdev -type f -size +100M |
-xdev 不跨到别的磁盘 |
| 删了文件空间没回来 | 无 | sudo lsof +L1 |
列出已删除但还被进程打开的文件 |
| 日志和镜像占了多少 | docker system df |
journalctl --disk-usage;docker system df |
这两样是服务器上最常见的大头 |
常见原因 → 处理:
- 删掉的日志还被进程开着:
df -h显示 100%,du加起来却少很多 → 重启那个进程或清空文件而不是删除,详见第 29 章 29.6。 - journald 日志太多 →
sudo journalctl --vacuum-size=500M并设上限,详见第 30 章 30.6。 - Docker 旧镜像和构建缓存:
docker system df显示几十 GB → 详见第 12 章 12.6。 - 数据库表膨胀:行数没怎么变,表却越来越大 → 详见第 21 章 21.6。
31.6 内存或 CPU 高
Section titled “31.6 内存或 CPU 高”先分清是整机资源紧张,还是一个进程失控:看完整机的数字,再按内存或 CPU 排序,找到排第一的进程,按 31.2 的方法看清它的命令行再决定怎么处理。
| 症状或报错 | Windows(PowerShell) | Linux | 说明 |
|---|---|---|---|
| 整机还剩多少内存 | Get-Counter '\Memory\Available MBytes' |
free -h |
Linux 看 available 列,free 列小是正常的,空闲内存被拿去做了缓存 |
| 整机 CPU 多忙 | Get-Counter '\Processor(_Total)\% Processor Time' |
top -bn1 | head -15 |
top 第一行的 load average 长期大于 CPU 核数(nproc)就是忙不过来 |
| 谁占内存最多 | Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 10 Id,ProcessName,WorkingSet64 |
ps aux --sort=-%mem | head |
WorkingSet64 单位是字节 |
| 谁占 CPU 最多 | Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Id,ProcessName,CPU |
ps aux --sort=-%cpu | head |
Windows 的 CPU 列是累计用了多少秒,不是此刻的占用;看此刻用任务管理器或 top |
| 进程突然没了,内存曲线掉到底 | 无 | journalctl -k -g 'killed process' --case-sensitive=false |
被内核的 OOM killer 杀了 |
| 哪个容器在吃资源 | docker stats --no-stream |
同左 | 每个容器一行 CPU 和内存 |
常见原因 → 处理:
- Redis 没设内存上限:内存一直涨,最后进程被杀,或报
OOM command not allowed→ 设maxmemory和淘汰策略,详见第 25 章 25.6。 - 缓存同时过期:缓存过期的那一刻数据库 CPU 冲到 100% → 详见第 25 章 25.6 的缓存击穿。
- 连接没关:数据库报
Too many connections,每个连接都占内存 → 详见第 22 章 22.6。 - 自己的程序失控(死循环、内存泄漏)→ 按 31.2 查清 PID 和命令行再结束;数据库这类进程不要
kill -9,详见第 29 章 29.6。
31.7 慢
Section titled “31.7 慢”慢要先分清慢在哪一段:DNS、建连接、等服务器处理,还是下载。浏览器里看 Network 面板的 Timing(第 19 章 19.2),命令行用 curl 的 -w 把一次请求拆成几段时间。
| 症状或报错 | Windows(PowerShell) | Linux | 说明 |
|---|---|---|---|
| 一个接口慢,不知道慢在哪 | curl.exe -o NUL -s -w "dns %{time_namelookup} connect %{time_connect} pretransfer %{time_pretransfer} ttfb %{time_starttransfer} total %{time_total}\n" <url> |
curl -o /dev/null -s -w 'dns %{time_namelookup} connect %{time_connect} pretransfer %{time_pretransfer} ttfb %{time_starttransfer} total %{time_total}\n' <url> |
每项都是从开始累计的秒数;ttfb 减 pretransfer 才是服务器处理的时间,connect 到 pretransfer 之间是 HTTPS 握手 |
| 整台机器都慢、命令都卡 | Get-Counter '\PhysicalDisk(_Total)\% Disk Time' |
vmstat 1 5 |
Linux 看 wa 列,长期两位数是在等磁盘;si、so 不为 0 是在用交换分区 |
| 数据库查询慢 | EXPLAIN ANALYZE <SQL>; |
同左 | 在 psql 或 mysql 里执行(MySQL 8.0.18 起支持);大表上出现全表扫描多半是缺索引 |
| Redis 偶尔卡几秒 | SLOWLOG GET 10 |
同左 | 在 redis-cli 里执行,列出最近的慢命令 |
| 日志里找慢请求 | 看应用日志的耗时字段 | journalctl -u <服务> --since '10 min ago' |
先缩小到慢的那段时间 |
常见原因 → 处理:
- 后端或数据库慢(
ttfb占了大头):全表扫描 → 按第 21 章 21.4 的“找出慢查询并加索引”做;表膨胀拖慢查询 → 详见第 21 章 21.6。 - Redis 被一条命令堵住:线上 Redis 突然卡几秒,所有请求一起超时 → 多是
KEYS *或删大键,详见第 25 章 25.6。 - 备份锁表:
mysqldump导出期间写入全部卡住 → 加--single-transaction,详见第 22 章 22.6。 - 响应太大或缓存没生效:
ttfb很短、total很长 → 压缩或分页(第 19 章 19.2 的计时图);发版后浏览器还在用旧文件 → 详见第 19 章 19.6。
31.8 深入阅读
Section titled “31.8 深入阅读”- Get-NetTCPConnection(Microsoft Learn):按本地端口、状态筛选 TCP 连接,OwningProcess 列给出占用端口的进程号;参数和输出字段的完整说明。
- Test-NetConnection(Microsoft Learn):测试到某台主机的连通性和指定端口,TcpTestSucceeded 为 False 时再加 -InformationLevel Detailed 看细节。
- Get-WinEvent(Microsoft Learn):在命令行里读事件查看器的日志,FilterHashtable 按日志名、级别、时间筛选。
- ss 手册页:-l、-t、-n、-p 各管什么,以及按端口、状态过滤的表达式写法(如 sport = :8080)。
- 可观测性(Ubuntu Server 文档):Ubuntu 服务器上汇总日志和监控的官方做法:Logwatch 每日日志摘要、Nagios 和 Munin 监控、日志与指标告警栈。
- 502 Bad Gateway(MDN):502 的含义:网关或代理从上游服务器收到了无效响应;同目录下有 503、504 等其余状态码的说明。