Linuxネットワーク受信処理を徹底解説|RX Ring Buffer・NAPI・SoftIRQ・Socket Buffer・TCP受信ウィンドウ

はじめに

Linuxでは、Webサーバやデータベース、SSHなど、さまざまなアプリケーションが日々大量のネットワーク通信を処理しています。しかし、ネットワークから受信したパケットが、Linuxカーネル内部でどのような経路を通り、最終的にアプリケーションへ届けられるのかを詳しく理解している方は意外と少ないのではないでしょうか。

以前公開した「Linuxネットワーク通信の仕組みを徹底解説」では、アプリケーションからソケット、TCP/IPスタック、NICドライバを経由してネットワークへ送受信される全体像について解説しました。

「通信処理は Application → Socket → TCP/IP → NIC Driver → Network の流れで行われる」

という流れを理解することで、Linuxにおけるネットワーク通信の基本的な仕組みを把握できます。Linuxネットワーク通信の仕組みを徹底解説|TCP/IPスタック・ソケット・NIC・仮想スイッチまで

一方で、実際の障害解析や性能分析では、さらに一歩踏み込んだ理解が求められます。

例えば、

・CPU使用率(%soft)が急激に上昇している
・ksoftirqd がCPUを消費している
・パケットロスが発生している
・rx_dropped が増加している
・Recv-Q が溜まり続けている

といった現象は、Linuxカーネル内部のどこかで処理が滞っていることを示している可能性があります。

このような問題を切り分けるためには、Linuxカーネル内に存在するバッファやキューの役割を理解することが重要です。

ネットワーク受信処理では、パケットは一度にアプリケーションへ届くわけではありません。受信したパケットは、NICの受信キューやリングバッファ、NAPI、sk_buff、TCP/IPスタック、Socket Receive Bufferなど、複数のバッファやキューを経由しながら処理されます。

これらの仕組みは、高負荷時に通信性能を維持するために重要な役割を果たす一方で、処理能力を超える負荷が発生すると、キューの滞留やバッファの枯渇、さらにはパケットロスの原因となることがあります。

本記事では、ネットワークから受信したパケットがLinuxカーネル内部でどのように処理されるのかを、バッファリングとキューイングの観点から詳しく解説します。また、高負荷時に各コンポーネントでどのような現象が発生するのか、実際の確認コマンドも交えながら紹介します。

前回の記事で解説したネットワーク通信の全体像をさらに深掘りし、「Linuxカーネル内部でパケットがどのように処理され、どこで滞留・ドロップする可能性があるのか」を理解することで、ネットワーク障害解析や性能分析に役立つ知識を身につけていきましょう。

Linuxカーネルにおけるネットワーク受信処理の全体像

ネットワークから受信したデータは、NICから直接アプリケーションへ渡されるわけではありません。Linuxカーネル内部では、複数のバッファやキューを経由しながら段階的に処理が行われ、最終的にアプリケーションが利用できるようになります。

まずは、受信処理全体の流れを見てみましょう。

ネットワークから受信したパケットは、まず物理NICによって受信されます。受信したパケットはDMA(Direct Memory Access)を利用してRX Ring Bufferへ格納され、Hardware IRQ(ハードウェア割り込み)によってLinuxカーネルへ受信を通知します。

LinuxカーネルはIRQ Handlerを実行し、napi_schedule()を呼び出してNAPI(New API)による受信処理を開始します。napi_schedule()が呼び出されると、処理対象のNAPIインスタンスがNAPI Poll Listへ登録されます。その後、NET_RX_SOFTIRQが実行され、NAPI Poll Listに登録されたNAPIインスタンスを順番に処理し、RX Ring Bufferから受信パケットをまとめて取得します。

取得したパケットごとにsk_buffが生成され、LinuxカーネルのTCP/IPスタックへ受け渡されます。IPスタックではIPヘッダを解析し、TCP/UDPスタックではTCPまたはUDPヘッダを解析して、宛先ポートに対応するソケットを特定します。

その後、受信データはSocket Receive Bufferへ格納されます。アプリケーションがrecv()やread()などのシステムコールを実行すると、受信データがカーネル空間からユーザー空間へコピーされ、アプリケーションが利用できるようになります。

ここで重要なのは、受信パケットは各処理段階でバッファやキューを経由しながら処理されるという点です。これらのバッファやキューは、一時的な通信量の増加を吸収し、安定したネットワーク通信を実現するために重要な役割を担っています。

