高トラフィックサーバーにおけるシングルコアのボトルネック
64個のCPUコアと10Gbpsのアップリンクを備えたサーバーを使用していても、ネットワークパフォーマンスが限界に達することがあります。トラフィックの急増時に top コマンドを実行すると、奇妙な光景を目にするかもしれません。他の63個のコアがほとんどアイドル状態であるにもかかわらず、CPU0だけが “si”(ソフトウェア割り込み)列で100%に張り付いているのです。これが典型的な「シングルコアのボトルネック」です。
デフォルトでは、多くのネットワークインターフェースカード(NIC)が、すべての着信パケットを単一のCPU割り込みで処理します。トラフィックが毎秒100万パケット(pps)以上に達すると、単一のコアでは割り込み処理が追いつかなくなります。その結果、カーネルがパケットをドロップし始め、200ミリ秒以上のレイテンシスパイクや接続タイムアウトが発生します。これを解決するために新しいハードウェアは必要ありません。Linuxカーネルに対して、その負荷をどのように分散させるかを指示するだけでよいのです。
解決策を理解する:RSS、RPS、RFS
Linuxには、ネットワーク処理をCPUトポロジー全体に分散させるための主なツールが3つあります。適切な選択は、ハードウェアが重い処理を担えるか、あるいはカーネルがソフトウェアを介して介入する必要があるかによって決まります。
1. RSS (Receive Side Scaling)
RSSは、ハードウェアレベルで処理が行われるため、最も効率的な方法です。NICは複数のハードウェア受信キューを使用します。パケットが到着すると、NICはIPアドレスとポート(4-tuple)に基づいてハッシュを計算し、そのパケットを特定のCPUコアに紐付けられた特定のキューに割り当てます。これはパケットがLinuxカーネルに到達する前に行われます。
2. RPS (Receive Packet Steering)
RPSは、RSSのソフトウェアベースの代替手段です。AWS EC2のt3インスタンスのような仮想化環境や、受信(RX)キューを1つしか持たない古いハードウェアにとっての救世主となります。単一のコアがパケットを受信すると、RPSがハッシュを計算し、さらなる処理のためにパケットを他のCPUに渡します。受け渡しのためにわずかなCPUサイクルを消費しますが、単一のコアがシステム全体のチョークポイントになるのを防ぐことができます。
3. RFS (Receive Flow Steering)
RFSは、アプリケーションの状態を考慮することでRPSを改良したものです。パケットをランダムに分散させるのではなく、アプリケーション(NginxやRedisなど)が実際にどのコアを使用しているかを追跡し、パケットをその特定のコアに誘導します。これにより、L1/L2キャッシュのヒット率が大幅に向上し、異なるCPUキャッシュ間でデータを移動させることによるレイテンシが削減されます。
各手法の比較
| 機能 | RSS | RPS | RFS |
|---|---|---|---|
| 実装レベル | ハードウェア (NIC) | ソフトウェア (カーネル) | ソフトウェア (カーネル) |
| 最適な用途 | 物理ベアメタル | 仮想マシン / 単一キューNIC | 低遅延が重要なアプリ |
| CPUオーバーヘッド | 極めて低い | 低〜中 | 中 |
| 効率性 | 高い | 適度 | キャッシュ局所性の最大化 |
最適化の優先順位
サーバーのネットワークスタックのスケーリングは、通常、論理的な手順に従います。まずハードウェアキューから始め、不足している部分をソフトウェアステアリングで補います。
- RSSを最大化する: NICが8つのキューをサポートしており、CPUが16コアある場合は、まず8つのキューすべてを構成します。
- RPSを重ねる: RPSを使用して、ハードウェアキューの数と総コア数の差を埋めます。例えば、RPSを使用して、これら8つのハードウェアキューを全16コアに分散させます。
- RFSで微調整する: データベースのワークロードや、キャッシュレイテンシのマイクロ秒単位の差が重要となる高コンカレンシーなWebサーバーを運用している場合に、RFSを有効にします。
実装ガイド
ステップ1:ハードウェアキューの設定 (RSS)
ethtool を使用してハードウェアの能力を確認します。ドライバがサポートしている “Combined” チャネルの数を確認してください。
# 現在のキュー設定と最大値を確認する
sudo ethtool -l eth0
現在の “Combined” の値が最大値より小さい場合は、CPUコア数(またはハードウェアが許容する最大値)に合わせて増やします。
# キューの数を8に増やす
sudo ethtool -L eth0 combined 8
ステップ2:ソフトウェアステアリングの設定 (RPS)
RPSはビットマスクを使用して、どのCPUがパケットを処理できるかを定義します。このマスクはCPUコアを16進数で表したものです。例えば、マスク f(バイナリ 1111)はコア0〜3を使用し、ff はコア0〜7を使用します。
# 最初の8コア(16進数でff)に対して、キュー0でRPSを有効にする
echo "ff" | sudo tee /sys/class/net/eth0/queues/rx-0/rps_cpus
NICに複数の受信キュー(rx-0, rx-1など)がある場合は、均等に分散させるためにすべてのキューディレクトリにこのマスクを適用する必要があります。
ステップ3:アプリケーションステアリングの有効化 (RFS)
RFSには2段階の設定が必要です。まず、カーネルが追跡すべき総フロー数のグローバル制限を設定します。負荷の高いWebサーバーの場合、32,768が適切な開始点です。
sudo sysctl -w net.core.rps_sock_flow_entries=32768
次に、個々のハードウェアキューのフロー数を設定します。これは通常、総エントリ数 / キューの数 として計算されます。単一キュー構成の場合は、グローバル値と同じ値を設定します。
# 最初のキューのフロー数を設定する
echo "32768" | sudo tee /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
結果の検証
コマンドがエラーなしで終了したからといって、設定が機能したと思い込まないでください。負荷がかかっている状態での実際のCPUの挙動を観察する必要があります。mpstat を使用して、ソフトウェア割り込みの分散をリアルタイムで監視します。
# すべてのCPUを1秒ごとに監視する
mpstat -P ALL 1
%soft 列に注目してください。これらの変更を行う前は、CPU0が90-100%で他のコアが0%だったはずです。設定が成功すると、マスク内のすべてのコアで %soft の値が分散されている(例:各コア10-15%など)のが確認できるはずです。
最後に
効率的なパケット分散は、単なるスループットの向上だけではなく、システムの予測可能性を高めることにもつながります。1つのコアが割り込みで限界に達すると、カーネルは他の重要なタスクのスケジューリングに苦労し、ジッター(遅延のゆらぎ)が発生します。RSS、RPS、RFSを実装することで、ネットワークスタックがハードウェアに合わせて線形にスケールし、サーバーが過酷なトラフィックを余裕を持って処理できるようになります。

