穏やかな速度でファイルを削除するプログラム gentle_unlink を書いた

穏やかな速度でファイルを削除するプログラム gentle_unlink を書いた#interest_aeおがた (@xtetsuji) です。

Linux サーバ上で作業をしていると、様々な一時ファイルがディスクを圧迫していて、ディスク容量を増やすために削除を行うことがあります。通常であれば rm コマンド一発で終わる単純なファイル操作ですが、ファイルが何百万やそれ以上といったオーダーで大量にあると、削除自体のコストが無視できなくなります。

Linux サーバ管理者の間ではたびたび問題になる大量ファイルの削除操作、各人コマンドを組み合わせて工夫しているようですが、より微調整をしたかった私は Perl を使ってコマンドを書くことにしました。その名も gentle_unlink。

最近はITエンジニアリングネタは Qiita に書くことが多いのですが、自分が書いたまとまったプログラムの紹介ということで、メインブログに書いてみることにしました。

初出の社内勉強会での紹介トーク、そしてスクリプトファイルは以下のリンクからどうぞ。

大量のファイル削除が重い理由

私は Linux OS の根底の処理についてそれほど詳しいわけではないので、正確な理解は専門家や専門書籍にあたってください(いつかは情報工学等に基づいた解説をしたいです)。この記事でも学問的な原理には踏み込まない、以下は私なりの雑な理解です。

rm などでファイル削除を行う際、Linux の低層の部分ではそれなりの数の命令が発行されます。この低層の部分での命令の事をシステムコールと呼びます。単発の削除の場合であればシステムコールが10や20発行されても大したことありません(今のPCは高性能ですから)が、とはいえこれが何百万何千万と積み重なると、それなりに無視できないものになってきます。

1つのファイルを削除するたびに発生するシステムコールの累積もそうですが、ファイル削除もディスクアクセスが発生することによりディスクI/O を専有し続けることになります。Linux の負荷の目安であるロードアベレージの数値は1つのI/Oの専有でも1加算されるので、何も仕事していない Linux サーバに時間のかかるファイル削除を rm コマンドで行わせると、ちょうどロードアベレージが1だけ上がるように観測されます。ちなみに通常の rm コマンドは複数ファイルの削除も同期的に行うので、I/O が多重になることは無く、1ちょうどの加算となります。

ファイル1つを取れば、数GBや数TB程度の大きなファイルと数KBや数MB程度の小さなファイルの削除時間は原理的には同じだと言われています。とはいえ、様々な要因で大きなファイルが小さなファイルより削除に時間が掛かるケースもあり、それも後で考察します。

ファイルシステムとファイル削除の動作

ファイル削除を考察する時、Linux という OS 以外にもファイルシステムの存在を無視するわけには行きません。ハードディスクといった物理的な記録媒体を、ファイルやディレクトリといった論理的な実体に見せるための、OS とハードウェアの間にあるシステムと考えると理解しやすいと思います。

今日の Linux では ext4 (fourth extended file system、通常エクストフォーと読まれる)が多く使われています。今回の解説でも ext4 を主に念頭に置いています。

ファイルシステムにもよりますが、ext4 など Linux で多く使われるファイルシステムでのファイル削除とは、ファイルという論理的実体をディスクに書かれた磁気情報等の物理的実体から切り離すことを指します。通常はディスクの物理的実体にファイルの論理的実体が1つだけ対応しているという状態ですが、論理的実体が複数対応していてもよく、この同じ物理的実体に対する複数の論理的実体のことをハードリンクと呼びます。ファイルに関する ls -l の出力で左側のパーミッションのすぐ右側の数字がハードリンクの数になっています。通常は1です。

良く知られたシンボリックリンクは、ファイルを指すファイルという仕組みであって2つのファイルに主従関係があるのに対し、ファイルとファイルのハードリンクは2つのファイルが同じ物理的実体を見ているだけという違いがあります。

編集でこそ両者が見ている同じ物理的実体の編集になるので、一方のファイルの編集結果がハードリンクの関係であるもう一方のファイルにも影響しますが、削除に関しては一方の削除が他方へ影響を与えることがありません。この削除に関する挙動は、シンボリックリンクとは違うハードリンクの特徴の一つです。

ディスクの物理的実体は、ハードリンクが0になるとファイルシステムとの関連が無くなり、ディスクの空き容量としてファイルシステムにより還元されます。ディスクの空き容量を増やす意味での削除は、ディスクの物理的実体をハードリンク的意味合いでみなしごにする削除とも言えます(ハードリンクを0にしない削除では空き容量は増えないので)。

削除という操作は rm の元となった remove であったり delete といった単語でよく表されますが、システムコールやプログラム言語では unlink という言葉が使われることがあります。これはまさに、ファイルという論理的実体とディスク上の情報という物理的実体のリンクを断ち切るという意味あいがあります。

穏やかに削除することで負荷を減らす様々な作戦

一般的なファイルシステムからみたファイル削除について考察しましたが、この一連の作業にはそれなりのコストがかかることは前述した通りです。

稼働中のシステムのファイルを削除して空き容量を減らしたいが、大量のファイルを rm コマンドが全力で削除するとシステムの負荷が上がりすぎて提供中のサービスに影響を与える場合には、「穏やかに削除する」方法が必要となってきます。

削除操作に関わるシステムコールなどの低層の命令は削減できないので、負荷を減らすのであれば単位時間あたりに削除するファイル数を減らす必要があります。普段何気なく rm コマンドで削除をしていましたが、このために知られたいくつかのテクニックがあります。

  • rm にディレクトリ削除をさせず、個別のファイルを渡していく
  • rm に渡す個別のファイルは単位時間あたり適切な数に制限する

