- Authors
- Name

- 概要
- Tempo アーキテクチャ
- デプロイモード
- Docker Compose でクイックスタート
- TraceQL クエリ構文
- スパンメトリクスとサービスグラフ
- Tempo vs Jaeger vs Zipkin 比較
- OpenTelemetry Collector 連携
- ストレージ最適化
- Grafana ダッシュボード構成
- トラブルシューティング
- 運用チェックリスト
- 障害事例と復旧
- 参考資料
概要
マイクロサービスアーキテクチャが一般化し、単一のリクエストが数十のサービスを経由して処理される環境が日常になった。この環境で障害の根本原因を追跡するには、分散トレーシングが不可欠である。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つのデプロイモードを提供しており、組織の規模と要件に応じて選択できる。
デプロイモード比較
| 項目 | Monolithic | Scalable Single Binary | Microservices |
|---|---|---|---|
| 構造 | 単一バイナリ、単一プロセス | 単一バイナリ、複数インスタンス | コンポーネント別独立プロセス |
| スケーラビリティ | 垂直スケーリングのみ | 水平スケーリング可能 | コンポーネント別独立水平スケーリング |
| 推奨トラフィック | 日量 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: スパンの固有属性(
name、status、duration、kind、rootName、rootServiceName、traceDuration) - 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.method、http.status_code、http.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 Tempo | Jaeger | Zipkin |
|---|---|---|---|
| 初回リリース | 2020(Grafana Labs) | 2015(Uber) | 2012(Twitter) |
| CNCF ステータス | - | Graduated | - |
| ストレージ方式 | オブジェクトストレージ(インデックスなし) | Elasticsearch、Cassandra 等 | Elasticsearch、Cassandra、MySQL |
| インデクシング | なし(Trace ID + TraceQL) | タグベースインデックス生成 | タグベースインデックス生成 |
| ストレージコスト | 低い(S3/GCS 単価) | 高い(インデックスストレージ含む) | 高い |
| 収集プロトコル | OTLP、Jaeger、Zipkin | OTLP、Jaeger | Zipkin、OTLP(限定的) |
| クエリ言語 | TraceQL | タグベース検索 | タグベース検索 |
| 内蔵 UI | Grafana 連携 | Jaeger UI | Zipkin 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 S3 | Google Cloud Storage | Azure Blob Storage |
|---|---|---|---|
| 設定キー | s3 | gcs | azure |
| 認証方式 | IAM Role、Access Key | Service Account、Workload Identity | Managed Identity、SAS Token |
| コスト(GB/月) | $0.023(Standard) | $0.020(Standard) | $0.018(Hot) |
| リージョン可用性 | 33+ リージョン | 40+ リージョン | 60+ リージョン |
| Tempo 互換性 | 完全サポート | 完全サポート | 完全サポート |
| ライフサイクルポリシー | S3 Lifecycle | Object Lifecycle | Lifecycle 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_encoding を zstd に設定すると、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 Logs、Traces to Metrics、Traces 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)の reclaimPolicy を Retain に変更して 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.id を dimensions から削除し Metrics Generator を再起動した。Prometheus で該当するタイムシリーズを削除してストレージを回収した。
教訓: dimensions に追加する属性のカーディナリティを必ず事前検証すること。カーディナリティが 1000 を超える可能性のある属性はメトリクスラベルではなく TraceQL 検索でのみ活用するポリシーを策定した。overrides.defaults.metrics_generator.max_active_series を設定してタイムシリーズ数を制限するセーフガードも追加した。