速報

Brotli脆弱性はPythonにも影響|Requestsの自動解凍と依存パッケージを点検

CATEGORY

Node.jsを安全に止める|SIGTERMで処理中のHTTP応答を待つ実装

※本ページのリンクにはプロモーションが含まれています。

新しい要求を停止し処理中のHTTP応答を期限内に完了させてからサーバーを終了する概念イラスト

目次

デプロイのたびにAPIの応答が途切れる場合、アプリを起動する手順だけでなく、古いプロセスを止める手順も見直す必要があります。終了シグナルを受けて即座にプロセスを落とすと、受け付け済みのリクエストまで途中で切断されます。一方、処理が終わるまで無期限に待つと、新しい版への切り替えが進まなくなります。

この記事は2026年10月24日にNode.jsの公式資料を確認して作成した実装解説です。新機能のニュースではありません。Linuxなどの非Windows環境で動く通常のHTTP/1.1サーバーを例に、受付停止、処理中の応答の完了待ち、期限超過時の打ち切りを分けて実装します。掲載コードの動作確認はNode.js v24.19.0のローカル環境で行いました。

スポンサーリンク

終了を一つの瞬間ではなく三つの段階として考える

終了処理では、まず新しい仕事を受け付けない状態にし、次に受け付け済みの仕事を待ち、最後に接続やプールなどの資源を閉じます。この順序を逆にして先にDB接続を閉じると、まだ動いているHTTP処理がDBへアクセスできなくなるおそれがあります。終了通知を受けた時刻と、業務処理が完了した時刻は別の記録にします。

Node.js公式のSignal eventsでは、非Windows環境のSIGTERM・SIGINTにリスナーを登録すると、通常の終了動作が取り除かれると説明されています。ハンドラーを書いた時点で「シグナルが来れば自動で終わる」という前提は使えません。自分のコードで終了条件を満たし、必要なハンドルを閉じる責任が生じます。

server.closeと強制切断の役割を分ける

Node.js公式のHTTPサーバー終了APIによると、server.close()は新規接続の受付を止め、処理中のリクエストや応答がない接続を閉じます。Node.js 19以降ではアイドル接続もこの呼び出しで閉じる仕様です。今回の例ではこの動作を使い、既存のHTTP応答を待ちます。

対してserver.closeAllConnections()は処理中のHTTP接続も切断する強い操作です。最初から呼ぶと、正常に完了できたリクエストまで落とす可能性があります。ここではserver.closeを先に呼び、猶予時間を超えた場合だけ使用します。WebSocketなどへアップグレード済みの接続はこのAPIの対象外なので、別の終了管理が必要です。

スポンサーリンク

動作を確かめる最小のHTTPサーバー

次のコードをserver.mjsとして保存します。通常のパスは2秒後に応答し、/readyはすぐに応答します。終了状態へ入った後にハンドラーへ到達する要求には503を返しますが、待受を閉じた後の新規接続は接続エラーになる場合があります。「すべての新規要求へ必ず503を返せる」実装ではありません。

import http from 'node:http';

let stopping = false;
const server = http.createServer((req, res) => {
  if (stopping) {
    res.writeHead(503, { Connection: 'close' });
    res.end('shutting down\n');
    return;
  }
  if (req.url === '/ready') {
    res.end('ready\n');
    return;
  }
  const timer = setTimeout(() => res.end('done\n'), 2000);
  res.once('close', () => clearTimeout(timer));
  console.log('request started');
});

