【実務・中級編】 NATゲートウェイのソースポート枯渇問題とスケーリングの限界 – クラウドインフラと仮想化ネットワーク実践ガイド

AWS NATゲートウェイの「ソースポート枯渇」と戦う:PATの限界値とスケーリング設計の全貌

こんにちは。クラウドインフラの現場を渡り歩いてきたシニアSREの私です。

夜中に突如鳴り響くPagerDuty、Slackに飛び交う「外部APIへの接続エラーが急増しています!」というアラート。慌ててメトリクスを確認すると、CPUやメモリは平穏そのものなのに、特定のサードパーティ製APIやマイクロサービス群へのリクエストがことごとくタイムアウトしている――。

インフラエンジニアなら誰もが一度は冷や汗をかいたことがあるであろう、このトラブル。その犯人の多くは、AWSのプライベートサブネットの心臓部である「NATゲートウェイ(NAT Gateway)」におけるソースポートの枯渇(Port Exhaustion)です。

今回は、パケットがVPCの境界線でどのように変換され、なぜネットワークのボトルネックが生まれるのか。そのメカニズムをRFCの仕様からAWSの物理的制約まで深掘りし、実務で使える回避策まで余すところなくお伝えします。

—

1. NATゲートウェイの裏側:なぜポートが枯渇するのか?

まず、私たちが普段何気なく使っているNATゲートウェイが、内部でどのようなネットワーク処理を行っているのかを思い出してください。

プライベートサブネットに配置されたEC2インスタンスやECSタスク(ここでは総称して「プライベートインスタンス」と呼びます)が、インターネット上の外部APIにHTTPSリクエストを投げる時、パケットは直接外に出られません。ルートテーブルの指示に従い、パケットはNATゲートウェイへとルーティングされます。

PAT(Port Address Translation)の仕組み

NATゲートウェイは、プライベートIPアドレスを持つインスタンスからのパケットを受け取ると、自身の持つグローバルIPアドレス(Elastic IP)へと送信元IPアドレスを書き換えます。これがいわゆるSNAT(Source NAT)です。

しかし、単なるIPの置き換え(1対1のNAT)では、何百、何千というプライベートインスタンスが同時に同じ外部IPへ通信した際、返ってきたパケットの宛先をどのインスタンスに戻すべきか、NATゲートウェイが判断できません。

そこで登場するのが PAT(ポートアドレス変換) です。
NATゲートウェイは、送信元IPアドレスの書き換えと同時に、「送信元ポート番号」も動的に割り当て直します。これにより、OSのネットワークスタックは「どのインスタンスの、どのプロセスからの通信か」を識別できるようになります。

致命的な制約:5つ組(5-tuple)の限界

TCP/IPの通信において、接続を一意に識別するためには以下の「5つ組(5-tuple)」が使われます。

1. 送信元IPアドレス(NATゲートウェイのElastic IP)
2. 送信元ポート番号(NATゲートウェイが割り当てたポート)
3. 宛先IPアドレス(外部APIサーバーのIP)
4. 宛先ポート番号(通常は 443 など)
5. プロトコル(TCP)

ここで大きな壁にぶつかります。RFC(特にTCPの仕様)やLinuxカーネルのネットワーク実装において、「同一の宛先IPアドレス・宛先ポート」の組み合わせに対して利用できる送信元ポートの最大数は、理論上 64,512(65,535 からシステム予約ポートを引いた数)が上限となります。

つまり、「1つのNATゲートウェイ」から「同一の宛先IP」に対して同時接続できる最大数は、約6万4千セッションが物理的な限界なのです。

—

2. 接続先ごとの制限(Destination IP Port Allocation)

AWSのNATゲートウェイは、高度にスケーリングするマネージドサービスですが、この「宛先IPアドレスごとの接続制限」には厳格なルールが存在します。

AWS公式ドキュメントでも言及されていますが、NATゲートウェイは単一の宛先IPアドレスに対して、最大で 55,000 の同時接続(ネイティブのポート割り当て)を処理できます。これを超えるトラフィックが流れると、新しいポートを割り当てられなくなり、パケットはドロップされるか、タイムアウト(ETIMEDOUT)を引き起こします。

ここで現場でよくある誤解を解いておきましょう。
「うちのVPCにはNATゲートウェイが1台あるから、全社で6万強のコネクションが使えるんだな」――これは大きな間違いです。

制限は「NATゲートウェイから見た宛先IPアドレス単位」で掛かります。
例えば、自社システムから特定のビッグテック系SaaS(例:単一のグローバルIPで受けている決済APIなど)に対して、数千台のECSコンテナが一斉にリクエストを叩き込んだ場合、あっという間にその宛先IPへのポートプール(55,000ポート)が枯渇します。他の宛先(例えばS3や別のクラウドサービス)への通信には全く影響がないため、障害原因の特定が遅れがちになるのがこの問題のイヤらしいところです。

—

3. 現場で起きた悲劇:パケットキャプチャとメトリクスから読み解く障害の兆候

ある日の本番リリース直後、決済連携APIからのレスポンスタイムが急激に悪化し、最終的に 504 Gateway Time-out が連発しました。

当時のデバッグ手順をそのまま再現してみましょう。

