Hatena::ブログ(Diary)

himazu blog このページをアンテナに追加 RSSフィード

 | 

2007-08-10

ディレクトリ内の大量のシンボリックリンクは大量のディレクトリより目立って重い

概要

ディレクトリにインデックスを持たずエントリをリニアサーチする種類のファイルシステム(BSDのFFSやLinuxのext3)では1つのディレクトリに大量のファイルやディレクトリを入れるのは得策ではない。しかし、そうなってしまうこともある。最近そういう状況になったの際にどのようなことが観察され、どう対処したか、更に観察内容の理由付けを以下に記す。

環境

ウェブ・アプリケーションがRedHat Linux上のApache 1.3+mod_perlで動いていて、データがNetApp社のNAS(Network Attached Storage)上に置かれている。

観察

1000個のサブディレクトリがあるディレクトリAがあるとしよう。そして100個のサブディレクトリがあるディレクトリBがあるとしよう。

$ ls -F1 A
a0001/
a0002/
a0003/
...
...
a1000/
$ ls -F1 B
b001/
b002/
...
b100/

そして、Bの中でAのサブディレクトリも見えるようにしたいとする。そのためにはBの中にシンボリックリンクを作ることになる。

$ cd B
$ ln -s A/a0001
$ ln -s A/a0002
...
...
$ ln -s A/a1000

こういう状況でA内のサブディレクトリをアクセスするのとB内のサブディレクトリをアクセスするのを比べると、Bのほうが目立って遅かった。尋常でないほどの遅さだった。BをアクセスしているプロセスはI/O待ちが多く、B内にシンボリックリンクがなかったときはロードアベレージが高くても2程度だったものが、シンボリックリンクを加えた結果20程度にまで上がってしまった。一方、Aをアクセスしている同様のプロセスもあり、そちらはそれほど重くない。

AもBもNAS上にある。大量のシンボリックリンクがあると遅くなるのは使っているNASの特性なのかも知れない。NASもLinuxやBSD由来のコードが使われていることが多いので、同様のことはLinuxやBSDでも見られるのではないか。

対策

尋常でない遅さはなんとかしなければならない。そして、BにアクセスしているプロセスがAの内容にもアクセスできるようにしなければならない。そこでどうしたかと言えば、プログラムを変更して、BになければAを見るようにした。こう言ってしまえば簡単だが、既存のそれなりの規模のプログラムだと、実際にはそう簡単でもないこともある。

観察内容の理由付け

以下、観察内容の理由を考えたのだが、十分に理由付けできていない。

B内のエントリの数はAよりたかだか1割多いだけである。なぜそれほどまでにBをアクセスするプログラムは遅くなったのか。シンボリックリンクの格納のされかたに起因しているはずである。

ディレクトリとはエントリ(ファイルまたはディレクトリ)の文字列と、エントリのiノード番号の一覧を格納したファイルである。

ディレクトリA:

エントリ名iノード番号
a00013152
a000248592
......
a100038429

上記はファイルあるいはディレクトリがディレクトリ内にある場合で、シンボリックリンクはその文字列がディレクトリ内に記録される。

ディレクトリB:

エントリ名iノード番号/リンク先
b00112742
b00247281
......
b10082731
a0001A/a0001
a0002A/a0001
......
a1000A/a1000

iノード番号は整数値で、通常は4バイトだろう。一方、シンボリックリンクの場合は、iノード番号の代わりにその文字列がそのまま入り多くの場合4バイトよりずっと多くなるはずだ。しかし、実際に比較してみると、AとBとで大きさに目だった差はなかった。Bのほうがずっと大きいだろうと予測していたのだが、当てが外れた。実際のディレクトリの大きさは約80Kバイトだった。

ディレクトリにインデックスを持たないファイルシステムではこの一覧がリニアサーチされる。ディレクトリがアクセスされるたびに、ディレクトリの内容が読み込まれてリニアサーチされる。1000個ものエントリがあると、そのリニアサーチに要する時間がばかにならなくなる。そして、大量のシンボリックリンクがあるディレクトリでは、そのリニアサーチにより多くの時間がかかっていることになる。

