何も考えずに OtelCollectorでSpanMetricsを使ったら、Prometheusのカーディナリティが爆発した話
はじめに
こんにちは。gumamon33です。
OpenTelemetry Advent Calendar 2024 6日目に空きがありましたので、表題の内容でブログを書かせていただきます!
この記事では、OpenTelemetry CollectorのSpan Metrics Connector を使用していて発生した問題と、その解決方法についてお話します。なお、この記事は先日登壇させていただいた OpenTelemetry Meetup 2024-11 の後日談的な内容になります。興味のある方は、ぜひ下記のスライドもご覧ください!
本題
SpanMetricsとは
SpanMetricsはOpenTelemetry Collector(OTelCollector) の Connector の一つです。主に以下の機能を提供します。
- Trace データを元に統計情報を収集し、Metrics として出力する
導入の目的
私が SpanMetrics を導入した主な理由は、「Span を発行するマイクロサービスの Web API の統計情報を取得したい」という目的でした。具体的には以下を可視化したかったのです:
- APIの利用数
- APIのレイテンシー分布
- APIのエラー率
これらの情報は、顧客が実際に体験したサービスの状況を捉える上で非常に有用です。SLI/SLO 設計やリリース後の問題検知、さらに新機能の利用状況分析など、幅広く活用できます。
dev 環境へのデプロイが完了し、しばらくは問題なく稼働していたのですが・・・・・・プロダクトチームが(プロダクトの)負荷検証を始めたタイミングで次の問題が発生しました。
問題発生
メトリクス基盤のストレージがすごいスピードで枯れ始める
メトリクス基盤(VictoriaMetrics)のストレージが急激に消費され、容量逼迫を示すアラートがあがりました。計算すると時速数十GBペース・・?さらに、メトリクス基盤の応答速度も著しく低下しました。
初めは原因がわかりませんでした。Prometheus は Pull 型のアーキテクチャで、アクセス数やスクレイプ周期とストレージ使用量は直結関連性が無いはずだからです。しかし、以下の状況から問題は SpanMetrics に起因していると考えました:
- 負荷検証のタイミングでのみ発生
- SpanMetrics を導入したクラスターでのみ発生
SpanMetrics を無効化したところ、ストレージ使用量の急増が収まりました。原因は何だったのでしょう?
原因特定
OtelCollector の設定を確認
以下は、問題発生時のOtelCollectorの設定の一部です:
exporters:
prometheusremotewrite:
endpoint: http://example.prometheus.svc/api/v1/write
tls:
insecure: true
connectors:
spanmetrics:
dimensions:
- name: http.route
- name: http.status_code
- name: http.method
- name: otelcol.pod.name
- name: service.version
histogram:
explicit:
buckets:
- 10ms
- 100ms
- 300ms
- 500ms
- 800ms
- 1s
- 3s
- 5s
- 13s
namespace: spanmetrics
service:
pipelines:
traces:
receivers:
- otlp
exporters:
- spanmetrics
metrics:
receivers:
- spanmetrics
exporters:
- prometheusremotewrite
一見、気になるところは特になく・・・SpanMetricsのExamplesとも大差ないように見えます。
しかしふと疑問に思ったのが「prometheusremotewrite
で出力されるメトリクスのラベルは何になっているんだ・・・?」という点です。
spanmetrics_calls_total
を確認すると、大量のタイムシリーズが生成されていました。
犯人1: service_name="ingress-nginx"
問題のメトリクスspanmetrics_calls_total
を確認したところ、まず目についたのは以下でした。
( が延々続く)
- service_name="ingress-nginx",span_name="HTTP GET example.com/api/resource?no=123"
上記のように、ingress-nginxがproxyした通信xクエリパラーメータが全て異なるタイムシリーズとして生成され、検索結果を埋め尽くしていました。
TSDBはタイムシリーズごとにデータを圧縮する仕組みになっているため、この状態だと全く圧縮がきかず、ストレージが大量に消費されることになります。また、クエリの検索効率が落ちたり、メモリ使用量が増加する問題も合わせて発生する原因にもなります。犯人1はこちらでした。
犯人2: 不要な span_kind ラベル
service_name="ingress-nginx"
以外にも大量のタイムシリーズを作っているやつが。
span_kind="SPAN_KIND_INTERNAL,SPAN_KIND_CLIENT" です(画像なし!すみません・・)。
span_kindはOTLPスパンの内部表現(Span Context)として、ネイティブに付与されるラベルです(SpanMetricsもこの値をそのままMetricsのラベルとして流用しています)。
- SPAN_KIND_INTERNAL:
- スパンが、境界で起こる操作ではなく、アプリケーション内の内部操作を表していることを示す
- exp. 関数を呼び出す都度等、多々生成
- SPAN_KIND_CLIENT:
- スパンがリモートサービスへのリクエストを記述していることを示す
- exp. DBへのクエリの都度等、多々生成
上記は、Traceの詳細情報としては有用ですが「WEB APIの利用状況の可視化」という目的においては不要です。「WEB API」の入力を表すSpanのSPAN_KINDが正しく設定されているかはTraceの実装に依存することになりますが、私の環境(主にopentelemetry-java-instrumentation)においては特に支障がなかったので、SPAN_KIND_INTERNAL,SPAN_KIND_CLIENTはメトリクスとして集計するのをやめてしまおう。と判断しました。
解決
以下のようにフィルタリングを行い、不要なメトリクスを排除しました!
processors:
filter/metrics:
metrics:
datapoint:
- attributes["service.name"] == "ingress-nginx"
- attributes["span.kind"] == "SPAN_KIND_INTERNAL"
- attributes["span.kind"] == "SPAN_KIND_CLIENT"
service:
pipelines:
metrics:
receivers:
- spanmetrics
processors:
- filter/metrics
exporters:
- prometheusremotewrite
まとめ
SpanMetrics を用いたメトリクス収集では、目的に応じたデータの取捨選択が重要(かもしれない)です。
オブザーバビリティの文脈においては「とりあえず全部取っておけ」的なことを言われがちですが、個人的には目的に合ったデータの取捨選択をするエコな実装が好みです。
また、これを実現できるコンポーネントとして OtelCollector は素晴らしく、引き続き色々と試して実装をしていきたいなと思っています!
本記事が皆さまの検証や課題解決の参考になれば幸いです。
以上です。皆さま、良いクリスマスを!!
Discussion