凌晨两点收到内存告警,一个跑批用的 Node 服务 rss 涨到容器上限被强制杀掉,重启脚本冷启动又把任务从头跑了一遍——排查时才发现,代码里连 process 的退出事件和信号处理都没接。process 是 Node.js 的全局对象,继承自 EventEmitter,免 require 直接用,一个 Node 进程从生到死的所有可观测信息与可控开关都挂在它身上:环境变量、命令行参数、标准流、内存占用、退出码、系统信号。下面按”信息读取—进程控制—事件与信号—场景落地”四层,把它常用的属性、方法和事件盘成清单。
一张表先看清 process 的 API 版图
process 的 API 分六大类,各自对应一类使用场景。先给全量速查,后面逐类展开:
| 分类 | 常用成员 | 主要作用 |
|---|---|---|
| 运行信息 | pid / platform / version / versions / uptime() / cwd() | 读进程标识、系统平台、版本与运行时长 |
| 环境配置 | env / title / execPath / chdir() | 读环境变量、设进程标题、定位可执行文件 |
| 命令行 | argv / argv0 / execArgv | 取脚本入参、启动器路径与运行时标志 |
| 标准流 | stdin / stdout / stderr | 控制台与管道读写 |
| 进程控制 | exit() / exitCode / kill() / abort() | 退出进程、发信号、强终止 |
| 调度与监控 | nextTick() / memoryUsage() / cpuUsage() / hrtime.bigint() | 微任务调度、资源度量、高精度计时 |
六类里使用频率较高的是”环境配置”与”进程控制”——配置读取几乎每个应用都要做,退出与信号处理则是生产部署的分水岭,接没接信号处理直接决定发布时会不会丢请求。
信息读取类属性清单
这类属性只读不写,用于标识”这个进程是谁、跑在哪”:
process.pid:当前进程的操作系统级 ID,写日志、排查杀进程都靠它;process.platform:运行平台字符串,如linux、darwin、win32,跨平台分支判断的依据;process.version:当前 Node.js 完整版本号(如v22.x.x形态);process.versions:V8、libuv 等各组件的版本集合,报 bug 时贴环境有用;process.uptime():进程已运行秒数,配合重启时间做健康检查;process.cwd():当前工作目录,注意它与脚本文件所在目录不是一回事,随启动位置变化;process.arch:CPU 架构(x64、arm64),下载平台相关二进制包时用。
一个小坑值得记:process.cwd() 返回的是”从哪个目录启动的”,而 __dirname 是”脚本文件在哪个目录”,CLI 工具里把两者混用,换个工作目录执行就会找不到文件。做跨平台脚手架时,涉及文件路径一律从 __dirname 推导更稳。
环境与命令行参数清单
配置从哪来、参数怎么取,是 process 使用频率较高的两个能力:
process.env:环境变量键值对,NODEENV、APIKEY、数据库连接串的标准读取口;process.argv:命令行参数数组,前两位固定是 node 路径与脚本路径,业务参数从下标 2 开始;process.argv0:启动器原始名称,与 argv[0] 略有差异的场景下使用;process.execArgv:传给 Node 运行时的选项(如--harmony),脚本参数不混进来;process.title:进程标题,赋值后ps命令里能一眼认出你的服务;process.execPath:当前 node 可执行文件的绝对路径,做重启自举时用得上。
下面这段代码把环境读取与参数解析合在一个可运行示例里,是 CLI 与服务配置的通用骨架:
// config.js —— 环境变量 + 命令行参数的最小配置层
const args = process.argv.slice(2); // 去掉 node 路径与脚本路径
function readArg(name, fallback) {
const i = args.indexOf(`--${name}`);
return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
}
const config = {
port: Number(readArg("port", process.env.PORT ?? 3000)),
env: process.env.NODE_ENV ?? "development",
dbUrl: process.env.DATABASE_URL,
};
console.log(`[${process.title}] pid=${process.pid} platform=${process.platform}`);
console.log("生效配置:", config);
// 运行:NODE_ENV=production node config.js --port 8080
这段解决的是”配置来源混乱”的问题:命令行参数优先级最高,其次环境变量,最后内置默认值,三层兜底写成四行读参函数,任何小工具都能直接复用。注意 process.env 的值全是字符串,端口号取出来必须手动转数字,这是新手常踩的类型坑。
进程控制与调度方法清单
这一组真正动进程的命运,每个都要知道适用边界:
process.exit([code]):立即终止进程并向操作系统返回退出码;process.exitCode:只设置退出码,让事件循环自然清空后退出,比 exit() 温和;process.kill(pid, signal):向指定进程发信号,名字叫 kill,实际只负责”发”;process.abort():立即中断并生成核心转储,仅调试崩溃现场用;process.nextTick(cb):把回调排到当前操作之后、所有 I/O 事件之前执行;process.chdir(dir):切换工作目录,失败会抛异常;process.umask(mask):读写进程文件权限掩码,写部署脚本时偶尔用到。
process.exit() 的一个隐患必须单独说:stdout 写入在部分场景是异步的,直接调用 exit() 可能把还没刷出去的日志截断。更稳的做法是设置 process.exitCode = 1 然后让循环自然退出,只在确实需要立即终止的异常路径上才用 exit()。nextTick 则是面试高频:它的回调排在微任务队列之前执行,甚至先于已 resolve 的 Promise,框架里常用它保证”同一次调用栈内状态已就绪”。
事件与信号清单
process 继承自 EventEmitter,进程级的事件与系统信号都在这里订阅:
process.on("exit", cb):进程即将退出时触发,是清理同步资源的末班车;process.on("uncaughtException", cb):捕获未处理的同步异常,避免进程无声崩溃;process.on("unhandledRejection", cb):捕获没有 catch 的 Promise 拒绝;process.on("SIGINT", cb):收到中断信号(通常是 Ctrl+C)时触发;process.on("SIGTERM", cb):收到终止信号(kill 默认信号、容器停止)时触发;process.on("warning", cb):Node 内部告警(如内存泄漏嫌疑)的监听口。
退出路径不止一条,行为差异放表里对照:
| 退出路径 | 触发方式 | 异步 I/O 是否等完 | 典型用途 |
|---|---|---|---|
| process.exit(0) | 主动调用 | 不等,可能截断输出流 | 致命错误立即止损 |
| exitCode = 0 自然退出 | 事件循环清空 | 等 | 常规任务收尾 |
| SIGINT/SIGTERM 默认行为 | Ctrl+C / kill | 不等 | 快速终止 |
| SIGINT/SIGTERM 接管处理 | 订阅信号事件 | 处理函数内自控 | 优雅关停 |
关键差异在最后一行:容器编排(Kubernetes 滚动更新、Docker stop)停止服务发的就是 SIGTERM,接了信号并主动收尾的服务才能做到”发布零请求丢失”,没接的服务在宽限期一到后被 SIGKILL 强杀,正在处理的请求直接断掉——这正是开头那次凌晨告警的根源。
应用场景与优雅关停的落地步骤
process 清单的落脚点是四类高频场景:
- 场景一:多环境配置——开发/测试/生产用
process.env.NODE_ENV区分,密钥与连接串只从 env 注入,不进代码仓库; - 场景二:CLI 工具——
process.argv取参数、process.exit(1)返回错误码,让 shell 管道能感知失败; - 场景三:生产监控——定时上报
memoryUsage()与uptime(),rss 持续增长就是泄漏嫌疑; - 场景四:优雅关停——订阅 SIGTERM,关流量、清连接、再退出。
优雅关停是四类里工程价值较突出的一段,按五步落地:
- 启动时先
server.close()预备:订阅 SIGTERM 与 SIGINT 两个信号; - 收到信号后停止接收新连接,让负载均衡把流量切走;
- 给在途请求设超时上限(如 10 秒),等待存量请求处理完;
- 依次关闭数据库连接池、消息队列消费者等外部资源;
- 写退出日志并设置
process.exitCode = 0,让进程自然退出。
配套代码可以直接抄进任何 HTTP 服务:
// graceful.js —— HTTP 服务的优雅关停骨架
const http = require("node:http");
const server = http.createServer((req, res) => {
setTimeout(() => res.end("ok"), 3000); // 模拟慢请求
});
server.listen(3000, () => console.log("listening on 3000"));
let shuttingDown = false;
["SIGTERM", "SIGINT"].forEach((sig) =>
process.on(sig, () => {
if (shuttingDown) return; // 防止重复触发
shuttingDown = true;
console.log(`收到 ${sig},停止接新请求…`);
server.close(() => {
console.log("在途请求处理完毕,进程退出");
process.exitCode = 0;
});
setTimeout(() => {
console.error("宽限期到,强制退出");
process.exit(1);
}, 10_000).unref();
})
);
process.on("uncaughtException", (err) => {
console.error("未捕获异常:", err.message);
process.exitCode = 1;
});
这段解决的是”发布与重启时丢请求”的问题:信号来了先摘流量、再等存量、最后有兜底强退,unref() 保证兜底定时器不会反过来阻止进程退出。加上 uncaughtException 兜底后,即使某处漏了 try/catch,进程也能带着非零退出码离开,让 PM2 或容器编排感知到失败并重启。把信号处理、配置读取、资源监控三件事做齐,process 这个对象才算真正用到位。
常见问题(FAQ)
Q1:为什么 process.exit() 有时会丢日志?
stdout 写入可能异步,exit() 立即终止进程会截断未刷出的输出。改设 process.exitCode,让事件循环自然清空更稳。
Q2:怎么让 Node 服务在发布时不丢请求?
订阅 SIGTERM 与 SIGINT,先停止接新连接,等在途请求完成并关闭外部资源后,再设置 exitCode 自然退出。
Q3:process.nextTick 能不能替代 Promise.then 用?
不能混用。nextTick 回调先于所有微任务执行,语义是”当前栈尾立刻执行”,滥用会导致 I/O 饥饿。