- Authors
- Name
- はじめに
- 分散トレーシングのコア概念
- OpenTelemetry SDK計装(Python/Go)
- OpenTelemetry Collectorの構成
- Jaegerバックエンドの構築と運用
- Grafana Tempo: 大規模トレースストレージ
- トレーシングバックエンド比較(Jaeger vs Tempo vs Zipkin)
- サンプリング戦略とコスト最適化
- 運用時の注意事項とトラブルシューティング
- 障害事例と復旧手順
- おわりに
- 参考資料

はじめに
マイクロサービスアーキテクチャにおいて、1つのユーザーリクエストは複数のサービスを経由して処理されます。API Gatewayから始まったリクエストが、認証サービス、注文サービス、決済サービス、通知サービスを順次的・並列的に呼び出す構造では、特定のリクエストが遅い原因を把握するために、呼び出しチェーン全体を追跡する必要があります。単一サービスのログやメトリクスだけでは、サービス間の因果関係を把握するのは困難です。
分散トレーシング(Distributed Tracing)はこの問題を解決します。リクエストがサービス境界を越えるたびに固有のTrace IDを伝播し、各サービスで実行した作業をSpanとして記録することで、リクエスト全体の流れを1つのトレースとして再構成します。Googleの Dapper論文(2010年)から始まったこの概念は、現在OpenTelemetryというCNCFプロジェクトとして標準化されており、JaegerやGrafana Tempoなどのバックエンドを通じて保存・分析できます。
この記事では、分散トレーシングのコア概念から始まり、OpenTelemetry SDKを活用したPython/Goの計装、OpenTelemetry Collectorの構成、JaegerとGrafana Tempoバックエンドの構築、サンプリング戦略、コスト最適化、運用時の注意事項まで、プロダクション環境で必要なパイプライン全体を実践的なコードとともに解説します。
分散トレーシングのコア概念
Trace、Span、Context Propagation
分散トレーシングの3つのコア概念を整理します。
Traceは、1つのユーザーリクエストがシステムを通過する全体の経路を表します。固有のTrace IDで識別され、複数のSpanの集合で構成されます。
Spanは、トレース内の1つの作業単位を表します。各Spanは固有のSpan ID、親Span ID、開始/終了時刻、属性(Attributes)、イベント(Events)、ステータス(Status)を持ちます。Spanの種類(SpanKind)には、SERVER、CLIENT、PRODUCER、CONSUMER、INTERNALの5つがあります。
Context Propagationは、Trace IDとSpan IDをサービス境界を越えて伝達するメカニズムです。HTTPヘッダー(W3C Trace Contextのtraceparent、tracestate)、gRPCメタデータ、メッセージキューヘッダーなどを通じてコンテキストが伝播されます。
W3C Trace Context標準
W3C Trace Contextは、分散トレーシングのコンテキスト伝播標準です。traceparentヘッダーの形式は以下の通りです。
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
| | | |
バージョン Trace ID (32文字) Parent Span ID Flags
OpenTelemetryはデフォルトでW3C Trace Contextを使用し、B3(Zipkin)やJaeger形式の伝播もサポートしています。
OpenTelemetryアーキテクチャ概要
OpenTelemetryは、テレメトリデータ(トレース、メトリクス、ログ)を生成、収集、送信するためのベンダー中立的なフレームワークです。主な構成要素は以下の通りです。
- API: 計装のためのインターフェース定義(ベンダー非依存)
- SDK: APIの実装体(サンプリング、バッチ、エクスポートなど)
- Collector: テレメトリデータを受信、処理、エクスポートする独立プロセス
- Instrumentation Libraries: ライブラリ/フレームワークの自動計装
OpenTelemetry SDK計装(Python/Go)
Python計装
PythonアプリケーションにOpenTelemetryを適用する方法を見ていきます。まず必要なパッケージをインストールします。
pip install opentelemetry-api \
opentelemetry-sdk \
opentelemetry-exporter-otlp \
opentelemetry-instrumentation-flask \
opentelemetry-instrumentation-requests \
opentelemetry-instrumentation-sqlalchemy
次に、FlaskアプリケーションにOpenTelemetryを適用する全体のコードです。
# tracing.py - OpenTelemetry 초기화 모듈
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.trace.propagation import TraceContextTextMapPropagator
from opentelemetry.baggage.propagation import W3CBaggagePropagator
def init_tracer(service_name: str, otlp_endpoint: str = "localhost:4317"):
"""OpenTelemetry TracerProviderを初期化し、グローバル設定を適用する。"""
resource = Resource.create({
SERVICE_NAME: service_name,
SERVICE_VERSION: "1.0.0",
"deployment.environment": "production",
})
provider = TracerProvider(resource=resource)
# OTLP gRPC Exporter 設定
otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint, insecure=True)
span_processor = BatchSpanProcessor(
otlp_exporter,
max_queue_size=2048,
max_export_batch_size=512,
schedule_delay_millis=5000,
)
provider.add_span_processor(span_processor)
# グローバルTracerProvider登録
trace.set_tracer_provider(provider)
# W3C Trace Context + Baggage 伝播設定
set_global_textmap(CompositePropagator([
TraceContextTextMapPropagator(),
W3CBaggagePropagator(),
]))
return provider
# app.py - Flask アプリケーションにトレーシングを適用
from flask import Flask, request, jsonify
from opentelemetry import trace
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from tracing import init_tracer
import requests
# トレーサー初期化
provider = init_tracer("order-service", "otel-collector:4317")
app = Flask(__name__)
# 自動計装: Flaskとrequestsライブラリ
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()
# 手動計装のためのトレーサー取得
tracer = trace.get_tracer("order-service", "1.0.0")
@app.route("/orders", methods=["POST"])
def create_order():
"""注文作成API - 手動Spanを追加して詳細な追跡を実行"""
with tracer.start_as_current_span(
"validate_order",
attributes={"order.type": "standard"}
) as span:
order_data = request.get_json()
span.set_attribute("order.item_count", len(order_data.get("items", [])))
# 在庫確認 - 外部サービス呼び出し(自動計装済み)
inventory_resp = requests.post(
"http://inventory-service:8080/check",
json=order_data["items"]
)
if inventory_resp.status_code != 200:
span.set_status(trace.StatusCode.ERROR, "Inventory check failed")
span.record_exception(Exception("在庫不足"))
return jsonify({"error": "insufficient_inventory"}), 400
with tracer.start_as_current_span("process_payment") as span:
payment_resp = requests.post(
"http://payment-service:8080/charge",
json={"amount": order_data["total"]}
)
span.set_attribute("payment.method", order_data.get("payment_method", "card"))
return jsonify({"order_id": "ORD-12345", "status": "confirmed"}), 201
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
Go計装
Goアプリケーションでは go.opentelemetry.io/otel パッケージを使用します。HTTPサーバーとgRPCクライアントにトレーシングを適用する例を見ていきます。
// tracing.go - OpenTelemetry 초기화
package main
import (
"context"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func initTracer(ctx context.Context, serviceName, endpoint string) (*sdktrace.TracerProvider, error) {
// gRPC接続でOTLP Exporterを作成
conn, err := grpc.NewClient(endpoint,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, err
}
exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
if err != nil {
return nil, err
}
// Resource定義 - サービスメタデータ
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(serviceName),
semconv.ServiceVersion("1.0.0"),
semconv.DeploymentEnvironment("production"),
),
)
if err != nil {
return nil, err
}
// TracerProvider作成
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter,
sdktrace.WithMaxQueueSize(2048),
sdktrace.WithMaxExportBatchSize(512),
),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(0.1), // 10%サンプリング
)),
)
// グローバル設定
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return tp, nil
}
// handleOrder - HTTPハンドラーで手動Spanを作成
func handleOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.Tracer("order-handler")
ctx, span := tracer.Start(ctx, "process-order",
trace.WithSpanKind(trace.SpanKindServer),
)
defer span.End()
// ビジネスロジックにコンテキストを渡す
orderID, err := processOrder(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
http.Error(w, "order failed", http.StatusInternalServerError)
return
}
span.SetAttributes(attribute.String("order.id", orderID))
w.WriteHeader(http.StatusCreated)
}
自動計装 vs 手動計装
| 区分 | 自動計装 | 手動計装 |
|---|---|---|
| 適用方式 | ライブラリ/エージェントを使用 | コードに直接Spanを作成 |
| コード変更 | 最小(初期化のみ) | ビジネスロジック各所に挿入 |
| カバレッジ | HTTP、DB、gRPCなどフレームワークレベル | カスタムビジネスロジックまで対応可能 |
| 細かい制御 | 制限的 | 属性、イベント、リンクを自由に追加 |
| 推奨戦略 | 基本骨格として使用 | コアビジネスロジックに補完的に追加 |
実践では、自動計装でHTTP/gRPC/DB呼び出しの基本Spanを作成し、手動計装でビジネスロジックの重要な区間を追加で計装するハイブリッド戦略を推奨します。
OpenTelemetry Collectorの構成
OpenTelemetry Collectorは、テレメトリデータを受信(Receivers)、処理(Processors)、エクスポート(Exporters)するパイプラインの役割を担います。アプリケーションから直接バックエンドにデータを送信する代わりに、Collectorを中間に配置することで、リトライ、バッチ、サンプリング、ルーティングなどを一元管理できます。
Collector構成ファイル
# 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: 1024
send_batch_max_size: 2048
memory_limiter:
check_interval: 1s
limit_mib: 1024
spike_limit_mib: 256
resource:
attributes:
- key: environment
value: production
action: upsert
- key: cluster
value: k8s-prod-01
action: upsert
# Tail-based サンプリング: エラーと遅いリクエストは100%収集
tail_sampling:
decision_wait: 10s
num_traces: 100000
expected_new_traces_per_sec: 1000
policies:
- name: error-policy
type: status_code
status_code:
status_codes:
- ERROR
- name: latency-policy
type: latency
latency:
threshold_ms: 1000
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 10
exporters:
otlp/jaeger:
endpoint: jaeger-collector:4317
tls:
insecure: true
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
debug:
verbosity: basic
extensions:
health_check:
endpoint: 0.0.0.0:13133
zpages:
endpoint: 0.0.0.0:55679
service:
extensions: [health_check, zpages]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, resource, tail_sampling, batch]
exporters: [otlp/jaeger, otlp/tempo, debug]
telemetry:
logs:
level: info
metrics:
address: 0.0.0.0:8888
Collector Docker Composeデプロイ
# docker-compose.yaml (Collector 部分)
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.116.0
command: ['--config=/etc/otel-collector-config.yaml']
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
ports:
- '4317:4317' # OTLP gRPC
- '4318:4318' # OTLP HTTP
- '8888:8888' # Collector メトリクス
- '13133:13133' # Health check
- '55679:55679' # zPages
restart: unless-stopped
deploy:
resources:
limits:
memory: 2g
cpus: '1.0'
healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:13133']
interval: 10s
timeout: 5s
retries: 3
Collectorの構成で注意すべき点はprocessorsの順序です。memory_limiterは必ず最初に配置してOOMを防止し、batchは最後に配置してネットワーク効率を高めます。
Jaegerバックエンドの構築と運用
Jaegerは、Uberが開発したオープンソースの分散トレーシングシステムで、CNCF Graduatedプロジェクトです。Jaeger v2はOpenTelemetry Collectorを基盤に再構築されており、ネイティブOTLPサポートを提供します。
Jaegerプロダクションデプロイ
プロダクション環境では、Jaeger all-in-oneではなく、分離されたコンポーネントでのデプロイを推奨します。Elasticsearchをストレージバックエンドとして使用する構成を見ていきます。
# docker-compose-jaeger.yaml
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- 'ES_JAVA_OPTS=-Xms1g -Xmx1g'
ports:
- '9200:9200'
volumes:
- es_data:/usr/share/elasticsearch/data
healthcheck:
test: ['CMD-SHELL', 'curl -f http://localhost:9200/_cluster/health || exit 1']
interval: 10s
timeout: 5s
retries: 10
jaeger:
image: jaegertracing/jaeger:2.4.0
environment:
- SPAN_STORAGE_TYPE=elasticsearch
- ES_SERVER_URLS=http://elasticsearch:9200
- ES_NUM_SHARDS=3
- ES_NUM_REPLICAS=1
- ES_INDEX_PREFIX=jaeger
- ES_TAGS_AS_FIELDS_ALL=true
- COLLECTOR_OTLP_ENABLED=true
ports:
- '16686:16686' # Jaeger UI
- '4317:4317' # OTLP gRPC
- '4318:4318' # OTLP HTTP
- '14250:14250' # gRPC Collector
depends_on:
elasticsearch:
condition: service_healthy
restart: unless-stopped
volumes:
es_data:
driver: local
Jaegerインデックス管理
Elasticsearch基盤のJaeger運用において、インデックス管理は必須です。古いトレースを自動削除するcronジョブを設定します。
#!/bin/bash
# jaeger-index-cleaner.sh - 14日以上経過したJaegerインデックスを削除
ES_URL="http://elasticsearch:9200"
RETENTION_DAYS=14
# 削除対象の日付を計算
CUTOFF_DATE=$(date -d "-${RETENTION_DAYS} days" +%Y-%m-%d)
echo "Deleting Jaeger indices older than ${CUTOFF_DATE}..."
# jaeger-span-* および jaeger-service-* インデックスの照会と削除
for INDEX_TYPE in "jaeger-span" "jaeger-service"; do
INDICES=$(curl -s "${ES_URL}/_cat/indices/${INDEX_TYPE}-*" \
| awk '{print $3}' \
| sort)
for INDEX in ${INDICES}; do
# インデックス名から日付を抽出(jaeger-span-2026-03-01 形式)
INDEX_DATE=$(echo "${INDEX}" | grep -oP '\d{4}-\d{2}-\d{2}')
if [[ "${INDEX_DATE}" < "${CUTOFF_DATE}" ]]; then
echo "Deleting index: ${INDEX}"
curl -s -X DELETE "${ES_URL}/${INDEX}"
fi
done
done
echo "Cleanup completed."
Jaegerの主要機能
- トレース検索: サービス名、操作名、タグ、時間範囲によるトレース検索
- トレース比較: 2つのトレースを並べて比較し、パフォーマンスの違いを分析
- 依存関係グラフ: サービス間の呼び出し関係をDAGで可視化
- SPM(Service Performance Monitoring): REDメトリクス(Rate、Error、Duration)の自動生成
Grafana Tempo: 大規模トレースストレージ
Grafana Tempoは、大規模な分散トレーシングのために設計されたバックエンドです。Jaegerとは異なりインデックスを作成せず、オブジェクトストレージ(S3、GCS、Azure Blob)にトレースを直接保存することで、コストを大幅に削減します。
Tempoの構成
# tempo-config.yaml
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
ingester:
max_block_duration: 5m
max_block_bytes: 1073741824 # 1GB
flush_check_period: 10s
compactor:
compaction:
block_retention: 336h # 14日
ring:
kvstore:
store: memberlist
storage:
trace:
backend: s3
s3:
bucket: tempo-traces-prod
endpoint: s3.ap-northeast-2.amazonaws.com
region: ap-northeast-2
# IAM Roleベースの認証を推奨(access_keyの直接入力は避ける)
wal:
path: /var/tempo/wal
local:
path: /var/tempo/blocks
pool:
max_workers: 100
queue_depth: 10000
querier:
search:
query_timeout: 30s
frontend_worker:
frontend_address: tempo-query-frontend:9095
metrics_generator:
registry:
external_labels:
source: tempo
cluster: k8s-prod
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]
Tempo + Grafana統合
Tempoの真の力は、Grafanaとの統合にあります。TraceQLを使用してトレースをクエリし、ログ(Loki)とメトリクス(Prometheus/Mimir)をトレースと相互連携できます。
GrafanaのデータソースにTempoを追加し、Trace to Logs、Trace to Metrics機能を有効にすると、トレースの特定のSpanから関連するログやメトリクスに直接ジャンプできます。これがGrafanaスタックの核心的な差別化ポイントである「相関分析(Correlation)」です。
TraceQLクエリ例
Tempo 2.0から導入されたTraceQLは、トレース専用のクエリ言語です。
// エラーステータスのSpanを持つトレースを検索
{ status = error }
// 特定のサービスで1秒以上かかったSpanを検索
{ resource.service.name = "order-service" && duration > 1s }
// HTTP 500エラーを返したSpanを検索
{ span.http.status_code = 500 }
// 2つのサービス間の呼び出し関係を検索
{ resource.service.name = "api-gateway" } >> { resource.service.name = "payment-service" }
トレーシングバックエンド比較(Jaeger vs Tempo vs Zipkin)
トレーシングバックエンドを選択する際は、環境と要件に合ったツールを選ぶことが重要です。主要なトレーシングバックエンドを比較します。
| 項目 | Jaeger | Grafana Tempo | Zipkin | AWS X-Ray |
|---|---|---|---|---|
| 開発元 | Uber (CNCF Graduated) | Grafana Labs | Twitter (現 X) | AWS |
| ストレージ | Elasticsearch, Cassandra, Kafka | S3, GCS, Azure Blob | Elasticsearch, MySQL, Cassandra | AWS独自ストレージ |
| インデックス | 完全インデックス | インデックスなし(Trace IDベース) | 完全インデックス | 独自インデックス |
| クエリ | サービス、タグ、時間ベース検索 | TraceQL、Trace ID検索 | サービス、タグ、時間ベース検索 | Filter Expressions |
| GBあたりストレージコスト | 高い(ESインデックス) | 非常に低い(オブジェクトストレージ) | 高い(ESインデックス) | 中程度(マネージド) |
| OTLPサポート | ネイティブ(v2) | ネイティブ | 別途Collector必要 | 別途SDK/Collector |
| スケーリング | 水平スケーリング可能 | 水平スケーリング優秀 | 制限的 | 自動(マネージド) |
| UI | 自前UI(優秀) | Grafana(卓越) | 自前UI(基本的) | AWSコンソール |
| メトリクス生成 | SPM(v2) | Metrics Generator | なし | 独自メトリクス |
| ログ連携 | 制限的 | Lokiネイティブ連携 | なし | CloudWatch連携 |
| 適した環境 | 独立トレーシングシステム | Grafanaスタック使用環境 | 小規模/学習 | AWSネイティブ |
選択ガイド
- Jaeger: 独立したトレーシングシステムが必要で、豊富な検索機能が重要であり、Elasticsearch/Cassandraの運用能力があるチーム
- Grafana Tempo: すでにGrafana、Loki、Prometheus/Mimirを使用しており、コスト効率が重要で、大規模なトレースを処理する必要がある環境
- Zipkin: 迅速なプロトタイピングや学習目的、またはレガシーZipkin形式との互換性が必要な場合
- AWS X-Ray: AWSサービス(Lambda、ECS、EKS)を主に使用し、マネージドサービスを好む環境
サンプリング戦略とコスト最適化
プロダクション環境ですべてのトレースを収集すると、ストレージコストとネットワーク負荷が急増します。効果的なサンプリング戦略は、コストと可視性のバランスを取るための鍵です。
Head-basedサンプリング
リクエストの開始時にサンプリングの可否を決定します。実装はシンプルですが、エラートレースを見逃す可能性があります。
# PythonでのHead-based確率的サンプリング設定
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased, ParentBased
# ParentBased: 親Spanがサンプリングされていれば子もサンプリング
# 親がないルートSpanは10%の確率でサンプリング
sampler = ParentBased(root=TraceIdRatioBased(0.1))
provider = TracerProvider(
resource=resource,
sampler=sampler,
)
Tail-basedサンプリング
トレースが完成した後、全体のSpanを見てサンプリングの可否を決定します。OpenTelemetry Collectorの tail_sampling プロセッサを使用し、エラートレースやレイテンシの大きいトレースを100%収集できます。ただし、Collectorにメモリ負荷がかかるため、decision_waitの時間とnum_tracesの設定を環境に合わせて調整する必要があります。
コスト最適化戦略
- 階層別サンプリング: 正常トラフィックは5~10%、エラー/遅いリクエストは100%収集
- サービス別差等サンプリング: コアサービスは高い割合、内部ヘルスチェックは0%
- ストレージティアリング: 最新データは高速ストレージ、古いデータは低コストストレージ
- TTL設定: トレースの保持期間を7~14日に制限(ほとんどのデバッグは48時間以内に発生)
- Attributeフィルタリング: 不要なSpan AttributeをCollectorで除去してデータサイズを削減
運用時の注意事項とトラブルシューティング
Context Propagationの欠落
分散トレーシングで最も一般的な問題は、コンテキスト伝播の欠落です。サービス間の呼び出しでtraceparentヘッダーが伝達されないと、トレースが途切れます。
症状: Jaeger/Tempoでトレースを見ると、Spanが1つしかないか、異なるTrace IDに分離されています。
原因と解決策:
- HTTPクライアントライブラリの自動計装が欠落している場合:
RequestsInstrumentor().instrument()またはotelhttp.NewTransport()を適用 - メッセージキュー(Kafka、RabbitMQ)を通じた呼び出しでコンテキストが伝播されない場合: メッセージヘッダーにtraceparentを手動で注入/抽出
- gRPCインターセプターが適用されていない場合:
otelgrpc.UnaryClientInterceptor()を追加
Collectorのボトルネックとバックプレッシャー
トラフィックが急増すると、Collectorが処理量に追いつけず、Spanが欠落する可能性があります。
モニタリング指標:
otelcol_exporter_send_failed_spans: エクスポートに失敗したSpan数otelcol_processor_batch_timeout_trigger_send: タイムアウトによる強制送信otelcol_receiver_refused_spans: 受信拒否されたSpan数
解決策:
memory_limiterプロセッサを必ず設定してOOMを防止- Collectorを水平スケールし、ロードバランサーの背後に配置
batchプロセッサのsend_batch_sizeとtimeoutを調整
高カーディナリティAttribute
Span Attributeにユーザー ID、セッションID、リクエストボディなどカーディナリティの高い値を入れると、インデックスコストが急増します(特にJaeger + Elasticsearch環境)。
ガイドライン:
- Attribute値のカーディナリティは数千以下に維持
- 一意な識別子はSpan Attributeの代わりにSpan Eventやログに記録
- Jaegerの
ES_TAGS_AS_FIELDS_ALL=falseに設定し、必要なタグのみインデックス化
障害事例と復旧手順
事例1: Collector OOMによるトレース消失
状況: Tail-basedサンプリングを使用するCollectorで、トラフィック急増によりメモリ使用量が限界を超え、プロセスが終了しました。約5分間、すべてのトレースが消失しました。
根本原因: tail_samplingプロセッサのnum_tracesが50,000に設定されていましたが、ピーク時には毎秒10,000件の新しいトレースが流入していました。decision_waitが30秒に設定されていたため、最大300,000個のトレースをメモリに保持する必要がありました。
復旧手順:
- Collector Podの自動再起動を確認(Kubernetes liveness probe)
num_tracesを200,000に増加、decision_waitを10秒に短縮memory_limiterのlimit_mibを実際のPodメモリ上限の80%に設定- Collectorインスタンスを3つに水平スケール
予防措置: Collectorのメモリ使用量に対するアラートルールを設定し、オートスケーリング(HPA)を適用します。
事例2: Elasticsearchインデックス爆増によるJaeger障害
状況: 新しいサービスをデプロイした後、Span Attributeにリクエストボディ全体を含む計装が追加されました。24時間でElasticsearchのディスク使用量が95%を超え、インデックスの書き込みがブロックされました。
根本原因: 開発者がデバッグ目的で span.set_attribute("request.body", json.dumps(request_body)) を追加しましたが、このAttributeの平均サイズが10KBで、毎秒1,000件のリクエストが発生し、1日あたり約800GBの追加データが生成されました。
復旧手順:
- 問題のあるサービスのSpan Attributeを除去するホットフィックスをデプロイ
- Elasticsearchのディスクウォーターマークを一時的に調整して書き込みを再開
- 問題期間のインデックスを手動削除してディスクを確保
- CollectorにAttribute サイズ制限プロセッサを追加
予防措置: CollectorレベルでAttributesプロセッサを使用してAttribute値のサイズを制限し、CI/CDパイプラインで計装コードレビューを義務化します。
事例3: クロックスキューによる異常なSpan順序
状況: Jaeger UIでトレースを見ると、子Spanが親Spanより先に開始されたり、Spanの時間がマイナスで表示されていました。
根本原因: コンテナホストノード間の時刻同期(NTP)が正常に動作しておらず、最大500msのクロックスキューが発生していました。
復旧手順:
- すべてのノードでNTP同期状態を確認:
chronyc tracking - NTPサーバー設定の修正と強制同期
- クロックスキューが大きい期間のトレースデータは無視
予防措置: Kubernetesノードにchronyまたはsystemd-timesyncdを設定し、NTPオフセットに対するモニタリングアラートを構成します。
おわりに
分散トレーシングは、マイクロサービスアーキテクチャにおいて不可欠なオブザーバビリティツールです。OpenTelemetryを通じてベンダー中立的に計装し、JaegerまたはGrafana Tempoをバックエンドとして使用すれば、プロダクション環境でリクエストフローを透過的に追跡できます。
まとめ:
- OpenTelemetryで計装を標準化します。自動計装で基本骨格を作り、手動計装でビジネスロジックを補完します。
- OpenTelemetry Collectorを中間に配置します。リトライ、サンプリング、ルーティングを一元管理します。
- バックエンドは環境に合わせて選択します。GrafanaスタックならTempo、独立システムならJaegerが適しています。
- Tail-basedサンプリングでコストを最適化します。エラーとレイテンシの大きいリクエストは必ず収集し、正常リクエストは低い割合でサンプリングします。
- 運用パイプラインをモニタリングします。Collectorとバックエンド自体のメトリクスを収集し、アラートを設定します。
分散トレーシングを導入する際に最も重要なのは「すべてを計装しようとしないこと」です。コアユーザージャーニー(Critical User Journey)をまず定義し、その経路のサービスを優先的に計装してから段階的に拡大するのが現実的なアプローチです。