🐧 第06章 · 故障排查

通用排查思路 + 本服务器案例

故障排查

这台服务器上跑着 Nginx、MySQL、Node 应用、GitLab、Xray、RustDesk……一旦出问题,该从哪里下手?这一章先给一套通用的排查方法论,再把它们落在本机真实案例上,帮你把"瞎试"变成"有章法"。

通用排查方法论:时间、范围、变化

故障排查的本质是缩小范围。三个问题最先问:

  1. 什么时候开始的? —— 是最近一次变更后出现的,还是突然冒出来?先想"最近改了什么"。
  2. 影响范围多大? —— 只是某个服务挂,还是整台机器?是特定用户,还是所有人?
  3. 系统的哪个层面? —— CPU、内存、磁盘、网络、应用、配置。

一套好用的排查顺序(从外到内、从粗到细):

# ① 机器还活着吗(CPU/内存/负载)
uptime && free -h
# ② 磁盘够吗
df -hT
# ③ 网络通吗
ping -c2 bangdream.xyz && ss -tlnp | head
# ④ 服务状态
systemctl list-units --state=failed --no-pager
systemctl status bangdream nginx mysql
# ⑤ 找日志
journalctl -u nginx --since "30 min ago" -e
tail -f /var/log/nginx/error.log 2>/dev/null

易错 很多人一上来就 tail 日志,但没先确认"机器本身是否 OK"。先看机器整体(负载/内存/磁盘),再落到具体服务,最后才看日志,否则可能漏掉磁盘写满这种"服务看起来报错其实根因在别处"的 case。

分层排查:三个高频失败点

层面 症状 首查命令
磁盘 服务 500、写不进日志、数据库拒绝连接 df -hT、df -i
端口/进程 服务 active 但连不上、端口被占 ss -tlnp、systemctl status
日志 崩溃、重载、权限被拒 journalctl -u、tail /var/log/...

案例 1:Node 应用 500,其实是磁盘满了

某日 bangdream(端口 3000)返回 500。别看代码,先看机器:

df -h                    # `/` 100%?→ 根因
du -h --max-depth=1 /var | sort -rh | head   # 谁占了
# 常见:journal 日志堆积,清一下
journalctl --vacuum-size=300M
df -h                    # 释放后再看服务是否恢复
curl -I http://127.0.0.1:3000   # 本机直连验证

易错 df 显示 100% 时,应用报错五花八门,但根因往往就是这个。先 df -h 排除"写不进去"这个最被低估的坑。另外很多服务在磁盘满时会 Cannot allocate memory 或 No space left on device,别被误导到内存/代码方向。

案例 2:端口被占,服务起不来

ss -tlnp | grep 3000        # 谁占着 3000
lsof -i :8090               # GitLab 反代端口是否被别的进程占用
kill $(lsof -t -i:3000)     # 结束占用者
systemctl restart bangdream

案例 3:服务 active 但访问不通(nginx 反代)

Nginx 外层终止 TLS 并反代 Node,若 curl https://bangdream.xyz 返回 502/503,逐层查:

curl -I http://127.0.0.1:3000          # ① 后端 Node 本身通吗?
curl -I http://127.0.0.1:3010          # ② lovelive 后端
systemctl status bangdream             # ③ Node 服务 up 吗
systemctl status nginx                 # ④ nginx up 吗
tail -n 50 /var/log/nginx/error.log    # ⑤ nginx 报什么错(connect failed?)
journalctl -u bangdream -e             # ⑥ Node 报什么错

502 的常见根因:后端进程挂了、后端端口没监听、nginx 反代 proxy_pass 写错端口。逐层 curl 内网判断"断在哪儿",比在 nginx 配置文件里瞎猜快得多。

案例 4:MySQL 连不上

systemctl status mysql
ss -tlnp | grep 3306                   # MySQL 是否监听 127.0.0.1
mysql -u root -p -e "show processlist;"   # 能否登进(先测认证)
journalctl -u mysql -e                 # MySQL 错误日志

易错 本机 MySQL 只监听 127.0.0.1:3306(不对外),这是安全的设计。外部应用连不上 MySQL 应先确认不是"试图从外部连"——正确做法是让 Node 应用在本地连,或通过 nginx/受控代理。

通用工具:journalctl 快速拦路虎

遇到任何服务问题,journalctl 几乎永远是第一步:

journalctl -u <服务> -n 100        # 最近 100 行
journalctl -u <服务> --since "1 hour ago" -e   # 过去 1 小时(时间升序)
journalctl -u <服务> -p err         # 只看错误级
journalctl --follow -u <服务>       # 实时 tail(new 日志)

时间与服务恢复:重启前先取证

很多人遇到故障第一反应是 systemctl restart,但先看日志再重启,否则证据就没了:

# 先取证
journalctl -u <服务> -n 50 --since "30 min ago" > /root/service_err.log
# 再重启
systemctl restart <服务>
# 重启后仍要盯着是否复发
systemctl status <服务>

安全提示 重启会丢失现场信息(崩溃前的进程状态、临时文件)。先抓取日志/快照再重启,否则下次同样的"隐形 bug"还会再来。对数据库、写入型服务务必谨慎重启。

系统级快速体检脚本

可以把上面常用命令组合成一个快照,故障时直接跑:

echo "==== 负载/内存 ===="; uptime; free -h
echo "==== 磁盘 ===="; df -hT
echo "==== inode ===="; df -i
echo "==== 失败服务 ===="; systemctl --state=failed --no-pager
echo "==== 监听端口 ===="; ss -tlnp
echo "==== 网络连通(fail2ban 是否误封) ===="; fail2ban-client status sshd

易错点

  • 不看机器整体直接翻日志:漏掉磁盘满、负载高这类跨层根因。
  • 故障当日第一件事就 restart:现场证据丢了。
  • 只看 systemctl status 的 active 就下结论:active 不代表能正常服务,要用 curl/探针验证。
  • 试图从外部连本地监听的服务:127.0.0.1 只在本地通。

相关命令

uptime、free、df、ss、systemctl、journalctl、curl、lsof、ping、tail、fail2ban-client。