LinuxのI/OスケジューラのDeadlineを調べてチューニングしてみた

Deadlineスケジューラの概要

キューに入ったリクエストに対して処理までの期限(Deadline)を設定し、期限を過ぎているリクエストが無ければ、可能な限りリクエストをセクタ順にソートする。デッドラインを過ぎているリクエストを優先で処理することで、レイテンシを一定に保つことを目指したスケジューラです。

  • ディスパッチキューの他に4つのキューを持っています。セクタ順に並べ換えるためのソートキュー(sort_list)と、リクエストの期限(Deadline)を管理するためのデッドラインキュー(fifo_list)の2種類×Read/Writeで4つのキューになります。
  • FIFOで管理されるデッドラインキュー(fifo_list)により、リクエストに対して最長の待ち時間を保証するように試みます。これによりリクエスト毎の最長の待ち時間を一定に保つてるようになります。
  • デフォルト設定ではWriteよりもReadが優先される設定となってます。(Writeは非同期、Readは同期であることが多いため)
  • 複数I/Oリクエストを纏めてバッチで送ります。バッチは、ReadもしくはWriteのリクエストを纏めたものです。(詳細はfifo_batchの項で書きます)

http://go.neowin.net/?id=2728X590260&site=neowin.net&xs=1&isjs=1&url=https%3A%2F%2Fs1.postimg.org%2Fu7ufqgtnj%2FLinux_io_deadline_scheduler.png&xguid=049ba77ab0bfdfff7fc06999765efef1&xuuid=947302c3be7146c15a6f83055f26e69b&xsessid=2c15873674c79c491a5e1e05018022f1&xcreo=0&xed=0&sref=https%3A%2F%2Fwww.neowin.net%2Fforum%2Ftopic%2F1228317-kernelpatcher-01-permanently-patch-your-kernel-to-use-a-different-scheduler-ramtweak%2F&xtz=-540

図の引用元:KernelPatcher 0.1 - Permanently patch your kernel to use a different scheduler + RamTweak - Linux / Unix Support - Neowin

チューニングパラメータ

パラメータ デフォルト値
write_expire 5000(ms) Writeリクエストの期限
read_expire 500(ms) Readリクエストの期限
writes_starved 2 読み取り処理の優先度
fifo_batch 16 1回のバッチに纏めるリクエスト数
front_merges 1 フロントマージ処理の有効化(1)/無効化(0)

write_expire,read_expire

Deadlineスケジューラの所以である、リクエストの処理期限を表す。

この数値(milliseconds)を超えたリクエストは優先的に処理される。 デフォルト値でわかるように同期処理されるReadの方が優先度が高くなっている。

デフォルト値:(read_expire) 500ms / (write_expire) 5000ms

writes_starved

この数値を上げるとWriteに帯するReadの優先度が上がる。

デフォルト値:2 の場合は2回のRead方向に対して1回のWrite方向になります。

ここで設定された方向(Read or Write)で、複数のI/Oリクエストが一つの纏まりとして処理されることになるので注意が必要です。

イメージ図

RRRRR->RRRRR->WWWWW->RRRRR->RRRRR->WWWWW

纏められるリクエストの数は後述の fifo_batch で設定されます。

ソースコード該当部分

linux/block/deadline-iosched.c

 if (reads) {
        BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[READ]));

        if (writes && (dd->starved++ >= dd->writes_starved))
            goto dispatch_writes;

        data_dir = READ;

        goto dispatch_find_request;
    }

    /*
    * there are either no reads or writes have been starved
    */

    if (writes) {
dispatch_writes:
        BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[WRITE]));

        dd->starved = 0;

        data_dir = WRITE;

        goto dispatch_find_request;
    }

fifo_batch

ReadまたはWrite方向のリクエストをこの数分で纏めてディスパッチします。

デフォルト値:16

デフォルトでは16リクエストを、一つに纏めてディスパッチ処理することになります。

注意点としてDeadlineスケジューラの売りの一つである write_expireread_expire によるリクエストの処理期限判定も、纏められたディスパッチ間でしか機能しないことに注意する必要があります。以下にイメージを。

