- Published on
TCPネットワークスタック完全攻略 — 状態マシン、輻輳制御 (Cubic vs BBR)、Nagle、Delayed ACK、そしてQUICへの進化 (2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
0. 知っているようで知らないTCP
毎日のHTTPリクエストの裏には常にTCPがある。次の問いに答えられるだろうか。
ss -tanのTIME_WAITはなぜ30秒残るのか?- "connection reset by peer"と"broken pipe"の違いは?
- 1KBメッセージを毎秒100個送るのに4秒もかかることがあるのはなぜか?
- 10Gbpsネットワークでiperfが1Gbpsしか出ない理由は?
- GoogleはなぜTCPではなくUDP上にQUICを作ったのか?
この記事がその答え。TCPの状態マシン、輻輳制御の進化、悪名高い相互作用バグ、そしてQUIC時代の到来まで。
1. TCPの状態マシン — 11状態の旅
1.1 接続確立: 3-Way Handshake
Client Server
| SYN (seq=x) |
|----------------->| [LISTEN -> SYN_RCVD]
| SYN+ACK(y,x+1) |
|<-----------------|
| ACK (ack=y+1) |
|----------------->| [SYN_RCVD -> ESTABLISHED]
なぜ3回か?双方向のISN (Initial Sequence Number) 同期のため。各方向ごとに独立のISNを相手に通知し、確認を受ける必要がある。
1.2 接続終了: 4-Way Handshake
A B
| FIN |
|------------->| [ESTABLISHED -> CLOSE_WAIT]
| ACK |
|<-------------|
[FIN_WAIT_1 -> FIN_WAIT_2]
| FIN |
|<-------------| [CLOSE_WAIT -> LAST_ACK]
| ACK |
|------------->|
[FIN_WAIT_2 -> TIME_WAIT] [LAST_ACK -> CLOSED]
4回なのはTCPが全二重 (full-duplex) だから。各方向を独立に閉じる。
1.3 TIME_WAIT — 誤解されがちな状態
2MSL (Maximum Segment Lifetime) 待つ — 通常30秒〜2分。理由:
- 遅延パケットが新接続に混入するのを防ぐ: 同じポート対で素早く再接続すると、前の接続の遅延パケットが届きうる。
- 最終ACKの喪失対策: 相手がFINを再送した場合にACKで応答する必要がある。CLOSED状態ではRSTを返してしまう。
1.4 TIME_WAIT爆発
クライアントが大量の短命接続を開閉するとTIME_WAITが数万個溜まり、ephemeral portが枯渇する。
間違った対処: TIME_WAITを0にする (危険、データ汚染の恐れ)。
正しい対処:
net.ipv4.tcp_tw_reuse=1: 安全な条件下でephemeral portを再利用。- Keep-Alive接続 (HTTP/1.1 persistent、HTTP/2 multiplexing)。
- Connection pool (DBドライバ、HTTPクライアント)。
1.5 CLOSE_WAIT — アプリバグの兆候
TIME_WAITは正常だが、CLOSE_WAITが溜まるのはアプリのバグ。相手がFINを送ったのにclose()を呼んでいない。
$ ss -tan | grep CLOSE_WAIT | wc -l
50000 # ソケットリーク中
原因: try-finallyの欠落、ファイルディスクリプタ管理の例外処理漏れ。
2. TCPの信頼性メカニズム
2.1 シーケンス番号とACK
全バイトにシーケンス番号。受信者は「次に期待するバイト番号」をACKで返す。
送信: [1000][1001][1002][1003]
受信: ACK=1004 (1000-1003受領、次は1004)
途中で損失すると受信者は同じACKを繰り返し (duplicate ACK)、3つのdup ACKでFast Retransmit発動。
2.2 Retransmission — RTOとFast Retransmit
- RTO: RTT測定に基づく動的タイムアウト (Jacobson, 1988)。
- Fast Retransmit: 3 dup ACKでタイムアウトを待たず即再送。
- SACK: 「1000-2000と3000-4000受領、2000-3000が欠落」を精密に通知。
2.3 フロー制御 — Receive Window
受信者はrwndを広告。送信者はunacked_bytes < rwndの間だけ送信。
2.4 Window Scaling
TCPの16ビットwindow = 65,535バイト。10Gbps x 100ms RTT = 125MB必要。RFC 1323でshift分だけrwndを拡大。
sysctl net.ipv4.tcp_window_scaling
sysctl net.core.rmem_max
sysctl net.ipv4.tcp_rmem
3. 輻輳制御
3.1 1986年のCongestion Collapse
UC Berkeley〜LBLリンクが32Kbpsから40bpsへ — 1000倍の低下。原因は再送の嵐。Van Jacobsonの1988年論文がTCPに輻輳制御を導入した。
3.2 Congestion Window (cwnd)
送信可能 = min(rwnd, cwnd)
rwndは受信者が通知、cwndは送信者がネットワーク容量を推定。
3.3 Slow Start
初期cwnd = 10 MSS。ACKごとにcwnd += 1 MSS、RTTごとに倍増。
cwnd: 10 -> 20 -> 40 -> 80 -> 160
名前は「slow」だが実質指数成長。
3.4 Congestion Avoidance
ssthresh到達後、cwnd += 1 MSS per RTT (線形増加)。
3.5 損失検知
- 3 Dup ACK (Fast Retransmit): cwnd半減。
- Timeout: cwnd = 1 MSS、Slow Startからやり直し。
これがAIMD (Additive Increase, Multiplicative Decrease)、Renoの核心。
3.6 Reno から Cubic へ
高速長距離ではRenoは保守的すぎる。CUBIC (Linuxデフォルト) はcwndを時間の3次関数で増加させ、直前の損失点まで迅速に復帰する。
3.7 BBR — Googleの革命 (2016)
従来アルゴリズムは「損失 = 輻輳」と仮定。現代のネットではWiFiノイズやバッファブロートによる非輻輳損失が多い。
BBR (Bottleneck Bandwidth and RTT) のアイデア:
「損失ではなく、実際の帯域とRTTを直接測定してcwndを決めよう」
- 周期的に送信レートを上げて帯域を探索。
- 最小RTTを記憶 (バッファブロート検出)。
- cwnd = BW x RTTに近づけ、キューイングを最小化。
3.8 BBRの成果
Google google.comとYouTubeのBBR移行後:
- YouTube再バッファリング4%減。
- google.com応答時間減。
- 開発途上国ユーザーに特に効果。
Linux 4.9+に搭載。sysctl net.ipv4.tcp_congestion_control=bbrで有効化。
3.9 BBRの公平性論争
BBR v1はCubicと共存すると帯域を多く取る — 公平性問題。BBRv2 (2019)、v3 (2023) で改善。
| アルゴリズム | 特徴 | 用途 |
|---|---|---|
| Reno | AIMD、古典 | レガシー |
| Cubic | 3次関数増加 | Linuxデフォルト、WAN |
| BBR | BW/RTT直接測定 | 高速 + 非輻輳損失 |
| DCTCP | ECNマーキング | データセンター |
| CoPA | 低遅延優先 | ビデオ会議 |
4. Nagle と Delayed ACK — 最悪の組み合わせ
4.1 Nagleアルゴリズム
小パケットの山はオーバーヘッドが大きい (40バイトヘッダ / 1バイトデータ = 2.5%)。
Nagle: 「ACK未到着の小パケットがあれば、新しい小データはACKを待ってまとめて送れ」。
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int));
効率に良いが遅延が増える。
4.2 Delayed ACK
受信側も効率のためACKを即送信せず、次のデータに piggyback するか最大200ms待つ (Linuxデフォルトは40ms)。
4.3 Nagle + Delayed ACK = 40ms遅延
A: 最初の小パケット送信
B: ACK保留 (もっと来るかも)
A: 2つ目の小パケット、Nagleが ACKを待機
-> Nagle: 「ACK来ない、待とう」
-> Delayed ACK: 「データ来ない、40ms後にACK」
-> 40ms後ACK到着
A: 2つ目送信
リアルタイムアプリ (Telnet, SSH, リモートゲーム) では明示的にTCP_NODELAY必須。
4.4 TCP_CORK — 逆方向
「バッファに溜めて一気に送れ」。sendfile + TCP_CORKがnginxの静的ファイル最適化。
int cork = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(int));
writev(fd, iov, 10);
cork = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(int));
5. TCP Fast Open — Handshakeをスキップ
HTTPリクエストごとに最低1.5 RTTの handshake コスト。
5.1 TFO (RFC 7413, 2014)
初回接続でサーバがcookieを発行。以降はクライアントがSYNにcookie + リクエストを乗せる。
1回目: SYN -> SYN+ACK(cookie) -> ACK -> GET
2回目: SYN(cookie, GET) -> SYN+ACK(data)
5.2 普及しなかった理由
- ミドルボックス (FW、NAT) が非標準SYNを破棄。
- サーバ側cookie状態管理の負担。
- HTTP/2のconnection reuseで緩和済み。
QUICの0-RTT handshakeが問題を回避。
6. よくあるエラーの本当の意味
6.1 Connection refused
SYNにRST応答 — 相手ポートがlistenしていない。
6.2 Connection reset by peer
ESTABLISHED中にRST受信 — プロセスクラッシュ、SO_LINGER 0、FWのactive reset。
6.3 Broken pipe
相手が既にクローズしたソケットにwrite。
6.4 Connection timed out
SYNを何度送っても応答なし (5-7回再送、60秒+)。ブラックホールFWや相手ダウン。
6.5 No route to host
ルーティングテーブルに経路なし — VPN切断、ルーティング設定ミス。
7. 実務チューニング — 主要sysctl
7.1 Backlog とキュー
net.core.somaxconn # listen backlog上限
net.ipv4.tcp_max_syn_backlog # SYN_RCVDキュー
net.core.netdev_max_backlog # NIC -> カーネルキュー
nginx listen 80 backlog=65535にはカーネル上限も必要。
7.2 TIME_WAIT
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=30
net.ipv4.ip_local_port_range
7.3 Keep-alive
net.ipv4.tcp_keepalive_time=600
net.ipv4.tcp_keepalive_intvl=60
net.ipv4.tcp_keepalive_probes=3
デフォルト2時間はLB背後には長すぎる。
7.4 バッファ
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem="4096 131072 16777216"
net.ipv4.tcp_wmem="4096 131072 16777216"
BDPに合わせる。10Gbps x 50ms = 62MB。
7.5 輻輳制御
net.ipv4.tcp_congestion_control=bbr
net.core.default_qdisc=fq
8. QUIC — TCPを捨てる
8.1 TCPの根本的限界
- Head-of-Line Blocking: 1パケット損失で後続全ストリーム停止。HTTP/2も同じTCP上で継承。
- Handshakeコスト: TCP 3-way + TLS 1.2 2-RTT = 3 RTT。
- ミドルボックスの硬直化: 新TCPオプションはISP FWで弾かれる。TFOやMPTCPが失敗した理由。
- カーネル依存: TCP改善にカーネルアップグレードが必要。
8.2 QUICの解法 — UDP上のユーザー空間トランスポート
Google実験 (2012) -> IETF標準 (RFC 9000, 2021) -> HTTP/3の基盤。
- UDP上 — ミドルボックスには通常UDPに見える。
- ユーザー空間ライブラリ — アプリと一緒に配布。
- TLS 1.3統合。
- 真のストリーム多重化 — ストリーム間のHoLなし。
8.3 0-RTT Handshake
再接続時は以前のsession ticketで初回パケットにデータを乗せる。
初回: QUIC handshake (1 RTT)
再接続: データ即送信 (0 RTT)
8.4 Connection Migration
Connection IDで識別するため、IPが変わっても接続維持。モバイルでWiFi <-> 4G切替が無停止。
8.5 HTTP/3 = HTTP over QUIC
HTTP意味論はHTTP/2と同じ、トランスポートのみQUICに置換。主要ブラウザとCDN (Cloudflare, Akamai, Fastly) が対応。
8.6 QUICの欠点
- CPU使用量増 (暗号化必須、ユーザー空間)。
- ミドルボックス互換性 (一部FWはUDP制限)。
- 実装複雑度。
- 観察困難 — 暗号化ペイロード、ツール少。
Google、Metaの社内計測: HTTP/3はモバイルで約10%遅延減、デスクトップは差小。
9. 実戦デバッグツールキット
9.1 接続状態確認
ss -tan
ss -tan state established
ss -tnp | grep :443
ss -s
9.2 パケットキャプチャ
tcpdump -i any -w capture.pcap 'port 443'
wireshark capture.pcap
9.3 輻輳制御の観察
ss -ti
出力例:
ESTAB ... cubic cwnd:10 ssthresh:7 bytes_acked:1234 bytes_received:5678 rtt:25.3/3.1 rcv_rtt:25.1 delivered:10 ...
cwndが小さくretransが多ければ輻輳発生中。
9.4 bpftrace
bpftrace -e 'kprobe:tcp_retransmit_skb { printf("retrans pid=%d\n", pid); }'
9.5 Mtr
mtr -r -c 100 example.com
各ホップの損失率 + RTTでISP品質診断。
10. おわりに — 50年のTCP、そしてその先
TCPは1974年のVint CerfとBob Kahnの論文に始まる。マイルストーン:
- 1988: Jacobson輻輳制御。
- 1992: Window Scaling。
- 1996: SACK。
- 2006: Cubic。
- 2016: BBR。
- 2021: QUIC (RFC)。
QUICはトランスポート層をユーザー空間へ移した。アプリごとに独自のトランスポート戦略を持て、カーネル更新なしに進化できる。Facebookのmvfst、Cloudflareのquiche、GoogleのgQUICが次の10年のインターネットを形作る。一方でTCPは消えない。トラフィックの80%+は今もTCP。
次回はTLS/SSLとPKIの内部 — 証明書チェイン、暗号スイート、0-RTTのreplay危険、QUICのTLS統合、そしてポスト量子暗号の到来。
参考資料
- RFC 9293 — Transmission Control Protocol (2022改訂)。
- Van Jacobson — "Congestion Avoidance and Control" (SIGCOMM, 1988)。
- Cardwell et al — "BBR: Congestion-Based Congestion Control" (ACM Queue, 2016)。
- RFC 9000 — QUIC。
- RFC 9114 — HTTP/3。
- "Computer Networks: A Systems Approach" — Peterson & Davie。
- Brendan Gregg — Linux Performanceブログ。
- Marek Majkowski (Cloudflare) — TCP内部関連記事。
- "High Performance Browser Networking" — Ilya Grigorik (O'Reilly, 2013)。