ロード トップ 参照元 逆検索 検索 ヘルプ

[logo] いまどきのファイルシステムに求められるもの/家のPCの性能検証_001


SuzTiki:いまどきのファイルシステムに求められるもの

ファイルサーバにする家のPC の性能を検証してみることにする。

マシンのスペック

これ以外に IDE のチップセットと カーネルのドライバも性能に影響がある。 Intel のはよくチューニングされているが、VIA の方は パッチを当てないと ちゃんとした性能にならないようだ。


サマリ

何が書いてあるか、書いた本人も時間が立つと意味不明になってくるとおもうので、 まとめておく。


検証その1 (シーケンシャル Write 性能)

ext2 のファイルシステムで 1G バイトのファイルを 3 回 書いてみる。 カーネルは 改造したもの。書くコマンドは以下のもの。

    time dd if=/dev/zero of=3 bs=1024k count=1000

結果は、

   real    0m27.834s  user    0m0.000s sys     0m17.920s
   real    0m29.229s  user    0m0.000s sys     0m18.970s
   real    0m29.149s  user    0m0.010s sys     0m18.690s

おおざっぱにいって 34MB/sec 程度出ている。

検証その2 (シーケンシャル Read 性能)

今度はその逆 上で作ったファイルの Read 性能をみてみる。

    time dd of=/dev/null if=3 bs=1024k count=1000

結果は、

   real    0m26.964s  user    0m0.010s  sys     0m18.840s
   real    0m27.501s  user    0m0.000s  sys     0m18.090s
   real    0m28.100s  user    0m0.020s  sys     0m18.560s

Write よりちょっと速く 35MB/sec ぐらいか。

検証その3 (tar 展開性能)

linux カーネルの展開性能を みてやる。

1 つのカーネルの tar ファイル は、120MB で、 おおよそ 500 のディレクトリと 9200 個の通常ファイルを含んでいる。

これを 20 回 違うところに展開してみる。

real    0m3.725s user    0m0.680s sys     0m3.040s
real    0m4.225s user    0m0.720s sys     0m3.070s
real    0m4.853s user    0m0.670s sys     0m3.510s
real    0m5.236s user    0m0.640s sys     0m3.420s
real    0m5.391s user    0m0.580s sys     0m3.550s
                       :
                       :
real    0m6.113s user    0m0.630s sys     0m3.590s
real    0m6.222s user    0m0.690s sys     0m3.470s
real    0m5.662s user    0m0.670s sys     0m3.540s
real    0m6.007s user    0m0.700s sys     0m3.570s
real    0m6.643s user    0m0.650s sys     0m3.780s

linux は、inode を できるだけ free しないので、 こういうケースでは段々効率が落ちていくのだが、 120MB を 6 秒ぐらいで書けるわけで、20MB/sec 程度の実力がある。 ( Disk 上のデータは、4KB 単位だから 実はこの何割か 余計に書いているし、 inode も別に書かないといけないので、実際はもっと効率が良いのだが、 とりあえず、20MB/sec としておく。 )

ext2 は 性能が出やすいわけだが、ジャーナリングファイルシステムでないと 使う気がしない。

このマシンで ext2 を使って実際に出た性能値

を、チューニングの目標とすることにする。

MB/sec の扱いについては、一応 1000 * 1024 バイト/秒 ということにする。


ここで、オペミスによって、サーバマシンの /lib を消してしまった。 ガーン。

nfs でマウントしている 内容は ちゃんと読めるようなので、 バックアップ作業をしてインストールしなおしすることになる。

バックアップ作業が最後までできれば、特にダメージはないのだが... はたして 最後までできるのか? (できなくたって特にダメージがあるわけじゃない けど ... さらに失敗を重ねるわけにはいかなくなる。)

ごみだらけなんだが、30GB も使っている。10MB/sec として バックアップに 1 時間ぐらいかかる予定。 最大の tar ファイルは、

    8137523200 バイト (8 GB)