このことを実現するため知られた一つのテクニックは、find コマンドを使った方法です。

# find $dir -type f -delete -exec "sleep 0.1" ;

いくつかのバリエーションがありますが、一つのファイルを削除するたびに sleep コマンドで 0.1秒から1秒程度の待ちを入れる方法が知られています。

とはいえ待ち時間の調整もやや難しく、負荷を下げるために待ち時間を多くしたら大量のファイル削除が何日経っても終わらないという悩ましい問題にぶちあたります。一瞬の負荷を我慢すればいい数万程度の数ではないので、進捗を確認したいという要望も出てきます。

検索して出てくる方法を参考にしつつも、微調整や進捗がわかりやすいコマンドを作成することにしました。その名も gentle_unlink。

gentle_unlink の概要

コマンドは GitHub の xtetsuji/encoreutils 以下で公開しています。

冒頭にも書きましたが、社内勉強会でのスライドも理解に役立つと思います。

Perl で書きましたが、インフラエンジニア的視点で重要な「どこでも動く」ことを相当意識しています使用しているモジュールはどれも Perl 5.8 で標準モジュールとして入っているものばかりです。よほど古い環境やよほど Perl を忌避しているディストリビューションでない限り、ほとんどの Linux 環境で動作するはずです。

以下では実行権限を与えてパスが通った場所に移動したことを想定していますが、普通に ./gentle_unlink とコマンドラインでパス指定しても問題ありません。

gentle_unlink --help とするとヘルプが見られます。

$ gentle_unlink --help
Usage:
 gentle_unlink remove_file_list.txt
 find garbage -type f | gentle_unlink
 ls -1 *.log | gentle_unlink --timeout=600 --progress --interval=2
 gentle_unlink [--timeout=SECONDS] [--progress] [--interval=SECONDS] 
 [--flexible] [--total-hint=TOTAL][--dry-run]

gentle_unlink も事故防止などを想定してディレクトリの削除は行いません

使い方としては、まず find コマンドを -type f つきで実行するなどして削除するファイル一覧を作ります

ヘルプの例にもありますが、削除するファイル一覧は標準入力、またはそれが書かれたテキストファイルを引数に与える形で行います。

以下のような利用の流れを想定しています。

  • 削除したいディレクトリを $DIR として、find $DIR -type f | tee delete-list.txt などして削除リストを作成
  • delete-list.txt の中身をページャで確認して、削除すべきではないファイルが含まれていないことを確認したり、さらに選定したりする
  • まずは dry-run モード(実際の削除操作はしない)で  gentle_unlink --progress --interval=0.1 --dry-run delete-list.txt などとコマンドを打って、正しい処理をなぞることを確認
  • 確認が終わったら dry-run をはずし gentle_unlink --progress --interval=0.1 delete-list.txt などとして実行

ファイルリストは相対パスでも問題ないですが、その場合はカレントディレクトリに気をつけてください。

gentle_unlink コマンドの標準の挙動ではフールプルーフ的観点から、与えられたファイル一覧で存在しない(ファイル一覧作成後に別のプロセスに削除されたなど)場合には処理を中断しますが、これが都合悪い場合には --flexible オプションで中断を抑制できます。

cron などで夜中のみ削除をして、朝までに終わらなければまた次の夜中に cron で呼び出すといった場合には --timeout オプションが便利です。

--interval オプションで秒数(小数指定でもOK)を与えなければ gentle_unlink はその名前に反して全力で削除していきます。なので名前の通りの動作をさせるのであれば --interval オプションの指定は必須といえます。全力で削除する場合も進捗表示の --progress オプションなどは rm コマンドにはない利点です。

負荷軽減とファイル削除の速度を両立するために、2016年6月バージョンの gentle_unlink では 20 ファイル(UNLINK_CHUNK_NUM 定数でハードコードされている)削除するごとに --interval コマンドで指定した秒数を待つという動作を繰り返します。削除を行うマシンの負荷に応じて、負荷軽減と削除速度を両立する --interval オプションを各使用者が調整する必要があります。

2016年6月バージョンの --progress オプションの進捗表示では、削除したファイルのファイルサイズなどを表示していきますが、手元の gentle_unlink では --total-hint オプションなどを新設してファイル削除の速度と推定残り時間を表示するバージョンを試験中です。

カラになったディレクトリの削除について

gentle_unlink によってファイルを削除していった場合、カラになったディレクトリを削除したいといったケースがあると思います。

このような場合は find $DIR -type d -empty などとしてカラディレクトリをリストアップして、それを xargs などの手法で rmdir コマンドに渡していくのが素直な作戦かと思います。

まとめ

上記の勘所さえ分かれば、gentle_unlink はシンプルかつ使いやすいコマンドではないでしょうか。

今後のバージョンでは、以下の改良を予定しています。

  • 削除速度の表示
  • 推定削除完了時間の表示
  • 進捗状況をコンパクトにわかりやすくするオプションの追加
  • ファイル断片化が激しい巨大ファイルの縮退削除
  • --exclude など rsync などで良く知られたオプションの導入
  • 著名なファイルシステムを内観するオプションの導入
  • ハードリンクを 1 から 0 にした操作のみを拾い、ディスク空き容量への寄与をより正しく表示

機能追加要望やバグ報告など、いただければ出来る限り対応したいと思います。

gentle_unlink で、インフラエンジニアの皆さんのファイル削除作業をさらに快適にできたら、これほど嬉しいことはありません。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です