Node.js 行程控制:掌握系統訊號與優雅停機實戰

當我們在生產環境中重新部署應用程式,或是容器 (Container) 需要重啟時,系統(如 Kubernetes 或 Docker)會向 Node.js 行程發送訊號。如果我們不予理會,Node.js 可能會被瞬間強制殺死。

這會導致:

  1. 資料損毀:正在寫入資料庫或磁碟的操作被腰斬。
  2. 請求中斷:正在處理到一半的使用者請求直接連線斷開。

這篇文章將帶你了解如何處理系統訊號,實作優雅停機 (Graceful Shutdown)

認識常見的系統訊號

Node.js 的 process 物件可以監聽 OS 發出的訊號:

  • SIGINT:當你在終端機按下 Ctrl + C 時發出。
  • SIGTERM:系統管理工具(如 Docker stop)要求程式停止的正式訊號,這是最常用的停機觸發點。

實務開發:實作優雅停機流程

一個完整且優雅的停機流程應包含以下步驟:

  1. 接收到停機訊號。
  2. 停止接收新的請求(讓 Load Balancer 知道你已準備離線)。
  3. 等待所有進行中的請求處理完畢。
  4. 關閉資料庫與快取連線。
  5. 正式退出行程。

示範程式碼

const http = require('http');

const server = http.createServer((req, res) => {
  // 模擬耗時的業務邏輯
  setTimeout(() => {
    res.end('處理完成');
  }, 5000);
});

server.listen(3000, () => {
  console.log('伺服器運行中,PID:', process.pid);
});

// 監聽 SIGTERM 訊號
process.on('SIGTERM', () => {
  console.info('收到 SIGTERM 訊號,準備開始優雅停機...');

  // 1. 停止接收新連線
  server.close(() => {
    console.log('HTTP 伺服器已關閉');

    // 2. 這裡可以放置關閉資料庫連線的邏輯
    // await db.close();

    console.log('所有資源已清理完畢,安全離線。');
    process.exit(0); // 0 代表正常退出
  });

  // 備用方案:如果 30 秒後還沒關掉,強制退出,防止程式卡死
  setTimeout(() => {
    console.error('停機超時,強制關閉');
    process.exit(1);
  }, 30000);
});

處理未預期的崩潰:Exception 與 Rejection

除了主動停機,我們也應監控程式碼中的致命錯誤,確保它們被妥善記錄後才離開。

// 處理未捕獲的 Promise 錯誤
process.on('unhandledRejection', (reason, promise) => {
  console.error('偵測到未處理的 Rejection:', reason);
  // 建議:發送警告到日誌系統並重啟
});

// 處理未捕獲的異常同步錯誤 (Last resort)
process.on('uncaughtException', (err) => {
  console.error('發生嚴重錯誤 (Uncaught Exception):', err);
  // 注意:發生此錯誤時,行程的狀態可能已經混亂,必須強制重啟
  process.exit(1);
});

為什麼要加上強制退出超時?

在生產環境中,有時資料庫連線可能會因為某些網路原因卡住而無法成功關閉。如果我們只寫了 server.close() 但它永遠不回呼,行程就會變成一個「殭屍行程」,佔用記憶體。因此,一定要設定一個強制退出的 Timer

總結

  1. SIGTERM 是後端工程師最需要重視的訊號。
  2. 優雅停機 能保證資料一致性並提升用戶體驗。
  3. 透過 process.on 監聽各種類型的事件,讓你的應用程式在運維層面更加穩健。