- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- 全体像 — 受信経路の大型ダイアグラム
- sk_buff — カーネル内のパケットの分身
- 割り込みからNAPIへ — 割り込み緩和の歴史
- マルチコア分散 — RSS、RPS、RFS
- オフロード — GRO、GSO、TSO
- 送信経路 — qdiscとキューイング
- XDP — スタックに入る前に処理する
- ソケットバッファと主要sysctlチューニング
- TCPの内部へ一歩 — 輻輳制御とBBR
- コンテナネットワーキング — パケットの追加の旅程
- 観測ツール一覧
- 診断シナリオ1 — パケットドロップの追跡
- 診断シナリオ2 — レイテンシ分析
- 落とし穴とアンチパターン
- 運用チェックリスト
- おわりに
- 参考資料
はじめに
Webサーバーのp99レイテンシが突然跳ね上がったり、モニタリングでパケットドロップのカウンタが増え始めたりすると、多くのエンジニアはまずアプリケーションコードを疑います。しかし実際には、パケットがアプリケーションに届く前に、カーネルの中ですでに数十の段階を通過しています。NICハードウェア、DMA、割り込み、softirq、プロトコルスタック、ソケットキュー — このどれか一つでもボトルネックになれば、アプリケーションがどれだけ速くても意味がありません。
この記事では、1つのパケットがネットワークカードに到着してからアプリケーションのread呼び出しに到達するまでの全行程をたどります。各段階で何が起きるのか、どこでパケットが捨てられうるのか、そして各ポイントをどう観測しチューニングするのかを、実務の視点で整理します。Kubernetesノードのチューニング、低レイテンシサービスの運用、ネットワーク障害の分析を行う方にとって、実用的な地図になれば幸いです。
全体像 — 受信経路の大型ダイアグラム
まず経路全体を一望します。以下のダイアグラムはパケット受信(RX)経路の主要段階です。
[物理ネットワーク]
|
v
+---------------------------+
| NICハードウェア | 1. フレーム受信、FCS検証
| (RSSハッシュでRXキュー選択) | 2. RSS: 5-tupleハッシュ -> キュー決定
+---------------------------+
|
v DMA (CPU介入なし)
+---------------------------+
| RXリングバッファ(記述子) | 3. カーネルが事前に確保した
| サイズ確認: ethtool -g | メモリへパケットを直接書き込み
+---------------------------+
|
v ハードウェア割り込み (IRQ)
+---------------------------+
| ハードIRQハンドラ(短い) | 4. 「パケット到着」だけ記録して
| napi_schedule() を呼ぶ | 即座にリターン (数マイクロ秒)
+---------------------------+
|
v NET_RX_SOFTIRQ
+---------------------------+
| softirq / ksoftirqd | 5. NAPIポーリング開始
| net_rx_action() | budget分のパケットをまとめて処理
+---------------------------+
|
v
+---------------------------+
| NAPI poll (ドライバ) | 6. リングバッファからパケットを取り出し
| sk_buff生成、GROマージ | sk_buffに変換、GROで結合
+---------------------------+
|
v
+---------------------------+
| netif_receive_skb() | 7. プロトコル分岐点
| (tcpdump/AF_PACKETタップ、| XDP genericやtc ingressも
| tc ingressフックの位置) | この付近で動作
+---------------------------+
|
v
+---------------------------+
| IP層 (ip_rcv) | 8. チェックサム、ルーティング判断、
| netfilter PREROUTING/ | netfilterフック通過、
| INPUTフック | ローカル宛なら上位へ
+---------------------------+
|
v
+---------------------------+
| TCP層 (tcp_v4_rcv) | 9. ソケットルックアップ、シーケンス処理、
| 輻輳制御、ACK生成 | 順序再構成、ACK応答
+---------------------------+
|
v
+---------------------------+
| ソケット受信キュー | 10. sk_receive_queueに積まれ、
| (sk_rcvbufが上限) | 待機中のプロセスを起床
+---------------------------+
|
v
+---------------------------+
| アプリケーション read/recv | 11. システムコールでユーザー空間へ
| (epollイベント発火) | データをコピー
+---------------------------+
この11段階のうち、パケットが捨てられうる地点は大きく4つです。リングバッファが満杯のとき(2〜3段階)、softirqが追いつかないとき(5段階)、netfilterルールによるもの(8段階)、そしてソケットバッファが満杯のとき(10段階)です。それぞれの診断方法は後述します。
sk_buff — カーネル内のパケットの分身
カーネルの中でパケットはsk_buff(socket buffer、略してskb)という構造体で表現されます。パケットデータそのものと、それを解釈するためのメタデータを一緒に持ち歩く構造です。
/* include/linux/skbuff.h から抜粋した概念的構造 */
struct sk_buff {
struct sk_buff *next; /* キュー連結用 */
struct sock *sk; /* 所属ソケット */
struct net_device *dev; /* 受信/送信デバイス */
unsigned int len; /* データ長 */
__u16 transport_header; /* L4ヘッダオフセット */
__u16 network_header; /* L3ヘッダオフセット */
__u16 mac_header; /* L2ヘッダオフセット */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head; /* バッファの先頭 */
unsigned char *data; /* 現在の層のデータ先頭 */
};
核心となるアイデアは2つです。
第一に、パケットデータはメモリ上に一度だけ置かれ、各プロトコル層はhead/data/tailポインタを調整するだけです。イーサネット層が処理を終えるとdataポインタを14バイト進めてIPヘッダを指すようにする、という具合です。層を上下するたびにデータをコピーしないことが性能の核心です。
第二に、skbはクローン可能です。tcpdumpがパケットをキャプチャするとき、データ全体をコピーするのではなく、同じデータを指すskbメタデータだけを複製します。だからキャプチャの負荷は思ったより小さいのです(もちろんタダではありません)。
割り込みからNAPIへ — 割り込み緩和の歴史
パケットが到着するたびに割り込みを発生させたらどうなるでしょうか。10GbEでは64バイトパケットが毎秒約1488万個到着しえます。パケットごとに割り込みをかければ、CPUは割り込み処理だけで終わってしまいます。これが割り込みライブロック(livelock)です。
NAPI(New API)はこの問題を割り込みとポーリングのハイブリッドで解決します。
トラフィックが少ないとき: 割り込みモード (レイテンシ最小化)
パケット到着 -> IRQ -> napi_schedule -> ポーリング
-> キューが空になればIRQ再有効化
トラフィックが多いとき: ポーリングモード (スループット最大化)
IRQは無効のまま -> softirqがbudgetの範囲内で
リングバッファをポーリングし続ける -> パケットが続く限り
IRQを再有効化しない
動作ルールを整理するとこうなります。
- 最初のパケットが到着するとハードIRQが発生し、ドライバは該当キューのIRQを無効化してNAPIポーリングを予約します。
- softirqコンテキストでnet_rx_actionが実行され、1回で最大budget(全体既定300、デバイスあたり64)個のパケットを処理します。
- リングバッファを空にできたらIRQを再有効化して割り込みモードへ戻ります。まだパケットが残っていればIRQを無効のまま、次のsoftirqラウンドで処理を続けます。
- softirqが長く占有しすぎる場合はksoftirqdカーネルスレッドへ引き継ぎ、スケジューラの公平性の中で処理します。
関連パラメータは以下のとおりです。
# 1回のnet_rx_actionで処理する総パケット数の上限
sysctl net.core.netdev_budget # 既定 300
# 1回のnet_rx_actionに使える時間の上限 (マイクロ秒)
sysctl net.core.netdev_budget_usecs # 既定 2000
# budget枯渇で処理が中断された回数 (3列目: time_squeeze)
cat /proc/net/softnet_stat
softnet_statの3列目(time_squeeze)が継続的に増えているなら、softirqがbudget内に仕事を終えられていないということです。budgetの増加やCPU分散(RPS)を検討すべきです。
ハードウェアレベルの割り込み緩和もあります。NICがパケットをいくつか溜めるか一定時間待ってから、割り込みを1回だけかける機能です。
# 割り込みcoalescing設定の確認/変更
ethtool -c eth0
ethtool -C eth0 rx-usecs 50 rx-frames 64
# 低レイテンシ重視ならrx-usecsを下げ、スループット重視なら上げます。
# adaptive-rx on にするとNICがトラフィックパターンに応じて自動調整します。
ethtool -C eth0 adaptive-rx on
マルチコア分散 — RSS、RPS、RFS
1コアでは高速NICのトラフィックを処理しきれません。パケット処理を複数コアへ分散する3つのメカニズムがあります。
| 技術 | 動作場所 | 分散基準 | 特徴 |
|---|---|---|---|
| RSS | NICハードウェア | 5-tupleハッシュでRXキュー選択 | 最も効率的、キュー数はハードウェア制限 |
| RPS | カーネルソフトウェア | ハッシュで処理CPUを選択 | RSSなしのNICやキュー不足時の補完 |
| RFS | カーネルソフトウェア | アプリが動くCPUへ誘導 | キャッシュ局所性の最適化、RPSの拡張 |
RSSは、NICがパケットの送信元/宛先IPとポートをハッシュして複数のRXキューのいずれかへ送るハードウェア機能です。各キューは別々のIRQを持つため、IRQをコアごとに分散すれば自然にマルチコア処理になります。
# RXキュー数の確認と変更
ethtool -l eth0
ethtool -L eth0 combined 8
# キューごとのIRQ確認
grep eth0 /proc/interrupts
# IRQを特定CPUに固定 (irqbalanceを止めて手動制御する場合)
echo 2 > /proc/irq/125/smp_affinity_list # IRQ 125をCPU2へ
RPSはRSSのソフトウェア版です。ハードIRQを受けたCPUがハッシュを計算し、パケット処理を別CPUのbacklogキューへ渡します。
# eth0のrx-0キューのパケットをCPU 0-3へ分散 (ビットマスク f = 0b1111)
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
# RPS backlogキューのサイズ (ドロップ防止)
sysctl -w net.core.netdev_max_backlog=16384
RFSはさらに一歩進んで、そのフローのデータを消費するアプリケーションが実行中のCPUへパケットを送ります。パケットデータがすでにそのCPUのキャッシュに載っている可能性が高くなり、レイテンシが下がります。
# グローバルフローテーブルのサイズ
sysctl -w net.core.rps_sock_flow_entries=32768
# キューごとのフロー数 = グローバル値 / キュー数
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
実務感覚で言うとこうです。最近のサーバーNICはキューが十分あるため、RSS + IRQアフィニティでほとんど解決します。RPS/RFSは、仮想化環境のvirtio-netのようにキューが不足する場合や、特定コアにトラフィックが偏る場合の補完として使います。
オフロード — GRO、GSO、TSO
セグメント単位の処理コストを下げるもう一つの軸が、「カーネルスタックは大きな塊で処理し、分割/結合は境界で行う」というオフロード戦略です。
| 技術 | 方向 | 実行主体 | 内容 |
|---|---|---|---|
| TSO | 送信 | NICハードウェア | カーネルが64KB級の塊を渡すとNICがMSSに分割 |
| GSO | 送信 | カーネルソフトウェア | TSO非対応時、ドライバ直前でソフトウェア分割 |
| GRO | 受信 | カーネル(NAPI段階) | 同一フローの連続セグメントを大きなskbへ結合 |
GROのおかげで、1500バイトのパケット40個が約6万バイトのskb 1つにまとめられ、IP/TCP層を一度だけ通過します。スタック通過コストがパケット数ではなく「まとまりの数」に比例するようになるのです。
# オフロード状態の確認
ethtool -k eth0 | grep -E "generic-receive|generic-segmentation|tcp-segmentation"
# オン/オフ
ethtool -K eth0 gro on gso on tso on
注意点があります。GROはパケットをわずかな時間保持してから結合するため、極端な低レイテンシワークロードでは無効化するケースもあります。また、ルータ/ブリッジのようにパケットをフォワードする装置でGRO後の再分割(GSO)が起きるとパケット境界が変わり、微妙な問題を生むことがあります。tcpdumpでMTUより大きいパケットが見えるのは大半がGRO/TSOによるもので、正常です。
送信経路 — qdiscとキューイング
送信は受信の逆順ですが、キューイング規律(qdisc)という重要な段階が加わります。
アプリケーション write/send
|
v
+---------------------+
| ソケット送信バッファ | sk_sndbufが上限。TCPなら輻輳ウィンドウと
| (sk_write_queue) | 受信側ウィンドウが送信レートを決定
+---------------------+
|
v
+---------------------+
| TCP/IP層 | ヘッダ作成、ルーティング、netfilter OUTPUT
+---------------------+
|
v
+---------------------+
| qdisc (キュー規律) | fq_codel(既定)、fq、pfifo_fastなど
| tcで制御 | ペーシング、シェーピング、AQMはここ
+---------------------+
|
v
+---------------------+
| ドライバTXリング | DMAでNICへ渡す
+---------------------+
|
v
| NIC -> 物理ネットワーク (TSO分割はここで)
qdiscは単純なFIFOではありません。現代のLinuxの既定値であるfq_codelは、フローごとの公平キューイングとCoDel AQM(能動的キュー管理)を組み合わせ、バッファブロートを抑制します。BBRを使うならペーシングをサポートするfq系が推奨です(最近のカーネルはTCP内蔵ペーシングもサポートします)。
# 現在のqdiscを確認
tc qdisc show dev eth0
# fqへ置き換え (BBRペーシングと相性が良い)
tc qdisc replace dev eth0 root fq
# qdisc統計: ドロップ、backlogの確認
tc -s qdisc show dev eth0
送信側でもう一つ知っておくべきは、TXリングバッファのサイズと、TCP Small Queues(TSQ)です。TSQは、1つのソケットがqdisc/ドライバキューを独占しないよう、ソケットあたりのin-flightバイト数を制限します。
XDP — スタックに入る前に処理する
XDP(eXpress Data Path)は、eBPFプログラムをドライバの最も早い地点、つまりsk_buffが作られる前に実行する技術です。
通常経路:
NIC -> DMA -> [skb割り当て] -> GRO -> netif_receive_skb
-> netfilter -> IP -> TCP -> ソケット (段階ごとにコスト累積)
XDP経路:
NIC -> DMA -> [XDPプログラム実行]
|-- XDP_DROP: 即座に破棄 (skb割り当てすらなし)
|-- XDP_TX: 同じNICから即座に折り返し
|-- XDP_REDIRECT: 別のNIC/CPU/AF_XDPソケットへ
+-- XDP_PASS: 通常スタックへ通過
XDPが速い理由は単純です。通常経路でパケット1つあたりに発生するコスト — skbの割り当てと初期化、メモリ回収、プロトコル層の通過、netfilterの評価 — をすべてスキップするからです。DDoS遮断のように「大半のパケットを捨てる」ワークロードでは、通常スタック比で数倍から10倍以上のスループット差が出ます。Cilium、Katran(FacebookのL4ロードバランサ)、CloudflareのDDoS防御はすべてXDPベースです。
モードは3つあります。ドライバがサポートするnativeモード、NIC自体が実行するoffloadedモード、そしてスタック進入後に実行され性能メリットの少ないgeneric(skb)モードです。性能目的なら、まずnative対応ドライバかどうかを確認すべきです。
# XDPプログラムのロード状態確認
ip link show dev eth0 # prog/xdp表示の有無
bpftool net list
ソケットバッファと主要sysctlチューニング
ソケットキューはパケットの旅の終着駅であり、最後のドロップ地点です。主要パラメータを表にまとめます。
| パラメータ | 既定値(目安) | 意味 |
|---|---|---|
| net.core.rmem_max | 212992 | ソケット受信バッファ上限(バイト) |
| net.core.wmem_max | 212992 | ソケット送信バッファ上限 |
| net.ipv4.tcp_rmem | 4096 131072 6291456 | TCP受信バッファ min default max、自動チューニング範囲 |
| net.ipv4.tcp_wmem | 4096 16384 4194304 | TCP送信バッファ min default max |
| net.core.netdev_max_backlog | 1000 | RPS/非NAPI経路のCPU別backlogキュー長 |
| net.core.somaxconn | 4096 | acceptキュー(完了済み接続)の上限 |
| net.ipv4.tcp_max_syn_backlog | 1024 | SYNキュー(未完了接続)の長さ |
| net.ipv4.tcp_congestion_control | cubic | 輻輳制御アルゴリズム |
| net.ipv4.tcp_notsent_lowat | 4294967295 | 未送信データのしきい値(低レイテンシ用) |
帯域幅遅延積(BDP)が大きい環境、たとえばRTT 100msで10Gbpsの経路なら、理論上約125MBのウィンドウが必要です。tcp_rmemのmaxを十分に大きくしないと、スループットはウィンドウに縛られます。
# 高帯域/高RTT環境の例 (/etc/sysctl.d/90-network.conf)
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 262144 134217728
net.ipv4.tcp_wmem = 4096 262144 134217728
net.core.netdev_max_backlog = 16384
net.core.somaxconn = 8192
重要な落とし穴を一つ。tcp_rmem/tcp_wmemのmaxはrmem_max/wmem_maxとは独立に動作します(TCP自動チューニングはtcp_rmemに従います)。一方、アプリケーションがsetsockoptでSO_RCVBUFを直接指定すると、その値はrmem_maxで切り詰められ、同時にそのソケットのTCP自動チューニングが無効になります。「バッファを大きくしようとsetsockoptを入れたら、かえって遅くなった」という事例の典型的な原因です。
TCPの内部へ一歩 — 輻輳制御とBBR
TCPの送信レートは、受信側ウィンドウと輻輳ウィンドウ(cwnd)の小さい方が決定します。輻輳制御はモジュールとして実装されており、差し替え可能です。
| アルゴリズム | シグナル | 特性 |
|---|---|---|
| CUBIC(既定) | パケット損失 | 損失ベース、バッファを埋める傾向、無難な既定値 |
| BBR | 配送レートとRTTの測定 | 損失に鈍感、バッファブロート回避、長距離/損失環境に強い |
| DCTCP | ECNマーキング | データセンター専用、スイッチのECN設定が必要 |
BBRは経路のボトルネック帯域と最小RTTを継続的に推定し、損失が起きる前にその推定値に合わせてペーシングします。無線区間や国際区間のように損失が輻輳と無関係に発生する環境では、CUBICよりはるかに安定したスループットを出します。
# 利用可能な輻輳制御モジュールの確認
sysctl net.ipv4.tcp_available_congestion_control
# BBRの有効化 (fq qdiscとの併用を推奨)
modprobe tcp_bbr
sysctl -w net.ipv4.tcp_congestion_control=bbr
sysctl -w net.core.default_qdisc=fq
# 接続ごとの実際の使用アルゴリズムと状態確認
ss -tin | grep -E "bbr|cubic"
ss -tiの出力でcwnd、rtt、retrans、pacing_rateを見れば、1つの接続の健康状態はほぼ把握できます。retransが着実に増えていれば経路の損失、cwndが小さいまま固定されていれば輻輳制御やバッファ制限を疑います。
コンテナネットワーキング — パケットの追加の旅程
コンテナ環境では、上記の経路にさらに段階が追加されます。典型的なブリッジ + veth構成の受信経路です。
NIC -> ホストスタック (IP/netfilter、NAT/conntrack)
-> ブリッジまたはルーティング判断
-> vethホスト側エンドへ送信
-> vethコンテナ側エンドで「再受信」 (softirq再突入)
-> コンテナnetnsのIP/TCPスタック通過
-> コンテナ内のソケット
vethペアは、片方に送ったパケットが反対側で受信される仮想ケーブルです。問題はこの「再受信」のために、プロトコルスタックの通過とnetfilterの評価が追加で発生する点です。さらにkube-proxyのiptablesルールとconntrackが加わると、ネームスペース境界ごとに数マイクロ秒から数十マイクロ秒のオーバーヘッドが積み上がります。CiliumのようなeBPFベースのCNIがnetfilter経路をバイパスしveth間リダイレクトを最適化するのは、まさにこのコストを削るためです。
診断の際は、「どのネームスペースのどのインターフェースで」問題が出ているかを絞り込むのが先決です。
# コンテナのnetnsに入ってインターフェース統計を見る
nsenter -t 12345 -n ip -s link
nsenter -t 12345 -n ss -tin
# conntrackテーブルの飽和確認 (満杯になると新規接続がドロップ)
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
dmesg | grep conntrack
観測ツール一覧
各段階を覗き込むツールをレイヤー別に整理します。
# 1. NIC/ドライバレベル: ハードウェアカウンタ (ドロップ、エラー、キュー別統計)
ethtool -S eth0 | grep -iE "drop|err|miss|fifo"
ip -s link show eth0 # rx_dropped、rx_missedなど
# 2. softirqレベル: budget枯渇、backlogドロップ
cat /proc/net/softnet_stat # 1列目処理数、2列目ドロップ、3列目time_squeeze
# 3. プロトコルレベル: スタック内部カウンタの報告
nstat -az | grep -iE "drop|retrans|listen|prune|collapse"
cat /proc/net/snmp # IP/TCP/UDP基本カウンタ
cat /proc/net/netstat # TcpExt拡張カウンタ
# 4. ソケットレベル: 接続ごとの状態
ss -tinm # TCP内部状態 + メモリ(skmem)
ss -lnt # リッスンソケットのacceptキュー使用量(Recv-Q/Send-Q)
# 5. カーネル関数レベル: eBPF
# カーネルがパケットを捨てるすべての地点と理由を追跡 (BCC)
dropwatch または BCCのtcpdrop
bpftrace -e 'tracepoint:skb:kfree_skb { @[args->reason] = count(); }'
特にカーネルのkfree_skbトレースポイントはドロップ理由(reason)フィールドを提供するため、「どこでなぜ捨てられたのか」を推測ではなく測定で答えられます。
診断シナリオ1 — パケットドロップの追跡
状況: モニタリングでノードのrx_droppedが増加し、アプリケーションのタイムアウトが断続的に発生しています。外側(NIC)から内側(ソケット)へ段階的に絞り込みます。
ステップ1、NIC/リングバッファを見ます。
ethtool -S eth0 | grep -iE "rx.*(drop|miss|no_buf|fifo)"
ethtool -g eth0 # リングバッファの現在/最大サイズ
rx_missedやfifo系カウンタが増えているなら、リングバッファで受けきれなかったということです。リングバッファを拡大し(ethtool -G eth0 rx 4096)、割り込みcoalescingとIRQ分散を点検します。
ステップ2、softirq段階を見ます。
awk '{print NR-1, "drops:", strtonum("0x"$2), "squeeze:", strtonum("0x"$3)}' \
/proc/net/softnet_stat
2列目(ドロップ)が増えればnetdev_max_backlog不足、3列目(squeeze)が増えればbudget不足か特定CPUの過負荷です。RPSで分散するか、backlog/budgetを増やします。
ステップ3、プロトコル/ソケット段階を見ます。
nstat -az | grep -iE "ListenDrops|ListenOverflows|PruneCalled|RcvCollapsed"
ss -lnt # リッスンソケットのRecv-QがSend-Q(somaxconn上限)に張り付いていないか
ListenOverflowsが増えればacceptキューの飽和です。アプリケーションのaccept処理速度、somaxconnとlistenのbacklog引数を併せて確認します。PruneCalled/RcvCollapsedは受信バッファ圧迫のシグナルで、tcp_rmemを見直します。
ステップ4、それでも見つからなければkfree_skbの理由を直接数えます。
bpftrace -e 'tracepoint:skb:kfree_skb /args->reason > 2/
{ @[args->reason] = count(); } interval:s:5 { print(@); clear(@); }'
NETFILTER_DROPならファイアウォールルール、NO_SOCKETなら誤った宛先かレース、SOCKET_RCVBUFFなら受信バッファ不足 — 理由コードが次の行動を直接教えてくれます。
診断シナリオ2 — レイテンシ分析
状況: スループットは正常なのにp99応答時間が周期的に跳ねます。レイテンシは「どの区間で時間が積み上がるか」の問題なので、区間ごとに切り分けます。
ステップ1、ネットワーク区間かホスト区間かを分離します。
ss -ti '( dport = :443 )' # rttの平均/偏差、retransの確認
ping -c 100 target-host # 基本RTTと偏差(mdev)
ssのrttがpingよりはるかに大きく変動も激しいなら、ホスト側(送受信キュー、スケジューリング)の可能性が高いです。retransも一緒に増えているなら、経路の損失により再送タイムアウトがレイテンシを作っているケースです。
ステップ2、ホスト内部ならsoftirq遅延とキューの滞留を見ます。
# softirq処理遅延の分布 (BCC)
softirqs -d 10 1
# キュー滞留: qdisc backlogとTXドロップ
tc -s qdisc show dev eth0
# ソケットバッファ滞留: Recv-Q/Send-Qがゼロでない接続
ss -tnp | awk '$2>0 || $3>0'
Recv-Qが溜まっているなら、カーネルはデータを渡したのにアプリケーションが読めていないということです。この場合、原因はネットワークではなくアプリケーションのスレッド不足やCPU競合(スケジューリング遅延)です。perf schedやrunqlat(BCC)でランキュー遅延を確認します。
ステップ3、周期性があるなら時間軸で犯人を捕まえます。GC周期、cron、バックアップトラフィック、CPUの深いアイドル状態(C-state)への突入などがよくある原因です。cpupower idle-infoで深いC-stateの復帰遅延を確認し、低レイテンシが必須ならカーネルブートパラメータやPM QoSで制限します。
落とし穴とアンチパターン
- すべてのsysctlを一度に変えること。一度に1つずつ変えて測定しないと因果がわかりません。
- ネットの「魔法のチューニング集」をそのまま適用すること。somaxconn 65535のような値は、ワークロードによってはメモリの無駄や問題の隠蔽になります。
- setsockoptでSO_RCVBUFを固定し、TCP自動チューニングを無効化してしまうこと(前述)。
- tcpdumpで巨大パケットを見て「MTU設定ミス」と誤判断すること。GRO/TSOの正常動作です。
- コンテナ環境でホストnetnsのカウンタだけを見ること。問題の半分はコンテナnetnsの中にあります。
- conntrackを忘れること。NATを使うノードの高接続ワークロードでは、nf_conntrack_maxの飽和は定番の障害原因です。
- irqbalanceと手動IRQアフィニティを同時に使うこと。互いに設定を上書きします。
運用チェックリスト
- ethtool -S のドロップ/ミスカウンタをモニタリングに収集しているか
- /proc/net/softnet_stat のドロップとsqueezeをノード指標として見ているか
- nstatのListenOverflows、RetransSegsをアラート対象にしているか
- RXキュー数とIRQアフィニティがCPUトポロジーに合わせて分散されているか
- リングバッファサイズがトラフィックバーストに十分か (ethtool -g)
- BDP計算に基づいてtcp_rmem/tcp_wmemのmaxを算定したか
- 輻輳制御(cubic/bbr)とqdisc(fq/fq_codel)の組み合わせを意図的に選択したか
- conntrackの使用量とmaxをモニタリングしているか
- コンテナnetns内部の指標を収集する手段(nsenter、eBPF)があるか
- 変更は一度に1つ、前後の測定とともに記録しているか
おわりに
パケットの旅をたどってみると、Linuxネットワーキングスタックは「割り込みを減らし(NAPI、coalescing)、まとめて処理し(GRO/TSO)、複数コアへ分散し(RSS/RPS/RFS)、必要ならスタック自体をスキップする(XDP)」という一貫した設計哲学で進化してきたことがわかります。性能問題に遭遇したとき、この地図を思い出してNICからソケットまで段階別カウンタを確認すれば、勘ではなく測定でボトルネックを見つけられます。次回はこの旅のCPU側の相棒であるスケジューラを扱います。
参考資料
- カーネル ネットワーキングスケーリング文書 (RSS/RPS/RFS): https://www.kernel.org/doc/html/latest/networking/scaling.html
- NAPI公式ドキュメント: https://docs.kernel.org/networking/napi.html
- セグメンテーションオフロード (GSO/GRO/TSO): https://docs.kernel.org/networking/segmentation-offloads.html
- AF_XDPドキュメント: https://docs.kernel.org/networking/af_xdp.html
- tcp(7) manページ — sysctl全リスト: https://man7.org/linux/man-pages/man7/tcp.7.html
- ss(8) manページ: https://man7.org/linux/man-pages/man8/ss.8.html
- eBPF公式サイト: https://ebpf.io
- BCCツール集 (tcpdrop、runqlatなど): https://github.com/iovisor/bcc
- Cilium BPF/XDPリファレンスガイド: https://docs.cilium.io/en/stable/reference-guides/bpf/
- BBR論文 (ACM Queue): https://queue.acm.org/detail.cfm?id=3022184
- CUBIC RFC 9438: https://datatracker.ietf.org/doc/html/rfc9438
- tc(8) manページ: https://man7.org/linux/man-pages/man8/tc.8.html