您现在的位置是:网站首页 > 零停机重启文章详情

零停机重启

零停机重启的概念

零停机重启是一种在不中断服务的情况下更新应用程序的技术。对于Node.js这类单线程应用来说,实现零停机重启尤为重要。传统的重启方式会导致服务短暂不可用,而零停机重启通过巧妙的进程管理,确保至少有一个进程始终能够处理请求。

为什么需要零停机重启

Node.js应用在更新代码或配置后通常需要重启。如果直接停止服务再启动,会导致:

  1. 正在处理的请求被中断
  2. 新请求无法被处理
  3. 负载均衡器可能将流量导向不健康的实例

零停机重启解决了这些问题,特别适合:

  • 高可用性要求的服务
  • 频繁部署的场景
  • 需要保持长连接的应用

实现零停机重启的基本原理

Node.js实现零停机重启主要依赖以下机制:

  1. 主从进程模型:主进程管理子进程,子进程处理实际请求
  2. 进程间通信:主进程通知子进程优雅退出
  3. 负载均衡:多个子进程共享端口
  4. 信号处理:捕获系统信号触发重启
// 基本的主从进程示例
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);
  
  // 衍生工作进程
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何TCP连接
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);
  
  console.log(`工作进程 ${process.pid} 已启动`);
}

使用cluster模块实现

Node.js内置的cluster模块是实现零停机重启的核心工具。以下是详细实现步骤:

  1. 主进程管理

    • 启动时创建多个子进程
    • 监听子进程退出事件
    • 处理重启逻辑
  2. 子进程工作

    • 实际处理HTTP请求
    • 监听主进程信号
    • 实现优雅退出
const cluster = require('cluster');
const process = require('process');

if (cluster.isMaster) {
  // 启动初始工作进程
  const worker = cluster.fork();
  
  // 监听SIGHUP信号触发重启
  process.on('SIGHUP', () => {
    // 先创建新工作进程
    const newWorker = cluster.fork();
    
    newWorker.on('listening', () => {
      // 新进程就绪后,优雅关闭旧进程
      worker.send('shutdown');
    });
  });
} else {
  require('./app'); // 你的应用代码
  
  // 处理优雅关闭
  process.on('message', (msg) => {
    if (msg === 'shutdown') {
      server.close(() => {
        process.exit(0);
      });
    }
  });
}

使用PM2实现生产级方案

PM2是Node.js进程管理的流行工具,内置了零停机重启功能:

  1. 安装PM2

    npm install pm2 -g
    
  2. 启动应用

    pm2 start app.js -i max
    
  3. 零停机重启

    pm2 reload app
    

PM2的工作原理:

  • 启动新进程并等待它们就绪
  • 将流量切换到新进程
  • 优雅关闭旧进程

处理长连接和状态

零停机重启在处理长连接时需要特别注意:

  1. WebSocket连接

    • 通知客户端重新连接
    • 使用代理层保持连接
  2. 数据库连接

    • 确保新进程建立新连接
    • 旧进程完成当前事务后关闭
// WebSocket优雅关闭示例
wss.on('connection', (ws) => {
  connections.add(ws);
  
  ws.on('close', () => {
    connections.delete(ws);
  });
});

process.on('SIGTERM', () => {
  // 通知所有客户端
  for (const ws of connections) {
    ws.send(JSON.stringify({ type: 'SERVER_RESTART' }));
    ws.close();
  }
  
  // 等待所有连接关闭
  setTimeout(() => {
    server.close();
    process.exit(0);
  }, 5000);
});

信号处理和优雅退出

正确处理系统信号对零停机重启至关重要:

  1. SIGTERM:默认的终止信号
  2. SIGINT:Ctrl+C触发
  3. SIGHUP:常用于触发重启
// 完整的信号处理示例
const server = require('http').createServer();

server.listen(3000, () => {
  console.log('Server started');
});

