【実務・中級編】 TCPのシーケンス番号と確認応答番号による信頼性制御 – ネットワーク基礎とWebセキュリティ実践ガイド

TCPのシーケンス番号とACKの正体:パケット迷子を防ぐ信頼性のメカニズム

インフラの構築やWeb APIの設計・運用現場において、私たちは普段、HTTPやgRPCといったアプリケーション層の抽象化された世界で仕事をしています。curl叩いてJSONが返ってきた、APIクライアントが200 OKを吐いた――それだけで「よし、通信正常だ」と判断しがちです。

しかし、夜中の3時に「特定の高負荷時に限り、APIレスポンスの一部が欠損する」「リバースプロキシとバックエンドの間で謎のコネクション切断(RST)が頻発する」といった泥臭い障害に直面したとき、アプリケーションコードをいくら眺めても答えは見つかりません。

そんな修羅場で最後に頼りになるのは、OSI参照モデルのトランスポート層、すなわちTCP(Transmission Control Protocol)が織りなすパケットの暗号のようなやり取りを読み解く力です。

今回は、TCPが保証する「信頼性」の根幹であるシーケンス番号と確認応答番号(ACK番号)の正体に迫ります。パケットが物理的なルーターやスイッチをどのように駆け抜け、どうやって順序制御と欠落検知を行っているのか。実務で役立つデバッグ手法やコード例を交えながら、徹底的に解説していきましょう。

—

1. なぜTCPは「信頼性」を語れるのか? IPの限界を知る

私たちが日常的に使っているIP(Internet Protocol)は、実は極めて無責任なプロトコルです。ルーターからルーターへベストエフォートでパケットを運ぶだけで、「宛先に届いたか」「順番通りに届いたか」「途中でデータが化けていないか」なんてことは一切気にしません。パケットが途中のルーターのバッファ溢れでドロップしようが、回り道をしたがために追い抜かされようが、IPは知らぬ存ぜぬの顔を決め込みます。

この「信頼性のないIP」の上で、ファイル転送やデータベースのトランザクション、そしてWeb APIの確実なデータやり取りを実現しているのがTCPです。

TCPは、コネクション確立(3ウェイハンドシェイク)から切断に至るまで、すべてのバイトデータに番号を振ります。これがシーケンス番号(Sequence Number)です。そして、相手から受け取ったデータに対して「ここまでちゃんと受け取ったよ、次は〇番のデータをちょうだいね」と返すのが確認応答番号(Acknowledgment Number:ACK番号)です。

—

2. シーケンス番号とACK番号のリアルな挙動

まずは、TCPのデータ転送時におけるシーケンス番号とACK番号の「定義」を正確に押さえましょう。ここを勘違いしていると、Wiresharkのパケットキャプチャを見たときに頭が真っ白になります。

  • シーケンス番号(Seq): 送信側が「このセグメントに含まれる先頭のデータバイトが、全体のストリームの中で何番目にあたるか」を示す番号。
  • 確認応答番号(ACK): 受信側が送信側に対して返す番号。これは単に「ここまでもらった」という過去形ではなく、「次に期待するシーケンス番号(Next Expected Sequence Number)」を指しています。

パケットの往復シミュレーション

例えば、クライアントからサーバーへWeb APIのリクエスト(JSONデータ)を送信するシーンを考えてみましょう。

1. コネクション確立(3ウェイハンドシェイク)

  • クライアント $\to$ サーバー: SYN (初期シーケンス番号: ISN = 1000)
  • サーバー $\to$ クライアント: SYN-ACK (サーバー側ISN = 5000, ACK = 1001 ※クライアントの1000番+SYNの1バイト分を進めた次を期待)
  • クライアント $\to$ サーバー: ACK (ACK = 5001)

これで双方向の通し番号のベースが決まりました。

2. データ送信とACKの交錯

  • クライアントが500バイトのHTTPリクエストを送信する。
  • Seq = 1001, Length = 500
  • サーバーはデータを受け取り、次に何番を期待するかを計算する。
  • 受け取った範囲は 1001 から 1500 までなので、次に期待する先頭番号は 1501。
  • サーバー $\to$ クライアントへACKを返す。
  • ACK = 1501