ファイルシステム上でのディレクトリデータの大きさはAとBとで同程度ではあるが、実際にそこからエントリを探し出すには、まずメモリに読み込まなければならない。メモリ上に置くべきデータはシンボリックリンクのほうが本質的に多い。リンク先の文字列は可変長なので固定長のiノード番号を扱うよりは複雑なはずである。これらの理由によりBのほうが処理にずっと多くの時間を要するのではないか。

2007-08-09

STDOUTとSTDERRをファイルにも出力するようにする

Perlで書いたプログラムを極力変更せずに、STDOUTとSTDERRを通常通り出力させたまま、それに加えてファイルに出力したくなった。そのプログラムはシステム管理的コマンドで、コマンド行から使い、その出力をリアルタイムで見るのだが、出力をファイルにも保存しておきたい。STDERRも記録したいのは、dieしたときなどはSTDERRに出てしまうからである。

そのPerlプログラムの名前をfooとしよう。fooを直接起動するのではなく、以下のようなシェルスクリプト(foowrapperと呼ぼう)から呼び出すことにすれば比較的簡単に目標はほぼ達成できる。

#!/bin/sh
foo 2>&1 | tee file

しかし、これではちょっと不便だ。fooとfoowrapperの両方が必要になる。fooがPATHの中にないといけない。foowrapperの中にfooを絶対パスで書くと、fooの場所を動かしたときに動かなくなる。

少し調べたら以下のような例が出ている。

open(STDOUT, "| tee file");

しかし、STDERRを加えて以下のようにすると、プログラムが終了しなくなる。改行を端末から入力すると終了するが、それは鬱陶しい。

open(STDOUT, "| tee file");
open(STDERR, "| tee -a file");

print "hi\n";
print STDERR "ho\n";

そもそも、teeを2つも起動するのは無駄だ。以下のようにすればteeは1つで済む。

open(STDOUT, "| tee file");
open(STDERR, ">&STDOUT");

print "hi\n";
print STDERR "ho\n";

これでもまだプログラムが終了しないままだ。

プログラムが終了しないのは、ファイルを閉じることができないからだろう。fooが開いているのはSTDIN, STDOUT, STDERRだけだ。試しにSTDOUTを閉じると、事態は悪化した。改行を入力しても終了しなくなった。

open(STDOUT, "| tee file");
open(STDERR, ">&STDOUT");

print "hi\n";
print STDERR "ho\n";
close(STDOUT);

STDERRを閉じて、更にSTDOUTを閉じたら、ちゃんと終了するようになった。

open(STDOUT, "| tee file");
open(STDERR, ">&STDOUT");

print "hi\n";
print STDERR "ho\n";
close(STDERR);
close(STDOUT);

ただし、fooを呼び出した側で標準エラー出力を見ても何も出てこず、すべて標準出力に出てしまう。外から見て標準エラー出力が普通に使われているようにしたまま、ファイルにも記録するのにはもう少し手間がかかりそうだ。

上記のことはRedHat Linuxでやっていたのだが、ためしにSolarisとCygwinで試したら同じ結果になった。Perlのバージョンは5.8である。Perlの標準出力・標準エラー出力の挙動について、これらのプラットフォームは同じ挙動を示している。

 | 
カレンダー
2005 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2006 | 01 | 02 | 03 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2007 | 01 | 02 | 03 | 04 | 08 | 09 | 10 | 11 |
2008 | 01 | 02 | 03 | 05 | 06 | 08 | 09 | 10 |
2009 | 01 | 02 | 04 | 10 | 11 | 12 |
2010 | 07 | 08 |
2011 | 01 | 03 | 07 | 08 |
2012 | 03 | 09 | 10 | 11 |
2013 | 04 | 06 | 07 | 09 | 10 | 11 |
2014 | 01 | 02 | 03 |