Prometheus 3.x メジャーバージョンアップ記録

はじめに

SREグループの古越です。 私たちSREグループでは、Prometheus を中核としたオブザーバビリティ基盤を数年前から運用してきました。 今回、Prometheusサーバーのメジャーバージョンアップを実施しましたので、既存構成の全体像を踏まえつつ、作業内容や注意点などを紹介します。 Prometheusは、2024年11月頃に7年ぶりとなるメジャーリリースとしてバージョン3.0が公開されました。このアップデートには非互換の変更も含まれており、バージョンアップ作業には注意が必要です。日本語での具体的な事例があまり見つからなかったため、本記事で紹介できればと思います。

バージョンアップのモチベーション

今回のバージョンアップは、定期的なコンポーネント更新の一環として実施しました。

私たちのSREグループでは、年数回の定期的なKubernetesアップデートを行っています。 それに伴い、Kubernetes上で動作するミドルウェアなどの各種コンポーネントも、古くならないように定期的なアップデートを心がけています。 この活動の一環として、Prometheusサーバーもアップデートすることにしました。

背景: オブザーバービリティ基盤の構成とPrometheusの立ち位置

バージョンアップの具体的な内容に入る前に、私たちの環境でのPrometheusの利用方法について説明します。

私たちSREグループが構築・運用しているオブザーバービリティ基盤では、Loki, Grafana, Tempo, Mimirといった、いわゆるLGTMスタックを利用しています。

参考: GOのオブザーバビリティ基盤強化: LGTMスタック移行とTempo導入事例

その中でPrometheusサーバーは、Mimirへメトリクスを送信する前の収集拠点としての役割を担っています。

Prometheusのデータフローに注目した図は以下の通りです。

現在の構成では、PrometheusサーバーをAgentモードで起動し、データのスクレイピング、ラベル処理、Remote Writeといった機能に特化させています。 具体的には、以下のフローでデータが処理され、グラフの可視化やアラートに繋がります。

  1. Exporter/エージェントからのスクレイピング

  2. Remote Write と Mimir による長期保管

    • スクレイピングしたメトリクスは、すべてGrafana Mimirへ書き込み
    • ローカルに保持するTSDBは一時的なキャッシュとしての位置づけとなり、過去データはMimirで集約・保管される
  3. 可視化とダッシュボード

    • GrafanaのデータソースとしてMimirを参照し、リアルタイムデータおよび過去データを含むグラフを表示
    • マイクロサービス、ネームスペース、クラスターごとにダッシュボードを分割
  4. アラート検知と通知

    • Mimir Rules(PromQLベースのアラートルール)で閾値超過や異常を検知
    • Alertmanagerを経由し、SlackやPagerDutyへ通知することで、エンジニアにアラートを配信

Prometheusサーバーダウン時の挙動

Prometheusサーバーがダウンすると、スクレイピングが停止するため、大半のメトリクスの入力が失われます。

ダウン時の主な挙動は以下の通りです。

  • Grafanaのグラフにデータの欠損や表示の乱れが発生
  • Mimir Rulesによるアラート判定が正常に機能しなくなる

そのため、サーバーの再起動やバージョンアップ時には、グラフ表示やアラート出力について注意深く確認する必要があります。

バージョンアップ前の準備

準備段階で調査したこと、実施したことを紹介します。

準備1. 最新リリースの確認

当時、Prometheus v2.53を使用していたため、そこから最新版までの差分と対応すべきことを確認しました。 まず、PrometheusのGitHub Releases情報とArtifact Hubの情報を確認しました。

  • Prometheus GitHub Releases: [CHANGE] と記載のある箇所に注目
  • Artifact Hub: Helm Chartの変更点などをざっと確認

冒頭でも触れましたが、調査を進めるとv3.0前後で大きな変更があると分かります。 v3.0をまたぐバージョンアップには注意が必要そうです。

準備2. Prometheus 3.x マイグレーションガイドの確認

公式ドキュメントにマイグレーションガイドが用意されているため、こちらを熟読しました。 Migrating to Prometheus 3.0 and newer

現在の構成において、特に注意すべきポイントとして以下に当たりをつけました。

  • Agentモードに関するコマンドラインフラグの変更
  • TSDBフォーマットの変更
  • le および quantile ラベルに関する変更

