Appearance
进程管理
服务出问题排查,起点几乎都是进程——进程还在不在、资源占用正不正常、谁启动的、归哪个管理器管。进程这一层搞不清楚,后面看日志、改配置都无从下手。进程查看、信号、systemd 服务管理——这三块是排查时绕不过去的核心,知道每一步该看什么、用哪个命令,效率差出一个量级。
一、进程基础概念
进程说白了就是正在运行的程序实例。一个程序可以启动多个进程,每个进程有唯一的 PID(进程 ID)。进程还可以继续创建子进程,形成父子关系——这点在排查时特别重要,光看命令名不够,还得看 PID、PPID(父进程 ID)、运行用户、启动来源,才能搞清楚这个进程从哪来、归谁管。
查看当前 Shell 自己的进程信息,感受一下"自己也是个进程":
bash
echo $$ # 当前 Shell 的 PID
ps -p $$ # 查看该进程详情查看进程树,直观看到父子关系:
bash
pstree -ppstree 不是默认装的,需要单独安装:
bash
yum install psmisc -y # RHEL 系
apt install psmisc -y # Debian 系进程由谁启动,直接决定了排查入口。systemd 拉起的进程、Shell 手动敲的进程、supervisor 托管的进程、容器运行时管理的进程——它们的日志位置、重启策略、环境变量、退出行为都不一样。看到一个陌生进程,先搞清楚"它是被谁拉起来的",后续排查方向才对。
二、ps 查看进程
ps 是查看进程快照的主力工具:
bash
ps aux # BSD 风格,显示所有用户的所有进程
ps -ef # Unix 风格,功能类似
ps aux | grep nginx查看指定 PID 的进程,只显示关心的字段,排查时更聚焦:
bash
ps -p 1234 -o pid,ppid,user,stat,pcpu,pmem,cmd几个常用字段的含义:PID 是进程 ID、PPID 是父进程 ID、USER 是运行用户、STAT 是进程状态(R 运行中、S 休眠、D 不可中断睡眠、Z 僵尸)、%CPU 和 %MEM 是资源占用、CMD 是启动命令行。
CMD 列在终端里经常被宽度截断,看不全完整启动命令。这时候直接读 /proc/PID/cmdline,内核会给你完整的:
bash
tr '\0' ' ' </proc/1234/cmdline/proc/PID/cmdline 里参数之间是 null 字符分隔的(不是空格),用 tr 把 null 转成空格,就能看到人类可读的完整命令行。排查"这个进程到底是怎么启动的、带了什么参数",这一招比 ps 靠谱。
三、top 和 htop
ps 给的是某一时刻的快照,要看实时变化就用 top:
bash
toptop 界面里几个高频交互键:按 P 按 CPU 排序、M 按内存排序、1 展开看每个 CPU 核心、k kill 指定进程(会提示输入 PID)、q 退出。
htop 是 top 的增强版,支持鼠标、彩色显示、进程树更直观,界面友好得多:
bash
yum install htop -y # RHEL 系
apt install htop -y # Debian 系排查 CPU 飙高的标准套路:先 top 定位是哪个进程吃 CPU,然后看这个进程内部是哪个线程在吃:
bash
top -H -p <pid>-H 就是显示线程级别。Java、Go、Node.js 这种多线程服务,排查 CPU 瓶颈时看线程级别比看进程级别有效得多——进程整体 CPU 200%,可能是某一个线程占了 190%,定位到具体线程才能往下查(比如 jstack 看那个线程在干嘛)。
四、信号和 kill
信号是 Linux 进程间通信的一种方式,运维最常用的场景就是通知进程终止或者重载配置。几个常用信号要记住:
| 信号 | 编号 | 干什么 |
|---|---|---|
| SIGTERM | 15 | 请求进程正常退出(可被进程捕获处理,能做清理) |
| SIGKILL | 9 | 强制终止(不可被捕获,内核直接动手) |
| SIGHUP | 1 | 常用于通知进程重新加载配置 |
bash
kill 1234 # 默认发送 SIGTERM(15)
kill -15 1234
kill -9 1234 # 强制终止SIGTERM 和 SIGKILL 的区别要理解透。SIGTERM 是"请你自己停掉"——进程收到之后可以做清理工作:关闭连接、刷新缓冲区、写完未完成的日志、释放资源,然后优雅退出。SIGKILL 是内核直接把进程干掉,进程没有任何机会做清理,半截的写入可能丢失、临时文件可能残留、连接可能没正常关闭。
所以默认先用 SIGTERM,给进程优雅退出的机会;实在不响应再用 kill -9。如果某个服务经常需要 kill -9 才能停,说明它的信号处理和退出逻辑有问题——正常服务收到 SIGTERM 应该能自己清理退出。
按名称批量操作进程:
bash
pkill nginx # 向所有名为 nginx 的进程发 SIGTERM
pgrep -a nginx # 先看有哪些匹配的进程pkill 的匹配范围可能比你预期的大——pkill nginx 可能也匹配到 nginx-exporter、nginx-dashboard 这些名字里含 nginx 的进程,一棍子打死。所以执行 pkill 前先 pgrep -a(-a 显示完整命令行)看清楚到底会匹配到哪些,确认无误再 kill。
五、前台和后台任务
Shell 里命令默认在前台运行,会阻塞 Shell 直到执行完。命令末尾加 & 可以放到后台:
bash
sleep 100 &
jobs # 查看当前 Shell 的后台任务前后台切换:fg %1 把 1 号后台任务切回前台、Ctrl+z 暂停当前前台任务、bg %1 让暂停的任务在后台继续跑。
临时命令用 & 或 Ctrl+z 没问题,但长期运行的任务靠 & 管理是隐患——SSH 一断开,进程可能收到 HUP 信号被终止;输出没地方去(除非手动重定向);进程状态全靠你记忆,重启没重启、有没有挂都不知道。所以长期任务要按场景选合适的管理工具:临时长命令用 tmux/screen 保持会话、服务进程用 systemd 托管、容器服务交给容器运行时或 K8s、定时任务用 systemd timer 或 cron。
六、systemd
systemd 是目前绝大多数 Linux 发行版的服务管理器,干的事不少:系统启动时按依赖关系拉起服务、监控运行状态、接管日志(journald)、进程异常退出时按策略重启。可以说现代 Linux 服务管理基本绕不开它。
systemd 管理的对象叫 unit,常见类型有几种:.service 定义服务进程、.timer 定时触发(cron 的替代方案)、.socket 基于 socket 激活服务(先监听,有请求再启动)、.mount 管理挂载点、.target 是一组 unit 的集合(相当于传统的运行级别)。
查看服务状态:
bash
systemctl status nginx
systemctl is-active nginx # 只看是否在运行
systemctl is-enabled nginx # 只看是否设了开机自启启动停止重启:
bash
systemctl start nginx
systemctl stop nginx
systemctl restart nginx # 整个重启,有短暂中断
systemctl reload nginx # 重新加载配置,不重启进程(平滑)开机自启:
bash
systemctl enable nginx # 设为开机自启
systemctl disable nginx # 取消开机自启查看服务日志,journald 把日志统一管了:
bash
journalctl -u nginx -f # 持续跟踪(类似 tail -f)
journalctl -u nginx --since "1 hour ago" # 只看最近一小时服务启动失败的排查流程要形成肌肉记忆:
bash
systemctl status service-name --no-pager # 看当前状态和最后几行日志
journalctl -u service-name -n 100 --no-pager # 看最近 100 行日志
systemctl cat service-name # 看实际加载到的 unit 内容systemctl cat 比手动去找 unit 文件靠谱——它会把你写的 /etc/systemd/system/ 下的覆盖配置(drop-in)也合并显示出来。很多时候你以为改了配置不生效,其实是 drop-in 文件覆盖了你的修改,systemctl cat 一看就明白。
七、unit 文件
unit 文件定义了 systemd 怎么管理一个服务。文件位置有几个:/usr/lib/systemd/system/(RHEL 系)或 /lib/systemd/system/(Debian 系)放软件包自带的 unit,/etc/systemd/system/ 放自定义 unit 和覆盖配置。自定义的放 /etc/systemd/system/,因为软件包升级时会覆盖 /usr/lib 下的,但不会动 /etc 下的。
一个最小可用的 service 文件长这样:
ini
# /etc/systemd/system/myapp.service
[Unit]
Description=My App
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/app
Restart=always
RestartSec=5
User=app
Group=app
[Install]
WantedBy=multi-user.target三个段各管一摊:[Unit] 描述服务本身和依赖关系、启动顺序;[Service] 定义进程怎么启动停止重启、以什么身份运行;[Install] 决定 systemctl enable 时挂在哪个 target 下面。
几个容易踩坑的字段说一下:
After只表示启动顺序,不表示依赖——对方启动失败不影响自己启动。真正要表达"必须依赖它"用Requires(强依赖,对方失败自己也失败)或Wants(弱依赖,对方失败不影响自己)ExecStart必须写绝对路径,systemd 的 PATH 跟你登录 Shell 的 PATH 不一样,相对路径或者没写全的命令会找不到Restart=always配合程序快速失败会变成疯狂重启——程序一启动就崩,systemd 立刻重启,又崩,无限循环,把 CPU 和日志都打爆。这种情况加RestartSec=5拉长重启间隔,或者改用on-failureUser/Group决定服务以谁的身份跑——服务长期用 root 跑风险偏高,能不用 root 就不用
Type 的选择直接影响 systemd 能不能正确判断服务状态,记住这几个:simple(程序前台运行,启动即就绪,最常用)、forking(程序自己 fork 到后台,老式守护进程)、oneshot(跑完就退出,用于初始化)、notify(程序主动通知 systemd"我好了")。现在写服务优先让程序前台跑,用 Type=simple——把守护化的事交给 systemd,日志和退出状态都好追踪,别让程序自己 daemonize。
编写和启用的完整流程:
bash
vim /etc/systemd/system/myapp.service
systemctl daemon-reload # 让 systemd 重新读取 unit 文件(改了 unit 必做)
systemctl start myapp
systemctl status myapp --no-pager
systemctl enable myapp # 设置开机自启systemctl daemon-reload 这一步特别容易忘——改了 unit 文件不 reload,systemd 还在用旧配置,你怎么 restart 都不生效,排查半天才发现是没 reload。养成习惯:改完 unit 必 daemon-reload。
最后列几个常见的"启动失败"现象和原因,排查时对照:
- 手动执行正常,systemd 启动失败:多半是环境变量缺失(登录 Shell 有的变量 systemd 没有)、工作目录不对、或者路径没写绝对路径
- 服务启动后立刻 failed:程序自己退出了(看 journalctl 报错),或者
Type写错(systemd 以为它该 fork 结果它没 fork) - 修改 unit 后不生效:99% 是忘了
systemctl daemon-reload - 服务反复重启:
Restart=always撞上程序快速失败,加RestartSec或者查程序为什么崩 - 日志找不到:程序把日志写到文件了而不是 stdout/stderr(systemd 只收 stdout/stderr),或者 journald 配了不持久化(重启后日志没了)