跳到主要内容

进程与系统资源管理

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关联终端,? 表示后台进程(脱离终端)
TIMECPU 时间(不是墙上时间)

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 信号机制

进程间通信和控制的最基础手段。前端必知信号:

信号编号含义能否捕获
SIGHUP1终端断开,约定俗成用作"重载配置"
SIGINT2Ctrl+C
SIGKILL9强杀,不可捕获
SIGTERM15优雅终止,默认信号
SIGSTOP19暂停
SIGCONT18继续
SIGUSR1/210/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空闲越高越好
waIO 等待>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 服务器登不上去

按概率排查:

  1. 网络通不通:本地 ping &lt;ip>traceroute &lt;ip>
  2. SSH 端口:telnet &lt;ip> 22nc -zv &lt;ip> 22
  3. 防火墙:云控制台看安全组,机器上 iptables -Lfirewall-cmd --list-all
  4. 进程满了:fork 不了新进程时 sshd 接不了新连接,等其他人退出释放 PID
  5. 磁盘 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. 延伸阅读