Skip to content
Published on

Grafana Tempo 分散トレーシングと TraceQL 運用ガイド 2026

Authors
  • Name
    Twitter
Grafana Tempo 分散トレーシングと TraceQL 運用ガイド 2026

概要

マイクロサービスアーキテクチャが一般化し、単一のリクエストが数十のサービスを経由して処理される環境が日常になった。この環境で障害の根本原因を追跡するには、分散トレーシングが不可欠である。Grafana Tempo は Grafana Labs が 2020 年に公開したオープンソースの分散トレーシングバックエンドで、オブジェクトストレージのみで運用できるため、インフラの複雑さとコストを大幅に削減できる。

Tempo のコア哲学はシンプルだ。トレースデータに対する個別のインデックスを作成せず、Trace ID ベースのルックアップと TraceQL クエリエンジンを通じてスパンを検索する。このアプローチにより、Jaeger や Zipkin と比較してストレージコストが大幅に削減され、ペタバイト規模のトレースも安定的に保管できる。

本記事では、Tempo の内部アーキテクチャ、3つのデプロイモード、TraceQL クエリ構文、スパンメトリクス生成とサービスグラフ、OpenTelemetry Collector 連携、ストレージ最適化、Grafana ダッシュボード構成、トラブルシューティング、そして実運用で経験した障害事例と復旧経験までを取り上げる。

Tempo アーキテクチャ

Tempo は内部的に複数のコンポーネントが協力してトレースデータを収集・保存・照会する。各コンポーネントの役割を理解すれば、障害発生時にボトルネックを迅速に特定できる。

コアコンポーネント

Distributor はクライアントからスパンデータを受信するエントリーポイントである。Jaeger、Zipkin、OpenTelemetry(OTLP)など様々なプロトコルをサポートし、受信したスパンを Trace ID ハッシュベースのコンシステントハッシュリングを使用して適切な Ingester にルーティングする。

Ingester は受信したスパンデータをインデックスし、一定時間が経過するとオブジェクトストレージにブロック単位でフラッシュする。WAL(Write-Ahead Log)を維持し、プロセスの異常終了時にもデータ損失を最小限に抑える。

Query Frontend は Grafana 等のクライアントが Trace ID ルックアップや TraceQL 検索をリクエストする際に呼び出されるコンポーネントだ。リクエストを複数の Querier に分散させ、並列でブロックデータを検索することで応答時間を短縮する。

Querier は Query Frontend から受け取ったリクエストを実際に処理するワーカーだ。Ingester のインメモリデータとオブジェクトストレージのブロックデータの両方を探索して結果を統合する。

Compactor はオブジェクトストレージに保存された小規模ブロックを定期的にマージして大規模ブロックにする。これによりクエリパフォーマンスが向上し、ストレージ使用量が最適化される。

Metrics Generator は受信したスパンデータから RED(Rate、Error、Duration)メトリクスとサービスグラフを自動生成するオプショナルコンポーネントだ。生成されたメトリクスは Prometheus 互換のリモートライトを通じて Mimir や Prometheus に送信される。

データフロー

[Application] --> [OTel Collector] --> [Distributor]
                                           |
                                    [Hash Ring]
                                           |
                                      [Ingester]
                                       /      \
                              [WAL]         [Object Storage]
                                                  |
                              [Compactor] <-------+
                                                  |
                              [Query Frontend] ---+---> [Querier]

スパンはアプリケーションから OTel Collector を経て Distributor に到達し、ハッシュリングを通じて Ingester に分配される。Ingester はまず WAL に書き込み、設定された周期(デフォルト30分)ごとにオブジェクトストレージにブロックをフラッシュする。Compactor が小規模ブロックをマージし、Querier は Ingester のインメモリとオブジェクトストレージの両方からデータを検索する。

デプロイモード

Tempo は3つのデプロイモードを提供しており、組織の規模と要件に応じて選択できる。

デプロイモード比較

項目MonolithicScalable Single BinaryMicroservices
構造単一バイナリ、単一プロセス単一バイナリ、複数インスタンスコンポーネント別独立プロセス
スケーラビリティ垂直スケーリングのみ水平スケーリング可能コンポーネント別独立水平スケーリング
推奨トラフィック日量 100GB 以下日量 100GB ~ 1TB日量 1TB 以上
運用の複雑さ低い中程度高い
高可用性限定的基本サポート完全サポート
適合環境開発/テスト、小規模中規模プロダクション大規模プロダクション、マルチテナント
Kubernetes 必須いいえ推奨必須