const gracefulShutdown = () => {
  console.log('开始优雅关闭');
  
  // 停止接受新连接
  server.close(() => {
    console.log('所有连接已关闭');
    process.exit(0);
  });
  
  // 强制超时
  setTimeout(() => {
    console.error('强制关闭');
    process.exit(1);
  }, 5000);
};

process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);

负载均衡和健康检查

零停机重启需要与负载均衡器配合:

  1. 健康检查端点

    app.get('/health', (req, res) => {
      res.status(200).json({ status: 'healthy' });
    });
    
  2. 就绪检查

    let isReady = false;
    
    // 应用初始化完成后
    database.connect().then(() => {
      isReady = true;
    });
    
    app.get('/ready', (req, res) => {
      if (isReady) {
        res.status(200).end();
      } else {
        res.status(503).end();
      }
    });
    

常见问题和解决方案

  1. 内存泄漏

    • 定期重启可以缓解
    • 使用--max-old-space-size限制内存
  2. 僵尸进程

    • 确保子进程正确退出
    • 设置超时强制终止
  3. 请求丢失

    • 确保负载均衡器有重试机制
    • 使用连接排空技术
// 连接排空示例
let connections = [];

server.on('connection', (connection) => {
  connections.push(connection);
  connection.on('close', () => {
    connections = connections.filter(c => c !== connection);
  });
});

const shutdown = () => {
  console.log('开始排空连接');
  
  // 关闭新连接
  server.close(() => {
    console.log('停止接受新连接');
  });
  
  // 关闭现有连接
  connections.forEach(conn => {
    conn.end();
  });
  
  // 强制关闭剩余连接
  setTimeout(() => {
    connections.forEach(conn => {
      conn.destroy();
    });
    process.exit(0);
  }, 10000);
};

高级技巧:共享socket

对于极致性能要求的场景,可以使用共享socket技术:

const socket = require('net').Socket({ fd: 3 });

if (cluster.isMaster) {
  const server = require('net').createServer();
  server.listen(3000, () => {
    const worker = cluster.fork();
    worker.send('server', server);
  });
} else {
  process.on('message', (msg, handle) => {
    if (msg === 'server') {
      const server = handle;
      server.on('connection', (socket) => {
        // 处理连接
      });
    }
  });
}

监控和日志

完善的监控对零停机重启至关重要:

  1. 进程状态监控

    cluster.on('online', (worker) => {
      console.log(`Worker ${worker.process.pid} is online`);
    });
    
  2. 重启日志

    process.on('SIGHUP', () => {
      console.log('收到重启信号');
    });
    
  3. 性能指标

    setInterval(() => {
      const memoryUsage = process.memoryUsage();
      console.log('内存使用:', memoryUsage);
    }, 5000);
    

容器化环境中的考虑

在Docker/Kubernetes环境中,零停机重启需要额外配置:

  1. Docker健康检查

    HEALTHCHECK --interval=5s --timeout=3s \
      CMD curl -f http://localhost:3000/health || exit 1
    
  2. Kubernetes探针

    livenessProbe:
      httpGet:
        path: /health
        port: 3000
      initialDelaySeconds: 5
      periodSeconds: 5
    readinessProbe:
      httpGet:
        path: /ready
        port: 3000
      initialDelaySeconds: 5
      periodSeconds: 5
    

性能优化建议

  1. 预热新进程

    • 在接收流量前执行缓存加载
    • 预编译模板或代码
  2. 连接池管理

    • 确保新进程建立自己的连接池
    • 旧进程完成当前查询后释放连接
// 数据库连接池预热
const initialize = async () => {
  await warmupCache();
  await precompileTemplates();
  await testDatabaseConnection();
  isReady = true;
};

initialize().catch(err => {
  console.error('初始化失败:', err);
  process.exit(1);
});

测试策略

验证零停机重启是否有效的方法:

  1. 自动化测试

    const test = require('tape');
    const http = require('http');
    
    test('零停机重启测试', (t) => {
      // 模拟请求
      // 触发重启
      // 验证请求是否持续处理
      t.end();
    });
    
  2. 压力测试

    ab -n 1000 -c 100 http://localhost:3000/
    
  3. 手动验证

    • 持续发送请求同时执行重启
    • 监控错误率和响应时间

