Linux TCP接続管理を徹底解説|SYN Queue・Accept Queue・backlog・accept()の仕組み

はじめに

WebサーバやAPサーバでは、毎秒数百~数万件ものTCP接続が確立されています。普段は意識することはありませんが、Linuxカーネルは接続要求を効率的に処理するために、複数のキューを利用してTCP接続を管理しています。

代表的なものが、SYN QueueAccept Queueです。

これらのキューは、TCPの3ウェイハンドシェイクの途中や接続確立後の状態を管理しており、Webサーバやデータベースサーバなど、TCP通信を利用するあらゆるアプリケーションで重要な役割を果たしています。

例えば、次のような障害に遭遇したことはないでしょうか。

  • アプリケーションは起動しているのに接続できない
  • 一時的に大量アクセスが発生すると接続エラーになる
  • CPU使用率はそれほど高くないのにタイムアウトが発生する

このような問題は、SYN QueueやAccept Queueの仕組みを理解することで原因を切り分けられる場合があります。

本記事では、TCP接続が確立されるまでの流れを整理したうえで、Linuxカーネルが内部でどのように接続を管理しているのかを詳しく解説します。また、SYN QueueとAccept Queueの違い、listen()やaccept()との関係、さらに障害発生時の調査方法やチューニングポイントについても紹介します。

LinuxでTCP通信を運用・設計・障害解析するエンジニアにとって、ぜひ理解しておきたい内容です。

なお、Linux通信の基本的な仕組み、パケット受信に関する情報は以下でまとめていますので、合わせてご参照ください。
Linuxネットワーク通信の仕組みを徹底解説|TCP/IPスタック・ソケット・NIC・仮想スイッチまで
Linuxネットワーク受信処理を徹底解説|RX Ring Buffer・NAPI・SoftIRQ・Socket Buffer・TCP受信ウィンドウ

TCP接続確立(3ウェイハンドシェイク)の流れ

TCPはコネクション型のプロトコルであり、データ通信を開始する前に、クライアントとサーバの間で接続を確立する必要があります。この接続確立の手順を3ウェイハンドシェイク(Three-way Handshake)と呼びます。

処理の流れは以下のとおりです。

それぞれのパケットには次の役割があります。

  1. SYN
    クライアントはサーバへ接続要求を送信します。この時点ではまだ接続は確立しておらず、「これから接続したい」という要求だけを送っています。クライアントのTCP状態は SYN_SENT になります。
  2. SYN+ACK
    サーバは接続要求を受信すると、要求を受け付けたことを通知するため、SYNとACKを同時に返します。この段階ではサーバ側もまだ接続を確立しておらず、「接続要求を受け付けたので、最後のACKを送ってください」という状態です。サーバのTCP状態は SYN_RECV になります。
  3. ACK
    クライアントは最後のACKを返信します。このACKをサーバが受信した時点で、TCP接続は正式に確立されます

TCP状態の変化をまとめると次のようになります。

クライアントサーバ内容
SYN_SENTLISTEN接続要求を送信
ESTABLISHEDSYN_RECVSYN+ACKを受信
ESTABLISHEDESTABLISHEDACK受信後、接続確立

ここで重要なのは、サーバはSYNを受信した時点ではまだ接続を確立していないという点です。TCP接続は一度に確立されるのではなく、Linuxカーネル内部では段階的に管理されているのです。

Linuxが管理する接続キュー

Linuxでは、TCP接続要求を受信すると、接続の状態に応じてSYN QueueAccept Queueを利用しながら管理しています。全体の流れを図で表すと、以下のようになります。

