「なぜ、あのコネクションは突然切れるのか?」NATゲートウェイのTCPタイムアウトと戦うエンジニアへ
現場のSREなら一度は経験があるはずです。「正常に動いていたはずのバッチ処理が、数分間の沈黙の後に突如 Connection reset by peer で落ちる」「APIリクエストがたまにタイムアウトする」。
パケットキャプチャを眺めて絶望する前に、一度立ち止まって考えてみてください。その裏で、AWSの NAT Gateway や GCPの Cloud NAT が、あなたのコネクションを「無駄なゴミ」と判断して強制的に掃除している可能性があることを。
今日は、クラウドネットワークの「静かなる掃除人」、NATゲートウェイのタイムアウト仕様とその防衛術について、現場の視点から深掘りします。
—
NATゲートウェイは「状態」をどこまで覚えているのか
パブリックサブネットにあるインスタンスがインターネットへ出る際、NATゲートウェイは「プライベートIP:送信元ポート」と「グローバルIP:変換後ポート」の対応表(NATテーブル)をメモリ上に保持します。
しかし、このテーブルは有限です。メモリを節約するため、クラウドプロバイダーは「一定時間通信がないコネクション(アイドル状態)」を容赦なく切り捨てます。
各クラウドのタイムアウト仕様(目安)
- AWS NAT Gateway: 350秒(約6分)でアイドル状態のコネクションを強制切断します。
- GCP Cloud NAT: デフォルトで600秒(10分)ですが、設定により30秒〜3600秒の間で調整可能です。
この「350秒」や「600秒」という数字。TCPのRFC 793が定義する「コネクションの正常終了(FIN/ACK)」を待たず、NATゲートウェイが一方的にテーブルからエントリを削除することに注意してください。クライアントもサーバーも、「まだ接続は生きている」と信じ込んでいるのに、経路上の関所が勝手に門を閉ざしてしまう。これが「突然の切断」の正体です。
—
パケットの裏側:なぜ「沈黙」が罪なのか
TCPは本来、切断の意思表示(FINやRST)がない限り、コネクションは生き続けます。しかし、NATゲートウェイからすれば、数分間パケットが流れない通信は「相手が死んでいるか、忘れている」とみなされます。
このとき、NATゲートウェイはエントリを消去しますが、クライアントには何も通知しません。 その後にクライアントが再度データを送ろうとすると、NATゲートウェイは「そんなコネクションは知らない」として RST パケットを返します。これが、アプリケーションログに Connection reset by peer が記録されるメカニズムです。
—
防衛術:キープアライブで「死んでない」と主張する
この問題の最も正攻法な解決策は、NATゲートウェイがエントリを削除するよりも短い間隔で、「私はここにいますよ」という生存確認パケットを流すことです。
1. TCP Keepalive をOSレベルで設定する(Pythonの例)
アプリケーションがコネクションを長時間維持する設計の場合、ソケットレベルで SO_KEEPALIVE を有効にします。
import socket
# ソケット作成
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# TCP Keepaliveを有効化
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# システム依存の設定例(Linux系の場合)
# 接続がアイドル状態で、どれくらいでKeepaliveパケットを送るか
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) # 60秒の沈黙で送信
# Keepaliveパケットの再送間隔
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
# 何回失敗したら切断とみなすか
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
sock.connect(("api.example.com", 443))
2. HTTPレイヤーでの対応(Fetch API / curl)
Web APIの場合、Keep-Alive ヘッダーを制御するのはブラウザやランタイムの責務ですが、接続プール(Connection Pooling)の再利用時間を短く設定するのが賢明です。
例えば、Node.jsの axios や http.Agent を使う場合、keepAliveMsecs をNATゲートウェイのタイムアウトより十分に短い値(例えば60秒)に設定します。
const http = require('http');
const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 60000, // 60秒毎にキープアライブ
maxSockets: 256
});
// このエージェントをHTTPクライアントに渡して利用する
3. インフラ運用でのデバッグ手順
もし、あなたの環境で原因不明の切断が多発しているなら、まずは以下のコマンドで「実際にパケットがどうなっているか」を確認してください。
# 特定のポートへの接続状態を監視
sudo tcpdump -i eth0 port 443 -nn -v
# 接続が切れた瞬間にRSTが返ってきていないか確認
# -S: シーケンス番号を確認し、通信の断絶を追跡する
—
アーキテクトからのアドバイス
「タイムアウトを延ばせばいいのでは?」と考えがちですが、それは対症療法です。クラウドのインフラは、基本的に「いつ切れてもおかしくない」という前提でアプリケーションを設計する(Resilienceの向上)のが、真のSREの流儀です。
1. コネクションプーリングの寿命(Max Age)をNATのタイムアウトより短くする。
2. リトライ処理を必ず実装する(指数バックオフを忘れずに)。
3. アプリケーションのログには、タイムアウトを検知した瞬間のIPやポートを記録する。
ネットワークのトラブルは、多くの場合「仕様のすれ違い」から生まれます。NATゲートウェイは意地悪で切断しているわけではありません。ただ、効率のために「過去を忘れる」だけなのです。その性質を理解した上で、いかに「忘れられない努力」をするか。それが、安定したシステムを支えるネットワークエンジニアの腕の見せ所です。
それでは、また次回の障害対応でお会いしましょう。健闘を祈ります。
コメント