fioを用いたディスクIOのパフォーマンス測定

 コンピュータを構成する主要コンポーネントの中でも、ストレージ系のパフォーマンスは他に比べてかなり劣るものとなっており、例えばハードディスクは容量的には順調に拡大し続けているものの、そのアクセス速度の発展ペースはRAMやCPUの速度向上に追いつけなくなっている。こうしたハードドライブの性能的限界がシステムパフォーマンスのボトルネックとなっている可能性を考えた場合、各自の所有するディスクやファイルシステムが発揮可能な速度および、ディスクのサブシステムに対してユーザが行える設定変更の影響を数値的に把握しておくことは重要な意味を帯びているはずである。またディスクのアクセス速度を向上させる手法の1つとしては、RAID-5のように複数のディスクを組み合わせて運用することが考えられる。

 Linuxの場合、物理ディスクに対するアクセス速度の基本的な情報であれば、hdparmツールに-Tおよび-tオプションを付けて実行することで取得できる。このうち-Tオプションは、Linuxのディスクキャッシュを利用することで高速アクセス可能なディスクからどれだけの情報量をシステムが読み込めるかの概要を得る際に使用する。同様に-tオプションもキャッシュを介したディスクからの読み込みを測定させるものだが、こちらは事前キャッシュを行わない場合の結果が得られる。つまり-tオプションを指定することで、ディスク上にシーケンシャル(連続的)に格納された情報に対する読み出し速度についての指標が得られるのである。

 しかしながらこのhdparmツールによって得られる情報は、実際のパフォーマンスに則した最適な指標という訳ではない。これはかなりの低レベルで動作するツールであるため、ディスクパーティションの1つに置かれたファイルシステムに関しては大幅に異なる結果を示す場合があるのだ。またシーケンシャルアクセスとランダムアクセスとでも得られる結果は大きく異なってくる。その他の要望としては、RAID化したディスク群の上に置かれたファイルシステムに対するベンチマークも測定したいところである。

 特定のディスクIOにおける作業負荷のベンチマーク用に作られたものとしては、本稿で解説する fio というツールが存在する。このツールが実施するのは、多数存在する同期/非同期IO APIの1つを用いたIOリクエストの送信をするという処理だが、単一のAPI呼び出しに付随して複数のIOリクエストを行うタイプのAPIを使用させることもできる。fioの使用するファイルのサイズについてはユーザによる調整が可能で、こうしたファイルIOのオフセットおよびIOリクエスト送信間の遅延の大きさ、あるいは個々のIOリクエスト間にファイルシステムの同期呼び出しを行うかなども指定できるようになっている。例えば同期呼び出しを実行させた場合、メモリ上のキャッシュ情報をディスク側に書き込み直す処理をオペレーティングシステムが行う関係上、かなりの量の遅延がもたらされるはずである。つまりfioには、こうしたIOパターンを詳細に指定するためのオプション群が用意されており、各自のディスクサブシステムがこれらのタスクを完了させるまでの所要時間を取得できるようになっているのだ。

 fioの入手法に関しては、Fedora 8用の標準リポジトリにてパッケージ化されており、openSUSEの場合はopenSUSE Build Serviceから取得できる。ただしDebianベースのディストリビューションを使用している場合は、「make; sudo make install」を用いてソースコードからコンパイルしなければならない。

 この種のツールでユーザが最初に行う一般的なテストは、ランダム読み出しに関するIOパフォーマンスの計測であろう。こうしたランダム読み出しをハードディスクが実行するには、雑多なデータをディスクヘッドが個別に探し出す(シーク)ことの繰り返しが必要であり、しかもこうしたディスクヘッドのシークはハードディスク内部で行われる処理の中でも極端に遅い部類に属すため、このタイプの処理はディスクIOにかけられる最大級の負荷の1つと見なされている。例えばアプリケーションの起動に必要なファイル群がハードディスク上に分散して格納されていると、そうしたアプリケーションの起動時にはこうしたランダムシークがディスクにて実行されることになる。fioに実行させるベンチマークのユーザ設定は、iniファイルの形で指定すればいい。実行に必要となるパラメータは限られており、例えば「rw=randread」はランダム読み出しを実行させるという指定、「size=128m」はテスト終了までに合計128メガバイトのデータを転送させるという指定であるが、「directory」についてはIOベンチマークの対象とするファイルシステムを明示的にパラメータ指定しておかなくてはならない。ちなみに今回のテストで実行対象とした/tmpファイルシステムは、3基の500GB Samsung SATAディスクで構成されたRAID-5アレイに格納されたext3ファイルシステムに置かれている。なお「directory」の指定を行わなかった場合、fioの実行対象はシェルのカレントディレクトリとされるが、通常こうした使用法はしないであろう。下記に示したのは、設定ファイルの内容および実行時の出力例である。