Monolithic モード

すべてのコンポーネントが1つのプロセスで実行される。ローカル環境や小規模ワークロードに適しており、最もシンプルな設定だ。

# tempo-config.yaml (Monolithic)
server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: '0.0.0.0:4317'
        http:
          endpoint: '0.0.0.0:4318'
    jaeger:
      protocols:
        thrift_http:
          endpoint: '0.0.0.0:14268'
    zipkin:
      endpoint: '0.0.0.0:9411'

ingester:
  max_block_duration: 5m
  max_block_bytes: 1073741824 # 1GB

storage:
  trace:
    backend: local
    wal:
      path: /var/tempo/wal
    local:
      path: /var/tempo/blocks
    pool:
      max_workers: 100
      queue_depth: 10000

compactor:
  compaction:
    block_retention: 72h

metrics_generator:
  registry:
    external_labels:
      source: tempo
      cluster: local
  storage:
    path: /var/tempo/generator/wal
    remote_write:
      - url: http://prometheus:9090/api/v1/write
        send_exemplars: true
  traces_storage:
    path: /var/tempo/generator/traces
  processor:
    service_graphs:
      dimensions:
        - service.namespace
        - deployment.environment
    span_metrics:
      dimensions:
        - http.method
        - http.status_code
        - http.route

overrides:
  defaults:
    metrics_generator:
      processors:
        - service-graphs
        - span-metrics

Scalable Single Binary モード

同一バイナリを複数インスタンスで実行して水平スケーリングを実現する。Monolithic と Microservices の中間地点で、設定の複雑さを大幅に上げることなくスケーラビリティを確保できる。各インスタンスは target フラグを scalable-single-binary に設定して実行する。

Microservices モード

各コンポーネントを独立したプロセスとしてデプロイし、個別スケーリングが可能だ。大規模環境では特定コンポーネント(例:Ingester)のみスケールアウトしたり、Querier をトラフィックパターンに合わせて調整できる。Kubernetes 環境では Helm チャート(tempo-distributed)を利用するとデプロイが便利だ。

Docker Compose でクイックスタート

ローカル環境で Tempo を素早く体験するには Docker Compose を活用する。以下の構成は Tempo(Monolithic)、OTel Collector、Grafana、Prometheus を一度に起動する例だ。

# docker-compose.yaml
version: '3.9'

services:
  tempo:
    image: grafana/tempo:2.7.1
    command: ['-config.file=/etc/tempo/tempo.yaml']
    volumes:
      - ./tempo.yaml:/etc/tempo/tempo.yaml
      - tempo-data:/var/tempo
    ports:
      - '3200:3200' # Tempo HTTP API
      - '4317:4317' # OTLP gRPC
      - '4318:4318' # OTLP HTTP
      - '9411:9411' # Zipkin
      - '14268:14268' # Jaeger HTTP
    networks:
      - observability

  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.118.0
    command: ['--config=/etc/otel-collector/config.yaml']
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector/config.yaml
    ports:
      - '4327:4317' # OTLP gRPC(アプリからのアクセス用)
      - '4328:4318' # OTLP HTTP
    depends_on:
      - tempo
    networks:
      - observability

  prometheus:
    image: prom/prometheus:v3.2.1
    volumes:
      - ./prometheus.yaml:/etc/prometheus/prometheus.yml
    ports:
      - '9090:9090'
    networks:
      - observability

  grafana:
    image: grafana/grafana:11.5.2
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
    volumes:
      - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
    ports:
      - '3000:3000'
    depends_on:
      - tempo
      - prometheus
    networks:
      - observability

volumes:
  tempo-data:

networks:
  observability:
    driver: bridge

docker compose up -d 実行後、http://localhost:3000 で Grafana にアクセスすると、Tempo データソースが自動的にプロビジョニングされ、すぐにトレースを検索できる。

TraceQL クエリ構文

TraceQL は Tempo 専用のクエリ言語で、PromQL や LogQL と類似した構文体系に従う。波括弧 {} でスパンセット(spanset)を選択し、パイプラインオペレーターでフィルターと集計を連結する。

