TL

ソケットとTCP接続のカーネル状態管理

ackなのに接続が詰まる、backlog溢れでSYNが捨てられる――その原因をカーネル内部から解明。送受信キュー・acceptキュー・TCP状態機械・TIME_WAITの実体が腑に落ちます。

応用TCPソケットLinuxカーネルネットワークbacklog最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.1つのlisten中ソケットは2段のキューを持つ。半接続(SYN受信〜ACK待ち)はSYNキュー、3ウェイ完了済みでaccept待ちはacceptキューに入る。後者の上限がbacklogで、溢れるとACKが黙殺され接続が成立しない。
  • 2.確立済み接続ごとに送信キュー(未ACKデータ)と受信キュー(未readデータ)がカーネル内に常駐し、その上限がTCPウィンドウとフロー制御の実体になる。
  • 3.TIME_WAITは最後にcloseした側が2*MSL保持する状態で、遅延パケットの誤配送と相手の再送FINへの応答保証のために必須。短絡するとデータ破損のリスクがある。

ソケットは「カーネル内のオブジェクト」である

アプリから見たソケットは1個のファイルディスクリプタ(fd)に過ぎませんが、その裏側でカーネルは、接続ごとの状態・バッファ・タイマー・キューを束ねた制御ブロックを保持しています。Linuxでは確立済みTCP接続は struct sock(とTCP固有の tcp_sock)で表現され、受信パケットはここに紐づけられて処理されます(パケットがNICからここへ届くまでは /os/kernel-network-datapath/ を参照)。本記事は、この制御ブロックの中でも実務障害に直結する4要素――送受信キュー・backlog・TCP状態機械・TIME_WAIT――を内部から解剖します。

送信キューと受信キュー:バッファがフロー制御の実体

確立済み接続は、カーネル内に2つのバッファを持ちます。

  • 受信キュー(receive queue):相手から届き、TCP的に受理(順序が揃いACK済み)されたが、まだアプリが read していないデータ。read するとここから取り出され空く。
  • 送信キュー(send/write queue):アプリが write したが、まだ相手のACKが返っていないデータ。再送に備えてカーネルが保持し、ACKを受けて初めて解放する。

ここで重要なのは、受信キューの空き容量がそのまま相手へ広告する受信ウィンドウ(rwnd)になることです。アプリが read を怠ると受信キューが埋まり、広告ウィンドウが縮み、最終的に 0 を広告して相手の送信が止まります。これがTCPフロー制御の正体で、「遅いアプリが相手の送信ペースを律速する」仕組みはアプリのI/O速度とカーネルバッファが直結している証拠です。

ウィンドウは「バッファの空き」そのもの

受信ウィンドウは抽象概念ではなく、受信キューの残容量という具体的なメモリ量です。SO_RCVBUF/SO_SNDBUF(および自動チューニングの tcp_rmem/tcp_wmem)がこの上限を決めます。帯域遅延積(BDP)に対しバッファが小さいと、ウィンドウが頭打ちになりスループットが伸びません。逆に大きすぎるとbufferbloatで遅延が悪化します。

観点受信キュー送信キュー
溜まる契機相手からデータ到着アプリが write
空く契機アプリが read相手から ACK 到着
対応する制御受信ウィンドウ(rwnd)再送・輻輳ウィンドウ(cwnd)
溢れそうな時ウィンドウを縮め相手を止めるwrite がブロック / EAGAIN
主な上限SO_RCVBUF / tcp_rmemSO_SNDBUF / tcp_wmem

SYNキューとacceptキュー:listenの「2段構え」

サーバーが listen() したソケットは、接続要求を2つのキューで受けます。ここを混同すると backlog 障害の診断を誤ります。

クライアント        サーバー(listen中)
   │── SYN ──────────→ │  ① SYNキューに半接続を登録
   │←─ SYN+ACK ──────── │     (状態: SYN_RECV)
   │── ACK ──────────→ │  ② 3ウェイ完了 → acceptキューへ移動
                        │     (状態: ESTABLISHED)
                        │  ③ アプリの accept() が取り出す
  • SYNキュー(半接続キュー):SYNを受けてSYN+ACKを返し、最後のACKを待っている接続。状態は SYN_RECV。上限は tcp_max_syn_backlog
  • acceptキュー(完成接続キュー):3ウェイハンドシェイクが完了し、アプリの accept() を待っている接続。上限は listen(fd, backlog)backlog(ただしシステム上限 somaxconn で頭打ち)。

決定的なのは、acceptキューが満杯のときの挙動です。Linuxでは既定(tcp_abort_on_overflow=0)で、溢れた接続の最後のACKを黙殺します。クライアントは「SYN+ACKが届かなかった」と解釈してACKを再送し、サーバーが捌けるまで粘ります。つまりアプリの accept() が遅いと、ハンドシェイクは終わっているのに接続が前に進まない、という「見えにくい詰まり」が起きます。

“接続できない”の半分はacceptキュー溢れ

高負荷時の「タイムアウトするが原因不明」の多くは、acceptキュー溢れです。ss -lntRecv-Q(acceptキューの現在長)と Send-Q(その上限=実効backlog)で可視化できます。Recv-QSend-Q に張り付いていれば、アプリのaccept速度がコネクション到着に負けているサイン。nstatListenOverflows/ListenDrops も併せて確認します。対策はbacklog拡大ではなく、まずaccept処理の高速化/os/epoll-io-uring/ のイベント駆動など)です。