TCP接続は、以下の順番で処理されます。

  1. クライアントからSYN送信
    クライアントはサーバとのTCP接続を開始するため、SYNパケットを送信します。
  2. Linux KernelがSYN Queueに登録
    サーバのLinux KernelはSYNパケットを受信すると、接続要求(request_sock)を生成し、一般的にSYN Queueと呼ばれる管理領域へ登録します。この時点ではTCP接続はまだ確立しておらず、接続状態はSYN_RECVです。
  3. Linux KernelがSYN/ACKを送信
    Linux KernelはSYNパケットに対する応答として、SYN/ACKパケットをクライアントへ送信します。これにより、クライアントへ接続要求を受け付けたことを通知し、最後のACKを待機します。
  4. クライアントがACK送信
    クライアントはSYN/ACKを受信すると、ACKパケットをサーバへ送信します。Linux KernelがこのACKを受信すると、TCP 3-wayハンドシェイクが完了し、TCP接続がESTABLISHED状態になります。
  5. Linux KernelがSYN QueueからAccept Queueへ登録
    ACKを受信してTCP接続が確立すると、Linux Kernelはrequest_sockからchild socketを生成し、Accept Queueへ登録します。Accept Queueには、アプリケーションがaccept()を呼び出すまでの間、接続済みのソケットが保持されます。
  6. Apache/Nginxがaccept()を呼び出し、Linux KernelがAccept Queueから接続を取り出す
    ApacheやNginxなどのWebサーバは、accept()システムコールを実行して新しい接続を受け付けます。accept()が呼び出されると、Linux KernelはAccept Queueから接続済みソケットを取り出してアプリケーションへ渡し、その後HTTPリクエストの処理が開始されます。

SYN QueueとAccept Queueの違い

両者は「接続待ち」という点では似ていますが、管理している接続状態は大きく異なります。

項目SYN QueueAccept Queue
接続状態SYN_RECVESTABLISHED
接続確立未完了完了
保持目的ACK待ちaccept()待ち
主な原因SYN Flood、通信遅延、アクセス集中アプリケーション処理遅延、CPU高負荷
主な対策tcp_max_syn_backlog、SYN Cookiebacklog、somaxconn、アプリケーション性能改善

障害調査では、この違いを理解しているかどうかで切り分けのスピードが大きく変わります。

2つのキューが必要な理由

接続途中と接続完了後では、管理すべき情報や処理内容が異なります。もし1つのキューですべてを管理すると、接続途中のものと接続済みのものを区別しながら処理しなければならず、管理が複雑になります。

そのためLinuxでは、

  • 接続途中はSYN Queue
  • 接続完了後はAccept Queue

というように役割を分離することで、高速かつ効率的に大量のTCP接続を処理しています。

SYN Queue

まずは、TCP接続が確立する前の接続を管理するSYN Queueについて詳しく見ていきます。ここで重要なのは、SYN Queueに登録されている接続はまだ接続途中であるということです。そのため、この段階ではアプリケーションはこの接続を利用できません。

SYN Queueに保持される情報

TCPでは3ウェイハンドシェイクが完了するまで接続は成立しません。しかし、サーバはSYNパケットを受信した時点で、接続に必要な情報を保持しておく必要があります。

以下のような情報が保持されます。

接続元・接続先情報

  • 送信元IPアドレス
  • 送信元ポート番号
  • 宛先IPアドレス
  • 宛先ポート番号

TCP接続状態

  • SYN_RECV 状態
  • 3-way handshake完了待ちの接続情報

TCP接続確立に必要な情報

  • クライアントから受信したSYNパケットの情報
  • サーバ側の初期シーケンス番号(ISN)
  • クライアント側の初期シーケンス番号
  • TCPオプション情報
    • MSS(Maximum Segment Size)
    • Window Scale
    • SACK Permitted
    • Timestamp

再送制御情報

  • SYN+ACK再送に必要なタイマー情報
  • 再送回数などの管理情報

接続要求管理情報

  • request_sockへの参照
  • 親となるlisten socketの情報
  • 3-way handshake完了後にchild socketを生成するための情報

ハッシュ管理情報

  • SYN管理領域(syn_table)内で管理するための情報
  • 受信した接続要求を検索するためのキー情報

これらの情報を保持しておくことで、最後のACKを受信した際に、すぐにESTABLISHED状態へ遷移できます。

