Linux のリアルタイムスケジューリングポリシーの挙動とちょっとした使い所

2024年09月17日 火曜日


【この記事を書いた人】
安形

研究所でシステムソフトウェアの研究に取り組んでいます。

「Linux のリアルタイムスケジューリングポリシーの挙動とちょっとした使い所」のイメージ

この記事では、Linux のプロセススケジューラに実装されているリアルタイムスケジューリングポリシーの挙動と、そのちょっとした用途を紹介します。

リアルタイム(システム)というと、ロボットの制御のような、ソフトウェアの処理の完了に時間的制約があるシステムが連想される場合が多いかと思いますが、この記事では、特定の環境下で、クライアントからのリクエストに対してレスポンスを返すサーバプログラムの応答性能を高めるような用途について紹介します。

注意事項

この記事は、プロセススケジューラを含むシステムの挙動を確認するための実験方法を記載しています。この記事に記載されている実験を追試される場合には、以下の点にご注意ください。

  • 注意1:実験には sudo コマンドを通して root 権限を利用します。実験を追試される場合は、破壊しても大丈夫な環境で試されることをお勧めいたします。
  • 注意2:実験完了後には、実験のために起動した全てのプログラムを忘れずに停止するようにしてください。

いかなる不利益が発生した場合も責任は負いかねますのでご了承ください。

背景・基本情報

汎用 OS と複数プロセス

汎用 OS では、1台のコンピュータ、さらに言えば1つの CPU コアで複数のプロセス(プログラム)を並列で実行することができるようになっています。

ブラウザで調べ物をしながらテキストエディタで資料を作成する、というようなことができるのは、OS が複数のプロセスを並列で扱うことができるようになっているおかげであると考えられます。

このような挙動を実現するための機能面でのポイントとして、1つの CPU コアは、基本的にある瞬間には1つのプログラム(プロセス)しか実行できず、CPU コアというハードウェア単体では(機能面で必要なサポートは実装しているものの)複数のプロセスを並列で扱えるようになっていないのですが、OS が実行するプロセス(プログラム)を適宜切り替えることで、複数のプロセスが同一の CPU コアの上で並列で動作することを可能にしています。

プロセススケジューリング

1つの CPU コアの上で実行すべき複数のプロセス(プログラム)があった場合に、OS はどのタイミングどのプロセスを実行するかの2点についてを決定する必要があり、その意思決定を行うことがプロセススケジューリングと呼ばれるものであると思われます。

この意思決定により、例えば、特定のプロセスに対して、他のプロセスより多くの実行時間を割り当てたり、また、応答性能が重要なプロセスを、応答性能が重要でないプロセスより優先的に実行する、というようなことができます。

簡単な実験その1

ここで、広く一般的に利用されているプロセススケジューラがどのような挙動をしているかについて簡単な実験を通して調べてみます。実験には Linux 6.2 を Ubuntu 22.04 で利用しています。

観測対象となるプロセスの起動

まず、ターミナル・コンソールを二つ開いて、yes コマンドを実行します。yes コマンドはひたすら y を出力し続けるプログラムで、CPU 時間を消費し続けます。

この出力はターミナル・コンソールに(特に ssh 等で接続しているリモートホストの場合、通信帯域に)負荷がかかるため、出力先を /dev/null にします。

また、今回は、taskset コマンドを利用して、yes コマンドが CPU コア0 で実行されるように設定します。

ターミナル・コンソール1
taskset -c 0 yes > /dev/null
ターミナル・コンソール2
taskset -c 0 yes > /dev/null

観測対象となるプロセスが消費している CPU サイクルの確認

次に、もう一つターミナル・コンソールを開いて、top コマンドを実行して、yes コマンドが消費している CPU サイクルを見てみます。

ターミナル・コンソール3
top

上記、top コマンドを実行すると、今回は以下のような出力が見られました。

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                     
    925 user      20   0    2788   1536   1536 R  50.0   0.0   0:03.10 yes                         
    926 user      20   0    2788   1536   1536 R  50.0   0.0   0:02.03 yes