イメージ図

RRRRRRRRRRRRRRRR-<判定>-WWWWWWWWWWWWWWWW-<判定>-RR....

セクタでソートされたリストから取り出しするため、物理的に近いI/Oリクエストが纏められて処理され、スループットが上がるという考えです。HDD向けの設計です。

この数値を下げれば、それぞれのリクエストは細かい単位で処理されるようになり、判定も頻度も上がることになります。リクエストが処理されるまでの待ち時間も下がると考えられるので、レイテンシは短縮されることになります。

イメージ図

RR-<判定>-RR-<判定>-WW..

ソースコード該当部分

linux/block/deadline-iosched.c

 if (rq && dd->batching < dd->fifo_batch)
        /* we have a next request are still entitled to batch */
        goto dispatch_request;

#------中略--------

dispatch_request:
    /*
    * rq is the selected appropriate request.
    */
    dd->batching++;
    deadline_move_request(dd, rq);

    return 1;
}

linux/Documentation/block/deadline-iosched.txt

ソースコード読む時のエントリ部分

Deadlineのソースコードはこのファイルになります。

linux/block/deadline-iosched.c

I/Oスケジューラはコールバックを登録していく形で実装するので、ここから読み始めるとわかりやすいです。

static struct elevator_type iosched_deadline = {
    .ops.sq = {
        .elevator_merge_fn =        deadline_merge,
        .elevator_merged_fn =       deadline_merged_request,
        .elevator_merge_req_fn =    deadline_merged_requests,
        .elevator_dispatch_fn =     deadline_dispatch_requests,
        .elevator_add_req_fn =      deadline_add_request,
        .elevator_former_req_fn =   elv_rb_former_request,
        .elevator_latter_req_fn =   elv_rb_latter_request,
        .elevator_init_fn =     deadline_init_queue,
        .elevator_exit_fn =     deadline_exit_queue,
    },

それぞれ何の処理を表しているかの詳細はこの辺りに書いてあります。 https://github.com/torvalds/linux/blob/b78b499a67c3f77aeb6cd0b54724bc38b141255d/Documentation/block/biodoc.txt#L907-L967

SSD向けのチューニング例

他のI/Oスケジューラにも言えますが、Deadlineはシーク時間の低減を目指した設計になっています。デフォルトの設定も、HDD向けにチューニングされたものになっているので、改善の余地はあると思われます。

良くSSDの場合はnoopの方がdeadlineより早いという話を見ますが、ここまで書いてきたとおりDeadlineがHDD向けに作られていて、そのままのチューニングになっていることも一因かと思います。または仮想マシンの場合はホスト側のスケジューラに任せて、noopという考え方もあるかと思います。確かに手元の環境でDeadlineをチューニングしても結局Noopとの差は大きくなく、手っ取り早くパフォーマンス改善したいなら「SSDならNoop」という判断もありかもしれません。

しかし、シンプルに順次処理を行うNoopと比較して、ReadとWriteの優先度を調整できるというDeadlineスケジューラの特性活かすこともできます。

設定項目 デフォルト値 設定値 理由
scheduler - deadline -
front_merges 1 0 物理的な距離を気にしないでいいのでマージを行わない(※1)
fifo_batch 16 1 判断タイミングを増やし、待ちを少なくしてレイテンシ短縮を図る。
writes_starved 2 10 Read処理の優先度を上がる(※2)
read_expire 500 50 Read処理の優先度を上がる(※2)
write_expire 5000 10000 Read処理の優先度を上がる(※2)
nr_requests 128 256 Write処理がキューに溜まることを見越してキューを拡張しておく

※1 SSDでも、ほとんどの場合はマージした方が良いと以下のなどには書かれています。しかし、手元の環境ではマージしない場合の方が安定した性能が出る環境もありました。試してみる価値はあるかもしれません。

nomerges

このパラメーターは、基本的にデバッグの助けとなるものです。ほとんどのワークロードでは、要求のマージが役に立ちます (SSD などの高速ストレージでも)。しかし、マージを無効にすることがよい場合もあります。例えば、先読みを無効にしたりランダム I/O を実行することなく、ストレージバックエンドが処理できる IOPS の数を知りたい場合などです。

https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/6/html/Performance_Tuning_Guide/ch06s04s03.html

※2 ReadやWriteの優先度はシステム特性によってチューニング値が大きく変わります。元々がReadに重きを置いている設定であることを知っているだけでも意味があるかもしれません。設定によっては予期しない遅延発生する場合もあるので、設定変更する場合は入念なテストをお忘れ無く。

手元のSSD環境では特に fifo_batch の設定が様々なパターンで性能向上や安定が確認できました。

設定方法

設定は動的に変更可能です。

echo deadline >/sys/block/xvda/queue/scheduler
echo 0 >/sys/block/xvda/queue/iosched/front_merges
echo 1 >/sys/block/xvda/queue/iosched/fifo_batch
echo 10 >/sys/block/xvda/queue/iosched/writes_starved 
echo 50 >/sys/block/xvda/queue/iosched/read_expire
echo 300000 >/sys/block/xvda/queue/iosched/write_expire
echo 256 > /sys/block/xvda/queue/nr_requests

設定反映の確認はcatで行います。

cat /sys/block/xvda/queue/scheduler
cat /sys/block/xvda/queue/iosched/front_merges
cat /sys/block/xvda/queue/iosched/fifo_batch
cat /sys/block/xvda/queue/iosched/writes_starved
cat /sys/block/xvda/queue/iosched/read_expire
cat /sys/block/xvda/queue/iosched/write_expire
cat /sys/block/xvda/queue/nr_requests

恒久的に設定したい場合は、カーネル引数に elevator=deadline と渡すことでスケジューラを設定可能です。

単純に以下のような行をそのまま /etc/rc.local に書いても良いと思います。

echo deadline >/sys/block/xvda/queue/scheduler
echo 0 >/sys/block/xvda/queue/iosched/front_merges
echo 1 >/sys/block/xvda/queue/iosched/fifo_batch

最後に

Deadlineを上記の内容を元にチューニングしてみると、デフォルトの設定よりはパフォーマンス出ることが確認できました。

しかし、ここまで書いておいてなのですが、今回チューニングした対象環境では最終的にはNoopを選びました。

Deadlineでテスト回数を増やしていくと、どんなチューニングしても時々大きな遅延が発生してしまうことあったためです。理由として考えられることは実際に動いているサーバが様々なM/Wを乗せている複雑な環境でWriteとReadの動きを規定しづらいところがあったことや、クラウド上の仮想マシンだったのでホスト側のスケジューラに任せた方がパフォーマンス出るということなどが考えられると思っています。

何度も書きますがパフォーマンステスト重要です。

参考文献とURL

SSD の並列性を引き出す I/O スケジューラに関する研究

http://www.arch.cs.titech.ac.jp/a/thesis/Mthesis-2016-02-okumura.pdf I/Oスケジューラの性能を考える上でのポイントや、性能計測、ソースコードの読み方までとても参考になった。理解しやすく纏まっているので是非一読をオススメします。

Redhatの情報

6.4.2. デッドライン I/O スケジューラー

● 公式Doc

GitHub - torvalds/linux: Linux kernel source tree

この辺りは特に参考になりました。 linux/Documentation/block/biodoc.txt4. The I/O scheduler の項目。

ソースコードの読み方」の項にも書きましたが、 4.1. I/O scheduler API はコードリーディングの起点になります。

4. The I/O scheduler
4.2 Request flows seen by I/O schedulers
4.3 I/O scheduler implementation
4.4 I/O contexts

こっちはDeadlineの説明 linux/Documentation/block/deadline-iosched.txt

● 詳解Linuxカーネル

当然こちらも参考にしました。

14章ブロック型デバイスイスドライバの項に、構造体の詳細や、処理の進み方が示されていて、コード読む時の参考になります。

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版