一、什么是进程信号?为什么需要处理它们?
想象你正在用手机看视频,突然来了个电话。这时候手机会暂停视频播放,优先处理来电——这就是典型的"信号处理"场景。在Node.js应用中,进程信号就是操作系统发给程序的通知,告诉程序有重要事情需要处理。
常见的信号包括:
- SIGTERM:温柔地请求程序退出(就像礼貌地说"请关门")
- SIGINT:用户按Ctrl+C时发出的中断信号
- SIGHUP:终端连接断开时发出
- SIGUSR1/SIGUSR2:用户自定义信号
不处理这些信号会导致突然断电般的效果:正在处理的请求被强行终止,数据库连接直接断开,可能造成数据损坏。这就好比直接拔掉电饭煲插头,饭可能煮不熟还浪费米。
二、基础信号处理实现
让我们从一个最简单的例子开始,看看如何捕获信号并做出响应。
// 技术栈:Node.js 16+
// 引入所需模块
const http = require('http');
// 创建简单HTTP服务
const server = http.createServer((req, res) => {
// 模拟长时间请求
if (req.url === '/long') {
setTimeout(() => {
res.end('Long request completed');
}, 5000);
} else {
res.end('Hello World');
}
});
// 启动服务
server.listen(3000, () => {
console.log('Server running on port 3000');
});
// 信号处理部分
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully...');
// 停止接受新连接
server.close(() => {
console.log('Server closed');
process.exit(0); // 退出进程
});
});
process.on('SIGINT', () => {
console.log('Received SIGINT (Ctrl+C), shutting down...');
server.close(() => {
process.exit(0);
});
});
这个例子展示了:
- 创建了一个简单的HTTP服务器
- 监听了SIGTERM和SIGINT信号
- 收到信号后先关闭服务器再退出
但实际生产环境需要更完善的方案,比如:
- 设置关闭超时时间
- 记录未完成请求
- 清理其他资源(数据库连接等)
三、进阶优雅停机实现
真正的生产环境需要更全面的处理方案。下面我们实现一个包含数据库连接管理的完整示例。
// 技术栈:Node.js + Express + MongoDB
const express = require('express');
const mongoose = require('mongoose');
const app = express();
// 模拟数据库连接
async function connectDB() {
try {
await mongoose.connect('mongodb://localhost:27017/mydb', {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('Database connected');
} catch (err) {
console.error('Database connection error:', err);
process.exit(1); // 连接失败直接退出
}
}
// 中间件和路由
app.get('/', (req, res) => {
res.send('Home page');
});
app.get('/data', async (req, res) => {
// 模拟数据库查询
await new Promise(resolve => setTimeout(resolve, 3000));
res.send('Data fetched');
});
// 启动服务
const server = app.listen(3000, async () => {
await connectDB();
console.log('Server started on port 3000');
});
// 记录活跃连接
const activeConnections = new Set();
// 监控新连接
server.on('connection', (conn) => {
const key = `${conn.remoteAddress}:${conn.remotePort}`;
activeConnections.add(key);
conn.on('close', () => activeConnections.delete(key));
});
// 优雅停机函数
function gracefulShutdown(signal) {
return () => {
console.log(`\nReceived ${signal}, starting graceful shutdown...`);
// 1. 停止接受新请求
server.close(async () => {
console.log('HTTP server closed');
// 2. 关闭数据库连接
await mongoose.disconnect();
console.log('Database disconnected');
// 3. 强制关闭残留连接(如果有)
if (activeConnections.size > 0) {
console.log(`Force closing ${activeConnections.size} connections`);
server.closeAllConnections();
}
console.log('Shutdown complete');
process.exit(0);
});
// 设置关闭超时(5秒)
setTimeout(() => {
console.error('Forcing shutdown due to timeout');
process.exit(1);
}, 5000);
};
}
// 注册信号处理器
process.on('SIGTERM', gracefulShutdown('SIGTERM'));
process.on('SIGINT', gracefulShutdown('SIGINT'));
// 处理未捕获异常
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
gracefulShutdown('uncaughtException')();
});
这个进阶方案实现了:
- 完整的HTTP服务与数据库连接
- 活跃连接跟踪
- 分步骤关闭资源
- 超时强制退出机制
- 异常处理
四、实现零停机重启
有时候我们需要不中断服务的情况下更新应用,这就是"零停机重启"。下面是实现方案:
// 技术栈:Node.js + cluster模块
const cluster = require('cluster');
const os = require('os');
const express = require('express');
if (cluster.isMaster) {
// 主进程逻辑
const cpuCount = os.cpus().length;
console.log(`Master ${process.pid} is running`);
// 创建工作进程
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
// 监听重启信号
process.on('SIGUSR2', () => {
console.log('Received SIGUSR2, starting rolling restart...');
const workers = Object.values(cluster.workers);
let currentWorker = 0;
function restartNextWorker() {
const worker = workers[currentWorker];
if (!worker) return;
console.log(`Restarting worker ${worker.process.pid}`);
// 1. 创建新工作进程
const newWorker = cluster.fork();
// 2. 新进程就绪后关闭旧进程
newWorker.on('listening', () => {
worker.send('shutdown');
worker.disconnect();
// 设置超时强制关闭
setTimeout(() => {
if (worker.isConnected) worker.kill();
}, 5000);
// 处理下一个工作进程
currentWorker++;
restartNextWorker();
});
}
restartNextWorker();
});
} else {
// 工作进程逻辑
const app = express();
app.get('/', (req, res) => {
res.send(`Hello from worker ${process.pid}`);
});
const server = app.listen(3000);
// 监听主进程的关闭指令
process.on('message', (msg) => {
if (msg === 'shutdown') {
console.log(`Worker ${process.pid} received shutdown signal`);
server.close(() => {
process.exit(0);
});
}
});
console.log(`Worker ${process.pid} started`);
}
这个方案实现了:
- 利用多核CPU的集群模式
- 使用SIGUSR2信号触发重启
- 滚动式逐个重启工作进程
- 确保始终有可用进程处理请求
五、实际应用场景与注意事项
应用场景
- 云环境部署:在Kubernetes或Docker中,容器停止前会收到SIGTERM
- 持续部署:自动化部署时需要确保旧进程正确处理完请求
- 负载均衡:从负载均衡器摘除节点前完成现有请求
- 配置更新:不重启服务的情况下重载配置
技术优缺点
优点:
- 避免数据损坏或丢失
- 提升用户体验(不中断正在进行的操作)
- 便于系统维护和升级
缺点:
- 实现复杂度较高
- 需要额外处理各种边缘情况
- 可能延长部署/关闭时间
注意事项
- 超时设置:总要设置强制关闭的超时,避免无限等待
- 连接状态:跟踪活跃连接,但注意内存泄漏风险
- 信号冲突:某些信号可能有默认行为,需要特别注意
- 日志记录:详细记录关闭过程,便于排查问题
- 测试验证:通过模拟测试验证各种场景下的行为
六、总结
处理进程信号就像给程序安装"礼貌开关"——让它们能体面地结束工作,而不是突然消失。从基础的单进程处理,到包含数据库的复杂应用,再到零停机的集群部署,我们看到了不同层次的实现方案。
关键点总结:
- 信号处理是生产环境应用的必备功能
- 优雅停机的核心是:停止接收新请求 → 完成进行中工作 → 释放资源 → 退出
- 集群部署可以实现无缝重启
- 一定要考虑各种边缘情况和超时处理
良好的信号处理能让你的应用更可靠、更专业。花时间实现这些机制,会在长期运维中带来巨大回报——就像系安全带,平时觉得麻烦,关键时刻能救命。
评论