【実務・中級編】 モバイル通信におけるNAT(CGNAT: Carrier Grade NAT)の動作とポート枯渇問題 – 家庭用ネットワーク・IoT・モバイル通信実践ガイド

「なぜかAPIが繋がらない」を解決する――CGNATの壁とポート枯渇の深淵

エンジニアとして現場に立っていると、一度は必ずぶち当たる壁があります。「ローカル環境や社内回線では正常に動くのに、特定のモバイル回線や特定のISP経由だと、APIリクエストが頻繁にタイムアウトしたり、接続拒否(Connection Reset)される」という現象です。

その犯人の多くは、通信キャリアの設備内で動いている CGNAT(Carrier Grade NAT)です。今日は、教科書には載っていない「現場の泥臭い挙動」と、エンジニアが知っておくべき回避策について深掘りしましょう。

CGNATが引き起こす「見えない壁」

IPv4アドレスが枯渇して久しい今、通信キャリアは1つのグローバルIPアドレスを、何百、何千人ものユーザーで共有しています。これが CGNAT です。

キャリア側のゲートウェイ(Large Scale NAT装置)は、あなたのデバイスが外部と通信する際、内側のプライベートIPアドレスと送信元ポートを、グローバルIPと特定のポートにマッピングします。しかし、ここで問題になるのが「ポートの枯渇」です。

なぜポートが枯渇するのか?

TCP接続には「送信元IP・送信元ポート・宛先IP・宛先ポート」の4要素が必要です。キャリアのNAT装置は、この接続状態を管理する「セッションテーブル」を持っています。

1. 短時間での大量リクエスト: 最近のWebアプリは、1ページ開くだけで数十のAPIを同時に叩きます。
2. Keep-Aliveの弊害: Connection: keep-alive を多用すると、セッションが長時間NAT装置を占有します。
3. ポートの使い回し制限: キャリア側のNAT装置は、セキュリティとリソース保護のために、1つのグローバルIPに対して使用できるソースポート数を制限しています。

この上限に達した瞬間、新しいコネクションは「パケットロス」ではなく、NAT装置によって「問答無用で破棄」されます。これが「たまに繋がらない」という、もっともデバッグが困難な現象の正体です。

実践的デバッグ:パケットの挙動を追う

もしAPIが不安定だと感じたら、まずはクライアント側で TCP SYN が届いているのか、それとも途中で RST が返ってきているのかを確認しましょう。

Pythonによる生存確認(スクリプト例)

単純な curl や Fetch API だけでなく、コネクションプールを明示的に制御してテストを行うコードを書いてみましょう。

import requests
from requests.adapters import HTTPAdapter

# セッションプールを明示的に管理する
session = requests.Session()
# 必要以上にコネクションを保持しすぎない設定
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10)
session.mount('https://', adapter)

try:
    # 連続でリクエストを送った時のNATの挙動を観察する
    for i in range(50):
        response = session.get('https://api.example.com/data')
        print(f"Request {i}: {response.status_code}")
except Exception as e:
    # ここで ConnectionResetError が頻発するならNATのポート枯渇を疑う
    print(f"Error occurred: {e}")

もしこのコードで特定環境下のみエラーが出るなら、NAT装置側で「短時間での接続多重化」がブロックされている可能性が極めて高いです。

インフラ運用・設計側でできる対策

モバイルユーザーをターゲットにしたサービスを設計・運用する場合、以下の対策は「必須」です。

1. HTTP/2 または HTTP/3 (QUIC) の採用

HTTP/1.1 では、1つのリクエストごとにTCPコネクションを張ることが多く、これがポート消費を加速させます。HTTP/2 の多重化(Multiplexing)を利用すれば、1つのTCPコネクションで複数のリクエストを捌けるため、NAT装置のセッションテーブルを圧迫しません。

2. クライアント側のタイムアウト設定を適切に

デフォルトのタイムアウトが長すぎると、接続できないままセッションが残り続け、NATテーブルを埋め尽くします。

// Fetch APIでのタイムアウト実装例
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒で切る

fetch('https://api.example.com/data', { signal: controller.signal })
  .then(response => response.json())
  .catch(err => {
    if (err.name === 'AbortError') {
      console.error('通信がタイムアウトしました:NATの制限の可能性があります');
    }
  });

3. API設計の工夫

  • バッチリクエスト: 複数の小さなAPIリクエストを1つのエンドポイントにまとめ、1回の接続で済ませる。
  • ロングポーリングの回避: WebSocketへの移行、あるいは必要最小限のポーリング間隔への調整。

最後に:ネットワークは「生き物」である

CGNATは、ISPが限られたIPv4資源をやりくりするための苦肉の策です。エンジニアが「通信は完璧に通るもの」という前提で設計すると、必ずモバイル環境という荒波で足元をすくわれます。

「繋がらない」と言われたとき、ソースコードのロジックだけでなく、パケットが通過するキャリア網のゲートウェイがどのような制限を課しているのか――その「物理的な制約」まで想像を巡らせることができるのが、一流のエンジニアです。

次回の運用改善では、ぜひ Keep-Alive の設定値や、コネクションの多重化を見直してみてください。それだけで、ユーザー体験は劇的に改善するはずです。

コメント

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