一方で、処理能力を超える大量のパケットが到着すると、各バッファやキューで滞留やあふれ(オーバーフロー)が発生し、パケットロスや通信遅延、CPU使用率の上昇といった問題につながることがあります。

Linuxカーネルに存在する受信関連のバッファ・キュー

Linuxカーネルでは、受信したパケットを効率よく処理するために、複数のバッファやキューが使用されています。

これらはそれぞれ役割が異なり、一時的な通信量の増加を吸収したり、CPUとNICの処理速度の差を調整したりする役割を担っています。

高負荷時には、これらのバッファやキューが滞留したり、容量を超えたりすることで、パケットロスや通信遅延が発生することがあります。そのため、各コンポーネントの役割を理解することは、ネットワーク障害の原因を切り分けるうえで非常に重要です。

主なバッファ・キューを以下の表にまとめます。

バッファ・キュー主な役割高負荷時の影響
RX QueueNICが受信したパケットを受け付ける受信キューです。RSS(Receive Side Scaling)が有効な環境では、CPUコアごとに複数作成されることがあります。特定のキューへ負荷が集中すると、受信性能が低下することがあります。
RX Ring BufferNICがDMAによって受信データを格納するリングバッファです。NICドライバはここからパケットを取得します。バッファが満杯になると、新たな受信パケットを破棄する場合があります。
NAPI Poll Listnapi_schedule()によって登録された受信処理待ちのNAPIインスタンスを管理します。NET_RX_SOFTIRQによって順番に処理されます。処理待ちが増えると、パケット処理の遅延やCPU負荷の増加につながります。
sk_buffLinuxカーネルがパケットを管理するためのデータ構造です。受信したパケットはsk_buffとしてTCP/IPスタック内を流れます。sk_buffの確保に失敗すると、受信パケットが破棄されることがあります。

<参考>受信データ処理とは別に、TCPサーバでは接続要求を管理するためのキューも存在します。以下の表にまとめます。

バッファ・キュー主な役割高負荷時の影響
SYN QueueTCPの3ウェイハンドシェイク中の接続要求を保持するキューです。キューがあふれると、新規接続要求が破棄されることがあります。
Accept Queue接続が確立し、アプリケーションがaccept()するまでの接続を保持するキューです。キューが満杯になると、新規接続の受付が遅延または失敗することがあります。

高負荷時の処理遅延・破棄の流れ

通常時は、Linuxカーネル内の各バッファやキューが受信パケットを効率よく処理するため、アプリケーションは遅延を意識することなく通信を行えます。しかし、サーバへ大量の通信が集中すると、各コンポーネントの処理能力を超え、バッファやキューにパケットが滞留するようになります。

高負荷時の典型的な流れを以下に示します。

それぞれの段階でどのような現象が発生するのかを見ていきます。

RX Ring Bufferがあふれる

受信したパケットは、まずNICによってRX Ring Bufferへ格納されます。

通常はNAPI Pollが十分な速度でパケットを取り出すため問題ありませんが、CPU負荷が高い場合や大量のパケットを短時間に受信した場合、パケットの到着速度が処理速度を上回ることがあります。

その結果、RX Ring Bufferが満杯になると、新たに到着したパケットを保持できなくなり、NICレベルでパケットが破棄される可能性があります。

直接的にRX Ring Bufferの使用量を確認する方法はないですが、NIC統計情報(ethtool -Sなど)から状況を確認できる場合があります。

以下にコマンド実行例を記載します。ドロップは起きていない状況が確認できます。

[root@localhost ~]# ethtool -S enp0s8 |grep rx
     rx_packets: 183130
     rx_bytes: 20132612
     rx_broadcast: 255
     rx_multicast: 0
     rx_errors: 0
     rx_length_errors: 0
     rx_over_errors: 0 ← 受信オーバーフロー(あふれ)エラー
     rx_crc_errors: 0
     rx_frame_errors: 0
     rx_no_buffer_count: 0 ← 受信バッファが足りずに受け取れなかった回数
     rx_missed_errors: 0 ← NICがパケットを取りこぼした回数
     rx_long_length_errors: 0
     rx_short_length_errors: 0
     rx_align_errors: 0
     rx_flow_control_xon: 0
     rx_flow_control_xoff: 0
     rx_long_byte_count: 20132612
     rx_csum_offload_good: 0
     rx_csum_offload_errors: 0
     alloc_rx_buff_failed: 0
     rx_smbus: 0
[root@localhost ~]#

