RSS Hash key の効果効用
RSSハッシュキーによる動作の変化を確認する
動機
igcのRSSハッシュキーの設定が行えるようにするパッチを出したので、実際に動作が変わっているかどうかを確認したい。
前提
RSSが有効なNICがパケットを受信した際、複数存在するキューのうちどのキューにパケットを積むかという部分に注目する。 端的に言うと、NICは受信したパケットの要素(IPアドレス、ポートなど)を抽出し、あらかじめ設定されたハッシュキーとハッシュ関数を用いてハッシュ値を計算し、そのハッシュ値をRETA(Redirection table, 別名 Indirection table)のサイズで割った剰余をインデックスとして、RETAから対応するキュー番号を選択する。
詳細についてはDPDKのブログやIntelのPDFに詳しく載っている。
詳しく知りたい方は、RSS Toeplitzなどでググっていただくと、RSSハッシュキーなどを調整してRSSの動作を予測可能にするといった、ある程度高度な情報も見つかるので面白いと思う。
実験
実際にパケットを受信して、以下の事項を確かめる
- デフォルトのランダムなハッシュキー環境下で、十分に分散が行われていること
- 値が全て0のハッシュキーをもつ極端な環境下で、パケットが一つのキューに集中すること
実験では以下の条件を利用する。
- デバイス: I226-V
- ドライバ: igc
- キュー数: 4
- ハッシュ関数: Toeplitz
- ハッシュキーのサイズ: 40 bytes
- ハッシュ計算に用いるフィールド: IP address (src, dst), UDP port (src, dst)
なお、igcでは、デフォルトではハッシュ計算の入力にIP address (src, dst)のみを利用するようになっているため、あらかじめ以下のようなコマンドでUDP port (src, dst)もハッシュ計算に利用するように設定しておく
# ethtool -N enp0s5 rx-flow-hash udp4 sdfn
上記の環境にて、UDP port (src, dst)がランダムな1000個のパケットを受信する。パケット生成側のコードは適当。
import socket
import time
import random
def send_udp(dst_port: int, dst_address):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(b"Hello", (dst_address, dst_port))
sock.close()
if __name__ == "__main__":
times = 1000
dst_address = "100.100.100.154"
for i in range(times):
send_udp(random.randint(3000, 65000), dst_address)
time.sleep(0.001)
1. デフォルトのランダムなハッシュキー環境
大抵のドライバではデフォルトのハッシュキーはランダムになっており(see netdev_rss_key_fill())、また通常RETAも分散しているため、ハッシュ関数への入力が分散している場合には、選択されるキューもある程度分散されることが期待される。
ハッシュキー、RETAの値は ethtool -x <dev> コマンドで確認可能であり、通常はこのようにランダムなハッシュキー、分散された比率のRETAが設定されている。
[root@localhost ~]# ethtool -x enp0s5
RX flow hash indirection table for enp0s5 with 4 RX ring(s):
0: 0 0 0 0 0 0 0 0
8: 0 0 0 0 0 0 0 0
16: 0 0 0 0 0 0 0 0
24: 0 0 0 0 0 0 0 0
32: 1 1 1 1 1 1 1 1
40: 1 1 1 1 1 1 1 1
48: 1 1 1 1 1 1 1 1
56: 1 1 1 1 1 1 1 1
64: 2 2 2 2 2 2 2 2
72: 2 2 2 2 2 2 2 2
80: 2 2 2 2 2 2 2 2
88: 2 2 2 2 2 2 2 2
96: 3 3 3 3 3 3 3 3
104: 3 3 3 3 3 3 3 3
112: 3 3 3 3 3 3 3 3
120: 3 3 3 3 3 3 3 3
RSS hash key:
b5:ea:d4:ca:f6:97:6b:b8:61:d4:ac:76:2e:1a:e9:e4:4f:54:d9:12:eb:c1:14:34:87:bf:c3:bc:b6:14:0a:c5:36:09:5e:44:d3:69:4f:3b
RSS hash function:
toeplitz: on
xor: off
crc32: off
RSS input transformation:
symmetric-xor: off
symmetric-or-xor: off
この状況でUDPパケットを1000個送ってみると、各キューで約250回の割り込みが増加しており(1000 パケット / 4キュー)、複数のキューへの分散が正常に行えている様子が伺える。なお、細かい話をすると、パケット数と割り込み回数は必ずしも対応しない。例えば、Interrupt coalescingやNAPIのポーリングなどによってその対応は変化する。もう少しパケットレートが高い場合には特に、数に乖離が出ることが想定されるが、その場合でも定性的な分散の様子は大差はないと予測している。
[root@localhost ~]# echo Before; grep enp0s5 /proc/interrupts
Before
35: 0 0 1 0 0 0 PCI-MSIX-0000:00:05.0 0-edge enp0s5
36: 0 0 0 1828 0 0 PCI-MSIX-0000:00:05.0 1-edge enp0s5-TxRx-0
37: 0 0 0 0 689 0 PCI-MSIX-0000:00:05.0 2-edge enp0s5-TxRx-1
38: 0 0 0 0 0 928 PCI-MSIX-0000:00:05.0 3-edge enp0s5-TxRx-2
39: 867 0 0 0 0 0 PCI-MSIX-0000:00:05.0 4-edge enp0s5-TxRx-3
[root@localhost ~]# echo After; grep enp0s5 /proc/interrupts
After
35: 0 0 1 0 0 0 PCI-MSIX-0000:00:05.0 0-edge enp0s5
36: 0 0 0 2077 0 0 PCI-MSIX-0000:00:05.0 1-edge enp0s5-TxRx-0
37: 0 0 0 0 931 0 PCI-MSIX-0000:00:05.0 2-edge enp0s5-TxRx-1
38: 0 0 0 0 0 1179 PCI-MSIX-0000:00:05.0 3-edge enp0s5-TxRx-2
39: 1134 0 0 0 0 0 PCI-MSIX-0000:00:05.0 4-edge enp0s5-TxRx-3
2. 値が全て0のハッシュキーをもつ極端な環境
さて、この記事の目的は、ハッシュキーの操作によって分散の度合いを変化させることであるので、最も単純なシナリオ(ハッシュキーが全て0である場合)を試す。 なお、重要な前提として、Toeplitzハッシュ関数では、ハッシュキーが全て0の場合ハッシュ値は入力に関係なく0となる。 そのため、常にRETA[0]番目のキューが選択されることとなり、パケットが一つのキューに偏るという状況を作り出せるはずである。
[root@localhost ~]# ethtool -X enp0s5 hkey 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
[root@localhost ~]# ethtool -x enp0s5
RX flow hash indirection table for enp0s5 with 4 RX ring(s):
0: 0 0 0 0 0 0 0 0
8: 0 0 0 0 0 0 0 0
16: 0 0 0 0 0 0 0 0
24: 0 0 0 0 0 0 0 0
32: 1 1 1 1 1 1 1 1
40: 1 1 1 1 1 1 1 1
48: 1 1 1 1 1 1 1 1
56: 1 1 1 1 1 1 1 1
64: 2 2 2 2 2 2 2 2
72: 2 2 2 2 2 2 2 2
80: 2 2 2 2 2 2 2 2
88: 2 2 2 2 2 2 2 2
96: 3 3 3 3 3 3 3 3
104: 3 3 3 3 3 3 3 3
112: 3 3 3 3 3 3 3 3
120: 3 3 3 3 3 3 3 3
RSS hash key:
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
RSS hash function:
toeplitz: on
xor: off
crc32: off
RSS input transformation:
symmetric-xor: off
symmetric-or-xor: off
先ほどと同様にUDPパケットを1000個送ってみると、キュー0(enp0s5-TxRx-0)の割り込み回数が約1000増加している。一方、その他のキューにおいては割り込み回数は増加していないか、微増程度(おそらくその他の何らかのパケットの影響)である。 この結果より、キュー0(= RETA[0])に意図的にパケットを偏らせることができたと見ていいだろう。
[root@localhost ~]# echo before; grep enp0s5 /proc/interrupts
before
35: 0 0 1 0 0 0 PCI-MSIX-0000:00:05.0 0-edge enp0s5
36: 0 0 0 2122 0 0 PCI-MSIX-0000:00:05.0 1-edge enp0s5-TxRx-0
37: 0 0 0 0 972 0 PCI-MSIX-0000:00:05.0 2-edge enp0s5-TxRx-1
38: 0 0 0 0 0 1274 PCI-MSIX-0000:00:05.0 3-edge enp0s5-TxRx-2
39: 1248 0 0 0 0 0 PCI-MSIX-0000:00:05.0 4-edge enp0s5-TxRx-3
[root@localhost ~]# echo after; grep enp0s5 /proc/interrupts
after
35: 0 0 1 0 0 0 PCI-MSIX-0000:00:05.0 0-edge enp0s5
36: 0 0 0 3124 0 0 PCI-MSIX-0000:00:05.0 1-edge enp0s5-TxRx-0
37: 0 0 0 0 979 0 PCI-MSIX-0000:00:05.0 2-edge enp0s5-TxRx-1
38: 0 0 0 0 0 1274 PCI-MSIX-0000:00:05.0 3-edge enp0s5-TxRx-2
39: 1248 0 0 0 0 0 PCI-MSIX-0000:00:05.0 4-edge enp0s5-TxRx-3
ここで、RETA[0]を変更してみて、本当にRETA[0]が選択されているのかを確認する。 なお、おそらくethtoolでは特定のRETAエントリのみを変更する機能がないため、適当なプログラム(set-custom-indir)を用意した。[1]
RETA[0] = 3 とし、残りの要素はデフォルトのままとする。このとき、キュー3にパケットが集中してほしい。
[root@localhost ~]# ./set-custom-indir
[root@localhost ~]# ethtool -x enp0s5
RX flow hash indirection table for enp0s5 with 4 RX ring(s):
0: 3 0 0 0 0 0 0 0
8: 0 0 0 0 0 0 0 0
16: 0 0 0 0 0 0 0 0
24: 0 0 0 0 0 0 0 0
32: 1 1 1 1 1 1 1 1
40: 1 1 1 1 1 1 1 1
48: 1 1 1 1 1 1 1 1
56: 1 1 1 1 1 1 1 1
64: 2 2 2 2 2 2 2 2
72: 2 2 2 2 2 2 2 2
80: 2 2 2 2 2 2 2 2
88: 2 2 2 2 2 2 2 2
96: 3 3 3 3 3 3 3 3
104: 3 3 3 3 3 3 3 3
112: 3 3 3 3 3 3 3 3
120: 3 3 3 3 3 3 3 3
RSS hash key:
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
RSS hash function:
toeplitz: on
xor: off
crc32: off
RSS input transformation:
symmetric-xor: off
symmetric-or-xor: off
前回までと同様にパケットを流すと、キュー3(enp0s5-TxRx-3)の割り込み回数が約1000増加している。 こちらの結果より、RETAの設定が実際に効いていること、およびRETA[0]が選択されていることが確かめられた。
[root@localhost ~]# echo before; grep enp0s5 /proc/interrupts
before
35: 0 0 1 0 0 0 PCI-MSIX-0000:00:05.0 0-edge enp0s5
36: 0 0 0 3501 0 0 PCI-MSIX-0000:00:05.0 1-edge enp0s5-TxRx-0
37: 0 0 0 0 1716 0 PCI-MSIX-0000:00:05.0 2-edge enp0s5-TxRx-1
38: 0 0 0 0 0 1706 PCI-MSIX-0000:00:05.0 3-edge enp0s5-TxRx-2
39: 1958 0 0 0 0 0 PCI-MSIX-0000:00:05.0 4-edge enp0s5-TxRx-3
[root@localhost ~]# echo after; grep enp0s5 /proc/interrupts
after
35: 0 0 1 0 0 0 PCI-MSIX-0000:00:05.0 0-edge enp0s5
36: 0 0 0 3501 0 0 PCI-MSIX-0000:00:05.0 1-edge enp0s5-TxRx-0
37: 0 0 0 0 1723 0 PCI-MSIX-0000:00:05.0 2-edge enp0s5-TxRx-1
38: 0 0 0 0 0 1706 PCI-MSIX-0000:00:05.0 3-edge enp0s5-TxRx-2
39: 2960 0 0 0 0 0 PCI-MSIX-0000:00:05.0 4-edge enp0s5-TxRx-3
結論
本実験で以下を確認した。
- RSS ハッシュキーがランダムである場合、パケットが複数キューに分散する
- ハッシュキーを全て0にすることで、意図的に全てのパケットをRETA[0]のキューへ割り振ることができる
- RETAエントリを変更することで、特定のハッシュ値のパケットを任意のキューに振り分けることができる
Appendix
[1] set-custom-indir: RETA[0] = 3 にするやつ
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/if.h>
#include <linux/sockios.h>
#include <linux/ethtool.h>
#define DEV "enp0s5"
#define RETA_SIZE 128
uint32_t reta[RETA_SIZE] = {
3, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
};
int main() {
struct ethtool_rxfh *rss;
struct ifreq ifr = {};
int ret = 0;
int fd;
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
rss = calloc(1, sizeof(struct ethtool_rxfh) + sizeof(reta));
if (!rss) {
perror("malloc");
ret = 1;
goto free_fd;
}
rss->cmd = ETHTOOL_SRSSH;
rss->indir_size = RETA_SIZE;
memcpy(rss->rss_config, &reta, sizeof(reta));
strcpy(ifr.ifr_ifrn.ifrn_name, DEV);
ifr.ifr_ifru.ifru_data = rss;
if (ioctl(fd, SIOCETHTOOL, &ifr) < 0) {
perror("ioctl");
ret = 1;
goto free_rss;
}
free_rss:
free(rss);
free_fd:
close(fd);
return ret;
}