一番左、PID 以下の値は、Process ID (PID) の意味で、OS が管理するプロセス一つずつに付与される、プロセスを識別するための ID 番号です。この例では、PID 925 と PID 926  のプロセスが yes コマンドを実行しています。

今回は、%CPU 以下に 50.0 という数字が表示されており、これはプロセス 925 と 926 が、それぞれ 50% ずつ CPU サイクルを消費していることを表しています。(100% は1つの CPU コア全ての時間を占有していることを意味します。)

先述の通り、OS のプロセススケジューラがこれら二つの yes コマンドのプロセスを一定時間ごとにが切り替えているため、両方が同じ CPU コアの上で並列に動作することができており、また、ここで動作確認をしている OS のプロセススケジューラは、プロセスに対して公平に実行時間を割り当てるようなスケジューリングポリシーを採用しているため、これら二つのプロセスそれぞれが 50% ずつ CPU サイクルを消費するような挙動になっています。

プロセスの切り替えタイミング・頻度の確認

次に、どのようなタイミング・頻度で OS のプロセススケジューラが切り替えを行っているのか見てみます。

ターミナル・コンソール3で実行していた top コマンドを停止して、以下のようなコマンドを実行してみます。試される場合は、以下のコマンド中の 925 と 926 の数字を、上の top コマンドで見られた yes コマンドの PID 以下の数字に置き換えてください。

ターミナル・コンソール3
sudo bpftrace -e 'tracepoint:sched:sched_switch { if (args->prev_pid == 925 || args->prev_pid == 926) { printf("[%lu]: prev %d next %d\n", nsecs - @ts[0], args->prev_pid, args->next_pid); @ts[0] = nsecs; } }'

この bpftrace というコマンドは Linux カーネル内部のトレーシングを容易に行うことを可能にするツールで、上記のコマンドでは、プロセス切り替えが発生する場合に呼び出される sched_switch というトレースポイントをフックし、切り替え元 (prev) と切り替え先 (next) のプロセスの番号を表示するプログラムを適用しています。一番左の [ ]: で囲まれた数字は、切り替え元のプロセスが継続して実行された時間(ナノ秒単位)を表示しています。

[15997155]: prev 926 next 925
[15999372]: prev 925 next 926
[15998610]: prev 926 next 925
[16000479]: prev 925 next 926
[15998441]: prev 926 next 925
[15997948]: prev 925 next 926

このプログラムを実行してみると、上のような出力が得られました。この出力から yes コマンドを実行しているプロセス 925 と 926 が交互に切り替えられていることがわかります。(これは一部を切り取っており、925 と 926 以外のプロセスも観測されます。)

また、上記の出力によると、今回の環境では約 16 ミリ秒間隔でプロセスの切り替えが発生していたようです。

プロセススケジューリングが行われるタイミング

上の実験で、カーネル内部のトレースポイントを利用して観測を行ったことからわかるように、Linux のような汎用 OS では、CPU が実行するプロセスを別のプロセスへ切り替える、という操作はカーネル空間で行われます。

つまり、汎用 OS にとってプロセスの切り替えを含むプロセススケジューリングを行うことが可能なタイミングは、CPU がカーネル空間の処理を実行している時になります。逆を言えば、CPU がユーザ空間のプログラムを実行している場合には、カーネルによるプロセススケジューリングとプロセスの切り替えを行うことができません。

具体的に、CPU がユーザ空間プログラムを実行している状態から、カーネル空間の処理を実行している状態、へ切り替わるパターンは大まかに二通りが考えられると思います。

  1. ユーザ空間プログラムがシステムコールを呼ぶことで、処理がカーネル空間へ切り替わる
  2. ユーザ空間プログラムは実行中であったが、タイマー等のハードウェア割り込みにより、強制的に処理がカーネル空間に実装されているタイマー等の割り込みハンドラへ移行する

ユーザ空間プログラムは必ずしもカーネルのプロセススケジューラがプロセス切り替えを行いたいタイミングで上記1のようなシステムコールを呼び出してくれるわけではないため、カーネルはコンピュータに一般的に搭載されているタイマー機能を利用してハードウェア割り込みを発生させることで、強制的に一定時間経過後に実行コンテキストをユーザ空間からカーネル空間に引き戻すことができるようになっています。