また、ethtool -gコマンドからRing Bufferに関する設定を確認することができます。RXリングバッファ数は256に設定されています。Ring Bufferが小さい場合は、ethtool -G “IF名” rx ”設定値”で変更しましょう。

[root@localhost ~]# ethtool -g enp0s8
Ring parameters for enp0s8:
Pre-set maximums:
RX:                     4096
RX Mini:                n/a
RX Jumbo:               n/a
TX:                     4096
TX push buff len:       n/a
Current hardware settings:
RX:                     256
RX Mini:                n/a
RX Jumbo:               n/a
TX:                     256
RX Buf Len:             n/a
CQE Size:               n/a
TX Push:                off
RX Push:                off
TX push buff len:       n/a
TCP data split:         n/a
[root@localhost ~]#

NAPI Pollによるパケット取得処理が追いつかない

Hardware IRQが発生すると、LinuxカーネルはNAPIをスケジュールします。その後、NET_RX_SOFTIRQが実行され、NAPI PollによってRX Ring Bufferからパケットをまとめて取得し、Linuxネットワークスタックへ受け渡します。

しかし、受信パケット数が非常に多い場合は、1回のポーリングで処理しきれず、複数回に分けて処理を継続します。NAPI Pollが追いつかない状態が続くと、RX Ring Bufferにパケットが滞留しやすくなり、最終的にはパケットロスにつながる可能性があります。

NAPI Pollの処理状況を直接確認できるコマンドはありません。しかし、softnet_statの値を確認することで、NAPI Pollが処理に追いついているか、あるいは処理が逼迫しているかを間接的に判断できます。

[root@localhost ~]# cat /proc/net/softnet_stat
0000414d 00000000 00000489 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[root@localhost ~]# cat /proc/net/softnet_stat
000091ce 00000000 00000be2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[root@localhost ~]#
項目説明確認ポイント
1列目processedNET_RX_SOFTIRQで処理した受信パケット数です。NAPI PollによってRX Ring Bufferから取得したパケット数がカウントされます。通信量に応じて増加するのが正常です。
2列目droppedCPUが受信パケットを処理しきれず、ソフトウェアレベルで破棄したパケット数です。0となっています。仮に本値が増加している場合はCPU負荷やネットワーク処理能力の不足が疑われます。
3列目time_squeezeNAPI Pollが1回のNET_RX_SOFTIRQで処理を完了できず、処理を次回へ持ち越した回数です。値が増えていることが確認できますが、それだけで異常ということではありません。急激に増加し、droppedやNICのドロップカウンタも増えている場合は、NAPI Pollが処理能力の限界に近づいている可能性があります。

NET_RX_SOFTIRQによる負荷上昇

大量の受信パケットが到着すると、NET_RX_SOFTIRQが実行される頻度や処理時間が増加し、CPUはネットワーク処理に多くの時間を費やすようになります。結果としてパケット受信処理が追い付かなくなるケースがあります。この状態では、%soft(ソフトウェア割り込みによるCPU使用率)が高くなることがあります。

sar -u コマンドでCPU使用率の各種値を確認した結果が以下です。CPU1の%softの値が非常に高くなっており、CPU0はほぼNET_RX_SOFTIRQ(=NAPI Pollを含む受信処理)の実行に専念していたことが分かります。

[root@localhost ~]# sar -u ALL -P ALL 1
Linux 5.14.0-570.12.1.el9_6.x86_64 (localhost.localdomain)      07/08/26        _x86_64_        (2 CPU)

07:01:18        CPU      %usr     %nice      %sys   %iowait    %steal      %irq     %soft    %guest    %gnice     %idle
07:01:19        all      1.57      0.00      7.09      0.00      0.00      0.00     36.22      0.00      0.00     55.12
07:01:19          0      2.70      0.00     12.16      0.00      0.00      0.00      0.00      0.00      0.00     85.14
07:01:19          1      0.00      0.00      0.00      0.00      0.00      0.00     86.79      0.00      0.00     13.21

ksoftirqdが動作する理由

通常、NET_RX_SOFTIRQは割り込み処理の延長として実行されます。しかし、受信パケットが非常に多く、1回の処理で終わらない場合は、Linuxカーネルは残りの処理をksoftirqdへ引き継ぎます。ksoftirqdはカーネルスレッドとして動作し、CPUの空き時間を利用して受信処理を継続します。そのため、高負荷時にはksoftirqdのCPU使用率が高くなることがあります。

高負荷時のtopコマンドの結果を以下に示します。CPU0のksoftirqdプロセスのCPU使用率が高まっていることが確認できます。

