
はじめに
メトリクスは、「何か」が間違っていることを示します。ログには、各サービスで「何が起こった」かがわかります。 分散トレース は、複数のサービスを経由するときにリクエストが遅いか失敗する「理由」を示します。
ユーザーからの 1 つの HTTP リクエストは、10 のサービス、20 のデータベース クエリ、5 つのキャッシュ ルックアップを通過する可能性があり、分散トレースはその過程全体を追跡します。
1. 基本概念
1.1 トレース、スパン、およびコンテキスト
Trace ID: abc123def456789 (định danh duy nhất cho toàn bộ request)
┌─ Trace ────────────────────────────────────────────────────────┐
│ │
│ Span: POST /api/orders (API Gateway) [0ms → 170ms] │
│ │ │
│ └─ Span: OrderService.createOrder() [2ms → 165ms] │
│ │ │
│ ├─ Span: DB INSERT orders [5ms → 10ms] │
│ │ │
│ ├─ Span: gRPC PaymentService.charge() [12ms → 132ms] │
│ │ │ │
│ │ ├─ Span: Validate card [15ms → 30ms] │
│ │ └─ Span: Process payment [30ms → 130ms] │
│ │ └─ Span: DB INSERT transactions [50ms → 80ms] │
│ │ │
│ └─ Span: Kafka produce order.created [133ms → 136ms]│
│ │
└────────────────────────────────────────────────────────────────┘
Total: 170ms
トレース: リクエストの行程全体を表し、次のように識別されます。 traceId。
スパン: トレース内の作業単位 (1 つの関数呼び出し、1 つのデータベース クエリ、1 つの HTTP 呼び出し)。はい:
spanId: スパンのIDparentSpanId: 親スパンのIDstartTime、endTimestatus: OK / エラーattributes: キーと値のメタデータevents: タイムスタンプ付きの注釈
コンテキストの伝播: 伝播方法 traceId そして spanId サービス境界経由:
# HTTP Headers (W3C Trace Context standard)
traceparent: 00-abc123def456789-span123-01
^^ ^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^
version traceId spanId flags
# Hoặc B3 format (Zipkin/Jaeger legacy)
X-B3-TraceId: abc123def456789
X-B3-SpanId: span456
X-B3-Sampled: 1
1.2 サンプリング戦略
本番環境ですべてのリクエストをトレース = 大量のストレージと CPU を消費します。サンプリングが必要:
Head-based Sampling (quyết định khi request bắt đầu):
├── Always Sample: 100% — chỉ cho dev/staging nhỏ
├── Probabilistic: 1-10% — random sampling
└── Rate Limiting: 100 traces/sec max
Tail-based Sampling (quyết định sau khi request hoàn thành, nhìn đủ dữ liệu):
├── Sample 100% requests có ERROR
├── Sample 100% requests > 1 giây
├── Sample 1% requests bình thường
└── Sample 100% requests với specific user ID
より優れた末尾ベースのサンプリング — エラーや遅いリクエストを見逃すことがありません。
2. OpenTelemetry — 標準化された可観測性
2.1 なぜ OpenTelemetry を使うのか?
OpenTelemetry が登場する前は、各ベンダーが独自の SDK (Jaeger、Zipkin、DataDog、New Relic、Dynatrace...) を持っていました。ベンダーを変更する = インストルメンテーション コード全体を書き直す。
OpenTelemetry (OTel) は、CNCF によって管理されるオープン スタンダードです。
Application Code
│
│ OpenTelemetry SDK (vendor-neutral)
▼
OpenTelemetry Collector
│
├──▶ Jaeger (self-hosted)
├──▶ Grafana Tempo (self-hosted)
├──▶ DataDog (SaaS)
├──▶ New Relic (SaaS)
└──▶ Honeycomb (SaaS)
Đổi backend = chỉ thay đổi Collector config, không đụng application code
2.2 OTel コンポーネント
- OTel API: 計測器コードへのインターフェース (トレーサー、メーター、ロガー)
- OTel SDK: API の実装、サンプリング処理、エクスポート
- OTel Collector: エージェント/ゲートウェイはテレメトリを受信、処理、エクスポートします。
- OTLP: OpenTelemetry プロトコル — データ転送プロトコル (gRPC ポート 4317、HTTP ポート 4318)
3. 計測器
3.1 自動インスツルメンテーション
最も簡単な方法 — コードの変更は必要ありません。
# Kubernetes — inject OTel auto-instrumentation
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: java-instrumentation
spec:
exporter:
endpoint: http://otel-collector:4317
propagators:
- tracecontext
- baggage
sampler:
type: parentbased_traceidratio
argument: "0.1" # 10% sampling
java:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
# Thêm annotation vào Deployment
metadata:
annotations:
instrumentation.opentelemetry.io/inject-java: "true"
Node.js を使用する場合:
instrumentation.opentelemetry.io/inject-nodejs: "true"
3.2 手動インストルメンテーション (Java)
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.Span;
@Service
public class OrderService {
private final Tracer tracer = GlobalOpenTelemetry.getTracer("order-service");
public Order createOrder(CreateOrderRequest request) {
// Tạo span cho business operation
Span span = tracer.spanBuilder("createOrder")
.setAttribute("customer.id", request.getCustomerId())
.setAttribute("order.items_count", request.getItems().size())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Validate
validateRequest(request);
// Save to DB — span auto-created bởi JDBC instrumentation
Order order = orderRepository.save(new Order(request));
// Call payment service — span auto-created bởi HTTP client instrumentation
paymentClient.charge(order);
span.setAttribute("order.id", order.getId());
span.setStatus(StatusCode.OK);
return order;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
3.3 手動インストルメンテーション (Node.js/TypeScript)
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service');
async function createOrder(request: CreateOrderRequest): Promise<Order> {
return tracer.startActiveSpan('createOrder', async (span) => {
try {
span.setAttributes({
'customer.id': request.customerId,
'order.items_count': request.items.length,
});
const order = await db.transaction(async (trx) => {
const o = await trx('orders').insert(request).returning('*');
await trx('outbox').insert({
topic: 'order.created',
payload: JSON.stringify(o),
});
return o;
});
span.setAttribute('order.id', order.id);
span.setStatus({ code: SpanStatusCode.OK });
return order;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
}
3.4 スパン属性のベスト プラクティス
# Standard semantic conventions (OTel Semantic Conventions)
# https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/
# HTTP
http.method = "POST"
http.url = "https://api.example.com/orders"
http.status_code = 201
http.request_content_length = 1024
# Database
db.system = "postgresql"
db.name = "orders"
db.operation = "INSERT"
db.statement = "INSERT INTO orders ..." # Cẩn thận PII!
# Messaging
messaging.system = "kafka"
messaging.destination = "order.created"
messaging.operation = "publish"
# Custom business attributes (namespace your attributes)
order.id = "O-001"
order.total_amount = 500000
customer.tier = "premium"
4. OpenTelemetry コレクター
4.1 導入モード
エージェント モード (DaemonSet): 各ノードにはコレクタがあり、そのノード上のポッドから受信します。
Pod → localhost:4317 → OTel Collector (DaemonSet) → Backend
ゲートウェイ モード (展開): すべてのエージェントから受信する集中コレクタ:
OTel Collector (DaemonSet) → OTel Collector (Gateway) → Multiple Backends
運用環境では 両方 を使用します。ポッドの近くにある DaemonSet エージェント、ゲートウェイはファンアウトとテールベースのサンプリングを処理します。
4.2 コレクターの構成
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Nhận từ Prometheus endpoints
prometheus:
config:
scrape_configs:
- job_name: 'services'
kubernetes_sd_configs:
- role: pod
processors:
# Batching để giảm số lượng connection
batch:
timeout: 5s
send_batch_size: 1024
# Enrich với resource attributes
resource:
attributes:
- key: environment
value: production
action: insert
# Tail-based sampling
tail_sampling:
decision_wait: 10s # Chờ 10s để có đủ spans
num_traces: 50000 # Giữ tối đa 50k traces trong memory
policies:
# Luôn sample khi có error
- name: errors
type: status_code
status_code: {status_codes: [ERROR]}
# Luôn sample khi chậm hơn 1s
- name: slow_traces
type: latency
latency: {threshold_ms: 1000}
# 5% còn lại
- name: probabilistic
type: probabilistic
probabilistic: {sampling_percentage: 5}
# Giới hạn memory
memory_limiter:
check_interval: 1s
limit_percentage: 75
spike_limit_percentage: 15
exporters:
# Self-hosted Jaeger
jaeger:
endpoint: jaeger-collector.monitoring:14250
tls:
insecure: true
# Grafana Tempo
otlp/tempo:
endpoint: tempo.monitoring:4317
tls:
insecure: true
# Metrics sang Prometheus
prometheus:
endpoint: 0.0.0.0:8889
# Logs sang Loki
loki:
endpoint: http://loki.monitoring:3100/loki/api/v1/push
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch, resource, tail_sampling]
exporters: [jaeger, otlp/tempo]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, batch, resource]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, batch, resource]
exporters: [loki]
5. イェーガー — トレースバックエンド
5.1 イェーガーアーキテクチャ
┌──────────────────────────────────────────────────┐
│ Jaeger │
│ │
│ ┌────────────────┐ ┌────────────────────┐ │
│ │ Jaeger │ │ Jaeger Query │ │
│ │ Collector │ │ (UI + API) │ │
│ │ port: 14250 │ │ port: 16686 │ │
│ └───────┬────────┘ └────────▲───────────┘ │
│ │ │ │
│ ┌───────▼──────────────────────┴───────────┐ │
│ │ Storage Backend │ │
│ │ Elasticsearch / Cassandra / BadgerDB │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
5.2 Yeger を Kubernetes にデプロイする
# Sử dụng Jaeger Operator
kubectl create namespace monitoring
kubectl apply -f https://github.com/jaegertracing/jaeger-operator/releases/latest/download/jaeger-operator.yaml -n monitoring
# Jaeger instance (production với Elasticsearch)
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: jaeger
namespace: monitoring
spec:
strategy: production
storage:
type: elasticsearch
options:
es:
server-urls: https://elasticsearch:9200
query:
replicas: 2
serviceType: ClusterIP
collector:
replicas: 3
resources:
limits:
memory: 1Gi
cpu: 500m
---
# AllInOne cho dev/staging (memory storage)
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: jaeger-dev
spec:
strategy: allInOne
allInOne:
image: jaegertracing/all-in-one:latest
6. トレース解析 — 実際のデバッグ
6.1 遅いトレースを見つける
イェーガー UI の場合:
- サービス、操作の選択
- フィルター:
min Duration > 500ms - 期間 DESC で並べ替えます
- 最長トレースをクリック→スパンウォーターフォールを分析
6.2 ウォーターフォールによる根本原因分析
Trace: abc123 — Total: 2.3s ← SLO vi phạm (target: < 500ms)
│
├─ API Gateway (5ms)
└─ OrderService.createOrder (2295ms) ← vấn đề ở đây
│
├─ DB INSERT orders (8ms) ← OK
│
├─ InventoryService.reserve (2250ms) ← BOTTLENECK
│ │
│ └─ DB SELECT inventory (2200ms) ← Missing index!
│ tags:
│ db.statement: SELECT * FROM inventory WHERE product_id = ?
│ ← Không có index trên product_id
│
└─ Kafka produce (25ms) ← OK
結論: inventory テーブルに上記のインデックスがありません product_id → フルスキャンを問い合わせます。
6.3 サービスの依存関係マップ
Yeter と Grafana Tempo は サービス マップ 視覚化を提供します。
- 相互に依存するサービスのグラフを表示
- エッジごとのエラー率とレイテンシ
- ボトルネックと単一障害点を簡単に特定
7. グラファナ・テンポ
Tempo は、トレース用のログ形式のストレージです。Elasticsearch を使用すると、Jaeger よりも安価です。
# values.yaml cho Tempo Helm chart
tempo:
storage:
trace:
backend: s3
s3:
bucket: tempo-traces
region: ap-southeast-1
retention: 336h # 14 ngày
# Grafana datasource
- name: Tempo
type: tempo
url: http://tempo:3100
jsonData:
nodeGraph:
enabled: true
tracesToLogs:
datasourceUid: loki
tags: ['service', 'namespace']
tracesToMetrics:
datasourceUid: prometheus
queries:
- name: Request Rate
query: rate(http_requests_total{$$__tags}[5m])
8. メトリクス、ログ、トレースの相関関係
可観測性の真の力は、次の 3 つの柱がすべて揃ったときに発揮されます。
1. Alert firing: "Error rate > 5% on order-service" (Prometheus → PagerDuty)
↓
2. Mở Grafana dashboard Order Service
→ Thấy error rate spike lúc 10:15
↓
3. Click "View Logs" trên dashboard panel (liên kết tới Loki)
→ Lọc level=ERROR, thấy: "PaymentService timeout after 3000ms"
→ Copy traceId: abc123def456
↓
4. Click traceId link → mở Jaeger
→ Xem trace: PaymentService.charge() = 3001ms (timeout)
→ Span attributes: db.statement thực hiện query không có index
↓
5. Fix: Thêm composite index vào payment_transactions table
Deploy → verify metrics trở về bình thường
概要
| コンセプト | 目的 |
|---|---|
| トレース/スパン | ユニットは多くのサービスにわたるリクエストを追跡します。 |
| W3C トレーサレント | 標準では、HTTP ヘッダーを介してコンテキストが伝播されます。 |
| オープンテレメトリー | ベンダー中立の計測標準 |
| OTelコレクター | テレメトリの受信、処理 (サンプリング)、およびエクスポート |
| テールベースのサンプリング | 結果に基づいたサンプル、エラーを見逃さない |
| イェーガー / テンポ | バックエンドストレージとクエリトレース |
| トレース相関 | Grafana のトレース ↔ ログ ↔ メトリクス リンク |
次の記事: サーキット ブレーカーと再試行パターン
教訓
- 独自のベンダー SDK を使用しないでください。最初から OpenTelemetry を使用してください。
- テールベースのサンプリングにより、ストレージをあまり消費せずに 100% のエラーを検出します
- 可観測性の真の価値は、3 つの柱が個別に使用される場合ではなく、相互に関連する場合にあります。
traceId相関関係が機能するには、ログ、メトリック ラベル、トレースの両方に存在する必要があります