その他の更新点も気になる部分はありましたが、検証環境でエラーメッセージを元に問題を特定できそうだったため、この時点では詳細な確認を見送りました。

準備3. バージョンアップ計画の策定

TSDBのフォーマット変更は影響範囲の予測が難しく、事後対応に手間がかかる可能性があったため、慎重に進めることにしました。

バージョンアップ方針は以下のようにしました。

  • 2段階(v2.53 → v2.55、v2.55 → v3.x)でのアップデートを実施
  • TSDBのバックアップは今回は取得しないが、スクレイピングジョブ、MimirへのRemote Write、グラフやアラートの出力状況に細心の注意を払いながら進める

TSDBの互換性に課題があることが分かったため、公式ドキュメントの案内に従い、安全策として2段階で実施することにしました。 慎重を期す手段として、TSDBのバックアップやスナップショットを取得することも検討しましたが、今回の構成ではAgentモードでRemote WriteによりMimirへデータを送信しているため、Prometheusサーバーローカルに残るデータの重要度は高くありませんでした。そのため、バックアップは取得しない代わりに、その他のデータフローに注意しながら進めることにしました。

バージョンアップ作業: v2.53 → v2.55

私たちの環境ではHelmを使用しているため、バージョンアップ作業自体は説明不要なほど簡単です。しかし、事前に行うべきこと、検証すべきこと、作業後に確認すべき点がいくつかあります。 v2.53からv2.55へのマイナーバージョンアップについてはバージョンアップ後に発生したエラーログの対応を説明します。

当初、このマイナーバージョンアップ作業は特に問題なく完了すると考えていましたが、実際に検証環境でバージョンアップ作業をすると、仕様変更に伴って多数の警告(warn)やエラーログが出力されることが判明しました。

サーバーエラー対応: (warn) Error on ingesting out-of-order samples

アップデート後、以下のようなメッセージが大量に出力されるようになりました。

caller=scrape.go:1817 level=warn component="scrape manager" scrape_pool=kubernetes-nodes-cadvisor target=https://kubernetes.default.svc:443/api/v1/nodes/ip-10-10-10-10.ap-northeast-1.compute.internal/proxy/metrics/cadvisor msg="Error on ingesting out-of-order samples" num_dropped=511

Prometheusサーバーのログレベルは warn でしたが、15秒に1度の頻度で出力されたため、対処することにしました。

調査した結果、以下のIssueと同様の事象であることが分かりました。バージョンv2.54からタイムスタンプの整合性チェックが厳格化されたことが要因のようです。 Scrape manager: Error on ingesting out-of-order samples #14973

タイムスタンプの厳格なチェックについて

Prometheusはデフォルトで、スクレイプ対象が出力するメトリクスに含まれるタイムスタンプをそのまま使用します。 しかし、スクレイプ対象の多くは別のPod、別のマシン、あるいは別のサービスからメトリクスを取得するため、タイムスタンプにごくわずかなズレが生じることがあります。 元々、多少のズレは許容されていましたが、v2.54からはより厳格な整合性チェックが行われるようになり、一部のメトリクスが取り込めなくなる事象が発生したようです。

このような事象への対策のためか、prometheus.yaml の設定でスクレイプジョブごとにタイムスタンプの扱いを指定できるようになっています。 具体的には、scrape_configs 配下で対象ジョブに honor_timestamps: false を指定すると、スクレイプ対象のタイムスタンプを破棄し、スクレイピングした時刻をメトリクスのタイムスタンプとして記録するよう設定できます。

1つのスクレイプジョブの設定を変更して確認したところ、他のスクレイピングジョブでも同様のエラーが出力されることが分かりました。 CloudWatch (yet-another-cloudwatch-exporter) やGCP Monitoring (stackdriver_exporter) のExporterから取得するメトリクスについても、同様にwarnログが出力されることを確認しました。

このエラーログの対策として、以下のような設定を追加しました。

    prometheus.yml:
      rule_files: []
      scrape_configs:
...

      - job_name: 'kubernetes-nodes-cadvisor'
+       # Prioritize scrape timestamp to prevent out-of-order sample errors caused by cAdvisor's timestamp discrepancies.
+       honor_timestamps: false