$ cat random-read-test.fio
; random read of 128mb of data

[random-read]
rw=randread
size=128m
directory=/tmp/fio-testing/data

$ fio random-read-test.fio
random-read: (g=0): rw=randread, bs=4K-4K/4K-4K, ioengine=sync, iodepth=1
Starting 1 process
random-read: Laying out IO file(s) (1 file(s) / 128MiB)
Jobs: 1 (f=1): [r] [100.0% done] [  3588/     0 kb/s] [eta 00m:00s]
random-read: (groupid=0, jobs=1): err= 0: pid=30598
  read : io=128MiB, bw=864KiB/s, iops=211, runt=155282msec
    clat (usec): min=139, max=148K, avg=4736.28, stdev=6001.02
    bw (KiB/s) : min=  227, max= 5275, per=100.12%, avg=865.00, stdev=362.99
  cpu          : usr=0.07%, sys=1.27%, ctx=32783, majf=0, minf=10
  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     issued r/w: total=32768/0, short=0/0
     lat (usec): 250=34.92%, 500=0.36%, 750=0.02%, 1000=0.05%
     lat (msec): 2=0.41%, 4=12.80%, 10=44.96%, 20=5.16%, 50=0.94%
     lat (msec): 100=0.37%, 250=0.01%

Run status group 0 (all jobs):
   READ: io=128MiB, aggrb=864KiB/s, minb=864KiB/s, maxb=864KiB/s, mint=155282msec, maxt=155282msec

