logrotateの設定とファイルのアクセスモードについて
大量にアクセスが来るサーバーのlogrotateは意外と考えることが多いです。大量にアクセスが来るなら当然それだけログファイルが簡単に肥大化します。そこで一定間隔でログファイルを別のファイル名に変更して、書き込み先を新しいファイルにする必要があります。
これについてどういう設定をすればいいのか、昔自分が調べたり、考えたりしたことを少し書いてみます。最初に書いておくと私はこの辺りについて詳しい自信はありません。もしエントリー内に誤った内容があればぜひ教えてください。
ちなみにこのエントリーの内容は以下の本を読めば分かるので正確な知識を身につけたい人は以下の本を読んでください。
本書には、Linuxの概要、カーネル、Cライブラリ、Cコンパイラなどプログラミングの基礎知識から、ファイルI/O、バッファサイズ管理、メモリマッピング、最適化技術、システムコール、メモリ管理まで、プログラマの観点から実践的なトピックが多く盛…www.oreilly.co.jp
ちなみに以下の資料を読んで書こうと思った次第です。
忙しい人のために先にまとめ
- ログファイルの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として使用されます。ファイルをオープンするとファイルディスクリプタが返され、以降の処理(読み書きなど)ではファイルディスクリプタが中心的なパラメータになります。
ファイルディスクリプタで重要なのはファイル名は含まれない点です。つまり
- ファイルをopenしてファイルディスクリプタを得る
- openしたファイルのファイル名を変更する
- 1で得たファイルディスクリプタに対して書き込みをする
- 書き込んだ内容は古いファイルに対して行われる
という挙動になります。ログをローテートするのは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フラグを同時に与えられた場合は例外)
以前に私が書いた以下のエントリーでも解説しています。
この記事は ピクシブ株式会社 Advent Calendar 2015 15日目の記事です。 インフラチームの @catatsuy です。…inside.pixiv.net
この3つのアクセスモードを与えることでログファイルに対する書き込みで考えることはほぼなくなります。ただ PIPE_BUF
(Linuxでは4kb)を超えると同時に書き込もうとしたログが混ざるケースがあるかもしれません。PIPE_BUF
以下のサイズならば混ざらないので安心して使用できます。
Ruby の Logger ってスレッドセーフ(&プロセスセーフ)なんだっけ?と疑問に思ったので調べてみました。特にマルチプロセス環境で Logger の shift_age…blog.livedoor.jp
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するまでの間にファイルが作られた場合はエラーになってしまうそうです。非常にややこしいので付けた方が安全だと私は考えています。
sharedscripts
を指定すると postrotate
を指定できます。これでlogrotateがファイルをmvした後に実行する処理をスクリプトとして書くことができます。アプリケーション側でpidファイルを出力するようにしておいて、そのpidに対してシグナルを送ります。アプリケーション側はそのシグナルを受け取ったタイミングで元のファイル名でopenし直します。これでちゃんとログファイルをローテートすることができます。
これをGoで実装する方法は以前紹介したので参考にしてください。
Goで書かれているサーバーのPIDファイルが欲しいとなったとします。start-stop-daemonとか使っているならPIDファイルを作ってくれる機能があるのでそれに乗っかるのがいいと思いますが、そういった機能がないものを使ってデーモン化…qiita.com
Goでシグナルを受け取ってゴニョゴニョしたくなったとします。SIGINTかSIGTERMを受け取ったら終了してSIGUSR1を受け取ったらそのことを通知するようなGoのプログラムは以下の様な感じで書けました。 package main…qiita.com
ちなみに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の実装ではデバイスが同じかどうかも確認しています。
つまり手順としては以下のようになります。
- ファイルをOpenしてファイルディスクリプタを得る
- ファイルディスクリプタからinode numberを取得して、既にOpen済みのファイルのinode numberが同じか確認する
- 同じならローテートされてないし、異なるならローテートされているはず
fluentdはログの取りこぼしがないように、ログローテートを検知してからデフォルトでは5秒間、古いログファイルを見続けます。その後に新しいファイルをopenして新しいファイルの監視を始めます。この値を変更したい場合は rotate_wait
で変更できます。詳しくはfluentdのドキュメントを確認してください。
The in_tail Input plugin allows Fluentd to read events from the tail of text files. Its behavior is similar to the tail…docs.fluentd.org
最後に
参考になれば幸いです。何か間違い等があれば指摘をお願いします。