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も分散しているため、ハッシュ関数への入力が分散している場合には、選択されるキューもある程度分散されることが期待される。
実践 fault injection framework
実践 fault injection framework
fault injection framework の存在自体は認知していたものの、実際に手を動かして試してみるといったことをしないまま日々を過ごしていたところ、ひょんなことからその利便性を体験することができたので、実際の利用方法や活用できそうなシナリオ、Tips (とよべるかどうかわからない情報) を残しておきます。
なお、一次情報はカーネルのソース・ドキュメントに十分存在するため、網羅性については諦めています。
fault injection framework とは
端的には「実行タイミング」における障害注入フレームワークであり、主に debugfs をインターフェイスとしてカーネルの実行時に注入の有無や条件を設定することが可能です。
対照的には、以下の最も単純な例に示すようにビルドタイミングにて障害注入を行うことも可能ですが、この極めてナイーブな例では条件を変える度
にビルドが必要となるなど、柔軟性に難点があります。
もちろん、何かしらのインターフェイスで操作可能な custom_should_fail() のような関数を用意して注入することも可能だとは思いますが、そこまでしたいなら素直に fault injection framework に乗っかるのが良いでしょう。
int foo() {
...
- if (bar())
+ if (true || bar())
return -ENOMEM;
...
return 0;
}
fault injection framework には、失敗させたい操作ごとに異なるトリガーが用意されています。
- failslab: slab allocation (kmalloc() などの kmem_cache_alloc() を叩くやつ) を失敗させる
- fail_page_alloc: page allocation を失敗させる
- fail_usercopy: ユーザー空間、カーネル空間におけるメモリのコピーを失敗させる
- fail_function: ビルド時に予めマークされている関数の戻り値にエラーを注入する
- そのほか多数
なお、基本的な利用方法のほとんどは fail* で共通であるため、今回はたまたま利用することになった failslab のみに焦点を当てることとします。 網羅的なガイドはConfigure fault-injection capabilities behaviorを参照ください。
うわっ・・・わたしのロードアベレージ低すぎ・・・?
はじめに
そもそもロードアベレージとはなんでしょうか。どの程度であれば正常で、どの程度の値であるべきでしょうか。
「サーバーのロードアベレージが530000になっている!これは異常だ!」と連絡が来ていますが、果たして本当に異常でしょうか。
本記事では、カーネルソースリーディングやカーネルモジュール作成を通じロードアベレージの理解を深めるとともに、あなたが未だかつてないロードアベレージを記録するためのTipsをお届けすることを目的としています。
ロードアベレージって何?
「実行中または実行可能 (R状態) 、および割り込み不可能 (D状態) なスレッドの数を移動平均したもの」です。
馴染みがない方は参考として Red Hat のナレッジベースを参照されることをお勧めいたします。
ロードアベレージの正体
kernel/sched/load.cを読んだことがある方、興味のない方は読み飛ばしてください。
簡単な定義・計算方法はコメントに記載のある通りであり、R状態およびD状態のスレッドを「アクティブスレッド」としてカウントし、それを指数移動平均することによって計算します。
* The global load average is an exponentially decaying average of nr_running +
* nr_uninterruptible.
*
* Once every LOAD_FREQ:
*
* nr_active = 0;
* for_each_possible_cpu(cpu)
* nr_active += cpu_of(cpu)->nr_running + cpu_of(cpu)->nr_uninterruptible;
*
* avenrun[n] = avenrun[0] * exp_n + nr_active * (1 - exp_n)
calc_global_load() が定期的に実行され、だいたい5秒に一度、「アクティブスレッド数」をもとにグローバル変数 avenrun を更新しています。
また、「アクティブスレッド数」は calc_load_tasks 変数で管理されており、本関数内部やそのほかのコンテキスト (calc_global_load_tick())で更新されています。