SYN Queueのサイズ

SYN Queueには保持できる接続数に上限があります。この上限は、カーネルパラメータ net.ipv4.tcp_max_syn_backlog で管理されます。

現在の設定は次のように確認できます。実行例を以下に示します。

[root@localhost ~]# sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 4096
[root@localhost ~]#

上記の通り値が4096の場合、3ウェイハンドシェイク中の接続を最大4096件保持できます。大量アクセスを受けるWebサーバでは、この値を大きく設定することで、一時的なアクセス集中に耐えやすくなります。

SYN Queueの状態確認

現在SYN Queueに存在する接続は、ssコマンドで確認できます。

[root@localhost ~]# ss -tan state syn-recv
Recv-Q     Send-Q                   Local Address:Port                    Peer Address:Port
0          0              [::ffff:192.168.56.111]:5201         [::ffff:192.168.56.101]:9682
0          0              [::ffff:192.168.56.111]:5201         [::ffff:192.168.56.101]:30457
0          0              [::ffff:192.168.56.111]:5201         [::ffff:192.168.56.101]:44086
0          0              [::ffff:192.168.56.111]:5201         [::ffff:192.168.56.101]:42776
0          0              [::ffff:192.168.56.111]:5201         [::ffff:192.168.56.101]:54836

通常、SYN_RECV状態の接続は短時間でESTABLISHEDへ遷移するため、多数表示されることはあまりありません。

一方で、大量のSYN_RECVが長時間残っている場合は、

  • SYN Flood攻撃
  • クライアントとの通信遅延
  • ACKパケットの欠落
  • SYN Queue不足

などを疑う必要があります。

SYN Queueが溢れるケース

SYN Queueが満杯になる主な原因は次のとおりです。

  • 一時的なアクセス集中
  • SYN Flood攻撃
  • クライアントからACKが返ってこない
  • ネットワーク遅延
  • tcp_max_syn_backlogが小さい

この状態では、新しいSYNパケットを保持する領域がなくなるため、Linuxカーネルは接続要求をそのまま受け付けることができません。

設定によって動作は多少異なりますが、代表的な挙動としては以下があります。

  • SYN Cookieを利用して接続情報を保持しない
  • SYNパケットを破棄する
  • SYN+ACKを再送する
  • クライアント側で接続タイムアウトが発生する

その結果、利用者から見ると、

  • 接続が非常に遅い
  • 接続タイムアウトになる
  • 一部のユーザーだけ接続できない

といった現象が発生します。

SYN Queueが溢れた場合の確認方法

SYN QueueはLinuxカーネル内部で管理されているため、SYN Queueが一杯になったことを直接表示するコマンドはありません。 そのため、複数の情報を組み合わせて判断します。

SYN_RECVの接続数を確認する

SYN Queueに登録されている接続数は、SYN_RECV状態のソケット数から確認できます。
ここでは検証用にSYN Queueの数を16に設定しております。SYN Queueの数が14となっており、SYN Queueが逼迫していると考えられます。

[root@localhost ~]# sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 16
[root@localhost ~]#
[root@localhost ~]# ss -tan state syn-recv | wc -l
14
[root@localhost ~]#

dmesgでカーネルメッセージを確認する

SYN Queueが満杯になりSYN Floodと判断されると、Linuxカーネルは以下のようなメッセージを出力します。

[root@localhost ~]# dmesg |grep flood
[ 9385.929152] TCP: request_sock_TCP: Possible SYN flooding on port [::]:5201. Sending cookies.
[root@localhost ~]#

このメッセージが表示された場合、LinuxカーネルはSYN Queueが逼迫したと判断しています。なお、「Sending cookies.」は後述するSYN Cookieの設定が有効な場合のみ表示されます。

nstatでTCP統計を確認する

LinuxカーネルはTCPの統計情報を保持しています。nstatコマンドで以下の通り確認することが可能です。累積件数なので、時間を空けて複数回コマンドを実行してください。

