【実務・中級編】 TCPキープアライブ(Keepalive)のパケット構造とタイムアウト設定 – ネットワーク基礎とWebセキュリティ実践ガイド

こんにちは。技術メディアの主筆、そして長年パケットの挙動と格闘してきたネットワークセキュリティスペシャリストの私です。

Web APIの設計やインフラ運用に携わっていると、避けては通れない「怪奇現象」があります。
「リクエストは正常なのに、長時間アイドル状態が続くとコネクションが突然死する」「ファイアウォール越しで通信がハングアップする」。こうしたトラブルの裏で、静かに、しかし決定的な役割を果たしているのがTCPキープアライブ(Keepalive)です。

今日は、教科書的な説明を飛び越えて、パケットがどのようにネットワークを駆け巡り、我々のセッションを守っているのか。その「鼓動」とも言える仕組みと、現場で即戦力となる設定のベストプラクティスを深掘りしていきましょう。

—

1. HTTP Keep-AliveとTCP Keepaliveの「決定的な違い」

まず、多くのエンジニアが混同しがちな点を整理しておきましょう。
OSI参照モデルで見ると、この2つは全く異なる階層で動いています。

  • HTTP Keep-Alive (Layer 7): アプリケーション層の話です。1つのTCPコネクションを使い回して複数のHTTPリクエストを投げる「効率化」の仕組みです。
  • TCP Keepalive (Layer 4): トランスポート層の話です。コネクションが「まだ生きているか」を確認し、アイドル状態での切断を防ぐ「維持・検知」の仕組みです。

今回スポットを当てるのは後者、TCP層でのサバイバル戦略です。

—

2. パケットの構造:なぜ「0バイト」のデータが送られるのか

TCP Keepaliveは、RFC 1122で規定されています。通信が途絶えた(Idle状態の)際、OSのカーネルが勝手に「おーい、生きてるか?」とパケットを投げます。

このパケット、実は非常にユニークな構造をしています。
通常、TCPパケットはシーケンス番号を進めますが、キープアライブ・プローブは「最後に確認されたシーケンス番号(SEG.ACK)から1を引いた値」をシーケンス番号として送ります。さらに、データ本体は空(0バイト)です。

受け取った側は、「あれ? 1つ古いシーケンス番号が来たぞ? でもACKは正しいな。とりあえず今のステータスを返しておこう」と、現在のシーケンス番号を含むACKを返します。
このやり取りこそが、パケットレベルでの「生存確認」の正体です。

通信シーケンスのイメージ

1. Idle状態: しばらく通信がない。
2. Probe送信: [TCP Keep-Alive] Seq=99, Ack=101, Win=500 (1つ前のSeqを送る)
3. 応答: [TCP Keep-Alive ACK] Seq=101, Ack=100 (正しいSeqで返ってくる)
4. 生存確認完了: コネクション維持。

もし応答がなければ、設定された回数だけ再試行し、最終的にRST(リセット)パケットを投げてコネクションをクローズします。

—

3. カーネルパラメータの三種の神器

Linuxサーバーにおいて、この挙動を制御するのは以下の3つのパラメータです。これらは /proc/sys/net/ipv4/ 配下に鎮座しています。

| パラメータ名 | 意味 | デフォルト値(一般的なLinux) |
| :— | :— | :— |
| tcp_keepalive_time | 最後にパケットを送ってからプローブを開始するまでの時間 | 7200 (2時間) |
| tcp_keepalive_intvl | プローブを送る間隔 | 75 (75秒) |
| tcp_keepalive_probes | 何回失敗したら「切断」とみなすか | 9 (9回) |

現場での注意点

デフォルトの「2時間」は、現代のWebインフラでは長すぎます。
例えば、クラウド環境のLoad Balancer(AWS ALBなど)やステートフル・ファイアウォールは、アイドルタイムアウト(多くは60秒〜数十分)を設定しており、それを超えるとサイレントにコネクションをドロップします。
サーバー側が「2時間待つ」設定だと、すでに経路上の機器で切断されていることに気づかず、アプリケーションがハングし続けることになります。

—

4. 実践:設定ファイルの記述例と反映

それでは、実務で使える設定を見ていきましょう。

Linuxカーネルの設定 (sysctl.conf)

高頻度のWeb API通信を行うサーバーなら、以下のように短縮するのが定石です。

# /etc/sysctl.conf に追記
# 最後の通信から10分(600秒)後にプローブ開始
net.ipv4.tcp_keepalive_time = 600
# プローブの間隔を15秒に設定
net.ipv4.tcp_keepalive_intvl = 15
# 5回連続で失敗したらコネクション異常と判断
net.ipv4.tcp_keepalive_probes = 5

# 設定を即時反映
# sudo sysctl -p

Python (Socket) での個別実装例

OS全体の設定を変えたくない場合、アプリケーションコード側でソケットごとに設定することも可能です。

import socket

# TCPソケットの作成
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Keepaliveを有効化
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

# 個別のパラメータ設定 (Linux固有の定数を使用)
# TCP_KEEPIDLE: tcp_keepalive_timeに相当
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
# TCP_KEEPINTVL: tcp_keepalive_intvlに相当
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
# TCP_KEEPCNT: tcp_keepalive_probesに相当
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)

# この後、s.connect(...) で接続

curl での確認

デバッグ時に curl でキープアライブを有効にするには --keepalive-time オプションを使います。

# 60秒間隔でプローブを投げる設定でリクエスト
curl --keepalive-time 60 -v https://api.example.com/v1/resource

—

5. トラブルシューティングの現場知恵

「設定したはずなのに、なぜか切れる」。そんな時は、以下のステップでパケットの生の声を聞いてください。

tcpdump でプローブを捕捉する

キープアライブ・プローブはデータを持たないため、通常のキャプチャでは見逃しがちです。

# 特定のホストとの通信で、データサイズが0のACKパケットを監視
sudo tcpdump -i eth0 'host 192.168.1.10 and tcp[tcpflags] & tcp-ack != 0 and (ip[2:2] - ((ip[0]&0xf)<<2) - ((tcp[12]&0xf0)>>2)) == 0' -vv

このコマンドは、IPヘッダとTCPヘッダの長さを計算し、ペイロードが 0 であるACKパケット(=キープアライブの可能性が高い)を抽出します。

クラウド・プラットフォームの「壁」

AWSのNLB(Network Load Balancer)やAzureのLoad Balancerを利用している場合、これらの中間機器自体が保持する「アイドルタイムアウト」の値を確認してください。
「サーバー側の tcp_keepalive_time < 中間機器のタイムアウト値」
この不等式を維持することが、コネクションの突然死を防ぐ黄金律です。

—

終わりに

TCP Keepaliveは、いわばネットワークにおける「生存確認のメッセージ」です。
派手な機能ではありませんが、この挙動を理解し、適切に数値をチューニングできるかどうかで、システムの堅牢性は劇的に変わります。

もしあなたが今、原因不明のタイムアウトに悩まされているなら、ぜひ一度 /proc/sys/net/ipv4/tcp_keepalive_time の値を覗いてみてください。そこには、数時間もの間、沈黙を守り続けているサーバーの姿があるかもしれません。

パケットの一つ一つに意味がある。それを読み解くのが、我々エンジニアの醍醐味ですから。

コメント

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