プロセス切り替え発生の要因

カーネルは、上記のプロセススケジューリングが行われるタイミングで通過する処理の何箇所かで、schedule という関数を呼び出すようになっており、プロセススケジューリングについての処理はその中で行われますが、ここで必ずしもプロセスを切り替える判断が行われるわけではなく、現在実行されているプロセスをそのまま実行させ続ける、という選択をすることがありえます。

プロセススケジューラが、現在実行されているプロセスから別のプロセスへ切り替えを行う要因になるパターンは以下の二通りが考えられると思います。

  1. ユーザ空間プロセス(プログラム)自体は可能であれば継続して動いていたいが、カーネルが時間の経過等を考慮するスケジューリングアルゴリズムによる意思決定の結果、強制的に別のプロセスへの切り替えを行う
  2. ユーザ空間プロセス(プログラム)が自発的に CPU サイクルを手放すことを許容する・現状これ以上実行されなくてよい旨をカーネルのプロセススケジューラに対して意思表示するシステムコールを呼び出す

上記のパターン1は上の yes コマンドを利用した実験で確認したもので、広く利用されているプロセススケジューラでは一定時間以上実行されたプロセスを切り替えるようになっています。

一方、パターン2については、ユーザ空間プログラムが自発的に特定の種類のシステムコールを呼び出すことで、カーネルに対して、現状実行すべき処理がないことを伝え、プロセススケジューリングの対象から外してもらう、その結果、これまで動作していたものとは別のプロセスへの切り替えが発生する、という場合です。

どのシステムコールがこの自発的に CPU サイクルを手放すことを許容するかというのは、カーネルが提供する実装に依存します。例えば通信等の入力を待機する際に利用される read、accept システムコールや、その他イベント入力の多重化に利用される select、poll システムコール、また指定した時間の間 CPU 時間を手放す sleep 系のシステムコールが該当します。

簡単な実験その2

上記、プロセス切り替え発生要因パターン2の、プロセスが自分から CPU サイクルを手放す場合の挙動を、通信系のプログラムを実行して調査してみます。

観測対象となる CPU サイクルを手放すタイプのプログラムの起動

ターミナル・コンソールを開いて nc コマンドを実行し、TCP 接続を 10000 番ポートで受け付けるサーバを立ち上げます。

ターミナル・コンソール1
nc -l 10000

観測対象となるプロセスが消費している CPU サイクルの確認

次に、もう一つターミナル・コンソールを開いて、上記 nc コマンドを実行しているプロセスの PID を調べます。

ターミナル・コンソール2
ps aux|grep "nc -l 10000"|grep -v grep

上のコマンドの実行結果として、今回は以下のような出力が得られました。

user         957  0.0  0.0   3536  1536 pts/1    S+   11:42   0:00 nc -l 10000

上の出力によると、今回、nc コマンドを実行しているプロセスの PID は 957 のようです。

次に、nc コマンドがどの程度 CPU 時間を消費しているかを top コマンドで見てみます。-p の次には top コマンドで確認したい対象のプロセスの PID を指定しています。

ターミナル・コンソール2
top -p 957

今回は、以下のような出力が得られました。

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                     
    957 user      20   0    3536   1536   1536 S   0.0   0.0   0:00.00 nc

%CPU 以下に、0.0 と表示されており、nc コマンドが CPU サイクルを利用していない、つまり実行されていないことがわかります。

これは nc コマンドが内部で、カーネルに対して、現状処理すべき事柄がないため、特定の入力イベントがあるまでスケジュールされる必要がない意思表示をするシステムコールを呼び出しているためこのような挙動になっています。

挙動の確認

次に、bpftrace を使って、この nc コマンドのプロセスがスケジュールされた時(scheduled/動き始めた時)とスケジュールから外された時(descheduled/CPU 時間を手放した時)を視覚的に確認します。追試される場合は、以下 bpftrace コマンド内の 957 の数字をお手元の環境に合わせて変更してください。

ターミナル・コンソール2
sudo bpftrace -e 'tracepoint:sched:sched_switch { if (args->next_pid == 957) { printf("process %d is scheduled\n", args->next_pid); } else if (args->prev_pid == 957) { printf("process %d is descheduled\n", args->prev_pid); } }'

