createとcopytruncate
一言でいうと
createはログを失いませんが、プロセスの協力が必要で、copytruncateは協力は不要ですが、ログを少し失います。どちらもタダではありません。
なぜ必要なのか
ログファイルが大きくなるとディスクがいっぱいになります。ところがプロセスがファイルを開いたまま書き込んでいるため、単純に削除できません。Linuxでrmはディレクトリエントリだけを消し、開いているファイルのブロックは最後のハンドルが閉じられるまで解放されません。そのためdfは満杯だと言い、duはファイルがないと言います。
ローテーションはこの問題を2つの方式で解決します。
create: 名前を変えて新しく作る
app.log → app.log.1 로 rename
새 app.log 생성
프로세스에 SIGHUP 을 보내 다시 열게 한다
renameはinodeをそのままにするため、プロセスは依然として古いファイルに書き込みます。そのため、必ずファイルを開き直させる必要があります。それをしないと、新しいapp.logは永遠に0バイトのままで、実際のログはapp.log.1へ流れ続けます。ログを失いはしませんが、誰も見つけられません。
copytruncate: コピーして切り詰める
app.log → app.log.1 로 복사
app.log 를 0바이트로 truncate
ファイルがそのままなので、プロセスは何も知らなくて済みます。そのかわり、コピーと切り詰めの間に書かれたログは消えます。毎秒数千行たまるログだと、その隙間は大きくなります。
どう動くのか
/var/log/app/*.log {
daily
rotate 14 # 14개까지 보관
size 100M # 또는 100MB 넘으면
compress
delaycompress # 직전 것은 압축하지 않는다(아직 쓰고 있을 수 있다)
missingok
notifempty
create 0640 app app
postrotate
kill -HUP $(cat /run/app.pid) 2>/dev/null || true
endscript
}
delaycompressが重要です。create方式ではプロセスがまだ古いファイルに書き込んでいる可能性があり、それをすぐ圧縮すると書き込みが壊れます。
よくある勘違い
コンテナでlogrotateを動かすこと: コンテナはたいていログをstdoutに出力します。するとコンテナランタイム(containerd)がファイルに書き出し、ランタイムがローテーションします。kubeletのcontainerLogMaxSize(デフォルト10Mi)とcontainerLogMaxFiles(デフォルト5)がその設定です。コンテナの中でlogrotateを動かす理由はありません。
ローテーションがコレクターを壊すこと: Fluent Bitのようなコレクターはinodeを基準にファイルを追跡します。createでrenameされると、コレクターが古いinodeを読み続けて新しいファイルを見逃すことがあります。Rotate_Waitの設定がその隙間を埋めます。
コンテナではローテーションを自分で行わない
ここまでの話は、ファイルに書き込むプロセスのものです。コンテナでは標準出力に書き込み、ローテーションはランタイムに任せます。
// /etc/docker/daemon.json 또는 containerd 설정
{"log-driver": "json-file",
"log-opts": {"max-size": "10m", "max-file": "3"}}
この設定がないと、ログファイルが際限なく大きくなり、ノードのディスクを埋めてしまいます。するとそのノードのすべてのPodが落ちます。KubernetesでノードがDiskPressureによってPodを追い出す事故の、よくある原因です。
kubelet側にも同じ設定があります(containerLogMaxSize、containerLogMaxFiles)。コンテナランタイムの設定とkubeletの設定のどちらが適用されるかはランタイムによって異なるため、ノードで実際のファイルサイズを確認するのが確実です。
du -sh /var/log/pods/* | sort -h | tail -5
ログが消える3つの場所
ローテーションとは別に、ログはいろいろな場所で静かに失われます。
バッファリング: プロセスが落ちるとき、バッファーにあったものは失われます。PythonではPYTHONUNBUFFERED=1、シェルスクリプトではstdbuf -oLで行単位に切り替えます。
コレクターのキュー溢れ: Fluent BitやVectorは、メモリバッファーがいっぱいになると新しいログを捨てます(デフォルトの方針)。ディスクバッファーに切り替えると欠落は減りますが、ノードのディスクを使います。
# Fluent Bit — 넘칠 때 디스크로
[SERVICE]
storage.path /var/log/flb-storage/
storage.max_chunks_up 128
[INPUT]
storage.type filesystem
コンテナの削除: Podが削除されると/var/log/pods以下も消えます。そのため、落ちたPodのログを後から見るには、コレクターがその前に取り込んでいなければなりません。コレクターが30秒ごとに走査していて、その間にPodが削除されると、最後のログが残りません。terminationGracePeriodSecondsを少し延ばすと、その窓が狭まります。
何をログに残すのか
ローテーションの設定をどれだけうまく行っても、量が多ければコストになります。次の3つを守ると大きく減ります。
- 正常系は要約だけにします。リクエストごとに1行あれば十分です。本番でデバッグログを有効にしたままにしておくのが、ログ量が急増する第一の原因です。
- 繰り返されるものは集計します。同じエラーが毎秒1,000回出るなら、1,000行の代わりに「このエラー1,000回」の1行のほうがよくなります。
- 構造化します。JSONで書くと後からフィールドで絞り込めるため、保持ポリシーを細かく分けられます。
実務で本当に大切なこと
保持期間はディスクではなく規定と調査の必要性で決めます。「ディスクに余裕があるから90日」ではなく、「障害調査には通常2週間が必要で、監査要件が1年」なら、ホットストレージ2週間とコールドストレージ1年に分けます。
そして圧縮はほぼ常に得になります。テキストログはgzipで10–20分の1に減ります。CPUを少し使い、ディスクを大きく節約します。