function shutdown(signal) {
  if (stopping) return;
  stopping = true;
  console.log(`draining: ${signal}`);
  const deadline = setTimeout(() => {
    console.error('shutdown deadline exceeded');
    server.closeAllConnections();
    process.exit(1);
  }, 10000);
  deadline.unref();

  server.close((error) => {
    clearTimeout(deadline);
    process.exitCode = error ? 1 : 0;
    console.log('http closed');
  });
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
server.listen(3000, '127.0.0.1', () => console.log('listening'));

この例はローカル確認のため127.0.0.1で待ち受けます。コンテナへ配置する場合は、その環境で必要な待受アドレスへ変更してください。2秒と10秒は説明用の設定値です。実際にはリクエスト処理の上限、外部APIの期限、デプロイ基盤がプロセスを強制終了するまでの猶予を合わせて決めます。

stoppingで終了処理の重複を防ぐ

SIGTERMとSIGINTの両方が届いても、stoppingがtrueなら同じ終了処理を開始しません。終了タイマーが複数できたり、プールのclose処理を重ねて呼んだりすることを避けるためです。シグナルの登録を一回限りにするだけでは、別のシグナルによる二重開始まで防げるとは限りません。

通常の終了経路ではprocess.exitCodeを設定し、HTTPサーバーの接続が閉じた後、イベントループが空になって終了する形にしています。この最小例にはDBプールも定期タイマーもありません。本番アプリへ組み込むと、それらがプロセスを生かし続けることがあるため、http closedというログだけで全体が終わったとは判断しないでください。

期限タイマーのunref()は、そのタイマーだけを理由にプロセスを生かし続けないためのものです。処理を早く打ち切る指定ではありません。HTTP接続などが残っていればイベントループは動き続け、期限に達すると打ち切り側が動きます。

スポンサーリンク

ローカルで処理中の応答が完了するか確認する

一つ目の端末でサーバーを起動し、別の端末から遅い応答を開始します。PIDには対象サーバーのプロセスIDを指定してください。別の業務プロセスへシグナルを送らないよう、起動したコマンドとPIDを先に照合します。

node server.mjs

# 別の端末
curl -v http://127.0.0.1:3000/slow

# サーバー側で request started を確認してから実行
kill -TERM <対象サーバーのPID>

本記事のローカル確認では、要求開始をログで検知してからSIGTERMを送りました。クライアントがdoneを受け取り、その後プロセスが終了コード0で終了することを確認しています。これは単一プロセスと一つのHTTP要求での結果であり、ロードバランサーを通る本番通信の無停止性を証明する試験ではありません。

終了状態への移行前に始まった要求が完了することと、移行後に新しい業務処理を増やさないことを別々に確認します。終了ログが出ただけでは前者は分かりません。クライアント側の応答本文とステータス、サーバーの終了コードも記録してください。

期限超過を成功した終了として数えない

10秒を超えた場合、この例は接続を強制的に閉じ、終了コード1で終了します。途中の処理が中断された可能性を、通常完了と区別するためです。確認時には応答待ちを30秒へ変更した別プロセスを使い、SIGTERM後に期限超過のログと終了コード1になることを確認しました。掲載する通常版の応答待ちは2秒のままです。

打ち切り側のprocess.exit(1)は即時終了なので、非同期の後片付けやログ転送がすべて完了する保証はありません。これは期限を超えた場合の最終手段です。処理が途中でも外部サービス側では更新が終わっている可能性があるため、クライアントの再試行や更新APIの冪等性は別に設計します。

DBプールとジョブを持つアプリへ組み込むとき

本番では、HTTP受付停止と同時に新規ジョブの取り込みや定期処理の起動も止めます。その後、進行中のHTTP処理とジョブを待ち、利用中のDB処理がなくなってからプールを閉じます。具体的な終了APIは利用しているドライバーやジョブ基盤の公式手順に従ってください。

HTTPの終了コールバックは、切り離して起動したPromiseや外部キューの処理完了を表すものではありません。応答を返した後に実行する処理があるなら、その処理数や完了Promiseを明示的に追跡します。DBの後片付けを待つ設計へ拡張する場合は、その完了前に全体の期限タイマーを解除しないようにします。

WebSocketや長時間のストリーム応答では、通常の短いHTTP要求と同じ待ち方が適さない場合があります。再接続通知、メッセージ処理の完了境界、接続を閉じる期限を別途決めます。今回の最小コードをそのまま貼って、すべての通信方式に対応済みと扱わないことが大切です。

デプロイ基盤との間に残る確認

実運用では、プロセスへシグナルが届くことを起動コマンドまで含めて確認します。シェルやプロセス管理ツールを挟む構成では、想定するNode.jsプロセスへ通知が伝わるかが確認点です。また、アプリの猶予時間より先に基盤が強制終了すると、コード側で待つ設計をしても完了できません。

ロードバランサーから対象を外す時間も考慮します。この例の/readyは動作確認用の最小エンドポイントで、配信先から外れるまでの伝播待ちを実装していません。本番ではヘルスチェックの間隔や接続の排出手順を確認し、必要なら業務受付を止めた状態で待つ段階を組み込みます。

  • 遅いHTTP要求の途中で終了通知を送り、応答の完了と終了コードを確認する
  • 長すぎる処理で期限超過が記録され、通常終了と区別されることを確認する
  • 複数の終了通知でも後片付けが重複しないことを確認する
  • DB、ジョブ、定期処理、長時間接続の終了条件を列挙する
  • 実際の起動方式と配信経路を使い、切り替え中のエラーを観測する

安全な終了処理は、単に待ち時間を長くする実装ではありません。どこから新しい仕事を止め、どの仕事まで待ち、どの時点で打ち切ったかを説明できる状態にすることが目的です。まずHTTPだけの小さい例で動きをつかみ、実アプリの資源と配信基盤に合わせて終了条件を増やしてください。

スポンサーリンク

参考資料

この記事を書いた人

Ryohei

Webエンジニア / アプリ開発者 / 技術ブログ運営者

福岡のWeb制作会社に勤務するWebエンジニアです。10年以上にわたりWeb制作・開発に携わり、ヒアリングや進行管理から、コーディング、CMSの構築、システム開発、サーバーの構築・保守・運用まで、Webサイト制作の工程を幅広く担当しています。Webサイトは、これまでに100以上の構築・保守を手がけてきました。

個人でもWebサイトの制作・改修やWebアプリの開発・運用を行い、AIを活用した開発にも取り組んでいます。近年はWebセキュリティにも注力しています。

2015年から技術メディア「Into the Program」を運営し、実務や開発で得た知識をもとに、プログラミングの実装方法やWebサイトの運用・セキュリティ情報を発信しています。

主な分野
Webサイト構築・保守・運用 / HTML/CSS / JavaScript / PHP / WordPress / サーバー構築・保守・運用 / アプリ開発 / セキュリティ対策
資格・試験合格
情報セキュリティマネジメント試験
基本情報技術者試験
AWS認定ソリューションアーキテクト アソシエイト
PHP8技術者認定ウィザード
メディア運営
2015年〜

記事の作成・更新について

セキュリティ記事は、ベンダーの公式アドバイザリやCVE・JVNなどの一次情報を参照し、出典と情報確認日を記載します。実環境での検証を行った場合は、その環境と確認範囲を明示します。重要な追加情報や誤りを把握した場合は、内容を確認して追記・訂正します。

関連する記事

おすすめの書籍

キーワードから探す

スポンサーリンク