しかも smb で アクセスできる領域で、漢字のファイルもあったり する。ちゃんと復元できるのか かなり不安。

インストールをしなおさないといけないのはショックだが、 reiserfs は、このマシンには向いていないようなので、構築しなおす チャンスと考えることにする。


2.4.18-0vl3 の性能

さて、インストールのついでに vine オリジナルの 2.4.18-0vl3 の性能を試してみる。 ( ただし VIA の ide ドライバのみパッチをあてる。)

どうも、カーネルを make しただけではダメのようだ。/lib/modules/2.4.18-0vl3 がなんかおかしく 立ち上がってこない。rename して存在しないようにすれば、 立ち上がって来た。

ハード的には同じ条件での比較ということになる。

うーん。圧倒的だなわが改造は... と思う前に、素の2.4.17 を試してみる ことにする。

記憶によると、素の2.4.17 と これほどの差はなかったはずなんだが...

2.4.17 の性能

2.4.18 は シーケンシャル I/O 性能が悪くなっているようだ。 ファイル生成の性能では、大差ないようだ。

結論は、ext2 方面では、わが改造は圧倒的だ。 ハードの性能をかなり綺麗に出して来る。

問題は、ジャーナル方面。real と sys の差を見れば... sys が 大きくなれば、real も伸びてしまうのが予想される。

で、一般に ジャーナリングファイルシステムは、ext2 と比べて sys が伸びるわけだ。


素の2.4.17のext3の性能

さて、上記を ext2 にして 性能を見てみる。

自分的改造版の real time と 素の 2.4.17 ext3 の system time の差を 見ればわかるのだが、ほとんど余裕がなくなってきている。

次に reiserfs の性能をみてみるが、system time が 大きすぎる ということになりそうだ。

素の2.4.17の reiserfs の性能

シーケンシャル I/O 特に Write の場合、DISK の性能より CPU の使用の 方が大きい。

tar を使った ファイル生成の場合 はもっと顕著になってきそうだ。

高度な DISK I/O の最適化をしても 家のPC --- C3 533Mhz 程度では CPU ネックになってしまって、改造しても メリットがないということに なる。

というわけで、CPU がおそいマシンでは、ext3 の方が良いという結論。


改造版の2.4.17のext3の性能

さて、ext2 方面では絶大な効果があったわが改造だが、ext3 の場合はどうなのか。

実は、write_some_buffers ではないパスで書き出されているらしく、 効果がないようなのだ。

これを確かめてみる。

かなしいかな、圧倒的性能はどこかに行ってしまった。

シーケンシャル Readまで、おそくなってしまったが、これはどうしてか。 予想は、mm/vmscan.c の shrink_cache で ダーティなページが沢山あると その処理に引きずられて、待ちが発生してしまうというものだったのだが、 どうも違うようだ。

一旦 umount して 書き残しがないことを保証して 再度 mount するとどうなるかというと

うーん。あんまりかわらん。

ext2 で再度やってみる。

かならずしも、Read 性能は安定しないようだ。基本的に DISK にどのように データが割り付けられるかによって、Read 性能は変わって来る。

綺麗に割り付けられないと Read 性能はでない。

read-ahead とか pre-read と呼ばれる先読み機構が役立つか どうか ... というのも関係してくる。Linux のコードをみると 先読みのデータは、メモリに残らない。うまくシステムの buffer に 先読みのデータを取りこめれば、すこしは性能が安定することが期待できそうだ が、無駄な I/O にならないようにバランスを取るのが難しそうだ。

ちなみに Write の場合、ダーティな領域が連続してさえいれば良い ようにしたので、必ずしも綺麗に割り付けられなくとも ある程度性能が 出せてしまう。

シーケンシャル I/O 性能は とりあえずおいておいて、 まずは、ext3 を使ったときの tar での展開性能のみ チューニングしてみることにしよう。


kernprof を使ってみる

