ソケットとTCP接続のカーネル状態管理
ackなのに接続が詰まる、backlog溢れでSYNが捨てられる――その原因をカーネル内部から解明。送受信キュー・acceptキュー・TCP状態機械・TIME_WAITの実体が腑に落ちます。
- 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_rmem | SO_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キュー溢れです。ss -lnt の Recv-Q(acceptキューの現在長)と Send-Q(その上限=実効backlog)で可視化できます。Recv-Q が Send-Q に張り付いていれば、アプリのaccept速度がコネクション到着に負けているサイン。nstat の ListenOverflows/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、自分は未close | FIN受信(要・自分のclose) |
| TIME_WAIT | 能動closeの最終待機 | 両方向のFIN/ACK完了 |
実務で頻発するのが CLOSE_WAIT の大量滞留です。これは「相手はFINを送ったのに、アプリが close() を呼んでいない」状態を意味します。カーネルはFINを受理しCLOSE_WAITに移りますが、その先の自分のFINを送るのはアプリの close() 次第。つまりCLOSE_WAITが溜まるのは**ほぼ確実にアプリ側のfdリーク(クローズ漏れ)**で、カーネルの問題ではありません。
試験・面接の鉄板です。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つの必然があります。
- 遅延パケットの誤配送防止:ネットワークに残った旧接続の遅延セグメントが、同じ4タプル(送信元/宛先のIP・ポート)で張り直された新接続に混入するのを防ぐ。2*MSLは「往復で残存しうる最大寿命」で、これを待てば旧パケットは確実に消滅する。
- 相手の再送FINへの応答保証:自分が返した最後のACKが失われると、相手はFINを再送する。TIME_WAITで状態を残しておけば、その再送FINに再度ACKを返せる。状態を消していると
RSTを返してしまい、相手のcloseが異常終了する。
TIME_WAITが大量に出ると「枯渇」を恐れて即排除したくなりますが、tcp_tw_recycle はNAT環境で正規接続を誤って弾く既知の地雷で、現在のカーネルでは撤去済みです。安全な緩和は (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、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。