【実務・中級編】 NATゲートウェイにおける最大接続数とポート一意性の維持アルゴリズム – クラウド&コンテナネットワーク実践ガイド

こんにちは、SREチームのシニアアーキテクトです。

夜中に突然PagerDutyが鳴り響き、「外部の決済APIへ接続するマイクロサービスから Connection Timeout が頻発している!」というアラートを受けたことはないでしょうか。ダッシュボードを開くと、CPUやメモリは平穏そのもの。しかし、VPC Flow Logsを漁ってみると、そこにはパブリックサブネットのNAT Gateway(以下、NATGW)が限界を迎え、静かに悲鳴を上げている絶望的な光景が広がっていたりします。

クラウドやKubernetesのネットワークを語る上で、プライベートサブネットから外の世界へ出るための「門番」であるNATGWの存在は避けて通れません。しかし、その内部でいかに激しいポートの奪い合いが行われているか、正確にイメージできているでしょうか。

今回は、NATGWの心臓部である「最大接続数」と「ポート一意性の維持アルゴリズム」の深層に迫ります。RFCの基本から、パケットがルーティングされるリアルな挙動、そして実務でハマる罠と回避策まで、現場の知見を交えて徹底的に解説しましょう。

—

1. なぜNATが必要なのか? そして、直面する「ポート枯渇」の正体

プライベートサブネットに配置されたKubernetesのPodやECG/Computeインスタンスが、インターネット上の外部API(例えばStripeやSendGridなど)を叩くとき、彼らはプライベートIPアドレス(例: 10.0.1.100)を持っています。プライベートIPはグローバルIP空間ではルーティングできないため、NATGWが自身の持つグローバルIPアドレスに変換(SNAT: Source Network Address Translation)して外の世界へと送り出します。

ここで基本に立ち返りましょう。TCP/IPの通信を一意に識別する仕組みは何だったでしょうか? そう、「4タプル(送信元IP、送信元ポート、宛先IP、宛先ポート)」です。

AWSのNATGWやGCPのCloud NATは、1つ(または複数)のグローバルIPアドレスを、背後にいる無数のプライベートリソースで共有します。グローバルIPアドレスは高価かつ有限です。1つのIPアドレスが持つポート数は理論上 65,535 個ですが、システム予約ポートなどを除くと、実際に使えるエフェメラルポート(動的ポート)は概ね 10,245 から 65,535 までの約 55,000 個程度に制限されます。

ポート一意性の維持アルゴリズム:NAPTの仕組み

複数のプライベートインスタンスが同時に外部へ通信する際、NATGWはNAPT(Network Address and Port Translation)を用います。

  • 同一の送信元(プライベートIP/ポート)から、異なる宛先へ通信する場合:

NATGWは、宛先IP/ポートが異なれば、異なる4タプルを構成できるため、原則として同じ外部ポートを再利用可能です。

  • 異なる送信元から、同一の宛先(同一IP/ポート)へ通信する場合:

ここが問題です。宛先が完全に同じである場合、外部から見てパケットを正しく送り返すためには、NATGW側で送信元ポートをユニークに書き換える(ポートマッピング)必要があります。

この「一意性を維持するためのハッシュ化とアロケーション」を、NATGWはハードウェア処理(または最適化されたカーネル空間)で高速に行っています。しかし、短時間に数万を超える同時接続(異なる宛先、または同一宛先への大量のリクエスト)が発生すると、利用可能なポートが底をつきます。これが世にも恐ろしい「SNATポート枯渇(Port Exhaustion)」の正体です。

—

2. 接続管理の裏側:RFC 5382とデースリー(Destination-Independent Mapping)

NATの挙動を規定する上で重要な標準規格が、RFC 5382(NAT Behavioral Requirements for TCP)です。これには、堅牢なパブリックネットワークを構築するための要件が定義されています。

特に重要なのが「ポートマッピングの一貫性」です。優れたNAT実装(AWSのNAT GatewayやGCPのCloud NATなど)は、同じ内部IP/ポートからの通信であれば、宛先が変わっても可能な限り同じ外部ポートを維持しようと試みます(Endpoint-Independent Mappingに近い挙動ですが、クラウドプロバイダーによって最適化アルゴリズムが異なります)。

ライフサイクルとタイマー管理

NATGWの内部セッションテーブルは、以下のようなタイマーで管理されています。

1. SYN_SENT:SYNパケットを検知してエントリ作成(短命)
2. ESTABLISHED:コネクション確立中。アイドル状態でも、通常は数分〜数十分のタイマー(TCP Keepalive等がない場合)で維持されます。
3. TIME_WAIT:接続終了後。パケットの迷子を防ぐため、一定時間(通常は数秒〜数十秒)ポートは解放されずに保持されます。

この TIME_WAIT の存在が厄介です。短命なHTTPリクエストを毎秒何千発も投げるアーキテクチャでは、接続が即座に閉じられてもポートはすぐに再利用できず、瞬く間に枯渇へと向かいます。

—

3. 実務での検証:Pythonとcurlで見るポート枯渇の再現と確認

言葉で聞いていてもピンと来ないかもしれないので、実際にクライアント側からどう見えるか、そしてどのように対策すべきかをコードで見ていきましょう。

シナリオ:同一宛先への過剰なコネクション生成