SGI では、linux 用のカーネルプロファイラを公開している。

とりあえず、2.4.17 で PC サンプリングでの データを採取してみて どうCPU が使われているか あたりをつけてみよう。

これで良いかどうかわからないが、いつも 次のようにしている。

   kernprof -r
   kernprof -b -t pc
   測定したい処理
   kernprof -e
   kernprof -i -m /boot/System.map > p002.out

サンプリングする期間は長ければ長いほど良いのだが、とりあえず カーネル tar 展開 10 回分。

__get_lru_hash_table というのは、改造したところで、隣り合った block がダーティかどうか調べるところ。

prune_icache というのは、inode を shrink しようとするところで、 inode のリストを 検索している。

ほぼすべての関数は、処理内容を知っているか想像できる。

はっきりいってよくわからん。 ざっと見て知っている関数の割合が少ないからどうやって動いているか 想像が働かない。

なさけないことに、それぐらいしかわからん。 ううむどうやって調べようか。

基本的に CPU の 効率をあげるのが目的でなくて、I/O 待ちが発生する ケースを極力へらしたいわけだ。

とりあえずは、kdb を入れて、測定の途中で 止め、どこで待ちが 発生するか見てやることにしよう。

メモ: カーネルプロファイラ の結果をみて気がついたこと

kdbで止めてスタックトレースをしてみる。

     :
   ext3_create
    ext3_new_inode
      ext3_mark_inode_dirty
        ext3_reserve_inode_write
          ext3_get_inode_loc
            bread
             __wait_on_buffer
               schedule

tar のプロセスが、こういうところでいつも待ちに入っている。 ログを取るために、シリアルコンソールが欲しいところ。
また、デフォルトだと画面がスクロールして、全部が見えない。 ここは、kdb/kdbmain.c の LINES を18 ぐらいにすれば、とりあえず OK。

ふうむ。create するたびに、I/O 待ちをすることになってしまっている わけだ。

/*
 * ext3_get_inode_loc returns with an extra refcount against the
 * inode's underlying buffer_head on success.
 */

