KVM上で稼動しているネットワーク負荷が高いVMのチューニングについて

# はじめに
KVM 上の VM でネットワーク負荷（今回はパケットの受信が膨大な環境を想定）が極めて高負荷になると、vCPU が複数 VM に割り当てされていても特定の CPU コアに負荷が偏ってしまい、パケット処理が詰まってしまう等の弊害が発生してしまう可能性があります。

本記事では、特定の CPU コアに負荷が偏ってしまう原因となっている可能性が高い、Linux のパケット受信処理の仕組みに観点を当ててそのチューニングの具体的方法を紹介したいと思います。
（もちろん、他要因によって負荷が偏ってしまう場合もあると思うので、その中の一例として紹介させていただきます。）

# Linux のパケット受信処理の仕組み
まずはじめに、チューニングするにあたって Linux のパケット処理の仕組みを理解する必要があるので簡単に紹介します。

## 概要
[RedHat の公式ページ](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/6/html/performance_tuning_guide/s-network-packet-reception)で概要について紹介されているので引用させていただきます。

> 1.ハードウェア受信: ネットワークインターフェースカード (NIC) が接続線上でフレームを受信します。そのドライバー設定により、NIC はフレームを内部のハードウェアバッファーメモリか指定されたリングバッファーに移動します。

> 2.ハード IRQ: NIC が CPU に割り込むことでネットフレームの存在をアサートします。これにより NIC ドライバーは割り込みを承認し、ソフト IRQ オペレーション をスケジュールします。

> 3.ソフト IRQ: このステージでは、実際のフレーム受信プロセスを実装し、softirq コンテキストで実行します。つまり、このステージは指定された CPU上で実行されている全アプリケーションよりも先に行われますが、ハード IRQ のアサートは許可します。

> (ハード IRQ と同じ CPU 上で実行することでロッキングオーバーヘッドを最小化している) このコンテキストでは、カーネルは実際に NIC ハードウェアバッファーからフレームを削除し、ネットワークスタックで処理します。ここからは、フレームはターゲットのリスニングソケットへの転送、破棄、パスのいずれかが行われます。

> フレームはソケットにパスされると、ソケットを所有するアプリケーションに追加されます。このプロセスは、NIC ハードウェアバッファーのフレームがなくなるまで、または device weight (dev_weight) まで繰り返されます。device weight についての詳細は 「NIC ハードウェアバッファー」 を参照してください。

> 4.アプリケーション受信: アプリケーションがフレームを受信し、標準 POSIX コール (read、recv、recvfrom) で所有されているソケットからキューを外します。この時点で、ネットワークで受信されたデータはネットワークソケット上には存在しなくなります。

## ポイント
この中で今回重要なのは 2 と 3 のステップとなります。

説明の通り、パケットを受信する際はハードウェア割り込み、ソフトウェア割り込みの 2 つの割り込みが起こります。
下記にて、それぞれの処理の内容を簡単に記載します。

- ハードウェア割り込み処理
    - 受信パケットを**受信キュー**に積み、ソフトウェア割り込みをスケジューリングする
- ソフトウェア割り込み処理
    - 受信キューからパケットを取り出し各プロトコルの受信ハンドラを呼び出し処理を行う

この割り込み処理は特定の CPU コアにて実施されるため、割り込み処理が行われる CPU コアに負荷がかかるということになります。
なので、この**ハードウェア割り込み処理/ソフトウェア割り込み処理をうまく複数 CPU コアで分散できれば、CPU 負荷も分散できる**ことになります。

また、パケットの受信キュー 1 つに対して IRQ が 1 つ対応するため、この受信キューが多ければその分、複数の IRQ で処理可能 = ハードウェア割り込みを処理する CPU コアの分散が可能ということになります。

# チューニングのやり方
それでは、ハードウェア割り込み/ソフトェア割り込みそれぞれの具体的な分散方法について説明します。
なお、今回は下記環境の KVM ホストを利用した説明となります。

- OS
    - CentOS 7.3
- libvirt
    - 2.0.0

## ソフトウェア割り込みのチューニング（RPS）
パケット受信時のソフトウェア割り込み処理の CPU 分散は**「RECEIVE PACKET STEERING (RPS)」**という機能を利用することで行います。
RPS を利用することで、ソフトェア割り込み処理時に処理している CPU コアから他の CPU コアへ CPU 間割り込みを発生させ、割り込み先の CPU コアにプロトコル処理を委任します。

このRPSですが、デフォルトは [RedHat の公式ページ](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/performance_tuning_guide/network-rps)にも記載の通り無効化されています。
**無効化されているということは、受信キューに対応した CPU コアがそのままプロトコル処理を行うということです。
（つまり、処理する CPU コアが分散できていない）**