例えば、Pythonの requests ライブラリを使って、コネクションプールを無効にした状態で外部APIを叩き続けるスクリプトを書いたとします。

import requests
import time

# 意図的にセッション(コネクションプール)を使わず、毎回新しいソケットを開かせる
TARGET_URL = "https://httpbin.org/ip"

def send_request(index):
    try:
        # コネクションの使い回しをしない(毎回新規TCPハンドシェイクが発生)
        response = requests.get(TARGET_URL, timeout=5)
        print(f"[{index}] Status: {response.status_code}, Response: {response.json()}")
    except Exception as e:
        print(f"[{index}] Error: {e}")

if __name__ == "__main__":
    print("ポート枯渇シミュレーションを開始します...")
    for i in range(100):
        send_request(i)

このコードを実行すると、バックエンドで何が起きているでしょうか?
毎回新規にTCPコネクションを張るため、NATGWは新しいエントリをセッションテーブルに追加し、エフェメラルポートを消費します。もしこれが数千・数万のスケールになると、あっという間にタイムアウトエラー(Connection refused や Socket operation timed out)が返ってくるようになります。

対策:HTTP Keep-Alive(コネクションの持続)の徹底

この問題を根本から解決する最も効果的なアプローチは、「TCPコネクションを使い回す(Connection Reuse)」ことです。

Node.jsの fetch や Pythonの requests.Session を用いて、HTTP/1.1の Keep-Alive や HTTP/2の多重化を利用します。

import requests

# セッションオブジェクトを使用することで、コネクションプールが有効になる
session = requests.Session()

# 内部的にTCPコネクションが維持され、新しいポートを消費し続けない
for i in range(100):
    response = session.get("https://httpbin.org/ip")
    print(f"Request {i}: {response.status_code}")

このように実装を変更するだけで、NATGW上のエポック消費量は劇的に削減されます。新規のSYNパケットが飛ばなくなるため、ポート枯渇のリスクはゼロに等しくなります。

—

4. インフラ・コンテナ層での設計Tipsとデバッグ手順

アプリケーションコードの修正だけでは防ぎきれないトラフィック量に直面した場合、インフラストラクチャ側でどのような手立てを打つべきでしょうか。SREが現場で実践している具体的なプラクティスを共有します。

1. GCP Cloud NAT / AWS NAT Gatewayのスケールアウト設計

  • GCP Cloud NATの場合、デフォルトでポート割り当てが動的(Dynamic Port Allocation)になっており、必要に応じて自動で追加のグローバルIPアドレスからポートが割り当てられます。複数のIPをあらかじめアタッチしておくことが極めて有効です。
  • AWS NAT Gatewayの場合、1台のNAT Gatewayあたりの最大パフォーマンスはリソース的にはスケーリングしますが、1つのNAT Gatewayが持つ単一のグローバルIPあたりのポート制限(約55,000)は物理的な制約です。トラフィックが多いシステムでは、マルチAZ構成で複数のNAT Gatewayを配置し、プライベートサブネットのルートテーブルを適切に分割して負荷分散(シャーディング)を行う必要があります。

2. Kubernetes(EKS/GKE)環境特有の罠

Kubernetesクラスターにおいて、すべてのPodが単一のNAT Gatewayに集中することはよくあります。ここでCiliumやCalicoなどのCNIを利用している場合、Node単位のSNAT(Masquerade)の設定を確認してください。
NodeのローカルIPを経由して外に出る際、ポッドの数 × 宛先の組み合わせが爆発的に増え、Node側のエフェメラルポート(net.ipv4.ip_local_port_range)が枯渇するケースもあります。

トラブルシューティング:現場のデバッグコマンド

もし「NATが怪しい」と感じたら、踏み台サーバーやデバッグ用Podから以下のコマンドで現状をあぶり出します。

# Linuxカーネルの現在のポートレンジと割り当て状況を確認する
sysctl net.ipv4.ip_local_port_range

# 現在確立されている(あるいはTIME_WAITの)外向きコネクション数をカウントする
ss -s

# 特定の宛先への接続状況と利用中のローカルポートをリアルタイムで監視する
ss -tan '( dport = :443 )'

VPC Flow Logsを有効にしている場合は、srcaddr, dstaddr, srcport, dstport, action=REJECT(ポート枯渇やセキュリティグループ起因のドロップ)のログをAmazon AthenaやGoogle Cloud Loggingでクエリし、特定の送信元IPが異常な数のポートを占有していないかを特定するのが常道です。

—

まとめ

NATGWにおける最大接続数とポート一意性の維持は、地味ながらもシステムの生死を分けるクリティカルな要素です。

1. RFCとアルゴリズムの理解:4タプルとエフェメラルポートの限界を常に意識する。
2. アプリケーション層の最適化:コネクションプーリング(Keep-Alive)を徹底し、無駄なTCPハンドシェイクを削減する。
3. インフラ層の拡張性:マルチIP化やNAT Gatewayの分散配置を設計段階から組み込む。

この3つを抑えておけば、突発的なバーストトラフィックやスケール時のトラブルにも慌てることなく、冷静に対処できるはずです。

「動いているから大丈夫」ではなく、「なぜこの設計でポートが枯渇しないと言い切れるのか」を常に問いかけながら、強靭なクラウドネットワークを構築していきましょう。それでは、また次回の現場の知見でお会いしましょう!

コメント

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