基本構造

TraceQL クエリは大きく3つの要素で構成される。

  • Intrinsics: スパンの固有属性(namestatusdurationkindrootNamerootServiceNametraceDuration
  • Attributes: カスタムキーバリューペアで、スコーププレフィックス(span.resource.link.event.)を使用
  • オペレーター: 比較(=!=><>=<=)、正規表現(=~!~)、論理(&&||)、構造(>>><<<~

TraceQL クエリ例一覧

// 1. 特定サービスのエラースパンを検索
{ resource.service.name = "payment-service" && status = error }

// 2. 500ms 以上かかった HTTP GET リクエストスパン
{ span.http.method = "GET" && duration > 500ms }

// 3. 特定ルートで 5xx レスポンスを返したスパン
{ span.http.route = "/api/v1/orders" && span.http.status_code >= 500 }

// 4. 2つのサービス間の呼び出し関係を追跡(構造オペレーター)
{ resource.service.name = "api-gateway" } >> { resource.service.name = "order-service" }

// 5. 親子直接関係のスパンフィルター
{ resource.service.name = "frontend" } > { span.http.status_code = 503 }

// 6. 兄弟スパン関係の探索
{ span.db.system = "postgresql" } ~ { span.db.system = "redis" }

// 7. 正規表現を使ったスパン名マッチング
{ name =~ "HTTP.*POST" && resource.deployment.environment = "production" }

// 8. トレース全体の持続時間基準でフィルタリング
{ traceDuration > 3s }

// 9. ルートサービス基準でフィルタリング
{ rootServiceName = "ingress-nginx" && duration > 1s }

// 10. 集計関数を使った分析
{ resource.service.name = "checkout-service" } | rate()

// 11. ヒストグラムでレイテンシー分布を確認
{ resource.service.name = "search-service" } | histogram_over_time(duration)

// 12. カウントベースの異常検知
{ status = error } | count() > 100

主要集計関数

関数説明
rate()秒あたりのスパン発生率{} | rate()
count()マッチしたスパン数{ status = error } | count()
avg(field)フィールド平均値{} | avg(duration)
max(field)フィールド最大値{} | max(duration)
min(field)フィールド最小値{} | min(duration)
p50/p90/p95/p99(field)パーセンタイル{} | p99(duration)
histogram_over_time(field)時間帯別ヒストグラム{} | histogram_over_time(duration)
quantile_over_time(field, q)時間帯別分位数{} | quantile_over_time(duration, 0.95)

スパンメトリクスとサービスグラフ

Tempo の Metrics Generator は、受信したスパンから自動的にメトリクスを生成する強力な機能だ。個別のメトリクス収集なしでも、トレースデータだけで RED メトリクスとサービス依存関係グラフを取得できる。

スパンメトリクス(Span Metrics)ジェネレーター

スパンメトリクスプロセッサは、すべての受信スパンからリクエスト率(Rate)、エラー率(Error)、レイテンシー分布(Duration)を Prometheus メトリクスに変換する。生成される主要メトリクスは以下の通りだ。

  • traces_spanmetrics_calls_total: スパン呼び出し総数
  • traces_spanmetrics_latency_bucket: レイテンシーヒストグラムバケット
  • traces_spanmetrics_size_total: スパンサイズ合計

dimensions 設定で http.methodhttp.status_codehttp.route などのスパン属性をメトリクスラベルとして追加でき、エンドポイント別の RED メトリクスをきめ細かく観察できる。

サービスグラフ(Service Graph)ジェネレーター

サービスグラフプロセッサは、クライアント-サーバースパンペアを分析してサービス間の呼び出し関係を自動マッピングする。Grafana のサービスグラフビューでサービストポロジーを視覚的に確認でき、各エッジにリクエスト率、エラー率、レイテンシーが表示される。

主要な設定パラメータは以下の通りだ。

  • max_items: 追跡する最大サービスペア数(デフォルト 10000)
  • wait: 不完全なエッジの待機時間(デフォルト 10s)
  • dimensions: サービスグラフに追加するカスタムラベル
  • histogram_buckets: レイテンシーヒストグラムバケット境界(デフォルト 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 6.4, 12.8)

Tempo vs Jaeger vs Zipkin 比較

分散トレーシングバックエンドを選択する際、各ツールの特性を比較することが重要だ。

項目Grafana TempoJaegerZipkin
初回リリース2020(Grafana Labs)2015(Uber)2012(Twitter)
CNCF ステータス-Graduated-
ストレージ方式オブジェクトストレージ(インデックスなし)Elasticsearch、Cassandra 等Elasticsearch、Cassandra、MySQL
インデクシングなし(Trace ID + TraceQL)タグベースインデックス生成タグベースインデックス生成
ストレージコスト低い(S3/GCS 単価)高い(インデックスストレージ含む)高い
収集プロトコルOTLP、Jaeger、ZipkinOTLP、JaegerZipkin、OTLP(限定的)
クエリ言語TraceQLタグベース検索タグベース検索
内蔵 UIGrafana 連携Jaeger UIZipkin UI
メトリクス生成内蔵(Metrics Generator)外部ツール必要外部ツール必要
スケーラビリティ優秀(PB 規模)普通限定的
Grafana 統合ネイティブプラグインプラグイン
メンテナンス主体Grafana Labs(商用サポート)CNCF コミュニティボランティアコミュニティ

選択基準まとめ: Grafana エコシステムを既に使用しており、大規模トレースを低コストで保管したい場合は Tempo が最適だ。独立したトレーシングシステムが必要でタグベースの豊富な検索が核心であれば Jaeger を検討すべきだ。小規模チームでトレーシングを迅速に導入したい場合は Zipkin も依然として有効な選択肢だ。

OpenTelemetry Collector 連携

Tempo にトレースを送信する最も推奨される方法は、OpenTelemetry Collector を中間パイプラインとして使用することだ。Collector は様々なソースからトレースを収集し、バッチ処理とリトライを実行した後、Tempo に安定的に送信する。

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: '0.0.0.0:4317'
      http:
        endpoint: '0.0.0.0:4318'

processors:
  batch:
    timeout: 5s
    send_batch_size: 10000
    send_batch_max_size: 11000

  memory_limiter:
    check_interval: 1s
    limit_mib: 4096
    spike_limit_mib: 512

  attributes:
    actions:
      - key: deployment.environment
        value: production
        action: upsert

  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    expected_new_traces_per_sec: 1000
    policies:
      - name: errors-policy
        type: status_code
        status_code:
          status_codes:
            - ERROR
      - name: slow-traces-policy
        type: latency
        latency:
          threshold_ms: 1000
      - name: probabilistic-policy
        type: probabilistic
        probabilistic:
          sampling_percentage: 10

exporters:
  otlp/tempo:
    endpoint: 'tempo:4317'
    tls:
      insecure: true
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 300s
    sending_queue:
      enabled: true
      num_consumers: 10
      queue_size: 5000

  debug:
    verbosity: basic

service:
  telemetry:
    logs:
      level: info
    metrics:
      address: '0.0.0.0:8888'

  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, attributes, batch]
      exporters: [otlp/tempo, debug]

この構成で核心的な部分は以下の通りだ。

  • tail_sampling: エラースパンは 100% 収集し、1秒以上かかった遅いトレースも全量収集し、残りは 10% の確率でサンプリングする。これにより重要なトレースを逃さずストレージコストを削減できる。
  • memory_limiter: Collector のメモリ使用量を 4GB に制限して OOM を防止する。
  • sending_queue: 一時的な Tempo 障害時にもキューにデータをバッファリングしてリトライする。
  • batch: スパンを 10,000 個ずつバッチにまとめて送信し、ネットワーク効率を高める。

ストレージ最適化

Tempo のストレージ設計はオブジェクトストレージ中心だ。本番環境では S3、GCS、Azure Blob Storage のいずれかをバックエンドとして選択する。

ストレージバックエンド比較

項目Amazon S3Google Cloud StorageAzure Blob Storage
設定キーs3gcsazure
認証方式IAM Role、Access KeyService Account、Workload IdentityManaged Identity、SAS Token
コスト(GB/月)$0.023(Standard)$0.020(Standard)$0.018(Hot)
リージョン可用性33+ リージョン40+ リージョン60+ リージョン
Tempo 互換性完全サポート完全サポート完全サポート
ライフサイクルポリシーS3 LifecycleObject LifecycleLifecycle Management

S3 バックエンド設定例

storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces-prod
      endpoint: s3.ap-northeast-2.amazonaws.com
      region: ap-northeast-2
      access_key: ${S3_ACCESS_KEY}
      secret_key: ${S3_SECRET_KEY}
      # または IAM Role 使用時は access_key/secret_key を省略
    wal:
      path: /var/tempo/wal
    block:
      bloom_filter_false_positive: 0.01
      v2_index_downsample_bytes: 1048576
      v2_encoding: zstd
    blocklist_poll: 5m
    pool:
      max_workers: 200
      queue_depth: 20000

compactor:
  compaction:
    block_retention: 336h # 14日間保持
    compacted_block_retention: 1h
    compaction_window: 4h
    max_block_bytes: 107374182400 # 100GB
    max_compaction_objects: 6000000
    retention_concurrency: 10
  ring:
    kvstore:
      store: memberlist

ストレージ最適化のヒント

ブロックエンコーディング: v2_encodingzstd に設定すると、snappy と比較して約 30-40% 高い圧縮率を達成するが、CPU 使用量がやや増加する。書き込みワークロードが多い場合は snappy、ストレージコスト優先なら zstd を選択すべきだ。

Bloom Filter チューニング: bloom_filter_false_positive を下げると(例:0.01 から 0.005)クエリ精度が向上するが、ブルームフィルターのサイズが大きくなる。クエリ頻度が高い環境では、偽陽性率を下げることが全体的なパフォーマンスに有利だ。

ブロック保持期間: block_retention をビジネス要件に合わせて設定する。14日(336h)が一般的だが、コンプライアンス要件がある環境では 90日以上に延長する必要があるかもしれない。この場合、オブジェクトストレージのライフサイクルポリシーで Infrequent Access(S3)や Nearline(GCS)ティアへの自動移行を設定するとコストを削減できる。

Compactor チューニング: max_block_bytes を大きくし過ぎると Compactor のメモリ使用量が急増し、小さくし過ぎるとブロック数が増えてクエリパフォーマンスが低下する。100GB 前後がバランスの取れた値だ。

Grafana ダッシュボード構成

Tempo は Grafana とネイティブに統合されており、個別の UI なしでも豊富なトレーシング可視化を提供する。以下は Grafana データソースプロビジョニング設定とダッシュボード構成の例だ。

データソースプロビジョニング

# grafana-datasources.yaml
apiVersion: 1

datasources:
  - name: Tempo
    type: tempo
    access: proxy
    url: http://tempo:3200
    uid: tempo
    jsonData:
      httpMethod: GET
      tracesToLogsV2:
        datasourceUid: loki
        spanStartTimeShift: '-1h'
        spanEndTimeShift: '1h'
        filterByTraceID: true
        filterBySpanID: true
      tracesToMetrics:
        datasourceUid: prometheus
        spanStartTimeShift: '-1h'
        spanEndTimeShift: '1h'
        tags:
          - key: service.name
            value: service
          - key: http.method
            value: method
      tracesToProfiles:
        datasourceUid: pyroscope
        profileTypeId: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds'
        tags:
          - key: service.name
            value: service_name
      serviceMap:
        datasourceUid: prometheus
      nodeGraph:
        enabled: true
      search:
        hide: false
      traceQuery:
        timeShiftEnabled: true
        spanStartTimeShift: '-30m'
        spanEndTimeShift: '30m'

ダッシュボード JSON スニペット

以下はサービス別リクエスト率とエラー率を表示する Grafana ダッシュボードパネル設定だ。

{
  "panels": [
    {
      "title": "Service Request Rate",
      "type": "timeseries",
      "datasource": { "uid": "prometheus", "type": "prometheus" },
      "targets": [
        {
          "expr": "sum(rate(traces_spanmetrics_calls_total{status_code!=\"STATUS_CODE_ERROR\"}[5m])) by (service)",
          "legendFormat": "{{ service }}"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "reqps",
          "custom": { "drawStyle": "line", "lineWidth": 2 }
        }
      }
    },
    {
      "title": "Service Error Rate",
      "type": "timeseries",
      "datasource": { "uid": "prometheus", "type": "prometheus" },
      "targets": [
        {
          "expr": "sum(rate(traces_spanmetrics_calls_total{status_code=\"STATUS_CODE_ERROR\"}[5m])) by (service) / sum(rate(traces_spanmetrics_calls_total[5m])) by (service) * 100",
          "legendFormat": "{{ service }}"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "percent",
          "thresholds": {
            "steps": [
              { "color": "green", "value": null },
              { "color": "yellow", "value": 1 },
              { "color": "red", "value": 5 }
            ]
          }
        }
      }
    },
    {
      "title": "P99 Latency by Service",
      "type": "timeseries",
      "datasource": { "uid": "prometheus", "type": "prometheus" },
      "targets": [
        {
          "expr": "histogram_quantile(0.99, sum(rate(traces_spanmetrics_latency_bucket[5m])) by (le, service))",
          "legendFormat": "{{ service }}"
        }
      ],
      "fieldConfig": {
        "defaults": { "unit": "s" }
      }
    }
  ]
}

主要連携機能

Grafana で Tempo を使用する際の最も強力な機能は、Traces to LogsTraces to MetricsTraces to Profiles の3つのクロスデータソース連携だ。

  • Traces to Logs: トレースビューで特定のスパンをクリックすると、該当時間帯の Loki ログに直接移動する。Trace ID と Span ID で自動フィルタリングされ、関連ログのみが表示される。
  • Traces to Metrics: スパン属性をベースに Prometheus メトリクスクエリにジャンプできる。遅いスパンが見つかった場合、該当サービスの CPU・メモリメトリクスを即座に確認できる。
  • Traces to Profiles: Pyroscope と連携すると、遅いスパンの原因をコードレベル(関数呼び出しプロファイル)まで追跡できる。

トラブルシューティング

Tempo 運用時によく発生する問題と解決方法を整理する。

Ingester メモリ不足(OOM)

症状: Ingester Pod が繰り返し OOMKilled ステータスで再起動される。

原因: トラフィック急増によりインメモリブロックが過大になったか、max_block_duration が長すぎる設定の場合に発生する。

解決策: ingester.max_block_duration を5分に短縮してフラッシュ周期を短くし、ingester.max_block_bytes を 500MB ~ 1GB の範囲に制限する。Kubernetes のリソースリクエストとリミットも十分に設定する必要がある。Ingester インスタンス数を増やして負荷を分散することも効果的だ。

TraceQL クエリタイムアウト

症状: TraceQL 検索時に「context deadline exceeded」エラーが繰り返し発生する。

原因: ブロック数が多すぎる(Compactor 未動作)か、検索範囲が広すぎる場合に発生する。

解決策: Compactor が正常動作しているか確認し、compaction_window を適切に調整する。query_frontend.max_retries を 3 に設定し、query_frontend.search.default_result_limit で結果数を制限する。クエリの時間範囲を狭めることも即座の緩和策だ。

スパン欠損(Missing Spans)

症状: トレースに一部のスパンが欠けており、不完全なトレースが照会される。

原因: Distributor と Ingester 間のハッシュリング不一致、ネットワークパーティション、またはサンプリングポリシーの不一致が原因であることが多い。

解決策: distributor ログで「ring not healthy」メッセージを確認する。Memberlist 通信ポート(デフォルト 7946)がファイアウォールで開いているか点検する。OTel Collector の tail_sampling ポリシーが意図通りに動作しているか検証し、debug exporter を一時的に有効にしてスパンフローを追跡する。

Compactor ブロックマージ失敗

症状: オブジェクトストレージのブロック数が増加し続け、クエリパフォーマンスが徐々に低下する。

原因: Compactor メモリ不足、オブジェクトストレージの権限問題、または max_compaction_objects リミット超過が原因だ。

解決策: Compactor のメモリ割り当てを増やし、ストレージ IAM 権限(ListBucket、GetObject、PutObject、DeleteObject)を再確認する。compaction.max_compaction_objects を段階的に増やして大規模ブロックも処理できるようにする。

運用チェックリスト

本番環境で Tempo を安定的に運用するためのチェックリストだ。

デプロイ前チェック

  • デプロイモード決定(日次トラフィック量基準:100GB 以下 Monolithic、100GB~1TB Scalable、1TB 以上 Microservices)
  • オブジェクトストレージバケット作成と IAM 権限設定
  • WAL 保存パスのディスク IOPS 確認(SSD 推奨、最低 3000 IOPS)
  • ネットワークポリシー設定(Memberlist 7946/TCP、OTLP 4317-4318/TCP)
  • TLS 証明書プロビジョニング(mTLS 推奨)
  • リソースリクエスト/リミット設定(Ingester:最低 4GB RAM、Compactor:最低 8GB RAM)

必須モニタリングメトリクス

  • tempo_ingester_live_traces: アクティブトレース数(メモリ圧迫指標)
  • tempo_ingester_bytes_received_total: 秒あたり受信バイト数
  • tempo_compactor_blocks_total: オブジェクトストレージブロック数(持続的増加で警告)
  • tempo_distributor_spans_received_total: 受信スパン数(ドロップ有無確認)
  • tempo_query_frontend_queries_total: クエリスループットとエラー率
  • tempo_discarded_spans_total: 破棄されたスパン数(0 でなければ即座に調査)

定期点検項目

  • 週次:Compactor ブロックマージ状態確認、ブロック数推移モニタリング
  • 週次:WAL ディスク使用量確認とフラッシュ正常動作検証
  • 月次:ストレージコストレビューと保持期間再評価
  • 月次:TraceQL クエリパフォーマンスベンチマーク(主要クエリパターンの応答時間追跡)
  • 四半期:Tempo バージョンアップグレード計画策定と互換性テスト

障害事例と復旧

事例 1:Ingester WAL 破損によるデータ損失

状況: Kubernetes ノードの突然のシャットダウンにより、3台中 2台の Ingester の WAL が破損した。Ingester が再起動時に WAL を復旧できず、約15分間のトレースデータが損失した。

復旧過程: まず破損した WAL ディレクトリを手動でクリアし、Ingester を再起動した。損失した時間帯のトレースは、OTel Collector の sending_queue にバッファリングされた一部データを再送信して部分復旧した。

教訓: Ingester の replication_factor を 3 に設定し、少なくとも 2台の Ingester に同じスパンが複製されるようにした。WAL パスをローカル NVMe SSD に固定し、PV(PersistentVolume)の reclaimPolicyRetain に変更して Pod の再スケジューリング時にも WAL が保持されるようにした。Ingester Pod の terminationGracePeriodSeconds を 300秒に延長し、シャットダウン時のフラッシュ時間を確保した。

事例 2:Compactor 障害によるクエリパフォーマンス崩壊

状況: S3 IAM ポリシー変更後、Compactor が DeleteObject 権限を失い、ブロックマージが2週間中断された。小規模ブロックが50万個以上蓄積し、TraceQL 検索の応答時間が通常の2秒から45秒に急増した。

復旧過程: S3 IAM ポリシーを即座に修正し Compactor を再起動した。しかし50万個のブロックを一度にマージしようとすると Compactor OOM が発生した。compaction.max_compaction_objects を100万から10万に下げ、compaction_window を1時間に短縮して段階的にブロックをマージした。完全な正常化に3日を要した。

教訓: tempo_compactor_blocks_total メトリクスにアラームを設定し、ブロック数が異常に増加した場合に即座に通知を受けるようにした。IAM ポリシー変更時に Tempo 関連の権限が影響を受けるかチェックする項目を変更管理プロセスに追加した。

事例 3:無分別なカスタム属性によるカーディナリティ爆発

状況: 開発チームがユーザー ID(user.id)をスパン属性として無分別に追加し、Metrics Generator の dimensions にこの属性が含まれてカーディナリティが数百万に爆発した。Prometheus リモートライトがボトルネックとなり、メトリクス収集全体が遅延した。

復旧過程: 即座に user.iddimensions から削除し Metrics Generator を再起動した。Prometheus で該当するタイムシリーズを削除してストレージを回収した。

教訓: dimensions に追加する属性のカーディナリティを必ず事前検証すること。カーディナリティが 1000 を超える可能性のある属性はメトリクスラベルではなく TraceQL 検索でのみ活用するポリシーを策定した。overrides.defaults.metrics_generator.max_active_series を設定してタイムシリーズ数を制限するセーフガードも追加した。

参考資料