...

      # yace
      - job_name: 'yace'
+       # Prioritize scrape timestamp to prevent out-of-order sample errors caused by CloudWatch's timestamp discrepancies.
+       honor_timestamps: false

...

      # prometheus-stackdriver-exporter
      - job_name: 'prometheus-stackdriver-exporter'
+       # Prioritize scrape timestamp to prevent out-of-order sample errors caused by stackdriver's timestamp discrepancies.
+       honor_timestamps: false

この対策を実施した結果、同様のwarnログは出力されなくなりました。

サーバーエラー対応: Remote Write 2.0, cortex-tenant 関連エラー

引き続き、多数出力されているログをチェックしたところ、以下のようなエラーログを発見しました。

caller=dedupe.go:112 component=remote level=error remote_name=cortex_tenant url=http://cortex-tenant:8080/push msg="non-recoverable error" failedSampleCount=2000 failedHistogramCount=0 failedExemplarCount=0 err="server returned HTTP status 400 Bad Request: failed pushing to ingester: user=drivers: the sample has been rejected because another sample with a more recent timestamp has already been ingested and out-of-order samples are not allowed (err-mimir-sample-out-of-order)."

これは、Remote WriteのターゲットからHTTPステータスコード400 (Bad Request) が返されたことを示すログです。 今回の構成では、PrometheusのRemote Writeターゲットはcortex-tenant (Proxy) を経由したMimirでした。

cortex-tenantについて

冒頭の構成図では省略していましたが、PrometheusからMimirへの書き込み処理は、cortex-tenant というProxyを経由しています。

cortex-tenantはCortexまたはMimirの前段に配置するProxyです。CortexはGrafana Mimirのfork元となっているOSSで、Prometheus系エコシステムの一つです。cortex-tenantの名前的にCortexが必要に思えますが、Mimir用のProxyとしても利用できるものです。 私たちの環境ではMimirをマルチテナント構成で運用するために導入しているもので、k8sネームスペース単位で異なるMimirテナントに送るようにしています。

cortex-tenant 400 エラーの直接原因

エラーの直接的な原因は、cortex-tenantの接続数上限設定が低いことでした。

まず、cortex-tenantの設定項目には max_conns_per_host という接続数設定があり、デフォルトで 64 に設定されています。 エラー発生時にcortex-tenant側のログを確認したところ、no free connections available to host というエラーが出力されていました。

バージョンアップ後、何らかの理由でPrometheusからcortex-tenantへの接続数が増加し、エラーに至ったようです。 試しにcortex-tenantの max_conns_per_host をデフォルト値の倍である 128 に調整したところ、エラーが解消されることを確認しました。 cortex-tenantの接続数を増やすとメモリ使用量も増加するため、状況に応じてメモリリソースの調整も必要になります。

接続数エラーの根本原因

では、なぜバージョンアップによって接続数が増加したのでしょうか。その原因は、v2.54.0で実験的にリリースされた機能にありました。 v2.54.0からは、Remote Write 2.0プロトコルがデフォルトで使用されるようになっています。

Remote Write 2.0で接続数が増加すると断定できる情報は見つけられませんでしたが、新仕様では複数の時系列データポイントを一括送信するバッチ処理がサポートされており、このバッチ処理が関係している可能性が考えられます。

推測の域を出ませんが、一部の処理がバッチ処理に変更されたことで、cortex-tenantへの接続数が瞬間的に増加するようになったのかもしれません。本題はcortex-tenantの接続数上限を増やして回避出来たため、それ以上の調査は切り上げる事にしました。

バージョンアップ作業: v2.55 → v3.4

マイナーバージョンアップを終え、いよいよメジャーバージョンアップに取り組みます。 v3.0は変更点が多く注意が必要ですが、その後のv3.1からv3.4までのマイナーバージョンは、特に大きな変更点は見受けられませんでした。

大まかな進め方は以下の流れで実施しました。

進め方

  1. マイグレーションガイドで判明している点について事前に対策を行う。
  2. 検証環境でメジャーバージョンアップを実施する。
  3. 検証環境で発生したサーバーエラーを確認し、対処する。
  4. 本番環境へ反映する。
  5. 事後確認を行う。