[root@localhost ~]# nstat -az | egrep "TCPReqQ|Listen|Syncookies"
TcpExtSyncookiesSent            2145014            0.0
TcpExtSyncookiesRecv            0                  0.0
TcpExtSyncookiesFailed          6                  0.0
TcpExtListenOverflows           0                  0.0
TcpExtListenDrops               4119081            0.0
TcpExtTCPReqQFullDoCookies      2145014            0.0
TcpExtTCPReqQFullDrop           0                  0.0
TcpExtTCPFastOpenListenOverflow 0                  0.0
[root@localhost ~]#
[root@localhost ~]# nstat -az | egrep "TCPReqQ|Listen|Syncookies"
TcpExtSyncookiesSent            2145014            0.0
TcpExtSyncookiesRecv            0                  0.0
TcpExtSyncookiesFailed          6                  0.0
TcpExtListenOverflows           0                  0.0
TcpExtListenDrops               4148005            0.0
TcpExtTCPReqQFullDoCookies      2145014            0.0
TcpExtTCPReqQFullDrop           0                  0.0
TcpExtTCPFastOpenListenOverflow 0                  0.0
[root@localhost ~]#

主な確認項目は以下のとおりです。

項目説明確認ポイント
TcpExtTCPReqQFullDoCookiesSYN Queue(TCP Request Queue)が満杯になったため、SYN Cookieで応答した回数。tcp_syncookies=1の場合に増加する。値が増加していれば、SYN Queueが逼迫し、LinuxがSYN Cookieへ切り替えたことを示す。
TcpExtTCPReqQFullDropSYN Queueが満杯となり、新規SYNパケットを破棄した回数。主にtcp_syncookies=0の場合に増加する。値が増加していれば、SYN Queueがあふれ、新規接続要求を受け付けられなかったことを示す。
TcpExtListenDropsLISTENソケットで処理できずに破棄された接続要求の回数。RHELでは、SYN Queueの逼迫時にこのカウンタへ計上される場合がある。RHEL環境では、この値の増加もSYN Queue逼迫の重要な判断材料となる。本検証では本値が増加しています。

tcpdumpで通信内容を確認する

実際にSYNパケットが滞留していることはtcpdumpでも確認できます。

[root@localhost ~]# tcpdump -i any tcp port 5201 -w tcpdump.pcap

例えば以下のようにクライアントからACKが返却されない場合、接続はSYN Queueに保持されたままとなります。

ちなみに、今回は検証用にクライアント側で以下のフィルタ設定を行うことで、ACKを返却しないようにしています。

[root@ ~]# iptables -I OUTPUT -p tcp --tcp-flags RST RST -j DROP

SYN Cookieによる対策

SYN Queueが満杯になると、新しい接続要求を保持できなくなります。この問題を軽減するため、LinuxにはSYN Cookieという仕組みがあります。SYN Cookieを有効にすると、SYN Queueへ接続情報を保持せず、SYN+ACKに必要な情報をシーケンス番号へ埋め込んで返信します。クライアントから最後のACKを受信したタイミングで接続情報を生成するため、大量のSYN要求を受けてもメモリを消費しにくくなります。

設定は以下のコマンドで確認できます。値が1の場合は設定が有効になっています。

[root@localhost ~]# sysctl net.ipv4.tcp_syncookies
net.ipv4.tcp_syncookies = 1
[root@localhost ~]#

一般的なLinuxディストリビューションでは、デフォルトで有効になっていることが多く、大量アクセスやSYN Flood攻撃への対策として利用されています。

補足:SYN QueueはFIFOキューではない

Linuxでは、一般的に「SYN Queue」と呼ばれていますが、これは概念的な名称であり、カーネル内部では単純なFIFOキューとして管理されているわけではありません。SYN_RECV状態の接続要求はrequest_sockとして生成され、hashベースの管理構造で保持されます。この管理に利用されるのが syn_table です。syn_table は、listen_sock が保持するハッシュテーブルであり、SYN受信後に生成された request_sock を格納します。