これで観測の準備ができたので、新しく別のターミナル・コンソールを開いて、nc コマンドで立ち上げているサーバへ telnet コマンドを使って接続してみます。

ターミナル・コンソール3(telnet コマンドを終了するには、Ctrl キーと ] キーを同時押しする、もしくは nc コマンドを Ctrl+C で停止します。)
telnet localhost 10000

上記の出力の後、ターミナル・コンソール2では、以下のような出力が見えます。これは、この nc コマンドがスケジュールされ、少し動いた後、またスケジュールから外されたことを意味しています。

process 957 is scheduled
process 957 is descheduled

また、ターミナル・コンソール3で適当な文字列を打ち込んでエンターキーを押してみると、打ち込まれた文字列が nc コマンドで立ち上げているサーバへ送られます。このとき、ターミナル・コンソール2では、文字列が送られるたびに nc コマンドを実行しているプロセスがスケジュールされ、少しの時間動き、その後スケジュールから外される挙動が確認できると思います。

CPU サイクルを手放すことを許容する・意思表示をするシステムコールの確認

また、別途、ターミナル・コンソール1側で strace nc -l 10000 として、strace を適用した上で同じ実験をしてみると、nc コマンドは主なループの中で accept4 と poll システムコールを呼び出して入力があるまで待機し、カーネル内のプロセススケジューラは通信の接続や入力イベントがあるとこのプロセスを待機から復帰させる、また、入力がない限りはこのプロセスをスケジュールしない、というような挙動をしていることがわかります。

既存のプログラムの実装と CPU リソース・電力消費への配慮

外部からの入力を待機する通信系のプログラムの多くは、上記 accept や poll のような、入力がない限り CPU 時間を手放すことを許容するシステムコールをある程度積極的に利用することで、むやみに CPU サイクルを消費しないように作られています。

また、特定の CPU コアに紐づけられているプロセス全てが入力待ちになると、カーネルは、(設定によりますが)その CPU コアの状態を電力消費の少ない休止モードに切り替えることができ、結果として消費電力を削減できます。(休止モードからの復帰はタイマーや入力デバイスからのハードウェア割り込みが起点になることが多いと思われます。)

プロセスの動作についての状態

少し踏み込んでプロセススケジューラの視点からのプロセスの扱いについて見てみると、上の2パターンにある「ユーザ空間プロセス(プログラム)自体は可能であれば継続して動いていたい」と「ユーザ空間プロセス(プログラム)が自発的に CPU サイクルを手放す」では、切り替えられた側のプロセスの、動作についての状態に違いがあります。

大まかな区分ですが、プロセススケジューラの視点から、プロセス(プログラム)は以下の状態をとります。

  • 実行中:文字通りプロセス(プログラム)が実行されている状態です。
  • 実行されたいが、実行されていない状態:この状態のプロセス(プログラム)は、可能であれば動作して処理したいことがありますが、プロセススケジューラがそのスケジューリングアルゴリズムに従い、そのプロセスを今は実行しないという決定をした結果、現在実行されていない、という状態です。この状態のプロセスは機会があれば動作したいと思っているため、プロセススケジューラは現在これを実行はしないものの、スケジューリングの候補としては扱います。プロセススケジューラは意思決定のタイミングで、「実行中」と「実行されたいが、実行されていない」状態のプロセスの中からどのプロセスを実行するかを選択します。
  • 実行される必要がないという意思表示がされた状態(待機・スリープ状態):通信等の入力を待機するようなプロセス(プログラム)は先述のとおり、CPU サイクルを手放すシステムコールを呼び出し、カーネル内のプロセススケジューラに対して、現在(また、特定の入力イベントが発生するまで)実行される必要がない、という意思表示をすると、プロセススケジューラはそのプロセスをスケジューリングの候補から外します。また、特定のイベント(通信の入力等)が発生すると、プロセスの状態が上記の「実行されたいが、実行されていない状態」へ切り替わり、プロセススケジューラにとってのスケジューリングの候補に加えられます。