事前準備: Agentモード切り替え

マイグレーションガイドに記載の通り、これまでfeature flag (--enable-feature=agent) で有効化していたAgentモードが、v3.0から正式なコマンドライン引数 (--agent)としてサポートされたため、設定の修正が必要です。 前後を省略しますが、Helm chartsで以下のように変更しました。

  server:
    enabled: true
    defaultFlagsOverride:
      - --config.file=/etc/config/prometheus.yml
-     - --enable-feature=agent
+     - --agent

事前準備: カスタムメトリクス用 PromQL修正

調査した結果、マイグレーションガイドの「Miscellaneous / le and quantile label values`」に記載されている内容に基づき、PromQLの修正が必要であることが分かりました。

簡単に説明すると、以下の通りです。

  • Prometheus v3では、classic histogramの le ラベル値(およびsummaryの quantile ラベル値)が浮動小数点数表記に正規化されるようになった(例: "50""50.0"
  • PromQLで le ラベルの値を文字列で完全一致比較している箇所は、新しい浮動小数点数表記 (float-like) に合わせる必要あり

私たちの環境では、一部マイクロサービス向けのSLIを作成するために以下のようなクエリを使用しており、対策が必要でした。

例:

sum(rate(istio_request_duration_milliseconds_bucket{le="50", app="foo", reporter="destination", response_code!="101"}[5m]))
  • PrometheusがIstioから収集したメトリクスを利用しMimirで上記のカスタムルールを定義
  • カスタムメトリクスを別途ダッシュボードを作成して観測

という形で作っています

これはPrometheusに対するクエリではないためRemote Write先のMimirでこの変更がどのように影響するか、ドキュメントを読む限りでは確実な判断ができませんでした。 検証環境でアップデートしたところ、実際にカスタムメトリクスの表示が途切れてしまったため、対策を行うことにしました。

具体的には、以下のような修正を加えました。

- sum(rate(istio_request_duration_milliseconds_bucket{le="50", app="foo", reporter="destination", response_code!="101"}[5m]))
+ sum(rate(istio_request_duration_milliseconds_bucket{le="50.0", app="foo", reporter="destination", response_code!="101"}[5m]))

この修正を反映するタイミングについては検討しましたが、メトリクスが一時的に欠損したとしても業務への影響は軽微であると判断し、Prometheusサーバーのアップデート後に反映することにしました。 今回は採用しませんでしたが、業務影響が大きい場合は、v2とv3の両方でクエリが機能するように正規表現を用いる(例: le=~"50(\\.0)?")などの対応も考えられます。

サーバーエラー対応: (WARN) Error on ingesting samples with different value but same timestamp

検証環境でアップデート作業を行うと、サーバーのエラーログがいくつか出力されました。

そのうちの一つは、以下のようなログでした。

level=WARN source=scrape.go:1902 msg="Error on ingesting samples with different value but same timestamp" component="scrape manager" scrape_pool=kube-state-metrics target=http://10.10.10.11:8080/metrics num_dropped=3

何らかのメトリクスが、同一タイムスタンプで値の異なるデータとして記録されようとしているようです。

kube-state-metrics 重複メトリクスの確認

エラーメッセージから、kube-state-metricsが出力するメトリクスが要因であると考えられたため、直接メトリクスを確認してみます。

kubectl port-forward を使用して、kube-state-metricsに直接接続できるようにします。

kubectl port-forward \
  -n your-ns svc/kube-state-metrics \
  8080:8080

別のターミナルで、以下のコマンドを実行して重複メトリクスをチェックします。

curl http://localhost:8080/metrics > metrics.txt
grep -v '^#' metrics.txt \
  | awk '{ $1=$1; $NF=""; print $0 }' \
  | sort | uniq -c \
  | grep -v '^ *1 ' \
  | sort -nr \
  | head -20

その結果、以下のメトリクスが重複していることが確認できました。

    2 kube_horizontalpodautoscaler_status_target_metric{namespace="test",horizontalpodautoscaler="hoge",metric_name="cpu",metric_target_type="utilization"}
    2 kube_horizontalpodautoscaler_status_target_metric{namespace="test",horizontalpodautoscaler="hoge",metric_name="cpu",metric_target_type="average"}
    2 kube_horizontalpodautoscaler_spec_target_metric{namespace="test",horizontalpodautoscaler="hoge",metric_name="cpu",metric_target_type="utilization"}

これらのメトリクスについて詳細を調査したところ、以下のIssueで報告されている事象と類似していることが分かりました。 kube_horizontalpodautoscaler_spec_target_metric and kube_horizontalpodautoscaler_status_target_metric can have duplicate series for same metric_name and target_type #2403

Issueを確認すると修正プルリクエストは提出されていますが、本ブログ執筆時点ではまだリリースされていない状況でした。 ただし、このログはwarningレベルであり、大きな問題にはつながらないと判断したため、修正版のリリースを待つこととし、今回はこのログを許容することにしました。

サーバーエラー対応: otelCollector関連メトリクスのRemoteWriteエラー

次に対応したログです。こちらは先ほどと異なりERRORレベルのログです。

level=ERROR source=queue_manager.go:1670 msg="non-recoverable error" component=remote remote_name=cortex_tenant url=http://cortex-tenant:8080/push failedSampleCount=2000 failedHistogramCount=0 failedExemplarCount=0 err="server returned HTTP status 400 Bad Request: received a series with an invalid label: 'service.instance.id' series: 'otelcol_exporter_send_failed_metric_points{app_kubernetes_io_component=\"standalone-collector\", app_kubernetes_io_managed_by=\"Helm\", app_kubernetes_io_name=\"ope' (err-mimir-label-invalid)\n"

これは、オブザーバービリティ基盤で実験的に導入しているOpenTelemetry Collectorに関連するエラーでした。 メトリクスもOpenTelemetry Collector自身を監視するためのメトリクスでして、取りこぼしてもマイクロサービス基盤の監視には影響無いエラーです。 エラーを見る限り service.instance.id のようにドット(.)を含むラベル名が使用されていることが問題のようです。

Prometheusサーバーの挙動には影響なく緊急性は低いのですが、エラーログが1分に1度の頻度で発生し、対象のメトリクスも取りこぼしているため、放置せず対策することにしました。

原因: ドット入りラベルの挙動変化について

同様の事象や直接的な言及がある記事は見つけられませんでしたが、調査の結果、複数の要因が絡み合って発生しているようでした。

調べた限りの結論

  1. Remote Write 2.0 の採用: Prometheus 3.x系では、リモート書き込みプロトコルが「Remote Write 2.0」に移行した。これにより、送信時にラベル名の自動サニタイズ(ドット . からアンダースコア _ への変換など)を行わず、取得したままのUTF-8文字列をリモートへ送信するようになった
    詳細: UTF-8 in Prometheus

  2. Mimir(Cortex)における厳格なラベル検証: Grafana Mimirの受信側では、元々 Prometheusのラベル名規約正規表現: [a-zA-Z_:][a-zA-Z0-9_:]*)に合致しないラベルをエラーとして拒否する。上記1の変更により、ドットを含むラベルがそのまま送信されるようになった結果、この規約に違反し err-mimir-label-invalid エラーが返されるようになる

という挙動かと思われます。

OpenTelemetry Collector バージョンアップ

ドットを含むラベルの根本的な原因解決のため調査を進めたところ、要因の一つはOpenTelemetry Collectorのバージョンが古いことだったようです。 opentelemetry-collectorのリリースノートを確認すると、Prometheus 3.0に関連するアップデートがいくつか含まれている事がわかりました。

OpenTelemetry Collectorもバージョンアップを行いましたが、問題のラベル名が service.instance.id から otel.signal に変わっただけで、同様のエラーログが出力され続ける状況は変わりませんでした。

バージョンアップ後もエラーが解消されなかったため、さらに調査を進めたところ、関連性の高いIssueを見つけました。 OTel Operator: Metrics with dots in names are not scraped by Prometheus #3898

しかし、本ブログ執筆時点では、このIssueに対する明確な解決策は見つかっていません。仕方なく別の方法で対処することにしました。

Remote Write前のrelabel実施

Prometheusには、Remote Writeを行う直前にラベルを処理する機能 (write_relabel_configs) があります。これを利用して、Prometheus v2以前で行われていたサニタイズ処理(ドットをアンダースコアに置換する処理)に近い動作を再現します。

    remoteWrite:
      - name: cortex_tenant
        url: http://cortex-tenant:8080/push
+       write_relabel_configs:
+         # 1st pass: replace first dot with underscore
+         - action: labelmap
+           regex: '(.*)\.(.*)'
+           replacement: '$1_$2'
+         # 2nd pass: handle labels that still contain a dot (if there were multiple dots)
+         - action: labelmap
+           regex: '(.*)\.(.*)'
+           replacement: '$1_$2'
+         # If you expect more than 2 dots, add additional passes as needed.
+         # Last pass: drop labels that still contain dots (as a fallback)
+         - action: labeldrop
+           regex: '.*\..*'

最適な記述方法ではないかもしれませんが、ラベル名にドットが複数含まれる場合の置換を単一の正規表現でシンプルに記述することが難しかったため、以下のような設定にしました。

  • ラベル名に含まれるドットは最大2個と想定し、labelmap処理を2回繰り返す
  • 3個以上のドットが含まれるケースが確認された場合は、labelmapの処理回数を増やす(任意)
  • 最後に、まだドットが残っているラベル(想定外の形式)をlabeldropで削除する

この設定を適用したところ、エラーはすべて解消されました。

prod環境 反映前のチェック

エラーログへの対応が完了した後は、本番環境へ反映する準備をします。 最終確認として、以下の観点で確認しました。

  • PrometheusサーバーのCPU使用率、メモリ使用量などの負荷状況に変化がないかを確認
  • Mimirサーバーの負荷に変化がないか確認
  • 作成済みのダッシュボードやアラートを一通り確認し、メトリクスの欠損が発生していないことを確認

prod環境 反映後

上記を一通り確認して特に懸念事項がなくなったため本番環境へ反映しました。 反映後に大きな問題はありませんでしたが、グラフの乱れが発生する箇所がありました。

影響: 一時的なグラフ崩れ

検証環境では気づかなかったのですが、本番環境へv3.4を反映した直後、一部のグラフが一時的に乱れる現象が発生しました。

これはレスポンスタイムを表示するグラフで、通常は 20~300ms 程度で推移するものです。バージョンアップ直後にレスポンスタイムが1時間近い異常な値に表示されてしまいました。

このグラフは、以下のようなPromQLで作成されています。

histogram_quantile(
  0.50,
  sum by (le) (
    rate(
      istio_request_duration_milliseconds_bucket{app=~"foo", reporter="destination", response_code!="101"}[$__rate_interval]
    )
  ) > 0
)

この現象の正確な原因までは深掘りしていませんが、前述の「Miscellaneous / le and quantile label values`」の変更と関連していそうなPromQLを書いています。