top - 09:17:15 up  3:07,  3 users,  load average: 0.89, 0.25, 0.14
Threads: 127 total,   2 running, 125 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.3 us,  0.5 sy,  0.0 ni, 45.4 id,  0.0 wa,  0.0 hi, 53.8 si,  0.0 st
MiB Mem :   1755.5 total,   1353.6 free,    324.3 used,    224.4 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used.   1431.2 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
     16 root      20   0       0      0      0 S  64.6   0.0  15:59.72 ksoftirqd/0
   1954 root      20   0    9980   3968   3456 R  29.1   0.2   0:20.89 iperf3
   1953 root      20   0       0      0      0 I   0.7   0.0   0:00.77 kworker/1:0-events
   1918 root      20   0       0      0      0 I   0.3   0.0   0:00.04 kworker/u8:0-events_unbound
   1959 root      20   0   19988   6984   5120 S   0.3   0.4   0:00.18 sshd

Socket Receive Bufferがあふれる場合

TCP/IPスタックで処理された受信データは、Socket Receive Bufferへ格納されます。

通常はアプリケーションがrecv()やread()を実行してデータを取り出しますが、アプリケーションの処理が追いつかない場合は、Socket Receive Bufferにデータが蓄積していきます。

以下のssコマンドでSocket Receive Bufferが溢れているかどうかを確認することができます。今回は、UDPで負荷をかけているので、オプション -u でUDPのみに絞って確認します。

[root@localhost ~]# ss -u -m -n
Recv-Q Send-Q           Local Address:Port            Peer Address:Port
10240  0      [::ffff:192.168.56.111]:5201 [::ffff:192.168.56.101]:34623
         skmem:(r10240,rb212992,t0,tb212992,f2048,w0,o0,bl0,d40302)
[root@localhost ~]#

<参考:ssコマンドのオプション>

オプション説明
-tTCPソケットのみを表示します。TCP接続(SSH、HTTP、HTTPSなど)の状態を確認する際に使用します。
-uUDPソケットのみを表示します。DNSやDHCP、iperf3(UDPモード)などの通信を確認する際に使用します。
-mソケットに割り当てられたメモリ情報(skmem)を表示します。Socket Receive BufferやSocket Send Bufferの使用状況、ドロップ数を確認できます。
-nIPアドレスやポート番号を名前解決せず、数値のまま表示します。障害解析時によく使用されます。
-lLISTEN状態のソケットのみを表示します。サーバが待ち受けしているポートを確認できます。
-aLISTEN中・通信中を含め、すべてのソケットを表示します。
-pソケットを利用しているプロセス名とPIDを表示します(root権限が必要)。
-iTCP内部情報(RTT、輻輳制御、再送回数など)を表示します。TCP通信の解析で利用します。

この出力では、skmemの各項目がSocket Bufferの状態を表しています。

項目説明
r現在Socket Receive Bufferに格納されているデータ量(Byte)
rbSocket Receive Bufferの最大サイズ(Byte)
t現在のSocket Send Buffer使用量
tbSocket Send Bufferの最大サイズ
dSocket Receive Bufferへ格納できず破棄されたパケット数

今回の結果では、skmem:(r10240,rb212992,…,d40302) となっています。これは以下のことを意味します。

  • Receive Bufferの最大サイズは212,992Byte(約208KB)
  • 現在は約10KBのデータが格納されている
  • これまでに40,302パケットがSocket Receive Bufferへ格納できず破棄された

特に注目すべきなのは d40302です。

dはSocket Receive Bufferでドロップしたパケット数を表しており、この値が増加している場合は、アプリケーションがrecv()などでデータを取り出す速度よりも、ネットワークから到着する速度の方が速く、受信バッファが溢れたことを意味します。

また、netstatコマンドのUdpRcvbufErrorsというカウンタを確認することで、カーネル全体でUDP受信バッファ不足によるパケット破棄が発生していることがないかを確認することができます。

[root@localhost ~]# nstat -az | grep UdpRcvbufErrors
UdpRcvbufErrors                 151318             0.0
[root@localhost ~]#

TCP Windowとの関係

Socket Receive Bufferが満杯に近づくと、Linuxカーネルは受信側の空き容量が少ないことを送信側へ通知します。これがTCPの受信ウィンドウ(Receive Window)です。TCP ACKを生成する際、カーネルはSocket Receive Bufferの空き容量をReceive WindowとしてTCPヘッダへ設定し、送信側へ通知します。