Linuxカーネルソース(include/net/request_sock.h)では、以下のように定義されています。

struct listen_sock {
    ...
    struct request_sock *syn_table[0];
};

include/net/request_sock.h – pub/scm/linux/kernel/git/jberg/mac80211-next – Git at Google

このため、SYN_RECV状態の接続要求はhashベースで管理され、受信順に並ぶFIFO構造ではありません。

一方、Accept Queueは FIFOキューで、アプリケーションがaccept()で順番に取得します。

Accept Queue

Accept Queueは、Accept Queueは、3-way handshakeが完了した接続を保持し、
アプリケーションがaccept()を呼び出すまで待機させるキューです。

クライアントから最後のACKを受信すると、TCP接続は正式に確立されます。しかし、接続が確立した瞬間にアプリケーションが通信を開始するわけではありません。Linuxカーネルは、接続済みのソケットをAccept Queueへ格納し、アプリケーションが受け取るまで待機させます。

Accept Queueに保持される情報

以下のような情報が保持されます。

接続元・接続先情報

  • 送信元IPアドレス
  • 送信元ポート番号
  • 宛先IPアドレス
  • 宛先ポート番号

TCP接続状態

  • 3-way handshake完了後の ESTABLISHED 状態
  • TCPセッションの状態管理情報

TCP制御情報

  • 送信シーケンス番号
  • 受信確認番号(ACK番号)
  • 受信ウィンドウサイズ
  • 輻輳制御に関する情報

送受信バッファ情報

  • 受信待ちデータ
  • 送信待ちデータ
  • ソケットバッファ使用量

ソケット管理情報

  • 子ソケット(child socket)への参照
  • ファイルディスクリプタ生成前のカーネル内部ソケット情報
  • 所属するプロセスへ渡すためのキュー管理情報

キュー管理情報

  • Accept Queueの先頭ポインタ(rskq_accept_head)
  • Accept Queueの末尾ポインタ(rskq_accept_tail)

Accept Queueのサイズ

Accept Queueには保持できる接続数に上限があります。この上限は、アプリケーションがlisten()で指定する backlog 値と、カーネルパラメータ net.core.somaxconn によって決まります。

SYN Queueが3ウェイハンドシェイク中(SYN_RECV状態)の接続要求を保持するのに対し、Accept Queueは3ウェイハンドシェイク完了後、アプリケーションがaccept()するまでの確立済み接続を保持します。

net.core.somaxconn の現在の設定は、以下のコマンドで確認できます。

[root@localhost ~]# sysctl net.core.somaxconn
net.core.somaxconn = 4096
[root@localhost ~]#

上記の例では、net.core.somaxconn が4096に設定されています。ただし、Accept Queueの最大サイズは、単純にこの値だけで決まるわけではありません。サーバアプリケーションがlisten()システムコールで指定するbacklog値も影響します。

例えば、アプリケーションが listen(sockfd, 10000) のようにbacklogを指定した場合でも、カーネルパラメータ net.core.somaxconn が4096であれば、実際に利用できるAccept Queueの最大サイズは4096に制限されます。逆に、listen()のbacklog値が128の場合は、Accept Queueの最大サイズは128になります。

つまり、Accept Queueの最大サイズは、listen()のbacklog値、net.core.somaxconnの小さい方の値になります。

大量アクセスを受けるWebサーバでは、リクエスト処理が一時的に追いつかない場合でも、確立済み接続を保持できるようにAccept Queueのサイズを適切に設定することが重要です。

しかし、Accept Queueを大きくすれば一時的に多くの接続を待機させることはできますが、アプリケーションの処理能力が向上するわけではありません。接続の到着速度がaccept()の処理能力を上回り続けると、いずれAccept Queueは満杯になります。

そのため、Accept Queueの調整だけでなく、ワーカースレッド数の見直しやCPU性能の向上、アプリケーション処理の高速化なども合わせて検討することが重要です。