histogram_quantile関数など、ヒストグラムやクォンタイルに関連する関数を使用しているダッシュボードやアラートでは、表示の乱れや誤検知に注意が必要かもしれません。

これ以外のグラフやアラートに特に大きな変化は観測されなかったため、影響はごく軽微ものだったかと思います。

感想

以上がPrometheus 3.0のアップデート記録となります。
Prometheusは、Exporterなどサブシステムが多いため、利用している機能次第でバージョンアップの難易度が変動します。 今回はマイナーバージョンアップ段階でも分かりにくいエラーに遭遇し、対応に時間を要しました。 次のメジャーバージョンアップは暫く先になると思いますが、今回の知見を活かしていきたいと思います。

まとめ

Prometheus 2.xから3.xへのメジャーバージョンアップで実施した内容を紹介しました。

  • TSDB関連で非互換の変更があるため、v2.54未満のバージョンを使用している場合は、2段階でのバージョンアップが推奨される
  • Prometheusサーバーの構成や利用状況によって、アップデート後に多数のエラーログが出力される可能性あり、ログ種類に応じた個別の対応が必要
  • Prometheusの運用方法によってはTSDBのバックアップを検討すると良い
    • AgentモードでRemote Write専用として運用している場合は、重要度が低い可能性あり
  • バージョンアップ後、グラフ表示やアラートが一時的に不正な状態になるケースもある 作業タイミングには注意が必要

以上となります。
この記事が何かの参考になれば幸いです。