服务突然 502,你 ssh 上去,第一条命令敲什么?
这个问题我问过好几个刚接手服务器的同学,答案五花八门。有人直接 tail 日志,有人上来就 top,也有人愣在那不知道从哪开始。命令本身大家都认识,卡住的是顺序,不知道该先确认什么再确认什么。
单纯背命令清单意义不大,网上一搜一大把,背完第二天就忘。真正有用的是把命令和排查场景绑在一起:进程还在不在、端口通不通、磁盘满没满、日志里写了什么、网络能不能出去。这篇按这个思路把日常用得最多的 Linux 命令重新归拢一遍,每组都说清楚为什么先查这个。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 服务出问题时的排查顺序,先看哪一层
- 查进程:
ps、pstree、top、pkill各自的位置 - 查端口和防火墙:端口没监听和端口被拦是两回事
- 查磁盘和内存:
df、du、free以及 inode 满了的诡异现象 - 查日志:管道组合怎么把几百兆的日志缩到几行
- 查网络:源、DNS、连通性
- 文件管理的基本功:软链接、重定向、压缩解压
- 找文件的四个命令,为什么
find排最后 - vi 在服务器上的应急操作
- 看机器配置和系统版本
# 一、先想清楚排查顺序
线上服务出问题,我的顺序基本是固定的这五步。
进程还在吗。服务被 OOM Killer 干掉、被人手动杀了、启动就崩了,这三种情况都表现为「访问不通」,但原因天差地别。先确认进程存在与否,能砍掉一大半的可能性。
进程在,那端口有没有监听。进程活着不等于服务可用,配置写错端口、绑定到了 127.0.0.1 而不是 0.0.0.0,进程都好好的但外面就是连不上。
端口在监听,那是不是被防火墙拦了。这一步和上一步的现象很像,都是「连不上」,但一个是本机没监听,一个是包根本没进来。
前面都正常,看资源。磁盘满了、内存被吃光了、CPU 跑满了,服务会表现出各种奇怪的间歇性故障。
最后才是看日志。不是说日志不重要,而是前面四步能快速缩小范围,带着假设去看日志比漫无目的地翻要快得多。
顺着这个顺序,下面一组一组说。
# 二、查进程
# 2.1 ps
ps 列出系统中运行的进程,包括进程号、命令、CPU 使用量、内存使用量。日常最常用的就两种写法。
# 找特定的进程
ps -ef | grep nginx
# 显示所有进程的完整信息,带 CPU 和内存占用
ps -aux
ps -ef 是 System V 风格,输出里有 PPID(父进程 ID),排查「谁把它拉起来的」时候有用。ps -aux 是 BSD 风格,多了 %CPU 和 %MEM 两列。两套参数混用一般也能跑,但看到的列不一样,知道有这么回事就行。
ps -ef | grep nginx 有个人人都会遇到的现象,输出里总会多出一行 grep nginx 自己。因为 grep 也是个进程,ps 抓快照的那一刻它正好在跑。经典的绕法是把首字母用中括号括起来。
ps -ef | grep [n]ginx
[n]ginx 作为正则匹配 nginx,但 grep 自己的命令行里写的是 [n]ginx 这个带括号的字符串,匹配不上自己。或者干脆用 pgrep,它就是为这个场景生的。
pgrep -a nginx
# 2.2 pstree
Linux 里每一个进程都是由父进程创建的。pstree 用树状图展示进程之间的父子关系。
用得最多的场景是看 master/worker 结构的服务。Nginx 的 master 带几个 worker、PM2 的守护进程下面挂了几个 Node 实例,一眼就看清楚了。ps 是平铺的列表,同样的信息得自己在 PPID 里对着找。
pstree -p | grep node
# 2.3 top
top 监视系统中不同进程使用的资源,显示的数据包括 PID、进程属主、优先级、%CPU、%MEM 等,可以用这些指示出资源使用量。
进去之后有几个交互键值得记住。P 按 CPU 排序,M 按内存排序,1 展开显示每个 CPU 核心的负载,q 退出。
看 top 的第一行还有个关键信息,load average 那三个数分别是 1 分钟、5 分钟、15 分钟的平均负载。判断标准是拿它和 CPU 核心数比,核心数是 4 而 load 长期在 8 以上,说明确实排队了。这里有个常见误判,load 高不一定是 CPU 忙,大量进程卡在磁盘 IO 上(状态 D)也会把 load 顶起来,这时候盯着 %CPU 看是找不到原因的。
# 2.4 pkill
pkill 根据进程名杀死进程,不用先查 PID 再 kill。
pkill nginx
但这个命令要小心,它是按名字模糊匹配的,名字写得太短可能会误杀。动手之前先用 pgrep -a 看一眼到底会命中哪些进程,这个习惯能避免很多事故。
另外 pkill 默认发的是 SIGTERM(15),进程可以捕获它做优雅退出。真的杀不掉才用 -9 发 SIGKILL,那个信号不能被捕获,进程没有机会清理临时文件和释放连接。习惯性上来就 -9 不是好事。
# 三、查端口和防火墙
进程活着但连不上,接下来看端口。
# 看所有监听中的 TCP 端口,带进程名
ss -tlnp
# 老一点的机器上可能只有 netstat
netstat -tlnp
# 看某个端口被谁占了
lsof -i:8080
重点看监听地址那一列。0.0.0.0:3000 表示所有网卡都能连,127.0.0.1:3000 表示只有本机能连。Node 服务用 app.listen(3000) 在某些环境下会绑到回环地址,本机 curl 一切正常,外面死活连不上,就是这个原因。
确认端口在监听但外部还是连不上,那就轮到防火墙了。
linux 查看防火墙状态及开启关闭命令,老的 CentOS 6 系用 service 方式。
# 查看防火墙状态
[root@centos6 ~]# service iptables status
# 开启防火墙
[root@centos6 ~]# service iptables start
# 关闭防火墙
[root@centos6 ~]# service iptables stop
也可以直接调 init 脚本。
[root@centos6 ~]# cd /etc/init.d/
# 查看状态
[root@centos6 init.d]# /etc/init.d/iptables status
# 暂时关闭防火墙
[root@centos6 init.d]# /etc/init.d/iptables stop
# 重启
[root@centos6 init.d]# /etc/init.d/iptables restart
想确认 80 端口是不是被放行了,直接查规则。
iptables -vnL | grep ":80 "
返回有内容说明开通,没返回内容则说明被阻止。
这块要补一句时效性的说明。上面这套是 CentOS 6 时代的做法,CentOS 7 之后 init 脚本换成了 systemd,防火墙前端也换成了 firewalld,对应的命令变成这样。
systemctl status firewalld
firewall-cmd --list-all
firewall-cmd --zone=public --add-port=80/tcp --permanent
firewall-cmd --reload
更新一点的发行版(Debian/Ubuntu 系)常用的是 ufw。原文那套命令在老机器上依然有效,遇到新系统换个前端工具就行,底层还是 netfilter 那一套。
还有个特别容易忽略的地方,云服务器除了系统防火墙,云厂商控制台上还有一层安全组。系统里 iptables 全开了外面还是不通,八成是安全组没放行。这个我踩过,在机器上折腾了半天规则,最后发现问题在网页控制台上。
# 四、查磁盘和内存
# 4.1 磁盘空间
# 查看各分区的使用情况
df -h
# 查看某个目录占了多少空间
du -h file
# 查看当前目录下各个文件/目录的大小
du -sh *
df 是从文件系统层面看,du 是从目录层面统计。日常排查是 df -h 先看哪个分区满了,再 du -sh * 逐层往下找是谁占的。
-h 是 human-readable,把字节数换算成 K/M/G。du -sh * 里的 -s 是 summarize,只输出每个参数的汇总,不加的话会把所有子目录一层层都列出来,几千行刷屏。
这里有个坑要注意。df 显示还有空间,但写文件就是报 No space left on device,这种情况看一眼 inode。
df -i
inode 是文件系统用来记录文件元信息的结构,数量在格式化时就定死了。海量小文件(session 文件、日志切片、缓存碎片)会把 inode 耗光,这时候空间还剩很多但一个新文件都建不了。第一次遇到这个现象是真的会懵。
另一个坑是删了文件空间没释放。有进程还开着那个文件句柄的话,磁盘空间不会回收,lsof | grep deleted 能把这些找出来,重启对应进程才会真正释放。前端项目里最常见的场景是日志文件被 rm 了但 Node 进程还在往里写。
# 4.2 内存
free -m
-m 以 MB 显示,-g 是 GB。
看 free 输出有个经典误解,很多人看到 free 那一列只剩几十兆就慌了。其实 Linux 会把暂时用不到的内存拿去做文件缓存(buff/cache),需要的时候随时能回收。真正该看的是 available 那一列,它才代表还能给新进程用多少。
# 五、查日志
日志文件动辄几百兆,直接 cat 是灾难,终端刷屏几分钟还找不到重点。日志排查的核心是用管道把范围一层层缩小。
管道命令 | 的作用是把前一个命令的输出交给后一个命令处理,最简单的例子是分页看。
ls -l /etc | more
真正干活的组合大概是这几种。
# 实时跟踪最新日志,部署后观察最常用
tail -f /var/log/nginx/error.log
# 只看最后 200 行,再从里面找关键字
tail -n 200 app.log | grep "ERROR"
# 找关键字并带出上下文各 5 行
grep -C 5 "Timeout" app.log
# 统计某个错误出现了多少次
grep -c "500" access.log
# 找出访问量最大的前 10 个 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
最后那条值得拆开说说,它是一个非常典型的管道流水线。awk '{print $1}' 取每行第一列(Nginx 默认日志格式里就是客户端 IP),sort 排序把相同的 IP 排到一起,uniq -c 统计每个 IP 出现的次数,sort -rn 按数字倒序排,head -10 取前十。
为什么 uniq 前面必须先 sort?因为 uniq 只能去掉相邻的重复行,不排序的话分散在各处的相同 IP 它认不出来。这个我一开始也搞错过,写完发现统计结果全是 1。
日志文件太大的时候还有个实用技巧,less 比 vi 安全得多。vi 会把整个文件读进内存,一个 2G 的日志能直接把机器拖死;less 是按需加载的,进去之后 /关键字 搜索、G 跳到末尾、q 退出。