はじめに

CentOS 6とCentOS 7とAmazon LinuxではデフォルトのI/Oスケジューラに違いがあるため、ioniceコマンドを実行するときやパフォーマンスチューニングをするときに注意が必要だ。

各OSでのデフォルトI/Oスケジューラ

CentOS 6ではI/Oスケジューラはcfqになっている。CentOS 7ではdeadlineに変更された。(https://access.redhat.com/solutions/32376)

In RHEL 7, the deadline IO scheduler is the default IO scheduler for all block devices except SATA disks. For SATA disk, the default IO scheduler is cfq.

Amazon Linuxではnoopになっている。

  • CentOS 6

    $ cat /etc/redhat-release && uname -r
    CentOS release 6.7 (Final)
    2.6.32-279.el6.x86_64
        
    $ cat /sys/block/sda/queue/scheduler
    noop anticipatory deadline [cfq] 
  • CentOS 7

    $ cat /etc/redhat-release && uname -r
    CentOS Linux release 7.3.1611 (Core) 
    3.10.0-229.el7.x86_64
        
    # SATAディスクではないので、デフォルトI/Oスケジューラはcfqではなくdeadlineになっているはず
    $ dmesg | grep sd[a-z] | grep SCSI
    [    1.029523] sd 2:0:0:0: [sda] Attached SCSI disk
        
    $ cat /sys/block/sda/queue/scheduler
    noop [deadline] cfq
  • Amazon Linux

    $ uname -r
    4.4.23-31.54.amzn1.x86_64
        
    $ cat /sys/block/xvda/queue/scheduler
    [noop]

I/Oスケジューラ

http://tfcenturion.hatenablog.com/entry/2015/11/07/091307にI/Oスケジューラの各種について、簡潔にまとめられているので引用する。

noop
最もシンプルなスケジューラで、I/O要求を単純に要求順に処理する。
noopはスケジューリング負荷が小さく、ランダムアクセスが高速なハードウェアに適していると考えられている。

deadline
ディスク上で位置が近いI/O要求を優先して処理を行うことで、HDDヘッドの移動量を削減する。位置が近いI/O要求を優先するため、HDDヘッドから遠くにあるI/O要求は後回しされるが、待ち時間の限界値(deadline)より長く待たされているI/O要求が発生した際には、そのI/O要求を優先する。
deadlineの設定によりレイテンシの上限が保証されるため、リアルタイムアプリケーションやデータベース管理に適していると言われている。

anticipatory(AS)
I/O要求を予測し、その予測に基づいて処理を行う。位置が近い場所へのI/Oがすぐ後に発行されると予測 した場合、I/O要求の処理を遅延させ、近隣のI/O要求が発行されるのを待ってから、I/O要求の処理を行う。つまり、いくつかのI/O要求を貯めてから処理を行う性質がある。
ただしDeadlineと同様に、待ち時間の長いI/O要求が発生した場合にはI/O待ちを中断し、待ち時間の長いI/O要求を優先して処理する。
anticipatoryは、データにシーケンシャルにアクセスするWebサーバなどに有効とされている。

cfq(Completely Fair Queuing)
スケジューラは内部に多数のキューを保持しておき、プロセス単位でI/O要求をそれらキューに割り振っていく。処理対象のキューを一定間隔で切り替えることで、プロセス間でI/O要求は公平に処理されていく。
また、Deadlineやanticipatoryと同様に待ち時間の長いI/O要求が存在した場合、それを優先的に処理していく。

I/Oスケジューラとパフォーマンス

クラウドと仮想化

Linux 2.6 カーネル ベースの仮想マシンでディスク I/O パフォーマンスが遅い (2094615)に書かれている通り、「仮想化された環境では多くの場合、I/O をホスト レイヤーとゲスト レイヤーの両方でスケジュールすることは有益ではなく」、ゲストが頑張るよりもホストに任せた方がいい。NOOP または Deadline のパフォーマンスが仮想化された Linux ゲストに対し向上したことが示されたとのことなので、クラウド全盛の現在は、デフォルトがNOOP または DeadlineになっているCentOS 7あるいはAmazon Linuxは、この観点では適切なのだろう。
noopでもdeadlineでもVMwareのテストでは効果がでたとのことだが、ホストに極力任せるという観点からはnoopの方がよさそう。

CentOS 6のEOLは2020/11/30だが、現時点でもCentOS 6のサーバをクラウド上で動かし、I/Oをヘビーユースするのであれば、I/Oスケジューラを変えた方がパフォーマンスがあがるかもしれない。
ディスク/dev/sdaのI/Oスケジューラを変更するのであれば、以下のように変更する。

$ sudo su -
# cat /sys/block/sda/queue/scheduler 
noop anticipatory deadline [cfq] 
# echo noop > /sys/block/sda/queue/scheduler
# cat /sys/block/sda/queue/scheduler 
[noop] anticipatory deadline cfq

サーバを再起動したら元に戻ってしまうので、/etc/rc.localに記入したり、initスクリプト化してchkconfig onにしたりするといい。
/dev/sdaだけのように各ディスク単位ではなく、サーバ全体でI/Oスケジューラを変更し、さらに永続化したければ、/etc/grub.confを編集する。/etc/grub.confのkernel行にelevator=deadlineやelevator=noopを追記して再起動すればいい。

CentOS 7であれば/etc/grub.confではなく/etc/default/grubのGRUB_CMDLINE_LINUX行にelevator=noopを書いてから、/boot/grub2/grub.cfgをrebuildするためにgrub2-mkconfig -o /boot/grub2/grub.cfgを実行する。
参考:https://access.redhat.com/solutions/32376

SSD

現在はクラウドとともにSSDを使うことも多くなっている。ランダムアクセスが高速なハードウェアに適しているnoopを選択することでパフォーマンス向上するかもしれない。
deadlineもanticipatoryもHDDヘッドの移動量が少なくなるようにするロジックなので、HDDヘッドがないSSDには適切なロジックではない。

ionice

ioniceはcfqじゃないと意味がない

パフォーマンス観点でcfqからnoopに変えた方がいいという話をしてきたが、パフォーマンスとは真逆のI/O負荷をできるだけ抑えながら処理をさせるioniceについても考えなければならない。

ioniceが効く場合は限られていて、I/Oスケジューラがcfqでないと効果がない。

ioniceが効く条件

  • I/Oスケジューラがcfqである
  • Read()である
  • Write()の場合は、-o syncでマウントされているかO_DIRECT,O_SYNCフラグを立てている

参考:
その ionice、ほんとに効いてますか?
【続編】その ionice、ほんとに効いてますか?

またLVMやソフトウェアRAIDを組んでいる場合も、ioniceは無意味となる。

CFQ is the default for scheduling actual physical disks, but small things like software RAID and LVM do not have disk schedulers at all and as far as I can tell ionice is completely ineffectual on them (for both read and write IO)

参考:Some notes on Linux's ionice

ioniceの代わり

コピーのときのio負荷を減らしたい場合

rsyncの--bwlimitで帯域制限を行う。R/W両方とも負荷が減る。

巨大なファイルを削除する場合

大量・巨大なファイル操作を低負荷で行う方法でtruncateを使った方法が紹介されていた。

#/bin/bash

set -euo pipefail

filepath=$1
# 10MBytes/sec
filesize=$(du -m ${filepath} | awk '{print $1}')
sleep_time=0.1

for num in $(seq 1 ${filesize});
do
 truncate -s $((${filesize}-${num}))M ${filepath}
 sleep ${sleep_time}
done

\rm ${filepath}