なので、上の二つの実験について考えると、

  • 実験1で利用した yes コマンドは、CPU サイクルを手放すシステムコールを呼び出さず、可能な限り実行されるような実装になっており、実行時間を考慮するアルゴリズムを採用している一般的なプロセススケジューラは時間の経過を理由としてプロセス切り替えを行います。その結果、同一 CPU コア上で並列で動作している二つの yes コマンドは、実行中と、実行されたいが実行されていない状態の二つを行き来します。
  • 実験2で利用した nc コマンドは、通信の入力があるまで CPU サイクルを手放すシステムコールを呼び出すことで、実行される必要がないという意思表示がされた状態に入り、通信の入力があると、nc コマンドのプロセスは実行されたいが、実行されていない状態へ移行しプロセススケジューラによるスケジューリングの候補に復帰した後、実際にスケジューリング対象に選択されると実行中に切り替えられます。

リアルタイムスケジューリングポリシーの挙動

Linux では、公式のドキュメントによると、拡張性のため、関数ポインタの集合である sched_class という構造体を通して、プロセススケジューリング機構を実装したモジュールを複数階層的に採用することができるようになっています。

スケジューリングクラス

具体的には、これで全てではありませんが、以下のようなスケジューリングクラスが実装されています。

  • fair クラス:基本的に特別な設定をせずにプログラム・プロセスを起動すると、fair クラスと呼ばれるスケジューリングクラスに実装されているスケジューリングポリシーが適用されます。
  • rt クラス:特定のシステムコールを呼び出す、また、そのシステムコールを内部で呼び出すコマンドを利用することで、任意のプロセスに対して rt (real time) クラスのスケジューリングポリシーを適用できます。この記事で、ここまで特段の指定なくリアルタイムスケジューリングポリシーと呼んでいたのは、この rt クラスに実装されているスケジューリングポリシーを指していました

それぞれが、sched_class 構造体に含まれる関数ポインタの参照先となる関数を実装しており、それらの内容によってスケジューリングについての挙動が異なります。

複数のスケジューリングクラスの兼ね合い

先述のとおり、カーネルの中でプロセススケジューリングを行う時には、schedule という関数が呼び出されますが、この関数は fair や rt 等複数あるスケジューリングクラスについて特定の順番でそれぞれが実装している sched_class 構造体に含まれる pick_next_task という関数ポインタから参照可能な関数を呼び出し、次に実行すべきプロセスが返された場合には、それをスケジュール・実行します。

ここでポイントは、「特定の順番で」とありますが、rt クラスは、fair クラスより先に pick_next_task が呼び出されるように設定されており、結果として rt クラスが次に実行すべきプロセスを返し続ける限り fair クラスが管理しているプロセスがスケジュールされることはありません。(fair クラスのように rt クラスより下位に位置づけられたクラスに管理されているプロセスが全くスケジュールされなくなってしまうことを避けるために、rt クラスにはスロットリング機能が実装されており、/proc/sys/kernel/sched_rt_period_us に設定されている時間あたり、/proc/sys/kernel/sched_rt_runtime_us で設定されている時間以上を rt クラスが管理しているプロセスが実行した場合、それらの実行がスロットリングされるようになっています。デフォルトでは最近のカーネルだと1秒間に 0.95 秒程度の実行時間を超過するとスロットリングがかかるようです。)

rt クラスが実装するポリシー

rt クラスは、各プロセスに対して優先度を表す数値を設定することができ、基本的に上述の実行中実行されたい状態にあるプロセスの中でこの数値の最も高いものが次に実行すべきとして選択されます。rt クラスでは、SCHED_FIFO (first-in-first-out)と SCHED_RR (round-robin)という2種類のスケジューリングポリシーを実装しており、優先度の数値が同じで実行されたい状態にあるプロセスが複数存在した時にSCHED_FIFO は、現在実行中のプロセスが自ら CPU サイクルを手放すシステムコールを呼び出す等がない限り実行が継続される一方、SCHED_RR では特定の時間の経過後にプロセスの切り替えが発生します。

ちょっとした用途

ここからは、上記のリアルタイムスケジューリングポリシーの挙動を利用するとこんなことができそう、ということについて説明します。

想定環境:プログラムの集積とコスト面での利点