Disk stats (read/write):
  dm-6: ios=32768/148, merge=0/0, ticks=154728/12490, in_queue=167218, util=99.59%

 このようにfioの実行するテスト結果は様々な数値情報をもたらしてくれる。この実行例の場合は、帯域幅は高く、レイテンシ(遅延)は小さくなっており、全体的には良好な結果を示しているとしていいだろう。

 bwに示されているのは、今回のテストで得られた平均の帯域幅(bandwidth)である。clatおよびbwの行はそれぞれ処理完了までに要したレイテンシ(completion latency)と帯域幅の情報を示しており、前者はリクエストの送信からその処理終了までに要した遅延時間を意味する。ここではレイテンシおよび帯域幅について、その最小、最大、平均、標準偏差という情報が示されている。特にこのテスト結果の場合、レイテンシと帯域幅の双方において平均値に比して標準偏差がかなり高い値を示しているが、このことから読み取れるのは、IOリクエストの一部のみが他に比して高速に処理されているという状況だ。その次にあるcpu行にはIO負荷がCPUに及ぼす影響が示されており、ここで得られる情報はIO側の速度に比してプロセッサ側が低速すぎないかを判定する際の参考になるはずである。その次にあるIO depthsの行が役立つのは、後で見る2番目の実行例のように、同時送信される複数IOリクエストの一部が随時保留されうる環境においてIO作業負荷を確認する場合である。ただしこの1番目の実行例では1度に1つのIOリクエストしか送信されないので、IO depthsも1のみが100パーセントを示している。IO depthsの下に一段下がった形で配置されている行の中には、個々のIOリクエストの終了までに要したレイテンシ情報を示しているものがあり、この実行例の場合IOリクエストの送出からその応答が返されるまでの時間は、全体のほぼ半数が4から10ミリ秒の間に集中していることが読み取れる。なおここに報告されるレイテンシ情報はインターバル的な集計がされており、例えば今見た「4=12.80%, 10=44.96%」という部分は、4ミリ秒から10ミリ秒の間に処理終了したリクエスト数が全体の44.96パーセントであることを示している。

 下から3つ目に位置するREAD行は、個々の実行スレッドないしプロセスにおける帯域幅の平均、最小、最大を示している。fioではベンチマーク時に実施する送信処理のスレッドおよびプロセスをユーザ設定することが可能であり、同期APIを用いたIO処理を行うスレッドを多数用意した上で、これらの全スレッドを同時実行させた状況でのベンチマーク結果を得ることもできる。こうしたものは、個々の接続クライアントの処理用にスレッドやプロセスが新規に作成されていくタイプのサーバアプリケーションを模したIO作業負荷のテストに相当すると考えればいいだろう。ただしこの実行例では、1つのスレッドしか使われていない。実際この場合のREAD行を見ると、ここでの単一スレッドは864Kbpsという総合帯域幅(aggrb:aggregate bandwidth)を示しているが、この情報から読み取れるのは、ディスクが低速すぎるかあるいはディスクシステムへのIO送信が不適切である結果、ディスクヘッドのシークが頻発して全体的なIO帯域幅が低くなっているということである。仮にディスクへのIO送信が適切に行われていれば、この数値はhdparmで計測される速度(通常は40から60Mbps程度)に近い値になっているはずである。

 次に行った実行例はLinuxの非同期IOサブシステムをダイレクトIOモードにて用いたテストで、ここでは「iodepth」パラメータを調整することにより、定常的にディスクIOを待機し続ける状態にシステムを置いて、送信される8つの非同期IOリクエストが実行しきれない可能性の生じるケースを再現している。ここでキュー(待ち行列)中に待機させるIOリクエストの最大数を8としたのは恣意的な設定だが、システム全体を停滞させないよう未処理のまま保留しておくリクエストの最大数を制限することは、実際のアプリケーションでも一般的に行われている措置である。そして今回のベンチマーク結果を見ると、ここでの帯域幅は前回の約3倍になっている。下記の出力例は主要部のみを抜き出したものであるが、そのIO depths行を見ると、今回のテスト中に送信された非同期IOリクエストの中でアプリケーションへのデータ返送が保留されていたものの発生頻度が確認できる。ここでの数値もインターバル的な集計がされたもので、例えば「8=96.0%」という情報は、このテストの実行中において5から8個のリクエストが非同期IOキューに置かれていた時間が全体の96パーセントを占めていたことを示しており、その左側にある「4=4.0%」は、3ないし4個のリクエストがキュー中に置かれていた時間が4パーセントであったという意味である。

$ cat random-read-test-aio.fio
; same as random-read-test.fio
; ...
ioengine=libaio
iodepth=8
direct=1
invalidate=1

$ fio random-read-test-aio.fio
random-read: (groupid=0, jobs=1): err= 0: pid=31318
  read : io=128MiB, bw=2,352KiB/s, iops=574, runt= 57061msec
    slat (usec): min=8, max=260, avg=25.90, stdev=23.23
    clat (usec): min=1, max=124K, avg=13901.91, stdev=12193.87
    bw (KiB/s) : min=    0, max= 5603, per=97.59%, avg=2295.43, stdev=590.60
...
  IO depths    : 1=0.1%, 2=0.1%, 4=4.0%, 8=96.0%, 16=0.0%, 32=0.0%, >=64=0.0%
