进程与系统资源管理
1. 概念与原理
进程是 Linux 调度的基本单位,每个 Node 进程、Nginx worker、Docker 容器进程都是一个进程。理解进程模型,你才能解释为什么 Node 是单进程多事件循环、为什么 Nginx 是 master + worker、为什么 PM2 cluster 模式能利用多核。
1.1 进程的核心属性
ps -ef | grep node
# UID PID PPID C STIME TTY TIME CMD
# alice 12345 12000 2 10:00 ? 00:00:15 node server.js
| 字段 | 含义 |
|---|---|
| PID | 进程 ID,唯一标识 |
| PPID | 父进程 PID。任何进程都有父进程,根是 PID 1(init/systemd) |
| UID | 启动进程的用户 ID,决定权限 |
| TTY | 关联终端,? 表示后台进程(脱离终端) |
| TIME | CPU 时间(不是墙上时间) |
1.2 进程状态
R (Running) — 运行中或可运行队列
S (Sleeping) — 可中断睡眠(等 IO、信号) ← 99% 的服务进程在这
D (Disk Sleep) — 不可中断睡眠(等磁盘 IO) ← 出现说明磁盘很慢
Z (Zombie) — 僵尸(已死,父进程没回收)
T (Stopped) — 已停止(Ctrl+Z 或 SIGSTOP)
top 里大量 D 状态进程 = 磁盘 IO 瓶颈,要查 iostat。大量 Z 状态 = 父进程有 bug 没 wait 子进程。
1.3 信号机制
进程间通信和控制的最基础手段。前端必知信号:
| 信号 | 编号 | 含义 | 能否捕获 |
|---|---|---|---|
| SIGHUP | 1 | 终端断开,约定俗成用作"重载配置" | 可 |
| SIGINT | 2 | Ctrl+C | 可 |
| SIGKILL | 9 | 强杀,不可捕获 | 否 |
| SIGTERM | 15 | 优雅终止,默认信号 | 可 |
| SIGSTOP | 19 | 暂停 | 否 |
| SIGCONT | 18 | 继续 | 可 |
| SIGUSR1/2 | 10/12 | 应用自定义 | 可 |
kill <pid> # 默认 SIGTERM
kill -9 <pid> # SIGKILL,强杀
kill -HUP <pid> # nginx -s reload 等价
kill -l # 列出所有信号
killall node # 杀掉所有名为 node 的进程
pkill -f "next dev" # 按命令行模式匹配杀
为什么生产优先 SIGTERM:SIGKILL 会让进程没机会清理(关闭数据库连接、写完日志、完成正在处理的请求),可能导致连接泄漏、数据损坏。Node 应用应该在收到 SIGTERM 时执行:
process.on('SIGTERM', async () => {
console.log('收到 SIGTERM,开始优雅退出')
server.close() // 拒绝新连接
await closeDbConnections() // 清理资源
process.exit(0)
})
K8s 默认给 Pod 30 秒 grace period(先 SIGTERM 等 30 秒,超时 SIGKILL),见模块 05。
2. 进程查看工具
2.1 ps — 静态快照
ps -ef # 所有进程,BSD 格式
ps aux # 所有进程,含 CPU/内存(最常用)
ps aux --sort=-%mem | head # 按内存倒序前 N
ps aux --sort=-%cpu | head # 按 CPU 倒序
# 看进程树
ps auxf
pstree -p # 树状显示,含 PID
pstree -p $(pgrep -f node) # 看 node 进程的子进程
ps 是快照,看的是某一瞬间的状态。要看动态变化用 top/htop。
2.2 top — 动态实时
top
# 按 P:按 CPU 排序
# 按 M:按内存排序
# 按 1:展开多核 CPU
# 按 c:显示完整命令行
# 按 k:输入 PID 杀进程
# 按 t:切换 CPU 显示模式
top 输出第一行:
top - 10:00:01 up 30 days, 3:14, 2 users, load average: 0.50, 0.65, 0.80
load average 是 1/5/15 分钟平均负载,不等于 CPU 使用率。粗略标准:单核机器 load > 1 就过载,多核机器除以核数判断。但 load 包含 D 状态进程,IO 阻塞也会推高 load。
第二行:
%Cpu(s): 5.0 us, 2.0 sy, 0.0 ni, 92.0 id, 1.0 wa, 0.0 hi, 0.0 si, 0.0 st
| 字段 | 含义 | 异常表现 |
|---|---|---|
| us | 用户态 CPU | 业务代码消耗,>80% 是应用本身慢 |
| sy | 内核态 CPU | 系统调用,>30% 可能是频繁 IO 或上下文切换 |
| id | 空闲 | 越高越好 |
| wa | IO 等待 | >20% 磁盘瓶颈 |
| si | 软中断 | 高网络流量会推高 |
| st | 被虚拟化偷走 | 云服务器超卖会出现 |
2.3 htop — 更友好的 top
htop # 彩色、可鼠标操作、可搜索
# F4 过滤、F5 树状、F6 排序、F9 杀进程
生产服务器装一个 htop 排障效率翻倍。CentOS 上 yum install htop,Ubuntu/Debian apt install htop。
2.4 pidstat — 进程级历史采样
pidstat 1 5 # 每秒一次,采 5 次
pidstat -u 1 -p $(pgrep node) # 看 node 进程 CPU 历史
pidstat -r 1 -p <pid> # 内存
pidstat -d 1 -p <pid> # 磁盘 IO
pidstat -w 1 -p <pid> # 上下文切换
比 top 更适合排查"过去几秒发生了什么"。
3. CPU 排查
3.1 CPU 飙到 100% 怎么定位
经典三步法:
# 1. 找出占 CPU 最高的进程
top -c
# 假设 PID 12345 的 node 占 200% CPU(多线程)
# 2. 找出该进程内占 CPU 最高的线程
top -H -p 12345
# 或 ps -T -p 12345
# 假设 TID 12350 占 99%
# 3. 把 TID 转 16 进制(Java 用,Node 也能用 perf)
printf "%x\n" 12350
# 301e
# 4. Node 用 perf 抓火焰图(需 root + perf 工具)
perf record -F 99 -p 12345 -g -- sleep 30
perf script > out.perf
# 配合 FlameGraph 工具生成火焰图
Node 应用更推荐 clinic.js(见模块 11):
npx clinic flame -- node server.js
npx clinic doctor -- node server.js
3.2 上下文切换过高
vmstat 1
# procs -----------memory---------- ---system---
# r b swpd free buff cache in cs
# 1 0 0 500M 100M 2G 5000 20000
cs上下文切换数,正常服务千级别,过万就异常in中断数,网络密集时会高
排查:pidstat -w 看哪个进程切换多。Node 应用上下文切换过高常见于线程池配置不当或大量 worker_thread。
4. 内存排查
4.1 free 与 buff/cache 的误读
free -h
# total used free shared buff/cache available
# Mem: 7.8G 2.0G 500M 100M 5.3G 5.5G
# Swap: 2.0G 0B 2.0G
新人最容易看错的是 free 列,看到 500M 就以为内存快满了。真正可用是 available(5.5G)。buff/cache 是内核为加速 IO 做的页缓存,应用要内存时会自动让出来。
判断内存压力的真正信号:
# 1. available 持续低(如 < 10% 总内存)
# 2. swap 在用(si/so 不为 0)
vmstat 1
# si = swap in,so = swap out,持续非零 = 内存不够在换页
# 3. dmesg 看到 OOM Killer 出手
dmesg -T | grep -i "out of memory"
# Out of memory: Kill process 12345 (node) score 900 or sacrifice child
# Killed process 12345 (node) total-vm:8000000kB, anon-rss:7000000kB
4.2 OOM Killer
物理内存耗尽时 Linux 会触发 OOM Killer,按 oom_score 杀进程。打分越高越优先被杀,受内存占用、运行时长、oom_score_adj 影响。
保护关键进程不被 OOM 杀:
echo -1000 > /proc/<pid>/oom_score_adj # -1000 = 永不被杀
但更应该解决根因:调小 Node 的 --max-old-space-size、加内存限制(Docker 的 -m、K8s 的 limits)。
4.3 进程内存详细分析
# 总览
ps aux | awk 'NR==1 || /node/'
# 详细分布
cat /proc/<pid>/status | grep -E "Vm|Rss"
# VmPeak: 最大虚拟内存
# VmSize: 当前虚拟内存
# VmRSS: 常驻内存(真正占用物理内存)
# VmData: 数据段
# VmStk: 栈
# VmSwap: swap 占用
# 内存映射详情
pmap -x <pid>
cat /proc/<pid>/smaps # 极详细
# 看哪些库被加载
lsof -p <pid> | grep mem
Node 应用排查内存泄漏推荐 node --inspect + Chrome DevTools 或 clinic heap,见模块 11。
5. 文件描述符
每个进程能打开的文件、socket、管道都是 fd。Node 应用 fd 用尽是高频生产事故。
# 系统级限制
cat /proc/sys/fs/file-max # 系统总上限
ulimit -n # 当前 shell 单进程上限,默认 1024(小到离谱)
# 进程级限制
cat /proc/<pid>/limits | grep "open files"
# Max open files 1024 4096
# 当前打开 fd 数
ls /proc/<pid>/fd | wc -l
lsof -p <pid> | wc -l # 含库映射,数会更大
提高限制:
# 永久(写 /etc/security/limits.conf)
* soft nofile 65535
* hard nofile 65535
# systemd 服务(写 service unit)
[Service]
LimitNOFILE=65535
# Docker
docker run --ulimit nofile=65535:65535 ...
# K8s(pod spec)
spec:
containers:
- name: app
# K8s 不直接配 ulimit,需要在镜像里 ulimit -n 或用 init container
前端实战:Node SSR 高并发时 fd 飙升,常见原因是 axios 没复用 keepAlive、数据库连接池满了。解法:
// 全局 keepAlive agent
const https = require('https')
const agent = new https.Agent({ keepAlive: true, maxSockets: 100 })
axios.create({ httpsAgent: agent })
6. 僵尸进程与孤儿进程
| 类型 | 定义 | 危害 |
|---|---|---|
| 僵尸 | 子进程已终止,父进程还没 wait 它,PCB 还在 | 占 PID,PID 耗尽时无法 fork 新进程 |
| 孤儿 | 父进程先死,子进程被 init/PID 1 收养 | 一般无害,init 会自动 wait |
排查僵尸:
ps aux | grep ' Z '
# 或
ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/'
杀僵尸:杀不掉,因为它已经死了。要杀它的父进程让 init 收养。如果父进程是关键服务,根因是父进程代码 bug,要修代码。
Docker / K8s 必知:容器里 PID 1 是你的应用进程,但应用通常没实现 init 的职责(不会 reap 孤儿)。多进程场景(如 Puppeteer fork chrome)会产生僵尸堆积。解法:
# 用 tini 做 PID 1
RUN apk add tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]
或 docker run 加 --init:
docker run --init myimage
7. systemd 与服务管理
现代 Linux 服务管理事实标准。前端要会的核心命令:
systemctl status nginx # 看状态
systemctl start nginx # 启动
systemctl stop nginx # 停止
systemctl restart nginx # 重启
systemctl reload nginx # 重载配置(不重启进程)
systemctl enable nginx # 开机自启
systemctl disable nginx
systemctl is-active nginx
systemctl is-enabled nginx
journalctl -u nginx -f # 看实时日志
journalctl -u nginx --since "1 hour ago"
写一个 Node 应用的 systemd 服务文件 /etc/systemd/system/myapp.service:
[Unit]
Description=My Node App
After=network.target
[Service]
Type=simple
User=nodeuser
Group=nodeuser
WorkingDirectory=/var/www/myapp
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
StandardOutput=journal
StandardError=journal
# 安全加固
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/www/myapp/logs
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
journalctl -u myapp -f
PM2 vs systemd:PM2 多进程管理(cluster 模式)、内置监控;systemd 更底层稳定。生产常见组合:systemd 启 PM2,PM2 管理 Node cluster。
8. cgroup 与资源限制
cgroup(control group)是容器的底层基石之一。可以给一组进程限制 CPU、内存、IO。
# 看进程在哪个 cgroup
cat /proc/<pid>/cgroup
# systemd 服务限制资源
[Service]
MemoryMax=2G
CPUQuota=200% # 2 核
TasksMax=1000
Docker 的 -m、--cpus 本质就是写 cgroup 文件。详见模块 04。
9. 故障排查实战
9.1 服务器登不上去
按概率排查:
- 网络通不通:本地
ping <ip>、traceroute <ip> - SSH 端口:
telnet <ip> 22或nc -zv <ip> 22 - 防火墙:云控制台看安全组,机器上
iptables -L、firewall-cmd --list-all - 进程满了:fork 不了新进程时 sshd 接不了新连接,等其他人退出释放 PID
- 磁盘 100%:
/var/log写不进去 sshd 启不了
如果只剩控制台救援:登入后 top 看负载,df -h 看磁盘,who -a 看登录用户。
9.2 Node 应用突然挂掉
# 1. 看进程是不是真的挂了
ps aux | grep node
systemctl status myapp
# 2. 看退出原因
journalctl -u myapp -n 100 --no-pager
dmesg -T | tail -100 # 看是不是被 OOM 杀
# 3. 看 core dump(如果开了)
ls /var/crash/ 或 coredumpctl list
# 4. 看资源限制
cat /proc/<pid>/limits # 进程还活着的话
9.3 CPU 100% 但不知道哪个进程
某些极端情况 top 看不出(如内核态 CPU 高)。用:
mpstat -P ALL 1 # 每个核分别看
sar -u 1 # 历史 CPU
perf top # 实时函数级 CPU 占用
10. 常见反模式
- kill -9 一切:跳过应用清理,导致连接泄漏、数据不一致。先 SIGTERM,等 30 秒不退再 -9
- 后台跑
node app.js &:终端关了进程也死。用 nohup、systemd、PM2、screen/tmux - 不设 ulimit:Node 应用千级并发就 fd 耗尽
- 容器里跑 PID 1 不用 init:僵尸堆积。用 tini 或 docker run --init
- 看到 free 列就觉得内存满了:看 available 列才对
- OOM 后只重启不查根因:内存泄漏会反复 OOM,必须用堆 dump 定位
- 生产服务器随手
top不退出:top 自身吃 CPU,养成习惯按 q 退出
11. 延伸阅读
- 《Linux 性能优化实战》(倪朋飞) — 中文最系统的 Linux 性能排查实战
- Brendan Gregg's USE Method — 性能排查方法论权威
- systemd.service man page — systemd 服务文件完整规范
- Linux Kernel Documentation: cgroup-v2 — cgroup 官方文档