> The default value of the rps_cpus file is zero. This disables RPS, so the CPU that handles the network interrupt also processes the packet.

実際に設定を見てましょう。
（上記 KVM ホスト上に構築した VM 上での設定確認となります。）

```Bash:/sys/class/net/eth0/queues/rx-0/rps_cpus
0
```

この値を処理させたい CPU コアに対して 2 進数でフラグを立てた値に書き換えてあげることで RPS の設定が可能です。
（2 進数を 16 進数に変換して記載することも可能です。）

下記に設定例を示します。

```Bash:/sys/class/net/eth0/queues/rx-0/rps_cpus
# CPU コアが 8 つ（0～7）ありすべてのコアに対して分散させたい場合
11111111
もしくは
ff

# CPU コアが 4 つ（0～3）あり 0, 1 のコアに対して分散させたい場合
0011
もしくは
3
```

この rps_cpus は **rx-0** 配下に設定ファイルがありますが、この **rx-X** のディレクトリは受信キューの数だけ用意されており、受信キュー毎に設定が可能となります。

## ハードウェア割り込みのチューニング（RSS）
パケット受信時のハードウェア割り込み処理の CPU 分散は**「RECEIVE-SIDE SCALING (RSS)」**という機能を利用することで行います。
RSS とは簡単に説明するとパケット受信キューを複数用意して、その間で分散を行うことで処理分散をしようという機能となります。

なお、前述しましたが**パケットの受信キュー 1 つに対して IRQ が 1 つ対応するため**、受信キューに対応した IRQ の処理を分散すること = ハードウエア割り込み処理の負荷分散となります。

しかし、KVM 上で構築した VM はデフォルトでこのパケット受信キューが 1 つしかありません。

```Bash
$ ls /sys/class/net/eth0/queues
rx-0  tx-0

$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
...
 28:          0          0          0          0   PCI-MSI-edge      virtio0-config
 29:         51    5453092          0          0   PCI-MSI-edge      virtio0-input.0
 30:          2          0         68          0   PCI-MSI-edge      virtio0-output.0
...
```

1 つでも、それに対応した IRQ の処理を分散することである程度の負荷分散は可能ですがネットワーク負荷が極めて高い環境では、入り口となるキューが 1 つでは足りません。
そのため、VM の NIC に対する受信キューを増加させる設定をする必要があります。

この設定方法については [RedHat の公式ページ](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/virtualization_tuning_and_optimization_guide/sect-virtualization_tuning_optimization_guide-networking-techniques)でも初回されている通り VM の定義ファイル（XML）に設定を追加することで可能です。

下記に設定例を示します。

- ホスト OS 側の設定
    - 対象の VM の定義ファイル（XML）に下記記述を追加します。（queues パラメータ部分）
        - N の部分にキューの数となる数値を入れてください。

```Bash
<interface type='network'>
      <source network='default'/>
      <model type='virtio'/>
      <driver name='vhost' queues='N'/>
</interface>
```

- ゲスト OS 側の設定
    - 対象の VM で下記コマンドを実行します。
    - N の部分にホスト OS 側で定義した数値を入力してください。

```Bash
$ ethtool -L <IF名> combined N
```

これらの設定をすると、`/sys/class/net/<IF名>/queues` や `/proc/interrupts` を参照した際、受信キューの数が増えていることが確認できます。

あとは、**それぞれの対象 IRQ 番号に対して irqbalance を利用して動的に分散するなり、処理する CPU コアをそれぞれの対象 IRQ 番号毎に CPU コアが分散するように固定するなりして**処理を分散させると良いと思います。

## 補足
最後に今回紹介した設定について、補足をします。

### 補足1 : 設定の初期化について
RPS および RSS（ethtool コマンド）については VM を再起動してしまうと設定がデフォルトに戻ってしまいます。
ですので、起動時スクリプトなどに仕込む必要があります。

### 補足2 : 分散しすぎについて
RPS ですが、当然 CPU 間割り込み処理が起こるので場合によってはそれがかえってオーバーヘッドになってしまう可能性もあります。

設定の思想としては、受信キューの数がマシンの CPU コア数よりも少ない場合に RPS の設定をしてさらなる分散をしてあげると良いと思います。
（受信キューの数 = CPU コア数の場合、RSSの時点で十分な CPU コア間の分散ができているため。）

# おわりに
物理マシンの NIC にパススルーしてしまえばそれで終わりかもしれませんが、物理 NIC 専有されてしまうのが好ましくない環境などではこのチューニングの効果がとても良く発揮されると思います。