logrotateの設定とファイルのアクセスモードについて

大量にアクセスが来るサーバーのlogrotateは意外と考えることが多いです。大量にアクセスが来るなら当然それだけログファイルが簡単に肥大化します。そこで一定間隔でログファイルを別のファイル名に変更して、書き込み先を新しいファイルにする必要があります。

これについてどういう設定をすればいいのか、昔自分が調べたり、考えたりしたことを少し書いてみます。最初に書いておくと私はこの辺りについて詳しい自信はありません。もしエントリー内に誤った内容があればぜひ教えてください。

ちなみにこのエントリーの内容は以下の本を読めば分かるので正確な知識を身につけたい人は以下の本を読んでください。

ちなみに以下の資料を読んで書こうと思った次第です。

忙しい人のために先にまとめ

  • ログファイルのopen時のアクセスモードは os.O_WRONLY|os.O_APPEND|os.O_CREATE を指定する
  • logrotateの設定には nocreate を渡す
  • logrotateではファイルをmvした後に行う処理が書けるので、そこでアプリケーションに対してシグナルを送ることでlogrotateの処理を実装できる
  • fluentdはログのローテートを頑張って検知している

ファイルディスクリプタについて

ファイルを open するとファイルディスクリプタを得られます。ファイルに書き込む際にはファイルディスクリプタ経由で書き込みを行います。

Linuxシステムプログラミング 23pをちょっと長めに引用します。

ファイルを読み書きする前にはオープンする必要があります。カーネルはオープンしたファイルをプロセスごとに管理しており、これをプロセスのファイルテーブル(file table)と言います。ファイルテーブル内のエントリは非負の整数、ファイルディスクリプタ(ファイル記述子、file descriptor。一般にfdまたはfdsと略されます)により順番に管理されており、オープンしたファイルに関する状態を保持します。メモリinode
(ファイルに対応するディスク上の inode をメモリ上へコピーしたもの)や、ファイルポジション(読み書きす るファイル内のオフセット)、アクセスモードなどのメタデータです。ユーザ空間、カーネル空間のどちらからも、ファイルディスクリプタはプロセスごとの一意なcookieとして使用されます。ファイルをオープンするとファイルディスクリプタが返され、以降の処理(読み書きなど)ではファイルディスクリプタが中心的なパラメータになります。

ファイルディスクリプタで重要なのはファイル名は含まれない点ですつまり

  1. ファイルをopenしてファイルディスクリプタを得る
  2. openしたファイルのファイル名を変更する
  3. 1で得たファイルディスクリプタに対して書き込みをする
  4. 書き込んだ内容は古いファイルに対して行われる

という挙動になります。ログをローテートするのは1つのファイルが肥大化しないように行うものです。古いファイルに書き込みされ続けていては何も意味がないどころか、想定していないファイルへの書き込みになるので害悪です。

ちゃんとローテートをしたい場合は以下のいずれかにすることが考えられます。

  • ファイルに書き込む度にファイルをopenして書き込み後にcloseする
  • ファイル名が変更されたタイミングを教えてもらい、元のファイル名でopenし直す
  • ファイル名が変更されたタイミングを自力で検知して、元のファイル名でopenし直す

PHPの場合はリクエスト間でリソースの共有を行うことがPHP上からは基本的に不可能なので一番上の方法を取ることになります。openはシステムコールを呼び出すのでシステムコール呼び出し分のコストがかかります。広告配信サーバーのようなパフォーマンスクリティカルな部分では使えない手法かもしれません。

2番目の方法が今回主に議論したい方法です。詳しくは後述しますがlogrotateでは postrotate endscript の中にファイルのmvをした後に行う処理を書くことができます。これを使ってプロセスに対してシグナルを送り、アプリケーション側でシグナルを受け取ったら元のファイル名でopenし直す実装をすれば実現できます。

3番目の方法は自分で実装することがあるかは分かりませんが、実はみんな大好きfluentdで実装されています。これについても後で紹介します。

ファイルをどうopenするか

ここではリクエストに起因して何らかのログをファイルに書き込むケースを想定します。ファイルをopenするときにアクセスモードを指定します。今回紹介するのは以下のアクセスモードです。ちなみに以下の事情があるのでGo言語で紹介します。

  • os.O_WRONLY: 書き込み専用。os.O_RDONLY, os.O_WRONLY, os.O_RDWRの3つのいずれかは必ず与える必要がある
  • os.O_APPEND: アペンドモードになる。書き込みは常にファイル末尾になるため複数のプロセスが同じファイルへ書き込む場合でもロックなどの同期処理を行う必要がない
  • os.O_CREATE: ファイルが存在しない場合は新たに作成する。ある場合は意味が無い(os.O_EXCLフラグを同時に与えられた場合は例外)

