「接続を切る」という残酷な作法:ロードバランサーのコネクションドレインを極める
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に入り、ターゲットに届き、処理されて返るまでの全工程を想像できたとき、あなたのインフラは真に堅牢なものになります。
次回の運用改善では、ぜひこの「撤退戦の作法」を意識してみてください。現場からは以上です。
コメント