...
Run status group 0 (all jobs):
   READ: io=128MiB, aggrb=2,352KiB/s, minb=2,352KiB/s, maxb=2,352KiB/s, mint=57061msec, maxt=57061msec

 ランダム読み出しの速度は、基本的にディスクヘッドのシーク時間の遅さで規定されてしまう。IOリクエスト数を最大8個としたここでの非同期IOテストでは、この上限に到達している間は待機中のリクエストが処理終了されるまで次のリクエストが行われないが、それ故に同一ディスクエリアでの読み出しが同時に終了する確率は高くなり、その結果としてIO帯域幅が増えているのである。

 ベンチマークの作業負荷を制御するオプションの詳細は、fioに同梱されているHOWTOファイルにて説明されている。その中で重要なパラメータの1つが「rw」であり、これは実行させる読み出しや書き込みの処理におけるシーケンシャルやランダムといった各種のモードを指定する際に使用する。同じく「ioengine」パラメータは、カーネルに対するIOリクエストの送信方式を指定する。「invalidate」は、ベンチマークの開始前にファイルのカーネルバッファおよびページキャッシュを無効化させるためのオプションである。「runtime」はテストの実行時間の上限を指定するためのもので、この指定時間を超過した段階でテストは終了したものと見なされる。「thinktime」はIOリクエストの間に設ける遅延時間を指定するものであるが、これはディスクから読み出し中のデータに対し何らかの処理を施していくタイプのアプリケーションをシミュレートする場合に役立つパラメータである。「fsync=n 」は、n回の書き込みを行うごとに同期呼び出しを実行させるための指定である。「write_iolog」および「read_iolog」は、送信したすべてのIOリクエストに関するログの書き込みおよび読み出しをfioに行わせるための指定である。これらの指定を用いると送信されたIOコマンドに関する厳密なログを取得できるだけでなく、例えば検討の対象となるIO作業負荷だけを残すようログを編集し、そうして整理されたIOリクエストについてのベンチマークを実行させるということもできる。また何らかのアプリケーションからIOアクセスパターンの情報をインポートして、fioに解析させるという操作も、これらiologオプションによって可能となる。

サーバのシミュレーション

 個々のIO処理に対して同時に複数のスレッドやプロセスを割り当てることで、サーバのファイルシステムにて行われるタイプのアクセスをベンチマークさせることもできる。例えば下記の実行例では、4つの異なるプロセスが同時に実行されると同時に、これらが各自のIO負荷をシステムに送信するようにされている。私がこの実行例を設定する際には、2つのメモリマップドクエリエンジン、1つのバックグラウンドアップデータスレッド、1つのバックグラウンドライタースレッドが存在する状況を想定した。これら2種類の書き込み用バックグラウンドスレッドの違いは、ライタースレッドはジャーナルへの書き込みをシミュレートし、アップデータスレッドは(アップデート)データの読み込みと書き込みを行うというものである。またここではbgupdaterのthinktimeを40マイクロ秒とすることで、個々のIO処理の終了後にプロセスを少しの間スリープさせるようにしている。

$ cat four-threads-randio.fio
; Four threads, two query, two writers.

[global]
rw=randread
size=256m
directory=/tmp/fio-testing/data
ioengine=libaio
iodepth=4
invalidate=1
direct=1

[bgwriter]
rw=randwrite
iodepth=32

[queryA]
iodepth=1
ioengine=mmap
direct=0
thinktime=3

[queryB]
iodepth=1
ioengine=mmap
direct=0
thinktime=5

[bgupdater]
rw=randrw
iodepth=16
thinktime=40
size=32m

$ fio four-threads-randio.fio
bgwriter: (g=0): rw=randwrite, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=32
queryA: (g=0): rw=randread, bs=4K-4K/4K-4K, ioengine=mmap, iodepth=1
queryB: (g=0): rw=randread, bs=4K-4K/4K-4K, ioengine=mmap, iodepth=1
bgupdater: (g=0): rw=randrw, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=16
Starting 4 processes