以前に私が書いた以下のエントリーでも解説しています。

この3つのアクセスモードを与えることでログファイルに対する書き込みで考えることはほぼなくなります。ただ PIPE_BUF (Linuxでは4kb)を超えると同時に書き込もうとしたログが混ざるケースがあるかもしれません。PIPE_BUF 以下のサイズならば混ざらないので安心して使用できます。

logrotateの設定をどうするか

logrotateはかなり色々な設定ができるのでmanを確認することをお勧めします。私は以下のような設定をよく使います。もちろんアプリケーションの特性に大きく依存するので参考程度にしてください。

/home/user/xxx.log {
su user group
daily
rotate 10
missingok
notifempty
size 200M
sharedscripts
postrotate
pid=/home/user/server.pid
test -s $pid && kill -USR1 "$(cat $pid)"
endscript
nocreate
}

ログが大きく、圧縮したい場合はcompress delaycompress を付けます。

su でユーザーを指定できるのでサーバーを実行しているユーザーにします。missingok をつけることでファイルが存在しなかったときにエラーにならないようにします。notifempty を付けると空のファイルを無駄にmvすることがなくなります。エラーログのような常に書き込まれ続けるわけではないファイルに対して無駄なlogrotateを防げます。

size を指定することで小さいファイルのlogrotateをしないようにします。logrotateはファイルが肥大化しないように行うものなので場合によっては便利です。

nocreate を付けるとlogrotateはファイルをmvするだけになります。デフォルトではlogrotateが空ファイルを作ります。先程紹介したようにアプリケーション側で os.O_CREATE を付けておけばファイルが無ければ勝手に作られます。アプリケーションの実装がそうなっていればこれを付けて困ることはまずありません。最初に紹介したスライドで書かれているようにlogrotate側がopen時に os.O_CREATE|os.O_EXCL を付けるので、logrotateがファイルをmvし、logrotateがファイルをopenするまでの間にファイルが作られた場合はエラーになってしまうそうです。非常にややこしいので付けた方が安全だと私は考えています。

postrotate を指定するとlogrotateがファイルをmvした後に実行する処理をスクリプトとして書くことができます。sharedscripts を指定すると複数のログファイルが指定されたときに1度だけ postrotate を実行できるようになります。アプリケーション側でpidファイルを出力するようにしておいて、そのpidに対してシグナルを送ります。アプリケーション側はそのシグナルを受け取ったタイミングで元のファイル名でopenし直します。これでちゃんとログファイルをローテートすることができます。

これをGoで実装する方法は以前紹介したので参考にしてください。

ちなみにlogrotateは logrotate -f <logrotateのconfig> を実行することで処理を実行できます。cronに登録すれば好きなタイミングで実行することが可能です。

fluentdがログのローテートをどうやって検知するか

今回のエントリー的にはおまけな感じですが紹介します。

またしても、Linuxシステムプログラミング 10pから引用します。

ファイルへアクセスする際にはファイル名(filename)を使用するのが常ですが、実はファイル名は直接にはファイルに対応していません。ファイルに実際に対応し、参照の際に使用されるのはinode(アイ-ノード、iノード。もともとはinformation nodeまたはindexed node)であり、一意な数字が割り振られています。この数字のことをinode番号(inode number)と呼びます。i-numberやinoと省略されることもよくあります。inodeには、更新時刻、オーナ、種類、サイズなどの、ファイルに対応するメタデータが保存されています。ファイルの内容(ファイルデータ)のディスク上の保存位置もinodeが保持します。しかしファイル名は持っていません。inodeはUnixのファイルシステムのディスク上に存在する物理的なオブジェクトであると同時に、Linuxカーネル内のデータ構造を表す論理的なエンティティでもあります。

つまりファイルをopenしてinode numberを比較することで同一のファイルか判定することができます。Go言語の os.SameFile というそのものずばりな関数があるので確認してみるとよいです。実際にはinode numberが一意になるのは同じファイルシステム上だけです。そこでGoの実装ではデバイスが同じかどうかも確認しています。

つまり手順としては以下のようになります。

  1. ファイルをOpenしてファイルディスクリプタを得る
  2. ファイルディスクリプタからinode numberを取得して、既にOpen済みのファイルのinode numberが同じか確認する
  3. 同じならローテートされてないし、異なるならローテートされているはず

fluentdはログの取りこぼしがないように、ログローテートを検知してからデフォルトでは5秒間、古いログファイルを見続けます。その後に新しいファイルをopenして新しいファイルの監視を始めます。この値を変更したい場合は rotate_wait で変更できます。詳しくはfluentdのドキュメントを確認してください。

最後に

参考になれば幸いです。何か間違い等があれば指摘をお願いします。

追記

将来の夢は隠居です

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store