具体的には、

  • 主に、クライアントのリクエストに対してレスポンスをなるべく短い時間で返すことが期待されているサーバプログラムを動作させるために用意した物理マシンの上で、
  • 処理すべきクライアントからのリクエストがない時、つまり隙間時間にベストエフォートでバッチ処理を行うようなプログラムを動かす、

というような用途について簡単な実験データを添えて説明します。

上記のような、クライアントのリクエストに応じてレスポンスを返すようなプログラムは、クライアントからリクエストが来ない場合には特にすべきことがないので、多くの場合先述の「ユーザ空間プロセスが自発的に CPU 時間を手放す」システムコールを呼び出し、入力を待機しています。

クライアントのリクエストがない間は CPU は他のプロセスを実行することが可能なので、処理すべきリクエストがない隙間時間にベストエフォートでバッチ処理等を行うと、リクエストへ応答するためのプログラムとバッチ処理を行うプログラムのそれぞれのために別々の CPU やマシンを用意する場合と比較して電力や金銭面でのコスト効率の向上が期待できそうです。

課題

ですが、実際にリクエストへ応答するためのプログラムとバッチ処理を行うプログラムを同じ CPU で動かしてみると、クライアントからリクエストが届いた時でも、本当は隙間時間だけに動いてほしいバッチ処理を行うプログラムが動いてしまい、応答性能が低下する、ということが観測されます。

解決策

今回は、上記の応答性能低下についての課題をリアルタイムスケジューリングポリシーを利用して低減できないか試してみます。

実験環境

以下の設定の物理マシン2台を利用し、片方をサーバマシン、もう一方をクライアントマシンとします。両者は下記の  NIC を通してケーブルで直接接続されています。

  • CPU:2 x 16-core Intel Xeon Gold 6326 CPU @ 2.90GHz
  • NIC:Intel X540-AT2 10 Gbps
  • OS:Linux 6.2

簡単な実験その3

今回は、よく利用されているキー・バリューストアである Redis サーバと、付属しているベンチマークツールである redis-benchmark を利用して実験してみます。また、バッチ処理プログラムを模すために sysbench の CPU ベンチマークを Redis サーバが動作している CPU コアの上で動作させてみます。

実行するコマンド

Redis サーバ、sysbench、redis-benchmark を実行するコマンドはそれぞれ以下のとおりです。(Redis サーバ、sysbench はサーバマシン、redis-benchmark はクライアントマシンの上で実行されます。Redis サーバが動作しているサーバマシンは IP アドレスを 10.100.0.20 に設定しています 。)

taskset -c 0 ./redis-server --bind 0.0.0.0
taskset -c 0 sysbench --time=100000 --report-interval=1 cpu --threads=1 run
./redis-benchmark -h 10.100.0.20 -p 6379 -n 3200000 -c 32 -t get --threads 32

基本性能の確認:Redis サーバと sysbench が CPU コアを共有しない場合

まず先に、Redis サーバと sysbench がそれぞれ CPU コアを共有しない場合、つまり1CPU コアを最大限利用できる場合の性能を確かめておきます。

Redis が 1 CPU コアを占有した場合の性能は以下のようになりました。

Summary:
  throughput summary: 271785.28 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        0.111     0.040     0.111     0.143     0.183    16.047

また、sysbench が 1 CPU コアを占有した時の1秒間あたりのベンチマークスコアは概ね 2830 程度でした。

課題の確認:Redis サーバと sysbench が CPU コアを共有し、特にスケジューリングに関する設定を行わない場合

次に、Redis サーバと sysbench を同じ CPU コアで動作させた上で同じ redis-benchmark を実行してみました。

Summary:
  throughput summary: 119452.02 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        0.259     0.032     0.111     0.143    11.175    16.215

このときの sysbench の1秒間あたりのベンチマークスコアは概ね 1580 程度でした。

上の 1 CPU コアを占有できる場合に比べて、スループットは 271785.28 requests/sec から 119452.02 requests/seq へ減少、99 パーセンタイルの遅延は 0.183 ms から 11.175 ms まで増加しました。

特に、99 パーセンタイルの遅延が大幅に増加してしまっているのが、応答性能が重要な Redis のようなシステムにおいてはつらいポイントです。

