up自体が信号だ — pullとTSDBの設計
一言でいうと
Prometheusがプルを選んだ理由は、性能ではなくオブザーバビリティのためです。サーバーが直接スクレイプしに行けば、「応答がない」という事実そのものがデータになり、それがupメトリクスです。TSDBは、そうして入ってきたサンプルをまずWALに順次記録し、2時間分のHead Blockにためてから、永続ブロックにコンパクションします。
なぜ必要なのか
プッシュモデルでは、データが来ないことと、プロセスが死んだことを区別できません。どちらも「何もない」に見えるからです。プルモデルは、サーバーが試みた記録を残すので、失敗が値になります。
up{job="checkout-api"} == 0
or
absent(up{job="checkout-api"})
2つの条件が別の状況であることが、試験によく出ます。up == 0は、ターゲットは見つかったがスクレイプが失敗した状態で、absent(up{...})は、ターゲットがディスカバリから丸ごと消えて、upの時系列自体がない状態です。後者を落とすと、サービスが削除されたときにアラートが静かに死にます。
もちろん、プルが万能ではありません。Prometheusがスクレイプしに来る前に終わってしまう短期のバッチジョブは、プルでは捉えられないため、Pushgatewayという例外の経路を用意しています。例外は例外として残しておくことが、設計の要点です。
どう動くのか
保存の経路は3層です。
| 階層 | 場所 | 性格 |
|---|---|---|
| WAL | wal/セグメント(既定128MB) |
順次書き込み、クラッシュ復旧用 |
| Head Block | メモリとchunks_head/のmmap |
直近の約2時間、書き込み可能 |
| 永続ブロック | ULIDディレクトリ | meta.json・index・chunks/・tombstones |
新しいサンプルはまずWALに書かれ、次にHead BlockのmemSeriesに付きます。アクティブなチャンクが120サンプルまたは2時間に達すると封印されて、chunks_head/にmmapファイルとして下ろされ、OSのページキャッシュがメモリ管理を代わりに行います。コンパクションはレベルベースなので、2時間のブロック複数個が6時間に、さらに18時間にまとめられます。削除はすぐには起こらず、tombstoneで印を付けておいて、コンパクションのときに実際に取り除かれます。
圧縮はGorillaの論文から来ています。タイムスタンプはdelta-of-deltaで保存するので、スクレイプが規則的ならほとんどが1ビットで済み、値は前の値とXORして、前後の0ビットを切り落とします。その結果が、サンプルあたり平均1.37バイトです。この数字を覚えておくと、容量の計算が楽になります。
検索は転置インデックスです。ラベルの名前と値のペアごとに、時系列IDのリスト(posting list)が整列した状態で保存されているので、job="prometheus"とinstance="localhost:9090"の共通部分を線形時間で求められます。ラベルセレクターが速い理由であり、カーディナリティが増えるとこのインデックスから重くなる理由でもあります。
設定で試験が狙うポイントは3つです。
- 設定ファイルの変更は、自動では検知されません。SIGHUPを送るか、
--web.enable-lifecycleを有効にしてから/-/reloadにPOSTする必要があります。 scrape_timeoutは、scrape_intervalより大きくしてはいけません。既定値は10秒です。- リテンションは、設定ファイルではなく、コマンドラインのフラグ(
--storage.tsdb.retention.time、既定15日)です。サイズベースと一緒に設定すると、先に達したほうが優先されます。
そしてstaleness。ターゲットが消えると、該当の時系列にstale marker(特殊なNaN)が付き、クエリはlookback delta(既定5分)の中で最新のサンプルを探して、stale markerに出会うと、その時系列を結果から外します。「5分が過ぎると消える」ではなく、「マーカーに出会えば即座に外れる」が、正確な記述です。
現場での姿
レコーディングルール2,000個を一度にデプロイしたことがあります。ルール1つがルート数だけ時系列を作るので、結果は約100万個の新しい時系列になり、すべてがHead Blockに入ってWALが急増しました。ルールファイルはコードのように見えますが、コストの面では新しいメトリクスを追加するのと同じです。デプロイの前に、「ルート120個×ウィンドウ5個×ルール50個」を掛け算してみる習慣が必要です。
収集の時点で防ぐ方法もあります。sample_limitを設定すると、爆発したターゲット1つがPrometheus全体を壊すのを防げます。上限に達すると、そのターゲットのスクレイプが丸ごと失敗するので、値は余裕を持たせて、必ず設定しておきます。上限がなければ、1つのサービスのミスが、モニタリング全体を止めます。
容量の感覚も一緒に押さえておきます。時系列1つあたり約8KBとすると、1,000万の時系列は80GBです。ホームラボの4C/14GBのミニPCのコントロールプレーンでは、この数字がそのまま限界線です。
次のラボですること
/root/pca-scrape/prometheus.ymlを最初から書きます。globalブロック、static_configsのジョブ、kubernetes_sdでPodを探すジョブ、relabelでターゲットを選んでアドレスを組み立て直すルール、metric_relabelで不要なメトリクスを捨てるルール、そしてsample_limitのガードレールまで入れます。最後に、そのファイルをクラスターにConfigMapとして上げ、今書いたkeepルールが実際に生かすPodを1つ作って確認します。