もしここで、ネットワークの気まぐれでクライアントからのデータが途中でロスト(消失)したとします。サーバーには何も届きません。サーバー側はいつまで経っても ACK = 1501 を返さないため、クライアントのTCPスタックに備わる再送タイマー(Retransmission Timer)が火を噴きます。

「おい、タイムアウトだ。さっきの500バイト、もう一回送るぞ」
クライアントは全く同じ Seq = 1001 のパケットを再送し、サーバーが無事に受け取って初めて ACK = 1501 が返る――これが、TCPの信頼性制御の基本原理です。

—

3. 実務で遭遇する「パケットの乱れ」とウィンドウ制御

実務のインフラ運用では、パケットが単に消えるだけでなく、「順番が入れ替わる(Out-of-Order)」現象にも直面します。複数の経路(マルチパスルーティング等)を通ってきたパケットが、宛先で前後して到着することがあるのです。

受信バッファと並べ替え(Reordering)

TCPの受信側は、仮に順番がバラバラにパケットが届いても、すぐに破棄したりパニックを起こしたりしません。各パケットが持つシーケンス番号を見て、受信バッファ(Receive Buffer)上で正しい順番に並べ替えます。

  • もし Seq = 2001 のパケットが先に届き、その手前であるはずの Seq = 1001 がまだ届いていない場合:
  • 受信側は「おや、手前のデータがまだだな」と察知し、直前の正常に受信している連続した境界である ACK = 1001 をあえて繰り返し送信します(これを重複ACK(Duplicate ACK)と呼びます)。
  • 送信側は同じACKが何度も返ってくることで、「おっと、途中のパケットが落ちているな」と察知し、タイムアウトを待たずに高速再送(Fast Retransmit)をトリガーします。

この一連の仕組みがあるおかげで、私たちは不安定なインターネット回線の上でも、バイト列が途切れたり壊れたりすることなく、安全にAPIのJSONや巨大なファイルをやり取りできているのです。

—

4. 実践:PythonとWireshark(tcpdump)でシーケンス番号を覗き見る

理屈は分かったところで、実際に私たちのコードがやり取りしているTCPセグメントの動きを覗き見してみましょう。

ここでは、Pythonの socket ライブラリを使ってシンプルなTCPサーバーを立て、そこに接続するクライアントコードを実行した際のパケットの挙動をイメージします。

バックエンドの挙動を模したPythonスクリプト

以下のコードは、クライアントからの接続を受け付け、データをエコーバックする簡易的なTCPサーバーです。実務のWebアプリケーション(Node.js, Go, Python等)がOSのソケット層でやっていることの本質はこれと同じです。

import socket

# TCP/IPソケットの作成 (IPv4, TCP)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 127.0.0.1のポート8080で待ち受け
server_socket.bind(('127.0.0.1', 8080))
server_socket.listen(1)

print("[*] サーバーがポート8080で待機中... 接続を待っています。")

while True:
    client_socket, client_address = server_socket.accept()
    print(f"[*] 接続確立: {client_address}")
    
    try:
        # クライアントからデータを受信 (最大1024バイト)
        data = client_socket.recv(1024)
        if data:
            print(f"[*] 受信データ: {data.decode('utf-8')}")
            
            # データをそのままオウム返し (ここでTCPのACKとSeqが裏で動く)
            client_socket.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")
    finally:
        client_socket.close()
        print("[*] コネクション切断\n")

このサーバーに対して、おなじみの curl コマンドでリクエストを飛ばしてみます。

curl -i http://127.0.0.1:8080/

パケットキャプチャ(tcpdump)での確認

この通信を裏で tcpdump などのパケットキャプチャツールで覗くと、まさに先ほど解説したシーケンス番号とACK番号のダンスが記録されます。

# 3ウェイハンドシェイク
12:00:00.100 IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [S], seq 100:100, win 65495...
12:00:00.101 IP 127.0.0.1.8080 > 127.0.0.1.54321: Flags [S.], seq 200:200, ack 101, win 65484...
12:00:00.101 IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [.], ack 201, win 65495...

