NATゲートウェイの「ポート枯渇」という悪夢:大規模トラフィックを捌くための防衛術
「なぜか一部のAPIリクエストだけが確率的にタイムアウトする」「負荷試験では問題なかったのに、スパイク時にだけ502/504が頻発する」……。
クラウドエンジニアであれば、一度は経験するこの「不可解なパケットロス」。その犯人の多くは、インフラの縁の下の力持ちである「NATゲートウェイ」のポート枯渇にあります。今日は、華やかなWeb API設計の裏側で起きている、シビアなTCPポートの生存競争について、現場の知見を交えて解説しましょう。
—
1. なぜ「65,535」という壁にぶつかるのか
まず、基本に立ち返りましょう。TCP通信は 送信元IP:送信元ポート と 送信先IP:送信先ポート の4つの組み合わせ(4タプル)で一意に識別されます。
プライベートサブネット内のインスタンスやPodがインターネットへ出る際、NATゲートウェイはプライベートIPを自身のグローバルIPに変換(SNAT)します。このとき、NATゲートウェイは「どの通信がどこからのものか」を管理するために、送信元ポート番号を書き換えます。
ここで立ちはだかるのが、ポート番号の理論上の上限である 65,535 です。
誤解されがちな「同時接続数」の正体
「NATゲートウェイ1つにつき65,535までしか繋げないのか?」と聞かれることがありますが、厳密には違います。NATゲートウェイは 送信先IP と 送信先ポート が異なれば、同じ送信元ポートを再利用できます。しかし、「特定の送信先サーバー(例: 外部API)」に対して大量の同時リクエストを送る場合、実質的にその上限がボトルネックとなります。
特に、Keep-Alive が無効なHTTP通信を大量に行うと、接続のたびに新しいポートが消費され、TIME_WAIT 状態のソケットがポートを占有し続けます。これが「ポート枯渇」の正体です。
—
2. 現場での検知手法:メトリクスとログから追い詰める
この問題の厄介なところは、NATゲートウェイ自体がエラーログを吐くわけではなく、ただ黙々とパケットをドロップし続ける点です。
AWS CloudWatch Metricsで異常を察知する
AWSを利用している場合、以下のメトリクスを監視するのが鉄則です。
ErrorPortAllocation: これがゼロより大きくなったら、即座にアラートを鳴らしてください。ポートの割り当てに失敗した回数を示します。IdleTimeoutCount: アイドルタイムアウトによるコネクション切断数。これも間接的なヒントになります。
パケットを捕まえるデバッグコマンド
コンテナ内で接続が詰まっている際、netstat や ss コマンドでソケットの状態を確認しましょう。
# TIME_WAIT状態のソケットが異常に溜まっていないか確認
ss -tan | grep TIME-WAIT | wc -l
# 特定の外部エンドポイントへの接続数を確認
ss -tan | grep <外部APIのIPアドレス>:443 | wc -l
—
3. 実践:コードと設計でポートを解放する
インフラ側で NATゲートウェイを増やす(サブネットを分割してNATを分散させる)のも一つの手ですが、まずは「使い捨てのポートを減らす」コードの最適化が先決です。
Python (requests) での接続再利用
デフォルトの設定では、リクエストのたびに新しい接続が作られる可能性があります。Session オブジェクトを使い、TCPコネクションを維持しましょう。
import requests
# Sessionオブジェクトを使うことでTCPコネクションをプーリング(再利用)する
session = requests.Session()
# HTTP Adapterでプールのサイズを調整するのも有効
adapter = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=50)
session.mount("https://", adapter)
# これにより、同じ送信先に対してポートを使い回せる
response = session.get("https://api.external-service.com/data")
Node.js (axios / http.agent) の設定
Node.jsでは keepAlive: true を明示しないと、高負荷時にポートが枯渇します。
const http = require('http');
const https = require('https');
// Keep-Aliveを有効にしたエージェントを作成
const keepAliveAgent = new https.Agent({
keepAlive: true,
maxSockets: 100 // 同時接続数を制限し、ポート消費を抑制する
});
axios.get('https://api.external-service.com', { httpAgent: keepAliveAgent });
—
4. アーキテクトからのアドバイス
ポート枯渇を根本的に解決する設計上のヒントを3つ残しておきます。
1. VPCエンドポイントを活用せよ: AWSのS3やDynamoDBへの通信であれば、NATゲートウェイを通さない「ゲートウェイ型VPCエンドポイント」を使いましょう。これでポート消費を劇的に減らせます。
2. NATゲートウェイの分散: どうしても回避できない大規模通信がある場合、サブネットをAZごとに分け、AZごとにNATゲートウェイを配置する「AZ分散」を行い、利用するIPアドレスを増やしてください。
3. プロキシの導入: 外部APIへの通信が多い場合、間に Envoy などのプロキシを挟み、接続をプーリングさせる構成が、大規模トラフィック下での鉄板構成です。
ポート枯渇は「見えない障害」です。しかし、ネットワークの基礎を理解していれば、必ず観測可能であり、制御可能です。皆さんのインフラが、スパイクに負けない強靭なものになることを願っています。
それでは、また次回の深掘りでお会いしましょう。
コメント