bgwriter: (groupid=0, jobs=1): err= 0: pid=3241
  write: io=256MiB, bw=7,480KiB/s, iops=1,826, runt= 35886msec
    slat (usec): min=9, max=106K, avg=35.29, stdev=583.45
    clat (usec): min=117, max=224K, avg=17365.99, stdev=24002.00
    bw (KiB/s) : min=    0, max=14636, per=72.30%, avg=5746.62, stdev=5225.44
  cpu          : usr=0.40%, sys=4.13%, ctx=18254, majf=0, minf=9
  IO depths    : 1=0.1%, 2=0.1%, 4=0.4%, 8=3.3%, 16=59.7%, 32=36.5%, >=64=0.0%
     issued r/w: total=0/65536, short=0/0
     lat (usec): 250=0.05%, 500=0.33%, 750=0.70%, 1000=1.11%
     lat (msec): 2=7.06%, 4=14.91%, 10=27.10%, 20=21.82%, 50=20.32%
     lat (msec): 100=4.74%, 250=1.86%
queryA: (groupid=0, jobs=1): err= 0: pid=3242
  read : io=256MiB, bw=589MiB/s, iops=147K, runt=   445msec
    clat (usec): min=2, max=165, avg= 3.48, stdev= 2.38
  cpu          : usr=70.05%, sys=30.41%, ctx=91, majf=0, minf=65545
  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     issued r/w: total=65536/0, short=0/0
     lat (usec): 4=76.20%, 10=22.51%, 20=1.17%, 50=0.05%, 100=0.05%
     lat (usec): 250=0.01%

queryB: (groupid=0, jobs=1): err= 0: pid=3243
  read : io=256MiB, bw=455MiB/s, iops=114K, runt=   576msec
    clat (usec): min=2, max=303, avg= 3.48, stdev= 2.31
    bw (KiB/s) : min=464158, max=464158, per=1383.48%, avg=464158.00, stdev= 0.00
  cpu          : usr=73.22%, sys=26.43%, ctx=69, majf=0, minf=65545
  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     issued r/w: total=65536/0, short=0/0
     lat (usec): 4=76.81%, 10=21.61%, 20=1.53%, 50=0.02%, 100=0.03%
     lat (usec): 250=0.01%, 500=0.01%

bgupdater: (groupid=0, jobs=1): err= 0: pid=3244
  read : io=16,348KiB, bw=1,014KiB/s, iops=247, runt= 16501msec
    slat (usec): min=7, max=42,515, avg=47.01, stdev=665.19
    clat (usec): min=1, max=137K, avg=14215.23, stdev=20611.53
    bw (KiB/s) : min=    0, max= 1957, per=2.37%, avg=794.90, stdev=495.94
  write: io=16,420KiB, bw=1,018KiB/s, iops=248, runt= 16501msec
    slat (usec): min=9, max=42,510, avg=38.73, stdev=663.37
    clat (usec): min=202, max=229K, avg=49803.02, stdev=34393.32
    bw (KiB/s) : min=    0, max= 1840, per=10.89%, avg=865.54, stdev=411.66
  cpu          : usr=0.53%, sys=1.39%, ctx=12089, majf=0, minf=9
  IO depths    : 1=0.1%, 2=0.1%, 4=0.3%, 8=22.8%, 16=76.8%, 32=0.0%, >=64=0.0%
     issued r/w: total=4087/4105, short=0/0
     lat (usec): 2=0.02%, 4=0.04%, 20=0.01%, 50=0.06%, 100=1.44%
     lat (usec): 250=8.81%, 500=4.24%, 750=2.56%, 1000=1.17%
     lat (msec): 2=2.36%, 4=2.62%, 10=9.47%, 20=13.57%, 50=29.82%
     lat (msec): 100=19.07%, 250=4.72%

Run status group 0 (all jobs):
   READ: io=528MiB, aggrb=33,550KiB/s, minb=1,014KiB/s, maxb=589MiB/s, mint=445msec, maxt=16501msec
  WRITE: io=272MiB, aggrb=7,948KiB/s, minb=1,018KiB/s, maxb=7,480KiB/s, mint=16501msec, maxt=35886msec