Step 1: CloudWatchメトリクスの確認

AWS Management Consoleを開き、NATゲートウェイのCloudWatchメトリクスを確認します。

  • ErrorPortAllocation (ポート割り当てエラー):この値が 0 より大きくなっている場合、まさにポート枯渇が発生しています。
  • PacketsDropCount:パケットドロップの急増。

Step 2: アプリケーション側のログ分析

Python製マイクロサービスのログを覗くと、お馴染みの例外が吐き出されていました。

# アプリケーションのエラーログ(抜粋)
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='api.payment-gateway.example.com', port=443): Max retries exceeded with url: /v1/charges (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f8a1c2b3d90>: Failed to establish a connection: [Errno 110] Connection timed out'))

このエラーが出た時、アプリケーションは「相手のサーバーが落ちている」と勘違いしがちですが、実際にはAWSのNATゲートウェイがポートをアサインできずに接続すら開始できていない状態です。

—

4. 根本的な解決策とコードによるアプローチ

このポート枯渇問題に対して、インフラエンジニアとして、そしてアプリケーション開発者として打てる手立てはいくつかあります。それぞれのレイヤーでの対策を見ていきましょう。

対策A:HTTP Keep-Alive(持続的接続)の徹底

最もコストがかからず、かつ最も効果が高いのがHTTP Keep-Aliveの活用です。

毎回新しいTCPコネクションを張って、リクエストを投げたらすぐに FIN パケットを送って切断するような実装(Short-lived connection)をしていると、TCPの TIME_WAIT 状態(通常はLinuxで60秒程度保持されます)も含めてポートが占有され続けます。

以下は、Pythonの requests ライブラリで Session を使い、コネクションを維持(Keep-Alive)させる模範的なコードです。

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_robust_session(pool_maxsize=50):
    """
    HTTP Keep-Aliveと適切なコネクションプールを持ったセッションを生成する
    """
    session = requests.Session()
    
    # リトライ戦略の設定
    retries = Retry(
        total=3,
        backoff_factor=0.5,
        status_forcelist=[500, 502, 503, 504],
        raise_on_status=False
    )
    
    # HTTPAdapterでプールのサイズと再利用を設定
    # pool_maxsizeを適切に設定することで、コネクションの使い回しを促進する
    adapter = HTTPAdapter(
        pool_connections=10,
        pool_maxsize=pool_maxsize,
        max_retries=retries,
        pool_block=False
    )
    
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    
    return session

# グローバルまたはシングルトンとしてセッションを保持し、使い回す
api_client = create_robust_session(pool_maxsize=100)

def call_external_api(payload: dict):
    url = "https://api.payment-gateway.example.com/v1/charges"
    try:
        # 同一のTCPコネクション(5つ組)を再利用するため、NATのポートを消費しない
        response = api_client.post(url, json=payload, timeout=5.0)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        # ログ出力やエラーハンドリング
        print(f"API通信エラー: {e}")
        raise

Node.js(Fetch APIやAxios)やJava(HttpClient)を使う場合でも同様に、コネクションプールの設定(keepAlive: true)が有効になっているかを必ず確認してください。

対策B:NATゲートウェイの分散配置(マルチAZ構成)

AWSのベストプラクティスですが、NATゲートウェイは各アベイラビリティゾーン(AZ)に1つずつ配置するのが鉄則です。

単一のAZに依存していると、そのAZの障害時にシステム全体が止まるだけでなく、ポートのプールも1台のNATゲートウェイに集中します。
マルチAZにNATゲートウェイを配置し、各プライベートサブネットから同一AZ内のNATゲートウェイへルーティングさせることで、ポートの総容量を物理的に数倍に拡張(スケールアウト)させることができます。

対策C:VPCEndpoints(AWS PrivateLink)の活用

もし接続先の外部サービスが、AWSの別のアカウントで提供されているSaaSや、AWSサービス(S3やDynamoDBなど)であるならば、NATゲートウェイを経由させる必要はありません。

VPCエンドポイント(Interface Endpoint / Gateway Endpoint)を利用すれば、インターネットゲートウェイやNATゲートウェイを完全にバイパスし、AWSのバックボーンネットワーク内で安全かつ高速に通信できます。これによってNATゲートウェイのポート消費をゼロに抑えることができます。

—

5. まとめ

NATゲートウェイのソースポート枯渇問題は、クラウドの「リソースは無限にある」という幻想を打ち砕く、非常にネットワークエンジニアリング的なリアルな課題です。

  • 5つ組とPATの仕組みを理解し、自身のシステムがどこでポートを消費しているかを知る。
  • アプリケーション層ではHTTP Keep-Aliveとコネクションプールの適切なチューニングを行う。
  • インフラ層ではマルチAZへのNATゲートウェイ分散やVPCエンドポイントの積極的な採用を検討する。

この3つを押さえておけば、突発的なトラフィック増によるポート枯渇パニックを未然に防ぐことができます。

明日のアーキテクチャ設計やコードレビューの際に、ぜひ「この通信、本当に毎回コネクション張ってないか?」という視点を取り入れてみてください。あなたのインフラが、より頑健で美しいものになることを応援しています。

コメント

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