实际案例:Express应用

完整的Express应用零停机重启实现:

const express = require('express');
const cluster = require('cluster');
const os = require('os');

const app = express();
const port = 3000;

app.get('/', (req, res) => {
  // 模拟长时间处理
  setTimeout(() => {
    res.send(`处理完成 by 进程 ${process.pid}`);
  }, 1000);
});

if (cluster.isMaster) {
  const cpuCount = os.cpus().length;
  
  for (let i = 0; i < cpuCount; i++) {
    cluster.fork();
  }
  
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} 退出`);
    cluster.fork();
  });
  
  // 模拟定期重启
  setInterval(() => {
    const workers = Object.values(cluster.workers);
    const oldWorker = workers[0];
    
    if (oldWorker) {
      const newWorker = cluster.fork();
      
      newWorker.on('listening', () => {
        oldWorker.send('shutdown');
      });
    }
  }, 10000);
} else {
  const server = app.listen(port, () => {
    console.log(`应用运行在 http://localhost:${port} PID: ${process.pid}`);
  });
  
  process.on('message', (msg) => {
    if (msg === 'shutdown') {
      server.close(() => {
        process.exit(0);
      });
    }
  });
}

多实例部署策略

大规模部署时的考虑因素:

  1. 滚动更新

    • 分批重启实例
    • 确保最小可用实例数
  2. 蓝绿部署

    • 准备完整的新环境
    • 切换流量
  3. 金丝雀发布

    • 先重启少量实例
    • 验证后全面部署
// 分批重启逻辑
const restartInBatches = (workers, batchSize = 1) => {
  const workerList = Object.values(workers);
  
  const restartNextBatch = () => {
    const batch = workerList.splice(0, batchSize);
    
    batch.forEach(worker => {
      const newWorker = cluster.fork();
      
      newWorker.on('listening', () => {
        worker.send('shutdown');
      });
    });
    
    if (workerList.length > 0) {
      setTimeout(restartNextBatch, 5000);
    }
  };
  
  restartNextBatch();
};

配置热更新

除了代码更新,配置热更新也是常见需求:

// 配置热加载示例
let config = require('./config');

process.on('SIGHUP', () => {
  // 清除require缓存
  delete require.cache[require.resolve('./config')];
  
  try {
    const newConfig = require('./config');
    config = newConfig;
    console.log('配置已更新');
  } catch (err) {
    console.error('配置更新失败:', err);
  }
});

错误处理和回滚

确保重启失败时能够回滚:

  1. 版本验证

    const version = require('./package.json').version;
    app.get('/version', (req, res) => {
      res.send(version);
    });
    
  2. 自动回滚

    let currentWorker = cluster.fork();
    
    currentWorker.on('exit', (code) => {
      if (code !== 0) {
        console.error('新工作进程启动失败,回滚到旧版本');
        // 触发回滚逻辑
      }
    });
    

性能基准测试

测量零停机重启的影响:

  1. 请求中断率

    • 目标:<0.1%
  2. 重启时间

    • 从触发到完全就绪
  3. 资源使用

    • 内存峰值
    • CPU使用率
// 性能测量示例
const start = Date.now();
let completedRequests = 0;
let failedRequests = 0;

setInterval(() => {
  http.get('http://localhost:3000', (res) => {
    completedRequests++;
  }).on('error', () => {
    failedRequests++;
  });
}, 10);

process.on('SIGTERM', () => {
  const duration = (Date.now() - start) / 1000;
  console.log(`测试结果:
    持续时间: ${duration}s
    完成请求: ${completedRequests}
    失败请求: ${failedRequests}
    成功率: ${(completedRequests/(completedRequests+failedRequests)*100).toFixed(2)}%`);
});

上一篇: 性能与扩展性

下一篇: 进程监控

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步