Disk stats (read/write):
  dm-6: ios=4087/69722, merge=0/0, ticks=58049/1345695, in_queue=1403777, util=99.74%

 この場合は予想どおり、クエリと書き込みプロセスの帯域幅は顕著な違いを見せている。つまり、ここでのクエリ群はいずれも500Mbps程度で処理されているのに対して、書き込みプロセスは1Mbpsないし7.5Mbps程度となっているのである。なお後者における2つの値は、読み込みと書き込みの双方を行う場合と、書き込み処理だけを行う場合による違いである。次にIO depthsを見ると、個々のIOリクエスト送信時にキュー中で待機していたIOリクエスト数を確認できる。例えばbgupdaterプロセスの場合、キューにて待機するリクエストの最大数は16個であるが、ここでの非同期IOリクエストの約1/4は、そのように待機させるリクエストが8個以下の状態で処理され終わっていることが分かる。これに対してbgwriterのリクエストの場合は、最大16個のリクエストをキューに待機させるものが全体の半数以上を占めるようになっている。

 このfour-threads-randio.fioについては、3基のディスクで構成したRAID-5使用時との比較用に、Western Digital 750GBドライブを1基だけ用いた環境下での試験も行ってみた。その場合、bgupdaterプロセスの帯域幅は半分以下となり、各クエリプロセス全体の帯域幅も1/3になっている。ただしこの比較試験を実施したコンピュータは、使用するドライブをWestern Digitalに変えたというだけではなく、搭載されているCPUおよびRAMの構成も異なっているので、これら2つのテスト結果の比較はその辺を割り引いて考える必要があるだろう。

bgwriter: (groupid=0, jobs=1): err= 0: pid=14963
  write: io=256MiB, bw=6,545KiB/s, iops=1,597, runt= 41013msec
queryA: (groupid=0, jobs=1): err= 0: pid=14964
  read : io=256MiB, bw=160MiB/s, iops=39,888, runt=  1643msec
queryB: (groupid=0, jobs=1): err= 0: pid=14965
  read : io=256MiB, bw=163MiB/s, iops=40,680, runt=  1611msec
bgupdater: (groupid=0, jobs=1): err= 0: pid=14966
  read : io=16,416KiB, bw=422KiB/s, iops=103, runt= 39788msec
  write: io=16,352KiB, bw=420KiB/s, iops=102, runt= 39788msec
   READ: io=528MiB, aggrb=13,915KiB/s, minb=422KiB/s, maxb=163MiB/s, mint=1611msec, maxt=39788msec
  WRITE: io=272MiB, aggrb=6,953KiB/s, minb=420KiB/s, maxb=6,545KiB/s, mint=39788msec, maxt=41013msec

 fioは、各種のAPIを用いたIOパターンのベンチマーク計測に対応しており、その際のIOリクエストの送信は様々な形態で実行できるようになっている。またこうしたfio設定を共通化させた計測をファイルシステムやハードウェアの異なる環境にて実施すれば、これら各レベルでの要件上の違いがどのようなパフォーマンス的な違いとして表れるかを検証できるはずだ。

 IO関連の要件が厳しいアプリケーションを構築する際に、それを実際に運用するハードウェアにおいてどのようなAPIやデザインが適しているかを判断できないという場合は、想定されるIOパターンを様々なIOリクエストシステムにてベンチマークすることで有用な知見が得られる場合がある。例えばディスクシステムとRAMは固定したまま、メモリマップドIOやLinuxのasyncioインタフェースを用いることで個々のIO負荷がどのように処理されるかを確認するといった使い方も考えられるだろう。ただしこうした考察を行う際には、各自のアプリケーションが主として用いるIOリクエストについての詳細な知識が必要となる。あるいはメモリマップドファイルなどを使用するツールが既に手元にある場合は、そうした既存ツールの頻用するIOパターンを取得し、それを各種のIOエンジンを用いたfio計測にかけることにより、例えば他のIO APIにアプリケーションを移植することでパフォーマンス的な向上が得られるかといった予測ができるようになるはずだ。

Ben Martinは10年以上にわたってファイルシステムに取り組んでおり、博士課程の修了後、現在はlibferris、ファイルシステム、検索ソリューションを中心としたコンサルティング業に従事している。

Linux.com 原文