# curlからのリクエスト送信 (Seqがどのように進むか)
12:00:00.102 IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [P.], seq 101:179, ack 201, win 65495, length 78
# サーバーからのACKとレスポンス返却
12:00:00.103 IP 127.0.0.1.8080 > 127.0.0.1.54321: Flags [.], ack 179, win 65484...

注目すべきは、seq 101:179 という部分です。これが「101バイト目から178バイト目まで(合計78バイト)」のHTTPリクエストデータが流れたことを示しており、それに対するサーバーからの返答には ack 179(「次は179バイト目を期待しているよ」)がしっかりと返されている点です。

—

5. インフラ・アプリエンジニアが知るべき実務上のTips

最後に、このTCPの信頼性制御の知識が、日々のインプラ・アプリ開発の現場でどのように活きるのか、実践的なTipsをいくつか授けましょう。

1. 「コネクションタイムアウト」と「再送タイムアウト(RTO)」の切り分け

APIがタイムアウトエラーを起こした際、アプリケーション層のタイムアウト(例: axios や requests の設定した timeout=5s)なのか、TCPの再送制御が限界を迎えた末の切断なのかを見極める必要があります。
ネットワークが細く、パケットロスが頻発している環境では、TCPの再送(Exponential Backoff:指数バックオフ)が繰り返された結果としてアプリケーションがタイムアウトエラーを吐きます。netstat や ss コマンドで RetransSegs(再送セグメント数)のカウンタが跳ね上がっていないかを確認する癖をつけましょう。

2. Linuxカーネルパラメータ(TCP Window / KeepAlive)のチューニング

高スループットを要求されるAPIサーバーや、膨大な同時接続を捌くリバースプロキシ(NginxやEnvoyなど)を構築する場合、TCPのウィンドウサイズやバッファサイズのチューニングが必須になります。
例えば、BDP(Bandwidth-Delay Product:帯域遅延積)が大きい広域ネットワーク(WAN)を跨ぐ通信では、デフォルトのTCPウィンドウサイズではパイプラインの太さを活かしきれません。/etc/sysctl.conf において以下のようなパラメータを適切に調整することが、実務の腕の見せ所となります。

# LinuxカーネルのTCP送受信バッファの最大値・デフォルト値の調整 (例)
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# TCP KeepAliveの調整(アイドル状態のゾンビコネクションを早期に検出して切断する)
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 60
net.ipv4.tcp_keepalive_probes = 5

3. MTUとMSSのミスマッチに潜む「パケットフラグメンテーションの罠」

意外と多いのが、VPN環境やクラウドのオーバーヘッド(VXLANやGeneveなど)を挟むネットワーク構成において、MSS(Maximum Segment Size)が適切にクランプされていないために、TCPセグメントが途中で分割され、パフォーマンスが著しく低下するトラブルです。
パケットのシーケンス番号やACKの往復以前に、レイヤー2/3の物理・ネットワーク層でパケットが断片化(Fragmentation)を起こすと、どれか1つのフラグメントが消えただけでTCP全体が再送を強いられます。トラブルシューティングの際は、必ず経路上のMSS(Path MTU Discoveryの動作など)を疑う視点を持ちましょう。

—

まとめ

TCPのシーケンス番号と確認応答番号(ACK番号)。一見すると枯れた地味な仕様に見えますが、この「一文字一文字に通し番号を振って、確実なバトンリレーを行う」という極めて泥臭い執念の積み重ねの上に、現代のモダンなWebアーキテクチャやクラウドインフラは成り立っています。

トラブルシューティングで壁にぶ1つ当たったとき、アプリケーションのログだけで悩むのではなく、「今、この瞬間にOSのネットワークスタックのなかで、どのようなシーケンスとACKのやり取りが行われているか」を脳内で、あるいはパケットキャプチャを通じてトレースできるようになれば、あなたも立派なネットワーク・インフラのスペシャリストです。

ネットワークのパケットは決して嘘をつきません。その声に耳を傾けられるエンジニアを目指して、日々のインフラに向き合っていきましょう。

コメント

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