【実務・中級編】 コネクションドレイン(Connection Draining / 登録解除の遅延 Deregistration Delay) – クラウド&コンテナネットワーク実践ガイド

「接続を切る」という残酷な作法:ロードバランサーのコネクションドレインを極める

SREとして現場を歩いていると、「デプロイのたびに5xxエラーが数件発生する」という相談をよく受けます。原因を掘り下げると、その多くがロードバランサー(ALB/NLB)の「コネクションドレイン(Deregistration Delay)」に対する理解不足に帰結します。

クラウドインフラにおいて、ターゲット(EC2やコンテナ)を切り離す際、単に「止める」のは素人仕事です。インフライト(処理中)のパケットをどう優雅に幕引きさせるか。今回は、この「安全な撤退戦」の極意を解説します。

—

1. なぜ「即座の切断」が罪なのか

ロードバランサーは、ターゲットが Deregister された瞬間、そのターゲットへの新規リクエストのルーティングを停止します。しかし、既に通信を開始しているコネクション(インフライトリクエスト)をどう扱うかが問題です。

もしLBが強制的にコネクションをリセット(TCP RST)すると、クライアント側は「予期せぬ接続断」に直面し、ブラウザなら「接続がリセットされました」、APIクライアントなら ECONNRESET を受け取ることになります。

これを防ぐための猶予期間、それが Deregistration Delay(AWS)や Connection Draining です。

通信フローの裏側

1. LBがDeregisterを検知: ターゲットグループから対象を除外。
2. 猶予期間開始: 設定された時間(デフォルト300秒など)の間、既存コネクションを通し続ける。
3. 猶予期間終了: まだ残っているコネクションを強制終了。

—

2. 実務で直面する「落とし穴」

多くの現場でありがちなミスが、「アプリ側のタイムアウト設定」と「LBのドレイン設定」の不整合です。

  • LBのドレイン時間 > アプリの最大処理時間: 理想的。全リクエストが正常完了する。
  • LBのドレイン時間 < アプリの最大処理時間: 最悪。処理の途中でLBが接続をぶった切る。

特に、Keep-Alive を維持した HTTP/1.1 や gRPC のような長寿命コネクションを使っている場合、LBはコネクションを「維持」しようとするため、この設定が効いていないと「古いPodにリクエストが刺さり続ける」という怪奇現象が発生します。

—

3. 実践:設定値の最適化とチューニング

AWS ALBであれば、ターゲットグループの属性で設定可能です。また、Kubernetes(AWS Load Balancer Controller)環境では、アノテーションでこれを制御します。

Kubernetes (Ingress) での設定例

metadata:
  annotations:
    # ターゲットグループの登録解除遅延を60秒に設定
    # アプリの最長レスポンス時間より少し長めに設定するのがコツ
    alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=60

なぜ60秒なのか?

闇雲に長くすれば良いというものではありません。長すぎればデプロイのたびに無駄な待機時間が発生し、デプロイ速度(MTTD)が落ちます。
「アプリがリクエストを処理する最大予測時間 + α(パケット再送の余地)」を計測し、それに合わせるのがプロの仕事です。

—

4. デバッグと検証のためのコードスニペット

実際にシステムがドレインを正しく処理しているか、curl で挙動をトレースしてみましょう。Connection: keep-alive を明示してテストするのがポイントです。

# ターゲットに対して長めのリクエストを投げつつ、
# 別ターミナルでデプロイ(Podの終了)をトリガーする
curl -v -H "Connection: keep-alive" http://your-alb-dns.com/api/slow-task

もし、サーバー側でシグナル(SIGTERM)を受けてから即座にプロセスを終了させているなら、curl は Empty reply from server や Connection reset by peer を吐くはずです。

対策:アプリ側での「Graceful Shutdown」

ロードバランサーのドレインを活かすには、アプリケーション側が SIGTERM を受け取った後、新しい接続を拒否しつつ、既存の処理が終わるまで待つ実装が不可欠です。

Node.js (Express) の例:

const server = app.listen(3000);

process.on('SIGTERM', () => {
  console.log('SIGTERM受信:Graceful Shutdown開始');
  
  // 1. 新規受付を停止
  server.close(() => {
    console.log('既存のリクエスト完了、プロセス終了');
    process.exit(0);
  });
  
  // 2. タイムアウトを設定して強制終了の備えもしておく
  setTimeout(() => {
    console.error('強制終了');
    process.exit(1);
  }, 55000); // ALBの60秒より少し短く設定
});

—

SREからの提言

「設定して終わり」ではありません。リリース時には必ず、「トラフィックがある状態でターゲットをデタッチし、エラーレートが跳ねないか」をモニタリングしてください。

もしドレイン設定を正しく行っても 5xx が出るなら、それは LB 側の問題ではなく、クライアント側の再試行(リトライ)戦略や、バックエンドのコネクションプーリング設定(keepAliveTimeout が ALB のアイドルタイムアウトより長い等)を見直すサインです。

ネットワークは「点」ではなく「線」です。リクエストがLBに入り、ターゲットに届き、処理されて返るまでの全工程を想像できたとき、あなたのインフラは真に堅牢なものになります。

次回の運用改善では、ぜひこの「撤退戦の作法」を意識してみてください。現場からは以上です。

コメント

タイトルとURLをコピーしました