見えない天井:低負荷時でもサーバーがパケットをドロップする理由
APIゲートウェイを通じて毎秒約80万パケット(PPS)を処理していた際、タイムアウトが発生しました。スペック上、サーバーは健全に見えました。10Gbpsリンクに対して帯域幅はわずか300Mbps、16コアマシンの合計CPU負荷は約15%程度でした。しかし、netstat -sを実行すると、毎秒数千個のパケットドロップが報告されていました。
topコマンドですぐにボトルネックが判明しました。15個のコアがほぼアイドル状態である一方、Core 0だけが%si(ソフトウェア割り込み)モードで100%に張り付いていたのです。サーバーはデータ量に苦しんでいたのではなく、割り込みオーバーヘッドで窒息していました。微細なパケット一つひとつがイベントをトリガーし、それをたった一つのコアが処理しようとしていたのです。数台の基本的なVMを超えてインフラをスケールさせるなら、この負荷を分散させる技術の習得は選択肢ではなく必須条件です。
シングルコアの罠:割り込みを理解する
ネットワークインターフェースカード(NIC)がパケットを受信すると、割り込みリクエスト(IRQ)を介してCPUに信号を送ります。デフォルトでは、多くのLinuxディストリビューションが特定のNICからのすべてのIRQをCPU 0にルーティングします。これがパフォーマンス上の大きな障害となります。
高頻度のマイクロサービスやUDPベースのゲームサーバーで一般的な、毎秒150万個の小さなパケットを受信する場合を想像してください。そのシングルコアは、NICを管理するために1秒間に150万回も現在のタスクを中断しなければなりません。たとえ64コアのEPYCやXeonプロセッサを搭載していても、ネットワークのスループットはそのたった一つのコアの速度によって制限されてしまうのです。
ハードウェアとソフトウェアによるスケーリング
この問題は、ワークロードを分散させることで解決できます。Linuxには主に3つのツールがあります。
- RSS (Receive Side Scaling): ハードウェア機能。NICがパケットを複数のキューに分散します。各キューが独自のIRQを持ち、異なるCPUで処理できるようになります。
- RPS (Receive Packet Steering): RSSのソフトウェア版。カーネルが単一のハードウェアキューからパケットを取り込み、プロトコル処理のために他のCPUに渡します。
- IRQ Affinity: 特定のIRQを特定のCPUコアにバインドする設定。カーネルがコア間でタスクを移動させるのを防ぎ、キャッシュパフォーマンスの低下を防止します。
ステップ1:ボトルネックの診断
まず、割り込みが偏っていないか確認します。リアルタイムで分布を確認するには、次のコマンドを実行します。
watch -n 1 "cat /proc/interrupts | grep eth0"
(eth0を、ens3やp4p1などの実際のインターフェース名に置き換えてください)。
特定のCPU列の数値だけが急増し、他が横ばいであれば、アフィニティ(親和性)の問題です。また、sysstatパッケージのmpstatも確認してください。
mpstat -P ALL 1
%soft列に注目してください。一つのコアが90〜100%を示し、他が0%であれば、そのコアはネットワーク割り込みでパンクしています。
ステップ2:RSS(ハードウェアキュー)の有効化
最近の10GbEや25GbE NICの多くは、マルチキューをサポートしています。ethtoolでハードウェアの制限を確認しましょう。
sudo ethtool -l eth0
“Combined”が1で、”Maximum”がそれより大きい場合、ハードウェアの潜在能力を十分に活用できていません。アクティブなキューの数をコア数に合わせて増やしてください(ほとんどのワークロードでは最大8で十分です)。
sudo ethtool -L eth0 combined 8
キューを増やすことで、NICが複数のCPUと同時に通信できるようになります。
ステップ3:IRQ Affinityの固定
通常、irqbalanceサービスがこれを管理しますが、高トラフィックのサーバーでは誤った判断をすることがよくあります。頻繁にコア間で割り込みを移動させるため、CPUキャッシュがフラッシュされ、レイテンシが急増します。予測可能なパフォーマンスを得るために、このサービスを停止しましょう。
sudo systemctl stop irqbalance
sudo systemctl disable irqbalance
次に、NICキューのIRQ番号を確認します。
grep eth0 /proc/interrupts | awk '{print $1}' | sed 's/://'
IRQ 45をCPU 0にマップするには、16進数のビットマスクをsmp_affinityファイルに書き込みます。このマスクでは、1がCPU 0、2がCPU 1、4がCPU 2、8がCPU 3を表します。
# IRQ 45をCPU 0にバインド
echo 1 | sudo tee /proc/irq/45/smp_affinity
# IRQ 46をCPU 1にバインド
echo 2 | sudo tee /proc/irq/46/smp_affinity
ステップ4:仮想化環境でのRPSの活用
AWSやDigitalOceanのようなクラウドプロバイダーを利用している場合、仮想NICが単一のキューしかサポートしていないことがあります。ここで役立つのがRPSです。ソフトウェア的に、複数のコアにわたってTCP/IPスタックの重い処理を分散させます。
RPSを有効にするには、CPUマスクをrps_cpusファイルに書き込みます。8コアのシステムで、マスクをff(バイナリで11111111)にすると、カーネルはすべてのコアを使用できるようになります。
# 全8コアでeth0의 RPSを有効化
echo "ff" | sudo tee /sys/class/net/eth0/queues/rx-0/rps_cpus
ステップ5:RFSによる微調整
RFS(Receive Flow Steering)は、最適化をさらに一歩進めます。実際にアプリケーション(NginxやHAProxyなど)を実行しているCPUを追跡し、パケット処理をその同じコアに誘導します。これにより、コストのかかるコア間のデータ転送を最小限に抑えます。
まず、グローバルフローテーブルの制限を設定します。
sudo sysctl -w net.core.rps_sock_flow_entries=32768
次に、各特定のキューに対して制限を設定します。
echo 4096 | sudo tee /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
本番環境での戦略
高負荷環境において、私はハイブリッドアプローチが最適であると考えています。
- ハードウェアを最大化:
ethtoolのキューを物理コア数に合わせます(最大8または16)。 - 手動固定: 各NICキューを独自の物理コアにバインドします。可能であればCPU 0は避けてください。通常、システムタイマーやディスクI/Oを処理しているためです。
- ソフトウェアによるフォールバック: ハードウェアキューの数がCPUコア数より少ない場合にのみ、RPSとRFSを使用します。
私たちのAPIゲートウェイにこれらの調整を適用した後、1つのコアで100%だった%si負荷が、全コアでバランスよく12%程度まで低下しました。ピーク時のレイテンシは40ms短縮され、パケットロスも解消されました。ネットワークスタックのチューニングは魔法ではありません。データがCPUに到達するための、クリアな多車線高速道路を確保することなのです。