送信側は受信ウィンドウのサイズに従って送信量を調整するため、受信側の処理が追いつかない場合は送信速度が自動的に抑制されます。

これはTCPのフロー制御と呼ばれる仕組みであり、受信側のバッファあふれを防ぐ重要な役割を果たしています。

受信側でtcpdumpにてパケットの情報を取得して、受信ウィンドウが変化していることを確認してみましょう。時間経過とともに、win の値が64240から8688に小さくなっていることが確認できます。

負荷発生直後
06:37:51.748882 IP (tos 0x0, ttl 64, id 47859, offset 0, flags [DF], proto TCP (6), length 60) 192.168.56.101.45766 > 192.168.56.111.5201: Flags [S], cksum 0x232e (correct), seq 1655216554, win 64240, options [mss 1460,sackOK,TS val 201091157 ecr 0,nop,wscale 7], length 0 

負荷継続中
06:38:46.376303 IP (tos 0x0, ttl 64, id 20453, offset 0, flags [DF], proto TCP (6), length 52) 192.168.56.111.5201 > 192.168.56.101.45782: Flags [F.], cksum 0xf24b (incorrect -> 0x7117), seq 1, ack 409953455, win 8688, options [nop,nop,TS val 2236347426 ecr 201150338], length 0

バッファ、キュー、ドロップ情報取得まとめ

パケット受信に関するトラブルが発生した際、確認すべき情報を以下に示します。RHEL9において実行するコマンドになります。

確認項目コマンド見る項目正常時の目安異常時に考えられること
パケット到達確認tcpdump -i <IF名>パケットが受信できているかリクエスト・レスポンスが確認できるNW機器、FW、ACL、ルーティング
NIC統計ip -s linkRX droppedRX errorsoverrun0件または増加しないNICバッファ不足、受信処理遅延
NIC詳細統計ethtool -S <IF名>rx_droppedrx_missed_errorsrx_no_buffer_countrx_crc_errorsカウンタが増えないNICリング不足、ハードウェア障害
CPUごとの負荷sar -u ALL -P ALL 1%idle%system%soft%idleが十分ある
、負荷が分散している
CPU不足、SoftIRQ過多、IRQ偏り
SoftIRQtop または top -HksoftirqdのCPU使用率数%程度パケット処理が追いついていない
IRQ分散cat /proc/interruptsNIC割り込みが複数CPUへ分散されているか複数CPUに分散1CPU集中
Socket受信バッファss -tniRecv-Qrcv_spaceRecv-Qが小さいアプリが受信できていない
TCP受信ウィンドウtcpdump -nn -vvwin (Window Size)十分なWindowサイズWindow縮小、Zero Window
TCP統計sar -n TCP,ETCP 1retrans/sactive/spassive/s再送が少ないネットワーク品質悪化
NICリングサイズethtool -g <IF名>RX Current Hardware Settings適切なサイズ小さすぎるとバーストに弱い
メモリsar -r%memusedkbmemfreeメモリに余裕メモリ不足
sk_buff不足dmesgpage allocation failureskb関連メッセージエラーなしメモリ不足、skb確保失敗
オフロードethtool -k <IF名>GRO、LRO、TSO、RX checksum有効であることが多いオフロード無効、設定不整合

まとめ

Linuxの受信処理では、パケットはNICから直接アプリケーションへ渡されるのではなく、RX Ring Buffer、NAPI、sk_buff、TCP/IPスタック、Socket Receive Bufferなど複数のコンポーネントを経由して処理されます。

高負荷時には、それぞれの段階でバッファ不足や処理遅延が発生し、パケットロスやCPU使用率の上昇、受信ウィンドウの縮小といった現象につながります。

障害解析では、ethtoolsarsstcpdumpなどを組み合わせることで、「どの段階でボトルネックが発生しているか」を効率よく切り分けることができます。

Linuxネットワーク障害を正しく分析するためには、パケットの流れだけでなく、各バッファやキューの役割を理解することが重要です。

参考文献

・Linux Kernel Documentation

Networking — The Linux Kernel documentation

Linuxカーネル公式のネットワークサブシステムに関するドキュメントです。NAPI、SoftIRQ、Socket Bufferなどの基本的な考え方を確認できます。

・Linux Kernel Documentation – NAPI

NAPI — The Linux Kernel documentation

NAPIの動作原理、割り込みからポーリングへの切り替えについて詳しく説明されています。本記事の「NAPI」「NET_RX_SOFTIRQ」の章の参考になります。

コメント