ディレクトリに入った大量のファイルへの操作
Linux マシンの 1 ディレクトリに、うっかり数十万・数百万ファイル作ってしまうことないですか? 自分は今日もやってしまいました。
ls
rm *
も帰ってこなくて/エラーが出て身動きがとれなくなってしまいます。
そんな困った時のオペレーションのやり方をまとめておきます。
下準備
まずは空の 100 万ファイルを生成します。
# time seq -w 1 1000000 | xargs touch
real 0m25.994s
user 0m3.118s
sys 0m17.361s
time を図る前に、メモリに乗った slab 等を以下のようなコマンドで drop すると正確な値が読み取れるんじゃないかと思います。 ただ今回は面倒なので、Cache/Slab をたっぷり 2GB 弱メモリに載せて drop させずに検証しています。
# echo 3 > /proc/sys/vm/drop_caches
ファイルを数える
まず、どんなファイルあるか知りたいですよね。 ただ、軽率に ls を叩くとコンソールが文字で埋まってしまいます。 慎重に数を数える所から行いましょう。
# time ls | wc -l
1000000
real 0m3.944s
user 0m3.410s
sys 0m0.534s
10 万ファイルあるようです。
数えるだけで、4 秒ほどかかってしまいました。
ls はデフォルトで結果をソートして表示するので、ソートを行わない -U
オプションを渡すと早くなります。
# time ls -U | wc -l
1000000
real 0m0.706s
user 0m0.275s
sys 0m0.440s
一部のファイル名を見てみる
先程使ったソートを行わないことで高速化する ls -U
を使用します。
# time ls -U | head
0095772
0571231
0043505
0708529
0119538
0247028
0258524
0876395
0721112
0217545
real 0m0.006s
user 0m0.000s
sys 0m0.008s
非常に高速に先頭 10 件のファイル名を出力しました。
# time ls -U > /dev/null
real 0m0.690s
user 0m0.292s
sys 0m0.398s
| head
が無いと、出力に時間がかかっていることも分かります。
一部のファイルの属性を見てみる
こういった大量にファイルがあるケースというのは、典型的な例で言うとログ等の機械生成されたファイルだと思います。 一部のファイルは退避させたかったりするので、調査のためにファイルサイズ・作成日時・容量などを知りたい場合が多いです。
普段は ls -lh
コマンドで詳細を出して | less
等で追うのですが、ファイル数が多いと大変時間がかかってしまいます。
以下の実行結果を見ると、最初のオプションを指定しなかった ls | wc -l
の倍ほどの実行時間がかかっていることが分かります。
root@kb:~/many-files# time ls -lh | head
total 0
-rw-r--r-- 1 root root 0 Mar 19 19:46 0000001
-rw-r--r-- 1 root root 0 Mar 19 19:46 0000002
-rw-r--r-- 1 root root 0 Mar 19 19:46 0000003
-rw-r--r-- 1 root root 0 Mar 19 19:46 0000004
-rw-r--r-- 1 root root 0 Mar 19 19:46 0000005
-rw-r--r-- 1 root root 0 Mar 19 19:46 0000006
-rw-r--r-- 1 root root 0 Mar 19 19:46 0000007
-rw-r--r-- 1 root root 0 Mar 19 19:46 0000008
-rw-r--r-- 1 root root 0 Mar 19 19:46 0000009
real 0m7.660s
user 0m4.791s
sys 0m2.867s
これはソートをやめても、実行時間は半分程度にしか減りません。
root@kb:~/many-files# time ls -lhU | head
total 0
-rw-r--r-- 1 root root 0 Mar 19 19:46 0095772
-rw-r--r-- 1 root root 0 Mar 19 19:47 0571231
-rw-r--r-- 1 root root 0 Mar 19 19:46 0043505
-rw-r--r-- 1 root root 0 Mar 19 19:47 0708529
-rw-r--r-- 1 root root 0 Mar 19 19:46 0119538
-rw-r--r-- 1 root root 0 Mar 19 19:46 0247028
-rw-r--r-- 1 root root 0 Mar 19 19:46 0258524
-rw-r--r-- 1 root root 0 Mar 19 19:47 0876395
-rw-r--r-- 1 root root 0 Mar 19 19:47 0721112
real 0m4.492s
user 0m1.615s
sys 0m2.879s
ファイルの一覧の取得に 1 秒・詳細情報を取得するのに 3 秒・ソートするのに 3 秒というような内訳でしょうか。
ls コマンドは -l
を指定すると、全ての結果を作ってから標準出力へと結果を出力するようです。
一部のファイルの情報だけ見たいのに、ほとんどのファイルの情報は捨てられてしまっています。
ls -U | head
はメチャクチャに早かったので、これの結果を ls -l
コマンドの後に指定することで早くなるのか試してみます。
# time ls -l `ls -U | head`
-rw-r--r-- 1 root root 0 Mar 19 19:46 0043505
-rw-r--r-- 1 root root 0 Mar 19 19:46 0095772
-rw-r--r-- 1 root root 0 Mar 19 19:46 0119538
-rw-r--r-- 1 root root 0 Mar 19 19:46 0217545
-rw-r--r-- 1 root root 0 Mar 19 19:46 0247028
-rw-r--r-- 1 root root 0 Mar 19 19:46 0258524
-rw-r--r-- 1 root root 0 Mar 19 19:47 0571231
-rw-r--r-- 1 root root 0 Mar 19 19:47 0708529
-rw-r--r-- 1 root root 0 Mar 19 19:47 0721112
-rw-r--r-- 1 root root 0 Mar 19 19:47 0876395
real 0m0.013s
user 0m0.007s
sys 0m0.009s
十分に早そうです。
名前で検索した上で、ファイルの属性を見てみる
ログなどの場合は、ファイル名に日付・タイムスタンプが利用されていることが多いと思います。
先月のログを削除する前に情報を見ておきたいと思って ls -l 2020-01-*
等と指定する事も多いと思います。
ですが、大量にファイルがある環境ではこの方法が使えません。 ダミーファイルは日付の名前が付いていないので、日付の範囲を指定することを模して 10 万~ 40 万番のファイルの詳細を確認してみようと思います。
# ls -l 0{1..4}*
bash: /bin/ls: Argument list too long
bash などのシェルでは、連番を生成する {1..4}
・ファイル名のワイルドカードを指す *
等は、シェルにて全て評価された後にコマンドの引数へと渡ります。
上記のコードでは、30 万の引数が ls
コマンドに渡ったということになります。
環境・コマンドによって引数の数の制限は異なりますが、典型的には 2MB・数千個程度等の制限が掛かっていることが多いようです。
こういった際は、find コマンドを利用すると良いです。find コマンドは、パイプを正しく扱い、ファイル名を素早く返します。
-name
フラグはシェル展開とは異なるパターンを受け付けますが、分かりにくいようであれば、 -iregex
-regex
で正規表現を利用することも出来ます。
パイプで一部のファイル名を返す場合・全てを /dev/null
に捨てる場合で実行速度にかなりの差があることが分かると思います。
# time find -type f -name '0[1234]*' | head
./0100001
./0100006
./0100010
./0100016
./0100022
./0100032
./0100034
./0100040
./0100062
./0100064
real 0m0.152s
user 0m0.076s
sys 0m0.081s
# time find -type f -name '0[1234]*' > /dev/null
real 0m1.423s
user 0m1.032s
sys 0m0.391s
ただ、これだけではファイルの詳細情報が分かりません。
find コマンドで検索した結果を head で 10 件取り出し、それを ls -l
の引数へと渡してみます。
# time ls -l `find -type f -name '0[1234]*' | head`
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100001
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100006
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100010
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100016
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100022
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100032
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100034
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100040
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100062
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100064
real 0m0.167s
user 0m0.076s
sys 0m0.102s
十分に高速になりました。
ただし、ファイル名にスペースが含まれていると正しく動きません。
これは xargs
コマンドと、ファイル名の区切りをスペースではなく NULL バイトで表すオプションを付与することで解決できます。
find
コマンドは -print0
、 head
コマンドは -z
、 xargs
コマンドは -0
フラグで、区切り文字を NULL バイトとして表現が可能となります。
# time find -type f -name '0[1234]*' -print0 | head -z | xargs -0 ls -l
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100001
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100006
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100010
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100016
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100022
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100032
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100034
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100040
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100062
-rw-r--r-- 1 root root 0 Mar 19 19:46 ./0100064
real 0m0.159s
user 0m0.083s
sys 0m0.097s
大量のファイルを削除する
ls と同じく、rm コマンドも引数の制限があるので、いつものようにシェル展開・ワイルドカード等を使うと削除が出来ません。
# time rm *
bash: /bin/rm: Argument list too long
これも、find と xargs を組み合わせることで解決できます。
# time find -type f -print0 | xargs -0 rm
real 1m0.537s
user 0m3.457s
sys 0m14.028s
スペースが含まれないことが分かっていれば、単にこのように書くことも出来ます。
# time find -type f | xargs rm
find -name
find -regex
フラグ等を利用し、ファイル名を検索した上で大量のファイルを削除することも容易でしょう。
今回は削除に時間がかかるので、他の例は載せません。