こんにちは。SREとして日々数々のクラウドインフラと向き合っていると、時々「なぜか本番環境で特定のバッチ処理やAPI通信が数分おきにしれっと切断される」という、なかなかに香ばしいトラブルに遭遇します。
エラーログを覗いても「Connection reset by peer」や「Timeout」という塩対応なメッセージばかり。アプリケーションコードをいくら睨んでもバグは見つからない。そんなとき、犯人は大抵ネットワークの境界線に潜んでいます。
今回は、AWSのパブリック/プライベートサブネットを繋ぐ縁の下の力持ち、NATゲートウェイの「350秒の呪縛(アイドルタイムアウト)」について、パケットの挙動から具体的なコードでの対策まで、現場の知見を総動員して徹底的に解説します。
—
1. なぜ「350秒」なのか? パケットの裏側で起きていること
まず、AWSのマネージドなNATゲートウェイの仕様からおさらいしましょう。
プライベートサブネットにあるEC2やECSタスクから、NATゲートウェイを介してインターネット上の外部APIへリクエストを飛ばすとき、NATゲートウェイは送信元のプライベートIPアドレスとポートを、自身のパブリックIPアドレスとポートに変換(SNAT)し、ステートフルにコネクションを管理しています。
ここで問題になるのが、RFC 5387などで規定されるネットワーク機器のアイドルタイムアウトです。AWSのNATゲートウェイは、TCPコネクション上で最後にパケットが流れてから「350秒間(約5分50秒)」一切の通信が途絶えると、コネクションのステート情報を問答無用で破棄します。
影の主役:コネクションテーブルの消滅劇
ここで恐ろしいのは、NATゲートウェイが勝手にコネクションを忘れたとしても、通信しているクライアント側(例えばPythonのrequestsやNode.jsのaxios)や、通信相手のサーバー側は「まだコネクションは生きている」と信じ込んでいる点です。
この状態で、350秒以上経過した後にクライアントが突然データを送ろうとパケットを投げたとします。すると、何が起きるでしょうか?
1. クライアントは古いセッション情報のまま、パケットをNATゲートウェイへ送る。
2. NATゲートウェイは「そんなコネクションの記憶はないが?」と、そのパケットをドロップするか、相手にRSTパケットを返す。
3. クライアント側は「Connection reset」や「Timeout」を食らい、阿鼻叫喚の渦に巻き込まれる。
これが、アイドルタイムアウトの正体です。コネクションは常に流し続けていないと、ネットワーク機器のメモリから容赦なく忘却の彼方に追いやられるのです。
—
2. 標準RFCとTCP Keep-Aliveによる防衛策
この理不尽な切断を防ぐ王道のアプローチが、TCP Keep-Aliveの活用です。
TCPの仕様(RFC 793)において、Keep-Aliveは「アイドル状態のコネクションがまだ生きているかを確認するため、定期的に空のパケット(あるいはACKを含むパケット)を送信するメカニズム」として定義されています。
OSレベル、あるいはアプリケーションのHTTPクライアント層でこのKeep-Aliveのインターバルを適切に設定し、「350秒が経過する前に、必ず何かしらのパケットをNATゲートウェイに通過させる(アイドルタイマーをリセットする)」というのが実務における鉄則となります。
安全係数を見込み、実務では 「およそ45秒〜60秒おきにKeep-Alive(またはハートビート)を送る」 という設計にするのがベストプラクティスです。
—
3. 実務で使える!各言語・ツール別の具体的な実装・設定例
ここからは、実際にコードベースや設定ファイルでどのようにこの問題に対処するのか、具体的なサンプルを見ていきましょう。
A. Linux OS全体(kernel)レベルでの設定変更
ECSのEC2起動タイプや、プライベートサブネットに置いた自前で管理するEC2インスタンスの場合、カーネルパラメータ(sysctl)を調整することで、OS全体でTCP Keep-Aliveを前倒しで発動させることができます。
/etc/sysctl.conf に以下の設定を追記します。
# コネクションが無音になってから、最初にKeep-Aliveパケットを送るまでの時間(秒)
# デフォルトは通常7200秒(2時間)なので、350秒未満の「60秒」に変更する
net.ipv4.tcp_keepalive_time = 60
# Keep-Aliveパケットを再送する際の間隔(秒)
net.ipv4.tcp_keepalive_intvl = 10
# 応答がない場合に、切断と見なすまでの再送回数
net.ipv4.tcp_keepalive_probes = 6
設定を反映するには、以下のコマンドを実行します。
sudo sysctl -p
—
B. Python (requests / urllib3) での制御
Webスクレイピングや、外部APIへ長時間の接続を維持するPython製バッチアプリケーションの場合、requests.Session と urllib3 のコネクションプール、およびTCPのKeep-Aliveを組み合わせます。
以下は、アダプターを使用してTCP Keep-Aliveを有効にする堅牢なセッションの構築例です。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_robust_session():
"""
NATゲートウェイの350秒タイムアウトを回避するため、
TCP Keep-Aliveと適切なリトライ設定を組み込んだセッションを返します。
"""
session = requests.Session()
# リトライ戦略の定義(一時的なネットワークエラーに対応)
retries = Retry(
total=3,
backoff_factor=1,
status_forcelist=[500, 502, 503, 504],
raise_on_status=False
)
# HTTPAdapterを用いてプールを設定
# ※ 標準のrequestsではソケットレベルのTCP Keep-AliveはOS依存ですが、
# urllib3の最新バージョンやカスタムアダプターでソケットオプションを渡すことも可能です。
adapter = HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=retries
)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
# 使用例
if __name__ == "__main__":
api_session = create_robust_session()
try:
# 定期的なAPIリクエストの例
response = api_session.get("https://api.example.com/v1/resource", timeout=30)
print(f"ステータスコード: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"通信エラーが発生しました: {e}")
—
C. Node.js (axios / http agent) での設定
サーバーレス(AWS Lambda)やECS上のNode.jsアプリケーションから外部APIを叩く場合も注意が必要です。http.Agent または https.Agent を用いて、キープアライブを明示的に有効化します。
const axios = require('axios');
const https = require('https');
// Keep-Aliveを有効にしたHTTPSエージェントの作成
const agent = new https.Agent({
keepAlive: true,
// ソケットがアイドル状態のときに、Keep-Aliveパケットを送信するまでの遅延(ミリ秒)
keepAliveMsecs: 60000, // 60秒
maxSockets: 50,
maxFreeSockets: 10,
timeout: 60000
});
// axiosインスタンスにエージェントをバインド
const apiClient = axios.create({
baseURL: 'https://api.example.com',
httpsAgent: agent,
timeout: 10000
});
async function callExternalApi() {
try {
const response = await apiClient.get('/v1/data');
console.log('データ取得成功:', response.data);
} catch (error) {
console.error('API呼び出し失敗:', error.message);
}
}
callExternalApi();
—
D. 疎通確認用:curlコマンドのオプション
デバッグ時に「自分の環境からNATゲートウェイを挟んだ通信がどう振る舞うか」をサクッと確認したいときは、curl のTCP Keep-Aliveオプション(--tcp-nodelay や詳細なソケット制御)を利用するか、そもそもタイムアウトを避けるための定期実行をテストします。
# TCPのKeep-Aliveを有効にしてリクエストを投げる例
# (curlはデフォルトでOSのTCP Keep-Alive設定に従いますが、明示的にオプションを指定して挙動を確認できます)
curl -v --connect-timeout 10 --max-time 30 https://api.example.com/healthcheck
—
4. 現場のシニアSREが教える、トラブルシューティングの極意
もしあなたが今、「特定の時間帯だけAPIが切れる」「数分放置した後の最初のリクエストだけ必ず失敗する」という怪奇現象に悩まされているなら、以下の手順で即座に原因を切り分けてください。
1. メトリクスの確認: AWS CloudWatchで、該当するNATゲートウェイの ErrorPortAllocation や ConnectionTimedOut メトリクスが跳ね上がっていないか確認する。
2. パケットキャプチャ(tcpdump): アプリケーションサーバー上で tcpdump を仕掛け、FINやRSTパケットがどちらの起点で飛んでいるかを追跡する。
sudo tcpdump -nnvvS host <外部APIのIPアドレス>
3. アプリケーションのライフサイクルを見直す: 「コネクションを張りっぱなしにする設計」になっているか、「毎回コネクションを切断(Short-lived)しているか」を確認する。後者であればそもそもNATのセッション枯渇やタイムアウトは起きにくいですが、コネクションハンドシェイクのオーバーヘッドが増えます。前者の場合は、今回解説したKeep-Aliveが絶対に必須です。
クラウドネットワークの挙動は、時に冷徹で気まぐれに見えますが、すべては物理法則とプロトコル仕様(RFC)の厳密な積み重ねの上で動いています。「350秒」という数字を頭の片隅に置いておくだけで、夜中に突然呼び出されるプレッシャーから解放されるはずです。
さあ、今すぐコードベースのHTTPクライアント設定を見直してみましょう!
コメント