「なぜか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 の設定値や、コネクションの多重化を見直してみてください。それだけで、ユーザー体験は劇的に改善するはずです。
コメント