int ext3_get_inode_loc (struct inode *inode, struct ext3_iloc *iloc)
{

                       :
        offset = ((inode->i_ino - 1) % EXT3_INODES_PER_GROUP(inode->i_sb)) *
                EXT3_INODE_SIZE(inode->i_sb);
        block = le32_to_cpu(gdp[desc].bg_inode_table) +
                (offset >> EXT3_BLOCK_SIZE_BITS(inode->i_sb));
        if (!(bh = bread (inode->i_dev, block, inode->i_sb->s_blocksize))) {

__wait_on_buffer で待ちに入るというのは、どういうことだろう。 本当に read しているのか、それとも write 中なので、write が終るまで 待つということなのか? とにかく I/O 中だから buffer_locked(bh) でない間 schedule() で待つわけだ。
後者だとすれば I/O 効率が既に悪いというのが原因の可能性が高くなってくる。 もしそうだとすれば....ここは単なる被害者である。inode block を 書き出しているところが悪いかといえば、そうも言えない。 ばらばらに I/O が出ているのが そもそもの原因であって、書き出しているところすら被害者という可能性がある。
うーん。だんだん面倒になってきた。

とりあえず、__wait_on_buffer の対象になっている buffer_head を見てみる。

なるほど、write_some_buffers の延長の write_locked_buffers で書かれる 場合は、__end_buffer_io_sync にしたから、それ以外で書かれたという ことか。それに該当するのは、sync_page_buffers ... ではなかった。 bp で break point をかけたがそこでは止まらない。 それ以外では、drivers/block/ll_rw_block.c の ll_rw_block 以外考えられない。

kdb で止める。ll_rw_block に bp する。break point で stack trace をして bc でクリアするというのをくり返すと。

という2つがひっかかった。後者は、

   log_wait_for_space
     log_do_checkpoint
       __flush_buffer
        __flush_batch
           ll_rw_block

というながれ、もしこれだとすれば、log の space がないってことだ。

ちょっとチェックしてみる。

  mkfs.ext2 -j
  だと。

  Creating journal (8192 blocks): done

となった。

   mkfs.ext2 -j -J size=128 /dev/hda3

  なら、

   Creating journal (32768 blocks): done

うーん。あんまりかわりばえしないなぁ。

あ、b_state に、BH_Uptodate が入っていないことを見落していた。 ということは、純粋に read しているってことか。

となると.... inode のまわりも 先読みしておいて、buffer に載せておく ってのが、正しい方法?

とりあえず、上記の ext3_get_inode_loc 専用の bread_zone というのを 作ってみた。

という改造。前にもしたことがあるが、再度挑戦。 今度は ワンポイントで使う。

結果は、

real     0m6.968s user    0m0.570s sys     0m5.700s
real     0m6.860s user    0m0.690s sys     0m5.780s
real    0m22.658s user    0m0.560s sys     0m5.860s
real     0m8.205s user    0m0.850s sys     0m5.460s
real     0m9.805s user    0m0.760s sys     0m5.660s
real    0m24.138s user    0m0.880s sys     0m5.470s
real    0m20.787s user    0m0.740s sys     0m5.730s
real    0m15.381s user    0m0.670s sys     0m5.820s
real    0m21.191s user    0m0.790s sys     0m5.600s

ダーティなバッファがスムーズに書き出されれば、6-7 秒台を維持できそう な気がしてきた。

さて、ちょっと 止めてみたところ write_full_page の延長で、

  journal_dirty_sync_data
      journal_dirty_data
        ll_rw_block
        wait_on_buffer

というパスで write しているのが見付かった。

どうもこれは、

                if (ext3_should_order_data(inode)) {
                        ret = walk_page_buffers(handle, page->buffers,
                                from, to, NULL, journal_dirty_sync_data);
                }

で動くところのようだ。 walk_page_buffers の意味は、foreach 程度に考えれば良いようなので、 ext3_should_order_data() あたりが本質的な意味を持っているように 思える。

/*
 * journal_dirty_data: mark a buffer as containing dirty data which
 * needs to be flushed before we can commit the current transaction.
                            :
int journal_dirty_data (handle_t *handle, struct buffer_head *bh, int async)
{

うーん。なんで、current transaction. するまでに、flush しておかないと いけないのか? ジャーナリングファイルシステムでは、 書き順があるのは確かなのだが、

なるほど、ディレクトリデータのことなのか。

        if (!S_ISREG(inode->i_mode))
                return 1;

これだけはさすがに ... とは思うが、xfs とかは、そういう処理だったかなぁ。

さて、何度か止めてみてみると

  write_some_buffers
    write_locked_buffers
      submit_bh
       generic_make_request:
         __make_request
           __get_request_wait
             schedule

で 待ちになっているケースが目立つ。

    if (q->rq[rw].count < batch_requests)

ということらしいんだが...

batch_requests はなんと 32 ... エ?

 drivers/block/ll_rw_block.c

        queue_nr_requests = 64;
        if (total_ram > MB(32))
                queue_nr_requests = 128;

        batch_requests = queue_nr_requests/4;

どうもこれがひっかかっているようだ。

とりあえず 4 倍にしてみる。

シーケンシャル Write でどこで止まっているのか?

kdb で止めつつ見ていると、__get_request_wait でやっぱり止まっているのと

read_block_bitmap の bread で止まっているのが みつかった。

この2つをやってみるとなんとか 40 秒を切った。

real    0m37.398s user    0m0.010s sys     0m28.250s
real    0m38.809s user    0m0.010s sys     0m29.250s
real    0m35.704s user    0m0.000s sys     0m29.090s
 14:      49339          XT-PIC  ide0
 14:      57752          XT-PIC  ide0


統計情報 (/proc/stat ) を見てみる。

disk_io: (3,0):(170765,42488,1199584,128277,23257056)
             : 
fstune 6161 197136 8732 254299 0 128147 34292 3920 0 0 0 0 0 0 0 0

一番下は、今回の改造で追加しているもの。

どう読むかというと、立ち上げから Disk に対し 42488 回の read と 128277 回の write が起きている。

Disk に対する block 数 (512B 単位)は、 read 1199584 blk / write 23257056 blk 。

fstune の方は、write_some_buffers で書いた 4k block は、 197136 blk および 254299 blk。

write 量で比較すれば、

write_some_buffers を通して書いたものは、全体の 15.5 % にしか過ぎない。

ただし、DISK への 平均 write サイズは、90 KB でそんなには小さくない。 write_some_buffers をみると、2 種類の書きかたがあって、4K / 116.49 KB 。

うーん。write_some_buffers 以外で書く量が多すぎるから効果がないわけか。


どこで Write が発生したか調べてみる。

シーケンシャル Write

ところが、おなじ ようにしても ext3 ではこんなに性能がでない。

ふうむ。journal_commit_transaction での ll_rw_block がメインな わけか。

では tar で展開したときはどうなんだろう?

   Disk Write 量           1376.748 MB
   Disk Write 平均サイズ   59 KB
   write_some_buffers 通過量 40.932 MB

   journal_commit_transaction 通過量 1277.532 MB

なるほど、ようやくわかった。journal_commit_transaction のしくみが どうなっているか 見て行けば いいわけだ。

ちょっとみてみると

        jh = commit_transaction->t_sync_datalist
        next_jh = jh->next_jh

        という感じで、journal_head がリストを形成している。

        それぞれの jh に対して jh->b_bh で buffer_head がリンクしていて
        最大 64 個の 配列に詰めて 一気に ll_rw_block で書き出す。

        それを空になるまで、くり返す。


となると、1 つの commit_transaction にいったい いくつの buffer_head がくっついているか。が重要だ。

シーケンシャル Write の場合 37 回の journal_commit_transaction で、 11471 ページを 書き出していた。平均 310.027 ページ。 時間は取っていないのでわからないんだけども だいたい 120 秒。

tar の展開では、63 回の journal_commit_transaction で、 5118 ページを 書き出していた。平均 81.238 ページ。 時間は取っていないのでわからないんだけども だいたい 180 秒。

   journal->j_commit_interval = (HZ * 5);

なんてコードがあるけども、フル性能で動いたときも 3 秒間隔ぐらいで journal_commit_transaction されるようだ。

で、ll_rw_block で リクエストキューにつなげてしまってエレベータシーク に頼ると、 59 KB 程度の I/O サイズにしかならない。 シーケンシャル Write の 116 KB でも不充分で Disk の性能を発揮する ところまではいかない。

正確かどうかはあやしいが、まぁだいたいそんな風に考えてよさそうだ。

ext2 で非常にうまくいくのは、123 KB での I/O サイズ を越えて 論理的には、1869 KB まで つながっているからだと考えている。

これをなんとかするには、ll_rw_block を直接呼ばないで、 一旦 バッファリングする。そこで、write_some_buffers でやっているような 手法で、I/O をまとめていく。 そういったことをすれば良いのだろう。

ただし、これをするのはとても面倒だし、はたして、効果がどれぐらい あるか...

もうすこし楽なやりかたでやってみたほうが良いかも。


fstune-2.4.17-001 の性能をみてみる。

reiserfs の場合

決して おそいわけではないし、ext2 のようにチューニングが効く んだが... いかんせんうちの CPU はおそい。

プロファイラで見てみると、特定の関数が CPUを使っているのは 明らかなので、改善されてくるかも知れない。

ext2 ふたたび

圧倒的性能は喪われていないか。

なんか Write も性能がばらつくし、Read は、20 秒台が出なくなってしまった。


(最終更新 Thu Mar 30 17:58:51 2006)