Accept Queueの状態確認

Accept Queueの状態はssコマンドまたはnstatコマンドを利用して確認できます。

ssコマンドによる確認

LISTEN状態のソケットに対して、以下のssコマンドを実行します。

[root@localhost ~]# ss -lnt
State           Recv-Q          Send-Q                     Local Address:Port                     Peer Address:Port
LISTEN          0               128                              0.0.0.0:22                            0.0.0.0:*
LISTEN          17              16                                     *:5201                                *:*
LISTEN          0               128                                 [::]:22                               [::]:*
[root@localhost ~]#

LISTEN状態のソケットでは、以下のような意味になります。

  • Recv-Q
    • 現在Accept Queueに保持されている接続数
    • 3-way handshake完了後、アプリケーションがaccept()する前の接続数
  • Send-Q
    • Accept Queueの最大サイズ
    • listen()のbacklog値とカーネルパラメータnet.core.somaxconnによって決まる上限値

Accept Queueの使用率が高くなると、アプリケーションが接続を取得する前に待機領域が不足し、新規接続の遅延や接続失敗につながる可能性があります。

今回の例では、Recv-Qが17、Send-Qが16となっており、Accept Queueが最大サイズまで使用されていることが分かります。

「Recv-QがSend-Qより大きいのはおかしいのでは?」と思うかもしれませんが、Linuxカーネルでは内部処理のタイミングによって、一時的にRecv-QがSend-Qを1件程度上回って表示されることがあります。そのため、このような表示は異常ではありません。

カーネル統計情報の確認

以下とおり、nstatコマンドを実行します。

[root@localhost ~]# nstat -az | grep Listen
TcpExtListenOverflows           656                0.0
TcpExtListenDrops               4591982            0.0
TcpExtTCPFastOpenListenOverflow 0                  0.0
[root@localhost ~]#
[root@localhost ~]# nstat -az | grep Listen
TcpExtListenOverflows           902                0.0
TcpExtListenDrops               4592228            0.0
TcpExtTCPFastOpenListenOverflow 0                  0.0
[root@localhost ~]#

確認対象となる主なカウンタは以下です。

  • TcpExtListenOverflows
    • Accept Queueが満杯になり、接続を処理できなかった回数
  • TcpExtListenDrops
    • Listen処理中に破棄された接続数

これらの値が増加している場合、Accept Queueの枯渇が発生している可能性があります。

その他状態確認

Accept Queueの枯渇は、ネットワーク障害ではなく、サーバ側の処理能力不足によって発生するケースがあります。

例えば、

  • クライアントからのSYNは正常に到達している
  • TCP 3-way handshakeは完了している
  • しかしアプリケーションがaccept()できていない

という場合、確立済み接続がAccept Queueに滞留します。

そのため、障害調査では以下を組み合わせて確認することが重要です。

  • ss -lntによるAccept Queue使用状況確認
  • nstatによるListenエラー確認
  • topやsarによるCPU使用率確認
  • Webサーバ・APサーバのログ確認

まとめ

本記事では、LinuxカーネルにおけるSYN QueueとAccept Queueの仕組みについて解説しました。

ポイントは以下のとおりです。

  • SYN QueueはSYN_RECV状態の接続要求を管理する
  • Linux内部ではrequest_sockとsyn_tableによるハッシュ管理となっている
  • Accept QueueはESTABLISHED状態の接続を保持するFIFOキュー
  • backlogやsomaxconnはAccept Queueのサイズに影響する
  • Queueの状態はssやnstatで確認でき、障害調査では重要な指標となる

TCP接続の仕組みを理解することで、「接続できない」「大量アクセスでタイムアウトする」といった障害の切り分けが容易になります。実際の運用では、キューのサイズだけでなく、アプリケーションの処理能力やCPU負荷もあわせて確認することが重要です。

参考サイト

TCP Socket Listen: A Tale of Two Queues (2022)

コメント