SYNキューに対しては別の防御もあります。SYNだけ送って最後のACKを送らないSYNフラッド攻撃でSYNキューを枯渇させられると正規接続が入れません。これを防ぐのが SYN cookieで、SYN_RECVの状態をキューに持たず、初期シーケンス番号に接続情報を暗号学的に埋め込んで返し、ACKが返って初めて状態を復元します。状態を持たないので枯渇しません。

TCP状態機械のカーネル表現

TCP接続は有限状態機械(FSM)であり、カーネルは各 sock にこの状態を1つ保持します。状態遷移は受信したセグメントのフラグアプリのシステムコールconnect/close/shutdown)で駆動されます。

状態意味代表的な遷移トリガ
LISTEN接続待ち受けlisten() で入る
SYN_SENT / SYN_RECVハンドシェイク途上connect / SYN受信
ESTABLISHEDデータ転送可能3ウェイ完了
FIN_WAIT_1/2, CLOSING自分から能動close進行中close() で FIN 送信
CLOSE_WAIT相手がclose、自分は未closeFIN受信(要・自分のclose)
TIME_WAIT能動closeの最終待機両方向のFIN/ACK完了

実務で頻発するのが CLOSE_WAIT の大量滞留です。これは「相手はFINを送ったのに、アプリが close() を呼んでいない」状態を意味します。カーネルはFINを受理しCLOSE_WAITに移りますが、その先の自分のFINを送るのはアプリの close() 次第。つまりCLOSE_WAITが溜まるのは**ほぼ確実にアプリ側のfdリーク(クローズ漏れ)**で、カーネルの問題ではありません。

CLOSE_WAIT と TIME_WAIT の責任分界

試験・面接の鉄板です。CLOSE_WAIT が多い=アプリのclose漏れ(コード修正案件)TIME_WAIT が多い=能動closeを大量に行った側で起きる正常現象で、原則チューニングは慎重に。どちらが「自分から閉じたか(能動close)」を握るかが切り分けの鍵で、TIME_WAITは先にcloseしてFINを送り、最後のACKを返した能動close側に現れます(受動close側ではなく、能動close側に偏る)。

TIME_WAIT:なぜ2*MSL待つのか

能動的にcloseした側は、最後に FIN への ACK を返したあと、即座に状態を消さず TIME_WAIT に入り、2*MSL(MSL=Maximum Segment Lifetime、典型的に各30秒〜60秒)だけ留まります。一見無駄に見えるこの待機には2つの必然があります。

  1. 遅延パケットの誤配送防止:ネットワークに残った旧接続の遅延セグメントが、同じ4タプル(送信元/宛先のIP・ポート)で張り直された新接続に混入するのを防ぐ。2*MSLは「往復で残存しうる最大寿命」で、これを待てば旧パケットは確実に消滅する。
  2. 相手の再送FINへの応答保証:自分が返した最後のACKが失われると、相手はFINを再送する。TIME_WAITで状態を残しておけば、その再送FINに再度ACKを返せる。状態を消していると RST を返してしまい、相手のcloseが異常終了する。
TIME_WAITの安易な短絡は危険

TIME_WAITが大量に出ると「枯渇」を恐れて即排除したくなりますが、tcp_tw_recycleNAT環境で正規接続を誤って弾く既知の地雷で、現在のカーネルでは撤去済みです。安全な緩和は (1) tcp_tw_reuse発信側で条件付きにTIME_WAITポートを再利用、timestamps前提)、(2) そもそも能動closeをサーバー側でなくクライアント側に寄せる設計、(3) コネクション再利用(keep-alive)でclose回数自体を減らすこと。状態を消す方向の力技は、上記2つの保証を壊しデータ破損や接続異常を招きます。

なお、TIME_WAITは「ポート」ではなく4タプル単位で接続を識別するため、宛先が異なれば同じローカルポートを多重に使えます。枯渇が現実問題化するのは、特定の単一宛先へ短命接続を大量に張る発信側に偏ります。ここを理解していれば、闇雲なカーネルパラメータ変更ではなく、接続設計で解くべき問題だと判断できます。

まとめ

まとめ

ソケットは「fd1個」ではなく、キュー・状態・タイマーを抱えたカーネル内オブジェクトです。確立済み接続の受信/送信キューはそれぞれ受信ウィンドウと再送の実体で、アプリのI/O速度がフロー制御に直結します。listenソケットは**SYNキュー(半接続)とacceptキュー(完成接続)**の2段構えで、後者の溢れ(=backlog超過)がACK黙殺による「見えない詰まり」を生みます。TCP状態機械ではCLOSE_WAIT滞留はアプリのclose漏れ、TIME_WAITは能動close側が遅延パケットと再送FINに備える正常な2*MSL待機――この責任分界を押さえれば、ss/nstat の数値から原因を一直線に特定できます。土台のシステムコールコストは /os/system-call/、到着通知の非同期機構は /os/interrupt-io/ を併読すると、ソケットの挙動が一枚の地図になります。

OS Article

ソケットとTCP接続のカーネル状態管理を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

TCP

比較で見る軸

難易度: advanced / カテゴリ: OS / タグ数: 6

導入後に効く点

確立済み接続ごとに送信キュー(未ACKデータ)と受信キュー(未readデータ)がカーネル内に常駐し、その上限がTCPウィンドウとフロー制御の実体になる。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
OS
タグ数
6

判断チェックリスト

  • 自社の用途が「TCP / ソケット」に近いか確認する。
  • 強みである「1つのlisten中ソケットは2段のキューを持つ。半接続(SYN受信〜ACK待ち)はSYNキュー、3ウェイ完了済みでaccept待ちはacceptキューに入る。後者の上限がbacklogで、溢れるとACKが黙殺され接続が成立しない。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

TCPソケットLinuxカーネルネットワークTCPソケットLinux
参考: 公式情報