NATゲートウェイの「見えない天井」:バーストトラフィックで落ちないための生存戦略
エンジニアの皆さん、こんにちは。現場で修羅場をくぐり抜けてきたSREとして、今日は皆さんが意外と見落としがちな「NATゲートウェイ(NAT GW)の帯域幅制限」という名の罠についてお話しします。
「クラウドだから勝手にスケールするんでしょ?」という甘い認識で設計していると、突発的なバーストトラフィックに襲われた瞬間、Web APIがタイムアウトの嵐に見舞われ、原因切り分けで数時間をドブに捨てることになります。今日は、その「勝手にスケールする」という魔法の裏側にある、泥臭い現実を紐解いていきましょう。
—
1. NATゲートウェイの「自動スケーリング」という幻想
AWSのNATゲートウェイを例に挙げましょう。ドキュメントには「自動的にスケーリングする」と書かれていますが、重要なのは「どのくらいの速度でスケーリングするのか」です。
NATゲートウェイは、開始時の帯域幅が5 Gbpsで、最大45 Gbpsまでスケールします。しかし、これは「瞬時に45 Gbpsになる」わけではありません。以前のトラフィック実績に基づき、過去の最大ピークの120%までがバーストの許容範囲とされています。
つまり、「普段ほとんど通信していないNAT GWに、いきなり10 Gbpsのトラフィックをぶつける」と、高確率でパケットロスが発生します。
なぜパケットロスが起きるのか
NAT GWは、急激なトラフィック変動に対して、スループットを段階的に引き上げる「プログレッシブ・スケールアップ」という挙動をとります。このスケールアップが追いつかない間のトラフィックは、容赦なく破棄されるか、またはシェーピング(遅延)されます。TCP通信であれば再送が発生し、レイテンシが跳ね上がり、結果としてAPIのHTTPステータス 504 Gateway Timeout や 502 Bad Gateway が返ることになります。
—
2. 現場で使えるデバッグと検証の作法
もしAPIの応答が急に悪くなったら、まずは「NAT GWがボトルネックになっていないか」を疑ってください。AWSであれば CloudWatch Metrics の ErrorPortAllocation や Throughput を見るのが定石です。
もし再現テストをするのであれば、iperf3 を使って疑似的にトラフィックを流すのが一番手っ取り早いです。
# パブリックサブネット側のEC2(クライアント)で実行
# ターゲットとなるNAT GW経由の通信をシミュレート
iperf3 -c <APIの接続先IP> -t 60 -P 10 --bandwidth 2G
# このコマンドで、NAT GWが追従できるかスループットを確認する
# 途中でパケットロス率が増加し、レイテンシが不安定になれば限界です
—
3. 実践:バーストに強いWeb API設計
インフラ側でどうしようもない「物理の壁」がある以上、アプリケーション側での防衛策が必須です。
Pythonによるリトライ戦略(指数バックオフ)
急激なバーストでパケットが落ちるなら、再送タイミングをずらす「指数バックオフ」を実装しましょう。これを怠ると、全クライアントが同時に再送を試みる「サンダリング・ハード(Thundering Herd)問題」を引き起こし、状況を悪化させます。
import time
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def get_session_with_retry():
session = requests.Session()
# 指数バックオフの設定: 最初の再送は1秒後、次は2秒、次は4秒...
retries = Retry(
total=5,
backoff_factor=1, # 再送間隔の倍率
status_forcelist=[502, 503, 504] # NAT GW起因のタイムアウトを想定
)
session.mount('https://', HTTPAdapter(max_retries=retries))
return session
# API呼び出し実行
session = get_session_with_retry()
try:
response = session.get("https://api.example.com/data")
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"致命的なエラー: {e}")
—
4. プロの設計判断:NAT GWを分ける勇気
もし皆さんが高トラフィックなマイクロサービスを運用しているなら、「NAT GWをサービスごとに分ける」という設計も検討してください。
- サービスA(高トラフィック): 専用のNAT GWを割り当て、常にトラフィックを流して「温めておく(ウォームアップ)」。
- サービスB(低トラフィック): 別のNAT GWに寄せる。
NAT GWを共有していると、サービスAのバーストが原因で、サービスBまで巻き添えを食らいます。コストは増えますが、可用性とトラブルシューティングの切り分けやすさは劇的に向上します。
設定のヒント(Terraform例)
# サービスごとのNATゲートウェイを作成する設計
resource "aws_nat_gateway" "service_a_nat" {
allocation_id = aws_eip.nat_a.id
subnet_id = aws_subnet.public_a.id
tags = {
Name = "service-a-nat-gw"
}
}
—
最後に:ネットワークを「生き物」として扱う
クラウドネットワークは、単なるパイプではありません。トラフィックの増減に合わせて微妙に挙動を変える「生き物」です。
「設計図通り動くはずだ」と信じ込むのではなく、「いつかNAT GWが悲鳴を上げるかもしれない」という前提で、適切な監視アラートを仕込み、アプリケーション側でリトライを制御し、必要であればNAT GWを分離する。 この泥臭い積み重ねこそが、大規模トラフィックを捌くSREの真髄です。
皆さんのシステムが、予期せぬバーストでも涼しい顔をして動き続けることを願っています。それでは、また現場でお会いしましょう。
コメント