findコマンドの正規表現
仕事で。とある動画系商用システムで、定期的に動かしていたコンテンツを別サーバと同期するバッチが凍り付くようになったとヘルプ。バッチを覗いてみると…
find /var/cache -type f -name '*.jpg' -exec rm -f {} \;
find /var/cache -type f -name '*.JPG' -exec rm -f {} \;
find /var/cache -type f -name '*.png' -exec rm -f {} \;
find /var/cache -type f -name '*.PNG' -exec rm -f {} \;
find /var/cache -type f -name '*.bmp' -exec rm -f {} \;
(これが20行くらい続く)
ouch!
仮に/var/cacheに10万個ファイルがあれば、ディレクトリサーチは10万x20行。開発者に聞いたところ「拡張子を列挙しようと思ったが()が効かないのでそういうものだと諦めた」だそうで。
Linuxなどでよく使われているGNUfindコマンドの場合、regextypeで正規表現を切り替えることができる。大文字小文字を区別しない-iregexもある。以下のように書くだけでディレクトリサーチの回数は1/20になる。
find /var/cache -type f -regextype posix-exteneded -iregex
'.+\.(jpg|png|gif|bmp|mpg|mp4|3gpp|ts|mp4a|aac)$' ....
ファイル削除の高速化
さらにバッチが凍り付く原因を調べると、動画からサムネイルを生成する
プログラムが壊れたアップロードファイルを読み込んで暴走し、cacheディレクトリに800万個くらいのJPEGを生成したことがわかった。そりゃ凍り付くわな….
findのもうひとつの盲点は-execオプションの扱い。ちまたのLinux入門書を見ると半ば定型句のように-exec rm -f {} \;が書いてある(俺もな!)。
実はこの指定、1ファイルごとにrmコマンドをfork()してexec()するので
とても遅い。
本来、手動で複数ファイルを消すときも、
$ rm hoge.txt piyo.txt aho.jpg ...
というように複数ファイルを指定することが多いはず。これを模倣してくれるのがxargsコマンド。findから受け取ったファイルリストをコマンド引数ギリギリの長さまで羅列して、rmコマンドに渡してくれる。
20万個の空ファイルを生成してベンチマークしてみる。
$ time find cache -type f -exec rm -f {} \;
real 2m38.417s
user 0m1.356s
sys 0m35.328s
ファイル名に空白を含む場合を想定して、findには-print0オプションを、
xargsには-0オプションをつけることにしよう。
$ time find cache -type f -print0 | xargs -0 rm -f
real 0m9.092s
user 0m0.276s
sys 0m2.812s
約18倍高速化
6時間かかる削除処理が20分で終わります…となると、実作業の効率はだいぶ異なってくる。顧客の心情も。
さて。これが最速か?実はわざわざxargsをパイプで結ばなくても、もともとfindコマンドには-deleteオプションがあり、一切外部コマンドを使わなくてもファイルを消すことだけはできる。ディレクトリサーチしているfindが直接unlink()システムコールを呼ぶから理論上は最速のはず….
$ time find cache -type f -delete
real 0m9.105s
user 0m0.144s
sys 0m2.692s
しかし(この環境では)あまり変わらなかったようだ。速度が全く同じであれば、rm -vfとして実際に消せたかログを残せるxargsコンビの方が多少有利か。
xargsコマンドの小技
ちなみに、xargsコマンドには-lオプションというのがあって何ファイルずつ引数に渡すかを指定できる。なお、-lのデフォルト行数は1なので以下の2行はほとんど等価である(等価に遅い)。
$ time find cache -type f -print0 | xargs -l -0 rm -f
$ time find cache -type f -exec rm -f {} \;
xargsに-lオプションを付けたいとき、というのは擬似的には
$ mv hoge.txt /var/trash
のようなファイル削除以外のファイル操作コマンドの、第2引数にコピー先を与えたいときだろう。本当は以下のように書きたい用途において。
$ mv hoge.txt piyo.txt ... /var/trash
GNU fileutilsに属するコマンドはよく考えられており、cp,mv等には-tオプションが存在する。-tは宛先ディレクトリ指定。ゆえに以下の2行は等価である。
$ mv hoge.txt piyo.txt /var/trash
$ mv -t /var/trash hoge.txt piyo.txt
おっ、宛先ディレクトリが前出しできた。-t /var/trash以降にはコマンドライン引数限度めいっぱいまでファイル名が並べられるので、xargsとベストマッチする。
これまでの内容を組み合わせて、例えば特定のディレクトリの特定種のファイルのみを別ディレクトリに移動する、といった場面を考えたとき、以下のようなコマンドが最速となる、だろう。
$ find /var/cache -type f -regextype posix-exteneded ¥
-iregex '.+\.(jpg|png|gif|bmp|mpg|mp4|3gpp|ts|mp4a|aac)$' ¥
-print0 | xargs -0 mv -t /var/trash
ま、本当に緊急回避的に/var/cacheを掃除したいなら
$ mv cache cache_orig
$ mkdir -p cache
が最速である(^^; これがオチでいいのか….