TT Lab
はじめる
学ぶ 学習パス コース

ログパイプラインの設計

バイト位置ひとつでは足りない

TT Labで続きを見る

一言でいうと

ファイルを追って読むコレクターは、どこまで読んだかをディスクに書き留めておく必要があり、その記憶はバイト位置1つでは足りません。同じ名前の別のファイルになったか(ローテーション)、読んだ位置より小さくなったか(切り詰め)も合わせて把握する必要があります。

なぜ必要なのか

夜間デプロイのあと、ダッシュボードのログがぷつりと途絶えました。コレクターのPodは生きていて、エラーログもありませんでした。再起動すると、再び流れ始めました。

原因はログローテーションでした。深夜0時にlogrotateがapp.logをapp.log.1に移し、新しいapp.logを作りました。コレクターは「3億2000万バイトまで読んだ」という記憶だけを持っており、新しいファイルはわずか数キロバイトでした。その位置から読もうとしても読むものがなく、新しいファイルがその位置を超えるまでの数時間、何も収集されませんでした。

どう動くのか

ファイルを追って読む処理は、3つの状態変化に対応する必要があります。

状況 ファイルに見えるもの すべきこと
追記 サイズだけが大きくなる。inodeはそのまま 続きから読みます
ローテーション(rename) 同じ名前に別のinode 新しいファイルを最初から読みます
切り詰め(truncate) inodeはそのままでサイズが小さくなる そのファイルを最初から読みます

そのため、位置の記録には少なくともinodeとoffsetが一緒に入ります。Fluent Bitはこの状態をSQLiteデータベース(DB設定)に保存し、Fluentdはpos_fileに、Promtailはpositions.yamlに保存します。名前は違っても、やっていることは同じです。

ここでもう1つ残る問題があります。ローテーションされたファイルに、まだ読んでいない末尾が残っている可能性があります。コレクターが一瞬止まっている間にローテーションが起きると、古いファイルの最後の数行を読めません。本物のコレクターは、開いているファイルディスクリプターをすぐには手放さず、しばらく保持してこの末尾を読みます(Rotate_Waitのような設定がそれです)。コンテナ環境ではローテーションの周期が短くファイルも小さいため、この時間の隙間がより危険です。

位置ファイルがないときの方針も決めておく必要があります。最初から読むと、すでに送った行をもう一度送ってしまい重複が生じ、末尾から読むと、それまでにたまった行をまるごと失います。どちらもタダではありません。そのため、位置ファイルはPodが再起動しても残る場所(ボリューム)に置く必要があり、それができない場合は、重複を受け入れて最初から読むほうがたいてい安全です。重複はあとから畳めますが、失った行は元に戻せません。

現場での姿

最もよくある事故が、上の「ローテーションのあとの静かな停止」です。症状が静かなので、ダッシュボードを見ている人がいなければ何日も続きます。コレクター自身のメトリクス(読み取ったバイト数、開いているファイル数)を計測し、0になったらアラートを出すことが唯一の防御です。

2つ目は、位置ファイルをemptyDirに置くことです。Podが再起動すると記憶が消え、方針によって重複が大量に発生するか、欠落が生じます。

3つ目はcopytruncate方式です。元のファイルをコピーしたあと元のファイルを0に切り詰めてしまうため、inodeはそのままでサイズが小さくなります。inodeだけを見るコレクターはこの変化を見逃し、ファイルが再び大きくなって古い位置を超えるまで何も読めません。

次のラボですること

決定論的に番号が振られたログを書く「アプリケーション」を動かしておき、続きから読むコレクターを自分で作ります。追記・ローテーション・切り詰めを順に起こし、そのたびに欠落と重複が0であることを番号で確認し、位置ファイルがないときに何が起きるかをコピーで実験して数字として残します。最後には、ローテーションと切り詰めが混ざった状況を一度で通過させます。