一、什么是进程信号?为什么需要处理它们?

想象你正在用手机看视频,突然来了个电话。这时候手机会暂停视频播放,优先处理来电——这就是典型的"信号处理"场景。在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);
    });
});

这个例子展示了:

  1. 创建了一个简单的HTTP服务器
  2. 监听了SIGTERM和SIGINT信号
  3. 收到信号后先关闭服务器再退出

但实际生产环境需要更完善的方案,比如:

  • 设置关闭超时时间
  • 记录未完成请求
  • 清理其他资源(数据库连接等)

三、进阶优雅停机实现

真正的生产环境需要更全面的处理方案。下面我们实现一个包含数据库连接管理的完整示例。

// 技术栈: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')();
});

这个进阶方案实现了:

  1. 完整的HTTP服务与数据库连接
  2. 活跃连接跟踪
  3. 分步骤关闭资源
  4. 超时强制退出机制
  5. 异常处理

四、实现零停机重启

有时候我们需要不中断服务的情况下更新应用,这就是"零停机重启"。下面是实现方案:

// 技术栈: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`);
}

这个方案实现了:

  1. 利用多核CPU的集群模式
  2. 使用SIGUSR2信号触发重启
  3. 滚动式逐个重启工作进程
  4. 确保始终有可用进程处理请求

五、实际应用场景与注意事项

应用场景

  1. 云环境部署:在Kubernetes或Docker中,容器停止前会收到SIGTERM
  2. 持续部署:自动化部署时需要确保旧进程正确处理完请求
  3. 负载均衡:从负载均衡器摘除节点前完成现有请求
  4. 配置更新:不重启服务的情况下重载配置

技术优缺点

优点

  • 避免数据损坏或丢失
  • 提升用户体验(不中断正在进行的操作)
  • 便于系统维护和升级

缺点

  • 实现复杂度较高
  • 需要额外处理各种边缘情况
  • 可能延长部署/关闭时间

注意事项

  1. 超时设置:总要设置强制关闭的超时,避免无限等待
  2. 连接状态:跟踪活跃连接,但注意内存泄漏风险
  3. 信号冲突:某些信号可能有默认行为,需要特别注意
  4. 日志记录:详细记录关闭过程,便于排查问题
  5. 测试验证:通过模拟测试验证各种场景下的行为

六、总结

处理进程信号就像给程序安装"礼貌开关"——让它们能体面地结束工作,而不是突然消失。从基础的单进程处理,到包含数据库的复杂应用,再到零停机的集群部署,我们看到了不同层次的实现方案。

关键点总结:

  1. 信号处理是生产环境应用的必备功能
  2. 优雅停机的核心是:停止接收新请求 → 完成进行中工作 → 释放资源 → 退出
  3. 集群部署可以实现无缝重启
  4. 一定要考虑各种边缘情况和超时处理

良好的信号处理能让你的应用更可靠、更专业。花时间实现这些机制,会在长期运维中带来巨大回报——就像系安全带,平时觉得麻烦,关键时刻能救命。