これは、sysbench が Redis サーバが特にすべきことがない隙間時間以外にもスケジュールされてしまっていることが原因であると考えられます。

この挙動は、sysbench には、Redis サーバの応答性能を犠牲にしないように、Redis サーバが処理すべきリクエストがない時にだけ動作してもらいたいという今回の想定環境・目標において理想的とは言えません。

解決策の適用:Redis サーバと sysbench が CPU コアを共有し、Redis サーバにリアルタイムスケジューリングポリシーを適用した場合

ここで、Redis サーバにリアルタイムスケジューリングポリシーを設定して、Redis サーバが応答すべきリクエストが届いている場合にはほぼ必ずスケジュールされるようにしてみます。

別のターミナル・コンソールを開いて、以下のコマンドを実行することで、リアルタイムスケジューリングポリシーを Redis サーバへ適用します。今回は SCHED_FIFO を適用しています。(試される場合は、REDIS_SERVER_PID の箇所を Redis の PID と置き換えてください。)sysbench はデフォルトのスケジューリングポリシーが適用されたままです。

sudo chrt -f -p 1 REDIS_SERVER_PID

また、上記スロットリングがほぼ作用しないように以下の設定を行いました。注意:この設定はリアルタイムスケジューリングポリシーが適用されたプロセス(今回であれば Redis サーバ)が動作するのと同じ CPU コアに紐づけられた他のプログラム・プロセスがほぼ実行されなくなることを許容するためのものであり、システム全体の挙動を不安定にする可能性があります。

sudo sh -c "echo 999990 > /proc/sys/kernel/sched_rt_runtime_us"

同様のベンチマークを実行すると、今回は以下のような結果が得られました。

Summary:
  throughput summary: 271877.66 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        0.110     0.040     0.111     0.143     0.159     2.631

このときの sysbench の1秒間あたりのベンチマークスコアは概ね 5 程度でした。

以下のテーブルに、上記三通りの結果をまとめます。

実験 Redis サーバ・スループット(リクエスト毎秒) Redis サーバ・99 パーセンタイル遅延(ミリ秒) sysbench・CPU ベンチマークスコア
プログラムが CPU コアを占有した場合(Redis サーバ、sysbench のそれぞれが 1 CPU コアを利用した場合の最高性能を表します) 271785.28 0.183 2830
Redis サーバと sysbench が CPU コアを共有し、リアルタイムスケジューリングポリシーを適用しない場合 119452.02 11.175 1580
Redis サーバと sysbench が CPU コアを共有し、リアルタイムスケジューリングポリシーを適用した場合 271877.66 0.159 5

今回のリアルタイムスケジューリングポリシーを Redis サーバへ適用した場合では、 Redis サーバが 1 CPU コアを占有した場合と近い性能を発揮できており、また、sysbench のベンチマークスコアが 5 程度まで大幅に低下していることから、sysbench が Redis サーバの実行の邪魔にならないようにスケジュールされていることがわかります。

内部の挙動

なぜ上記のような挙動になったかということについてですが、

まとめ

コストの観点から複数プログラムを1つの CPU コアで動作させるような時、特に、遅延が重要なプログラムの応答性能を犠牲にすることなく隙間時間にベストエフォートでバッチ処理等を動かしたいという場合には、Linux ではリアルタイムスケジューリングポリシーを適用すると思ったような挙動が実現できるかもしれない、ということをご紹介しました。

注意として、今回の用途についての実験では CPU 時間の割り当てについてしか調査しておらず、他の通信帯域やディスク性能が考慮されていないため、実際の運用環境等に導入する際には更に多面的な検証と注意が必要と考えられます。

その他の用途

リアルタイムスケジューリングポリシーを利用して、通常、カーネル空間が担当しているプロセススケジューリング を、ある程度ユーザ空間プログラムで行うことができないか、ということを模索した論文を 15th ACM SIGOPS Asia-Pacific Workshop on Systems (APSys 2024) で発表しました。

そちらについては、こちらの記事もしくは論文をご参照ください。

 

安形

2024年09月17日 火曜日

研究所でシステムソフトウェアの研究に取り組んでいます。

Related
関連記事