【AWS/GCP】その「TIME_WAIT」がSNATポート枯渇を招く!NATゲートウェイの裏側とポート再利用制限の罠
こんにちは、シニアSREの私です。
プロダクション環境で突然、外部APIへのリクエストがタイムアウトし始めた――。モニタリング画面を見ると、CPUやメモリは平穏無事なのに、なぜかエラーレートだけが跳ね上がっている。そんな夜を経験したことはありませんか?
原因を深掘りしていくと、大抵たどり着くのが「SNATポートの枯渇」です。そして、その枯渇を引き起こす主犯格こそが、パブリッククラウドのマネージドNATゲートウェイ(AWSのNAT GatewayやGCPのCloud NAT)が抱える、TCPの TIME_WAIT ステートとポートの再利用制限にほかにありません。
教科書には「TCPの接続終了時には TIME_WAIT が120秒間保持されます」とサラリと書いてありますが、これがクラウドの巨大なスケールと組み合わさったとき、現場のインフラエンジニアにとってどれほど凶悪な牙をむくか。今回は、パケットの挙動からコードレベルの対策まで、泥臭い実務の知見を交えて徹底的に解説します。
—
1. パケットはこう流れる:NATゲートウェイとSNATポートの宿命
まずは、プライベートサブネットにいるアプリケーションサーバー(コンテナ)から、外部のSaaSやWeb APIへリクエストが飛ぶときのネットワークの「姿」をイメージしてください。
プライベートIPアドレスしか持たないKubernetesのPodやEC2インスタンスが、インターネット上の外部APIと通信するためには、パケットの送信元IPアドレスをNATゲートウェイのパブリックIPアドレスに書き換えるSNAT(Source Network Address Translation)という処理が必要です。
接続の識別と「ポート」という有限資源
OSのネットワークスタックやNATゲートウェイは、TCPの接続を以下の「5タプル(5-tuple)」で一意に識別しています。
1. 送信元IPアドレス
2. 送信元ポート番号
3. 宛先IPアドレス
4. 宛先ポート番号
5. プロトコル(TCP)
AWSのNAT GatewayやGCPのCloud NATでは、一つのパブリックIPアドレスにつき、最大で約64,000個($65,535 – 予約ポート$)のポートが利用可能です。つまり、「同じ宛先IP・宛先ポート」に対して同時に確立できるTCPコネクションの上限は、理屈の上でも約64,000本が限界になります。
—
2. 悪夢の始まり:なぜ TIME_WAIT がSNATポートを食いつぶすのか?
ここでTCPの基本に立ち返りましょう。TCPコネクションを切断する際、最後にパケットを送信した側(あるいは両者)は、ネットワーク上に迷子になった古いパケット(ロストしたパケットの残骸など)が後から届いて新しいコネクションを汚染するのを防ぐため、TIME_WAIT というステートに遷移します。
標準的なRFC 793では、この TIME_WAIT の期間は最大セグメントライフタイム(MSL)の2倍である240秒と定義されており、多くのOSやクラウドのネットワーク機器では120秒に設定されています。
クラウド環境における致命的な非対称性
ここで問題になるのが、プライベートサブネット側のクライアントと、NATゲートウェイの「見え方の違い」です。
- アプリケーション(クライアント)側:
SO_REUSEADDRなどのソケットオプションや、Linuxカーネルのチューニング(net.ipv4.tcp_tw_reuseなど)により、比較的素早くポートを再利用できます。 - NATゲートウェイ側: マネージドサービスであるため、ユーザーが直接カーネルパラメータ(
sysctl)をいじってTIME_WAITのタイマーを短縮することはできません。NATゲートウェイは、配下の大量のクライアントから来るバラバラのコネクションをさばきながら、RFCに忠実に、一定時間(通常120秒前後)そのポートの「クールダウン期間」を強制します。
短時間に数千・数万のリクエストを外部APIに投げ捨てるようなバーストフルなアーキテクチャ(例えば、大規模なバッチ処理や、頻繁に外部SaaSを叩くマイクロサービス)では、次のような悪循環が発生します。
1. アプリが毎秒1,000件のリクエストを発火する。
2. コネクションが瞬時に確立・切断され、NATゲートウェイ上で TIME_WAIT のポートが山のように積み上がる。
3. 120秒間は同じポートの組み合わせが再利用できないため、約12万本分のポート容量が必要になる計算になり、あっという間にSNATポートが枯渇する。
4. 新規のTCPハンドシェイク(SYNパケット)がNATゲートウェイでドロップされ、アプリケーション側で Connection timed out が連発する。
—
3. 実務で直面するアンチパターンとトラブルシューティング
現場でよく見かけるのが、「HTTP/1.1のデフォルト挙動をナメていた」というケースです。
やりがちな失敗:コネクションの都度破棄(Short-lived Connections)
例えば、Pythonの requests や Node.jsの fetch を使って、何の考えなしにループ内でリクエストを投げているコードを見てみましょう。
import requests
# 【アンチパターンの例】毎回セッションを作らずにリクエストを投げる
def call_external_api_bad(target_url, payload):
for i in range(1000):
# ここで毎回 TCP 3-way handshake が行われ、直後に FIN -> TIME_WAIT へ突入する
response = requests.post(target_url, json=payload)
print(response.status_code)
このコードを実行すると、requests.post はデフォルトで使い捨てのセッション(HTTP接続)を作成するため、リクエストが完了するたびに FIN パケットが飛び、コネクションが切断されます。結果として、NATゲートウェイ側には膨大な数の TIME_WAIT コネクションが残り、数分も経たずにポートが枯渇します。
—
4. 解決へのアプローチ:コードとインフラの改善策
この問題に立ち向かうには、アプリケーション層での「コネクションの維持(Keep-Alive)」と、インフラ層での適切な負荷分散が不可欠です。
対策A:HTTP Keep-Alive(コネクションプーリング)の徹底
最も効果的かつ根本的な対策は、TCPコネクションを毎回切断せず、使い回す(Keep-Alive)ことです。これにより、3-way handshakeのオーバーヘッドが消え、TIME_WAIT の発生頻度を劇的に減らすことができます。
Python (requests.Session) の実装例
import requests
def call_external_api_good(target_url, payloads):
# Session オブジェクトを使用することで、裏側で HTTP Keep-Alive が有効になり
# 同一ホストへの TCP コネクションが維持・再利用されます。
with requests.Session() as session:
for payload in payloads:
response = session.post(target_url, json=payload)
print(response.status_code)
Node.js (fetch / http.Agent) の設定例
Node.jsの標準 fetch や http.Agent を使う場合も、keep-aliveの設定が有効になっているかを確認してください。特に古いバージョンや特定のライブラリでは、デフォルトでコネクションが閉じる設定になっていることがあるため注意が必要です。
const http = require('http');
// Keep-Alive を有効にしたカスタムエージェントを作成
const agent = new http.Agent({
keepAlive: true,
maxSockets: 100,
keepAliveMsecs: 1000
});
// リクエスト時にエージェントを渡す
// (※ 実際のフレームワークやライブラリの仕様に合わせて実装してください)
—
対策B:DNSラウンドロビンや複数NAT Gatewayによるスケールアウト
アプリケーション側の改修が難しい場合や、どうしてもトラフィック量が単一のNATゲートウェイの限界を超える場合は、インフラストラクチャ側でのスケールアウトが必要です。
- AWSの場合: 複数のアベイラビリティゾーン(AZ)にそれぞれNAT Gatewayを配置し、プライベートサブネットのルートテーブルを適切に分割・分散させることで、SNATポートの総数を物理的に増加させます。
- GCPの場合: Cloud NATの機能である「動的なポート割り当て(Dynamic port allocation)」を有効にし、トラフィック量に応じて自動的にパブリックIPやポート数がスケールするように設定します。
—
5. デバッグ手法:今、何が起きているかを暴く
もしあなたが今まさに障害対応の真っ最中であれば、以下の手順でNATやソケットの状態を確認してください。
1. アプリケーションコンテナ内でのソケット確認
Kubernetes上のPodであれば、エフェメラルコンテナなどから ss コマンドや netstat を叩いて、ローカル側のステートを確認します。
# 自身のコンテナから、TIME_WAIT の状態にあるソケット数をカウントする
ss -s
2. クラウドメトリクスによるSNATポート使用率の監視
- AWS: CloudWatchの
ConnsToPortAllocationやBytesOutToDestination、特にSNATPortAllocationErrorsのメトリクスを監視します。この値が0より大きくなった瞬間、ポート枯渇が発生しています。 - GCP: Cloud Monitoringの
router/nat/port_utilizationを確認し、使用率が80%を超えている場合はアラートを飛ばすように設定しておきましょう。
—
まとめ
パブリッククラウドのNATゲートウェイは、私たちが意識せずともインターネットへの扉を開いてくれる非常に便利な黒魔術です。しかし、その裏側にあるTCPの物理法則――特に TIME_WAIT によるポートの占有制限――を無視して大量のトラフィックを流し込むと、ある日突然システム全体が沈黙するというしっぺ返しを食らいます。
「アプリケーションが短命なコネクションを大量生産していないか?」
「HTTP Keep-Aliveは正しく機能しているか?」
インフラのメトリクスを眺めるだけでなく、コードを書く段階からネットワークの「戻りのパケット」や「コネクションの寿命」に思いを馳せることが、真にレジリエントなシステムを作り上げるシニアエンジニアの流儀です。
それでは、また次回の現場でお会いしましょう。
コメント