HDE BLOG

コードもサーバも、雲の上

正しいログの削除の仕方

おはこんばんちは。 尾藤 a.k.a BTO です。

先日弊社のとあるサービスで、ログが溢れてしまいサービスが正常に稼働しなくなった事がありました。 こういう時は、ログを削除してディスクスペースを確保すればいいのですが、ログの削除の仕方を間違ってしまうとサービスの再起動に時間がかかってしまいます。そこで今回はログの削除の方法について書いてみます。

失敗例

ログが溢れた時の、ログ削除の失敗例はだいたいこんな感じになります。

  • ログが大量に書き込まれてディスクフルになる
  • 他のデータが書き込めなくなって、サーバが正常に動作しなくなる
  • rmコマンドでログファイルを削除する
    • dfコマンドで確認しても、あれ? 減ってない。なんで?
  • デーモンを再起動(sudo service foo restart)
    • あれ? 戻ってこない。なんで?
  • kill -KILLで無理矢理プロセスを落とそうとする
    • あれ? プロセスが死なない。なんで?
  • 新規にプロセスを立ち上げようとする
    • address already in useで怒られて立ち上がらない
  • 何これ? とりあえずググる
  • 30分〜1時間ぐらい格闘してたら、いつの間にか復活してる

なぜこんなことが起こるのでしょうか。全ては正しい手順でログファイルを削除していないのが原因です。

rmコマンドはファイルを削除しない

rmReMoveの略ですから、当然ファイルを削除するコマンドだと思っている方が多いと思いますが、rmコマンドはファイルを削除しません。ここが大きな勘違いなのです。

仮にrmコマンドがファイルを削除できたとして、他のプロセスがファイルをオープンしてた場合はどうなるでしょか。そういう場合でも、ちゃんと問題なく動作するように、UNIXではファイルが必要なくなったタイミングで削除するようになっています。

では、rmコマンドは何をするのでしょうか。rmコマンドはunlinkというシステムコールを呼び出して、リンクを削除します。リンクとは、誤解を恐れず平たく言うとファイル名です。UNIXではファイルの実体にinodeが付けられて、inodeを使ってファイルを参照しますが、inodeだけでは人が管理しづらいので、inodeを参照するリンクを作って、管理するようになっています。

リンクハードリンクとも言います。元々はリンクでしたが、後にシンボリックリンクが登場してから、区別するためにハードリンクと呼ばれるようになりました。

UNIXではユーザープロセスはどんなに頑張ってもファイルを削除することはできません。ファイルを削除するのは、ユーザープロセスの仕事ではなく、カーネルの仕事なのです。

では、ファイルはいつどのようにして削除されるのでしょうか。ここを理解することで、正しいログファイルの削除の仕方が分かってきます。

参照カウンタ

ファイルがどのように削除されるか理解するために、まずは参照カウンタについて説明します。

プログラマの方なら、ガーベッジコレクションという単語を聞いた事があると思います。これは名前の通り、ガーベッジ(ゴミ)になったオブジェクトを回収する処理です。ゴミを回収しないと、そのメモリ領域が使えませんから、必要なくなったオブジェクトは回収して、別のオブジェクトを割当てる領域として利用するわけです。

ガーベッジコレクションは何もプログラミング言語だけの概念ではありません。ゴミになったオブジェクトを回収して、再利用したい処理には同じ考えが適用できます。UNIXファイルシステムにおいても、ガーベッジコレクションの考え方が使われています。

ガーベッジコレクションには、様々なアルゴリズムがありますが、最も簡単で有名なのは恐らく参照カウンタではないかと思います。参照カウンタでは、オブジェクトを参照している数をカウントします。参照カウンタが0になると、そのオブジェクトはどこからも参照されていない(=ゴミ)になりますので、回収することができます。

参照カウンタが0になるとファイル削除

UNIXファイルシステムでも、参照カウンタが使われています。そのファイルを参照している数が0になると、そのファイルが使用されないことが確定しますので、ファイルの削除処理が発生します。

当然削除する対象のファイルが巨大だと、ファイルの削除に時間がかかって、そのプロセスはファイル削除が終了するまで待たなくてはいけません。ファイル削除中は、外部からSIGKILLシグナルを送って(kill -KILL)もプロセスは終了しません。なぜならシステムコールの実行中だからです。

失敗例の詳細

先ほどの失敗例の詳細は次のようになります。

  • デーモンを落とした時に、close()が呼ばれて、ファイルの参照カウンタが0になった
  • カーネルが巨大なログファイルの削除処理を開始(システムコール)
  • プロセスはファイル削除が完了するまでwait
  • 外部からSIGKILL送っても無効(システムコール実行中)
  • ネットワークソケットはcloseされていない
    • 従って同じポートを開こうとしても、address already in use

つまり、プロセスが終了するタイミングで参照カウンタが0にならないようにすれば、問題なく再起動することができるわけです。

正しいログファイルの削除の仕方

以上を踏まえて、正しいログファイルの削除手順は次のようになります。

  • mvコマンドでファイル名を変更する
    • mv foo.log foo.log.old
  • デーモンを再起動
    • 新しく foo.log が作られる
  • ログファイルを削除する
    • rm foo.log.old

たったこれだけです。

まず最初にmvコマンドでファイル名を変更するのは、デーモンがclose()でログファイルを閉じた時に、参照カウンタを0にしないようにするためです。この場合foo.log.oldが参照していますから、参照カウンタが0になることはありません。

次にデーモンを再起動します。これにより古いログファイルへの参照はなくなり、新たに0バイトのログファイルが作られます。今後はこっちの新しいログファイルにログが書き込まれます。

最後にrmコマンドでログファイルを削除します。ここで参照カウンタが0になるので、rmコマンドは実行に時間がかかりますが、サービスは既に復活しているので全然問題ありません。

結論: rmコマンドは最後に実行する

rmコマンドを最後に実行するようにすれば何も問題ありません。またUNIXがどのようにしてファイルを削除するのかを理解すれば、なぜこのような挙動になるのかも分かってきます。

最後にプレゼン用の資料を貼付けておきますので、よかったらご参照ください。

ちなみにHDEでは、英語化に力をいれておりまして、このプレゼンも頑張って全て英語で発表しました。