- 1. はじめに: なぜ我々の分散システムは今なお迷宮の中なのか?
- 2. [Takeaway 1] W3C Baggage: ビジネスコンテキストを伝播させる秘密兵器
- 3. [Takeaway 2] 自動計装の限界とハイブリッド戦略の必要性
- 4. [Takeaway 3] テールベースサンプリング: データの洪水から本当の問題だけを選び出す知的フィルタ
- 5. [Takeaway 4] Semantic Conventions: データの標準化とコラボレーションの価値
- 6. [Takeaway 5] OTLP 전송 방식의 선택: gRPC vs HTTP
- 7. OTel Collector 배포 토폴로지: Agent vs Gateway
- 8. 옵저버빌리티 백엔드 비교
- 9. 프로덕션 도입 로드맵
- 10. 결론: 옵저버빌리티 카르텔로부터의 탈출
- 참고 자료
1. はじめに: なぜ我々の分散システムは今なお迷宮の中なのか?
1.1 マイクロサービスのパラドックス: 分割したのに複雑さは増した
マイクロサービスアーキテクチャ(MSA)は、独立デプロイ、技術の多様性、チームの自律性という魅力的な約束を掲げてモノリスを置き換えた。しかし、一つのユーザーリクエストがAPI Gatewayから始まり、認証サービス、商品カタログ、在庫管理、決済、通知まで 10~30個以上のサービスを連鎖的に貫通する という現実において、問題が起きたときに根本原因を追跡することは、まるで迷宮をさまようようなものだ。
ユーザーリクエストの旅路 (一般的なeコマース)
============================================
[Client] ──▶ [API Gateway] ──▶ [Auth Service]
│
├──▶ [Product Service] ──▶ [Search Engine]
│ │
│ └──▶ [Recommendation Service] ──▶ [ML Model]
│
├──▶ [Cart Service] ──▶ [Redis Cache]
│
├──▶ [Order Service] ──▶ [Inventory Service] ──▶ [Warehouse DB]
│ │
│ └──▶ [Payment Service] ──▶ [External PG API]
│
└──▶ [Notification Service] ──▶ [Email/SMS/Push]
一つの「注文完了」に最低12個のサービスが関与する。
p99レイテンシの原因を探すには? どこから見ればよいのか?
従来のAPM(Application Performance Monitoring)ツールは、この問題を部分的に解決してきた。しかし、大半の商用APMは 独自エージェントと専用プロトコル に依存する。Datadog AgentはDatadogにしかデータを送らず、New Relic AgentはNew Relicにしかデータを送らない。これがまさに Vendor Lock-in である。
1.2 Vendor Lock-inの実質的なコスト
Vendor Lock-inは単なる技術的な不便を超えて、実質的なビジネスコスト へとつながる。
| 影響領域 | 問題点 | 実際のコスト |
|---|---|---|
| ライセンス費用 | データ量ベースの課金、年々上昇する単価 | 年間で数十億ウォン規模 (大規模サービスの場合) |
| 移行コスト | 専用エージェントの再導入、ダッシュボード再構築、通知ルール再設定 | 6~12か月のエンジニアリング投入 |
| 技術的負債 | ベンダー専用SDKに結合したコード、ベンダー専用クエリ言語 | コードベース全体にわたる侵襲的な変更が必要 |
| 戦略的な柔軟性の喪失 | より良いツールが出ても移行不可、価格交渉力の低下 | 長期的な競争力の低下 |
1.3 OpenTelemetry: "Instrument Once, Export Anywhere"
OpenTelemetry(OTel)はCNCF(Cloud Native Computing Foundation)で 2番目に活発なプロジェクト(Kubernetesに次ぐ)であり、2019年にOpenTracingとOpenCensusの統合によって誕生した。OTelの中核となる哲学は、単純でありながら革命的だ。
「計装(Instrumentation)は一度だけ行い、望む場所どこへでもエクスポートする」
OTelが提供するものは大きく三つある。
- 標準ワイヤプロトコル (OTLP): テレメトリーデータの転送規格。gRPCとHTTPの両方をサポートする。
- SDKとAPI: すべての主要言語(Java, Python, Go, .NET, Node.js, Rust, C++, PHP, Ruby など)で利用できる計装ライブラリ。
- OpenTelemetry Collector: テレメトリーデータを受信、加工、ルーティングするベンダー中立なパイプライン。
OpenTelemetryのアーキテクチャ概要
============================================
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service A │ │ Service B │ │ Service C │
│ (Java SDK) │ │ (Python SDK)│ │ (Go SDK) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ OTLP │ OTLP │ OTLP
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ OpenTelemetry Collector │
│ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │
│ │Receivers │→ │Processors │→ │ Exporters │ │
│ └──────────┘ └───────────┘ └──────────────┘ │
└────────┬──────────────┬──────────────┬──────────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Jaeger │ │ Tempo │ │ Datadog │
│ (Traces) │ │ (Traces) │ │ (All-in-1)│
└───────────┘ └───────────┘ └───────────┘
OTelを導入すれば、ベンダーの乗り換えは Collectorのexporter設定を変えるだけ で完了する。コードを一行も修正することなく、JaegerからTempoへ、あるいはDatadogからGrafana Cloudへ移行できる。
本記事では、OpenTelemetryを実戦で効果的に活用するための 5つの決定的戦略 をアーキテクトの視点から深く分析する。単なる「Getting Started」レベルではなく、プロダクションレベルの意思決定に必要な深さを提供する。
2. [Takeaway 1] W3C Baggage: ビジネスコンテキストを伝播させる秘密兵器
2.1 Trace ContextとBaggageの区別
分散トレーシングにおける コンテキスト伝播(Context Propagation)は、根本的に二つに分かれる。
| 区分 | W3C Trace Context | W3C Baggage |
|---|---|---|
| W3C標準 | W3C Trace Context | W3C Baggage |
| HTTPヘッダー | traceparent, tracestate | baggage |
| 目的 | Trace ID、Span ID、サンプリングフラグの伝播 | 任意のビジネスkey-valueの伝播 |
| 必須かどうか | トレーシングのために必須 | 任意 (ビジネス要件に応じて) |
| データ例 | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 | tenantId=acme,userTier=premium,featureFlag=newCheckout |
重要なポイント: BaggageはTrace Contextと 独立して動作する。トレーシングが無効になっていてもBaggageは伝播でき、逆にトレーシングが有効であってもBaggageを使わないでいられる。この独立性がBaggageをより柔軟なツールにしている。
2.2 ビジネスユースケース: なぜBaggageが必要なのか?
Baggageの真の価値は、インフラレイヤーを超えて、ビジネスコンテキストをサービス境界をまたいで伝播できるという点にある。具体的な活用事例を見てみよう。
マルチテナント分離 (Tenant ID Propagation)
SaaS環境で、一つのリクエストがどのテナントに属するのかをダウンストリームサービスまで伝播すれば、各サービスが独立してテナント別のrate limiting、データ分離、リソース割り当てを実行できる。
Feature Flagの伝播
フロントエンドで決定されたfeature flagの状態を、バックエンドサービスチェーン全体にわたって一貫して適用できる。A/Bテストにおいて、ユーザーが実験群なのか対照群なのかをすべてのサービスが認識できる。
ユーザー等級ベースのQoS (Quality of Service)
プレミアムユーザーのリクエストにより高い優先度を与えたり、より余裕のあるタイムアウトを設定したり、より精密なサンプリング比率を適用したりできる。
コスト帰属 (Cost Attribution)
リクエストにコストセンタータグを付与することで、どの部署、どのプロジェクトがインフラコストをどれだけ消費しているかを追跡できる。
2.3 多言語での実装例
Java (Spring Boot + OTel SDK)
import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.api.baggage.BaggageEntryMetadata;
import io.opentelemetry.context.Scope;
import io.opentelemetry.api.trace.Span;
// ---- API GatewayでBaggageを設定 (リクエストのエントリポイント) ----
@RestController
public class GatewayController {
@PostMapping("/api/orders")
public ResponseEntity<?> createOrder(
@RequestHeader("X-Tenant-Id") String tenantId,
@RequestHeader("X-User-Tier") String userTier,
HttpServletRequest request) {
// W3C Baggageの設定 - ダウンストリームサービス全体に伝播される
Baggage baggage = Baggage.builder()
.put("tenantId", tenantId,
BaggageEntryMetadata.create("tenant context"))
.put("userTier", userTier,
BaggageEntryMetadata.create("qos context"))
.put("entryPoint", "order-api",
BaggageEntryMetadata.create("routing context"))
.put("requestRegion", determineRegion(request),
BaggageEntryMetadata.create("geo context"))
.build();
// Baggageを現在のContextにattach
try (Scope scope = baggage.makeCurrent()) {
// このscope内で呼び出されるすべてのダウンストリームサービスは
// 自動的にbaggageをHTTPヘッダーとして受け取る
return orderService.processOrder(request.getBody());
}
}
}
// ---- ダウンストリームのOrder ServiceでBaggageを読む ----
@Service
public class OrderService {
public void processOrder(OrderRequest order) {
// 伝播されたBaggageからビジネスコンテキストを抽出
String tenantId = Baggage.current().getEntryValue("tenantId");
String userTier = Baggage.current().getEntryValue("userTier");
// 現在のSpanにビジネス属性として追加 (検索/フィルタリング用途)
Span currentSpan = Span.current();
currentSpan.setAttribute("business.tenant_id", tenantId);
currentSpan.setAttribute("business.user_tier", userTier);
// ユーザー等級に応じたQoS分岐
if ("premium".equals(userTier)) {
processWithPriority(order);
} else {
processNormally(order);
}
}
}
Python (FastAPI + OTel SDK)
from opentelemetry import baggage, trace, context
from opentelemetry.baggage.propagation import W3CBaggagePropagator
from opentelemetry.context.context import Context
from fastapi import FastAPI, Request, Header
from typing import Optional
app = FastAPI()
tracer = trace.get_tracer("order-service")
# ---- API GatewayでBaggageを設定 ----
@app.post("/api/orders")
async def create_order(
request: Request,
x_tenant_id: Optional[str] = Header(None),
x_user_tier: Optional[str] = Header(None, alias="X-User-Tier"),
):
# Baggageの設定
ctx = baggage.set_baggage("tenantId", x_tenant_id or "unknown")
ctx = baggage.set_baggage("userTier", x_user_tier or "standard", context=ctx)
ctx = baggage.set_baggage("featureFlag", "new-checkout-v2", context=ctx)
# Contextを有効化してダウンストリーム呼び出しに自動伝播
token = context.attach(ctx)
try:
with tracer.start_as_current_span("process-order") as span:
# Baggageの値をSpan属性としても記録
tenant = baggage.get_baggage("tenantId")
tier = baggage.get_baggage("userTier")
span.set_attribute("business.tenant_id", tenant)
span.set_attribute("business.user_tier", tier)
result = await order_processor.process(request)
return {"status": "created", "order_id": result.id}
finally:
context.detach(token)
# ---- ダウンストリームのInventory ServiceでBaggageを読む ----
@app.get("/api/inventory/{product_id}")
async def check_inventory(product_id: str, request: Request):
# OTel SDKが自動的にHTTPヘッダーからBaggageを抽出
tenant_id = baggage.get_baggage("tenantId")
user_tier = baggage.get_baggage("userTier")
with tracer.start_as_current_span("check-inventory") as span:
span.set_attribute("business.tenant_id", tenant_id)
# テナント別に分離されたデータソースへアクセス
inventory = await get_tenant_inventory(tenant_id, product_id)
return {"available": inventory.quantity > 0}
Go (Gin + OTel SDK)
package main
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/attribute"
)
var tracer = otel.Tracer("order-service")
// API GatewayでBaggageを設定
func CreateOrderHandler(c *gin.Context) {
tenantID := c.GetHeader("X-Tenant-Id")
userTier := c.GetHeader("X-User-Tier")
// W3C Baggageメンバーを生成
tenantMember, _ := baggage.NewMember("tenantId", tenantID)
tierMember, _ := baggage.NewMember("userTier", userTier)
flagMember, _ := baggage.NewMember("featureFlag", "new-checkout-v2")
bag, _ := baggage.New(tenantMember, tierMember, flagMember)
// ContextにBaggageを付与
ctx := baggage.ContextWithBaggage(c.Request.Context(), bag)
// ダウンストリーム呼び出し時に自動伝播
ctx, span := tracer.Start(ctx, "process-order")
defer span.End()
span.SetAttributes(
attribute.String("business.tenant_id", tenantID),
attribute.String("business.user_tier", userTier),
)
result, err := processOrder(ctx, c.Request.Body)
if err != nil {
span.RecordError(err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, result)
}
// ダウンストリームでBaggageを読む
func processOrder(ctx context.Context, body io.ReadCloser) (*OrderResult, error) {
bag := baggage.FromContext(ctx)
tenantID := bag.Member("tenantId").Value()
userTier := bag.Member("userTier").Value()
ctx, span := tracer.Start(ctx, "validate-order")
defer span.End()
span.SetAttributes(
attribute.String("business.tenant_id", tenantID),
)
// ビジネスロジックの処理...
return &OrderResult{ID: "ord-12345"}, nil
}
2.4 Baggageのセキュリティと性能に関する考慮事項
Baggageは強力だが、セキュリティと性能の面で慎重なアプローチが必要だ。
セキュリティリスク
Baggageの伝播経路とセキュリティリスク
============================================
[内部サービス] ──HTTPヘッダ──▶ [内部サービス] ──HTTPヘッダ──▶ [外部 API]
│ │ │
│ baggage: tenantId=acme, │ baggage: tenantId=acme, │ ⚠️ 外部へ
│ userId=12345, │ userId=12345, │ Baggageが
│ email=user@acme.com │ email=user@acme.com │ 流出する!
│ │ │
└──────────────────────────────┘ │
Trust Boundary 内部 Trust Boundary 外部 ───┘
⚠️ Baggageは平文のHTTPヘッダーで送信される!
⚠️ PII(個人識別情報)は絶対に入れるな!
必須のセキュリティ対策:
- PII禁止: メールアドレス、電話番号、住民番号などの個人識別情報は絶対にBaggageに入れない。
- Trust Boundary Sanitization: 外部サービス呼び出し時にBaggageを除去またはフィルタリングする。
- 許可リスト(Allowlist)ベースの伝播: 許可されたキーだけを伝播するよう、CollectorやSDKでフィルタを設定する。
- 値のサイズ制限: W3C Baggageの仕様では合計8,192バイトを推奨しているが、実務ではできる限り小さく保つ。
# OTel CollectorでのBaggageフィルタリング設定例
processors:
# 特定のbaggageキーだけを許可するattributesプロセッサ
attributes/baggage-filter:
actions:
# 許可されたビジネスキーのみ維持
- key: baggage.tenantId
action: upsert
- key: baggage.userTier
action: upsert
# 機微なキーは削除
- key: baggage.email
action: delete
- key: baggage.userId
action: delete
性能オーバーヘッド
Baggageは サービス間のすべてのHTTPリクエストのヘッダーに含まれるため、ネットワークオーバーヘッドが発生する。
| Baggageのサイズ | 毎秒10,000リクエスト時の追加帯域 | 影響レベル |
|---|---|---|
| 100 bytes | ~1 MB/s | 無視できる |
| 500 bytes | ~5 MB/s | 軽微 |
| 2 KB | ~20 MB/s | 注意が必要 |
| 8 KB (仕様上の最大値) | ~80 MB/s | 深刻なオーバーヘッド |
Best Practice: Baggageには短い識別子(ID)だけを入れ、実際のデータはサービスがそのIDで照会する設計にする。たとえば、ユーザープロフィール全体の代わりに tenantId=acme だけを伝播し、各サービスが必要なときにテナント設定をキャッシュから照会する。
3. [Takeaway 2] 自動計装の限界とハイブリッド戦略の必要性
3.1 自動計装(Zero-Code Instrumentation)がカバーする領域
OpenTelemetryの自動計装は、コードを一行も修正せずに アプリケーションへオブザーバビリティを追加する機能だ。言語ごとにメカニズムは異なるが、中核となる原理は同じで、フレームワークとライブラリの入口/出口をintercept(横取り)して自動的にSpanを生成する。
自動計装がキャプチャするもの:
- HTTPリクエスト/レスポンス: インバウンド(サーバー)およびアウトバウンド(クライアント)のHTTP呼び出し
- データベースクエリ: JDBC、SQLAlchemy、database/sqlなどのDBドライバ呼び出し
- メッセージキュー: Kafka、RabbitMQ、SQSなどのproduce/consume処理
- gRPC呼び出し: unaryおよびstreaming RPC
- Redis、Memcachedなどのキャッシュアクセス
- フレームワーク内部のルーティング: Spring MVC、Express、Djangoなど
これだけでも インフラレイヤーの80~90%をカバーできる。大半のレイテンシ問題、エラー率の急増、サービス間の依存関係の把握が可能になる。
3.2 言語別の自動計装サポート水準
| 言語 | サポート水準 | メカニズム | 非侵襲性 | 性能オーバーヘッド | 主な特徴 |
|---|---|---|---|---|---|
| Java | 非常に高い | Bytecode Manipulation (Java Agent) | -javaagent JVMオプションを足すだけ | 3~7% CPU | 200+ ライブラリを自動サポート |
| Python | 高い | Monkey Patching | opentelemetry-instrument CLIラッパー | 5~10% CPU | Django、Flask、FastAPIなどをサポート |
| .NET | 高い | CLR Runtime Hooks | 環境変数の設定だけで有効化 | 3~5% CPU | ASP.NET Core、EF Coreをサポート |
| Node.js | 高い | Module Loading Hooks (require/import) | --require フラグを追加 | 5~8% CPU | Express、Fastify、NestJSをサポート |
| Go | 中程度 | eBPFベースまたはコンパイル時ラッピング | eBPFは非侵襲的、ラッピングはコード修正が必要 | 1~3% (eBPF) | eBPF方式はカーネル4.x+が必要 |
| Rust | 低い | 手動計装が必須 (tracing crateと連携) | コード修正が必須 | 最小 | tracing-opentelemetryクレートを使用 |
| C++ | 低い | 手動計装が必須 | コード修正が必須 | 最小 | OTel C++ SDKを直接使用 |
3.3 自動計装が取りこぼすもの: ビジネスロジックのブラックボックス
自動計装の根本的な限界は、ビジネスロジックの内部をのぞき込めないという点だ。次のようなシナリオを考えてみよう。
自動計装だけで見えるもの vs 実際に知る必要があるもの
============================================
自動計装がキャプチャするSpan:
[POST /api/orders] ──▶ [SELECT * FROM products] ──▶ [POST /payments]
200 OK, 1.2s 50ms 800ms
見える情報: 「注文APIに1.2秒かかり、DBは50ms、決済は800ms」
分からない情報: 残りの350msはどこで消費されたのか?
実際にその350msの中で起きていること:
├── 在庫可用性の検証ロジック (50ms)
├── 割引クーポン適用ルールエンジン (120ms) ← これがボトルネック!
├── 配送料の計算ロジック (30ms)
├── 不正検知(Fraud Detection)スコアの算出 (80ms)
└── 注文イベントのシリアライズ (70ms)
自動計装はこの350msを「ビジネスロジック」という
一つの不透明な塊としてしか見せてくれない。
3.4 ハイブリッド戦略: 自動計装と手動計装の組み合わせ
最適な戦略は、自動計装でインフラレイヤーをカバーし、手動計装でビジネスクリティカルなロジックを精密に計装することだ。
Java: 自動計装の設定 + 手動Spanの追加
# 1段階: 自動計装 (JVMエージェント方式)
# OTel Java Agentのダウンロード
curl -L -o opentelemetry-javaagent.jar \
https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
# JVM起動時にエージェントをアタッチ
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
-Dotel.exporter.otlp.protocol=grpc \
-Dotel.resource.attributes=service.namespace=ecommerce,deployment.environment=production \
-jar order-service.jar
// 2段階: ビジネスクリティカルなロジックに手動Spanを追加
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.common.Attributes;
@Service
public class OrderProcessingService {
// 自動計装エージェントが初期化したグローバルTracerを使用
private static final Tracer tracer =
GlobalOpenTelemetry.getTracer("order-processing", "1.0.0");
public OrderResult processOrder(OrderRequest request) {
// 自動計装: HTTPの入口Spanはすでに生成されている
// 手動計装: ビジネスロジックの詳細な段階を明示的に計装
// 割引適用ロジックの計装
Span discountSpan = tracer.spanBuilder("apply-discount-rules")
.setAttribute("business.coupon_code", request.getCouponCode())
.setAttribute("business.original_amount", request.getTotalAmount())
.startSpan();
try (var scope = discountSpan.makeCurrent()) {
DiscountResult discount = discountEngine.calculate(request);
discountSpan.setAttribute("business.discount_amount", discount.getAmount());
discountSpan.setAttribute("business.discount_type", discount.getType());
discountSpan.setAttribute("business.rules_evaluated", discount.getRulesCount());
} catch (Exception e) {
discountSpan.setStatus(StatusCode.ERROR, e.getMessage());
discountSpan.recordException(e);
throw e;
} finally {
discountSpan.end();
}
// 不正検知の計装
Span fraudSpan = tracer.spanBuilder("fraud-detection")
.setAttribute("business.user_id", request.getUserId())
.setAttribute("business.order_amount", request.getTotalAmount())
.startSpan();
try (var scope = fraudSpan.makeCurrent()) {
FraudScore score = fraudDetector.evaluate(request);
fraudSpan.setAttribute("business.fraud_score", score.getValue());
fraudSpan.setAttribute("business.fraud_decision", score.getDecision());
if (score.getValue() > 0.8) {
fraudSpan.addEvent("high-fraud-risk-detected",
Attributes.of(
AttributeKey.doubleKey("score"), score.getValue(),
AttributeKey.stringKey("reason"), score.getReason()
));
}
} finally {
fraudSpan.end();
}
// ...残りのビジネスロジック
}
}
Python: 自動計装の設定 + 手動Spanの追加
# 1段階: 自動計装パッケージのインストール
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install # 検出されたライブラリ用の計装パッケージを自動インストール
# 2段階: 自動計装でアプリケーションを起動
OTEL_SERVICE_NAME=order-service \
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
OTEL_EXPORTER_OTLP_PROTOCOL=grpc \
OTEL_RESOURCE_ATTRIBUTES=service.namespace=ecommerce,deployment.environment=production \
opentelemetry-instrument python -m uvicorn main:app --host 0.0.0.0 --port 8000
# 2段階: ビジネスロジックに手動Spanを追加
from opentelemetry import trace
from opentelemetry.trace import StatusCode
tracer = trace.get_tracer("order-processing", "1.0.0")
class OrderProcessor:
async def process(self, order: OrderRequest) -> OrderResult:
# 割引ロジックの手動計装
with tracer.start_as_current_span(
"apply-discount-rules",
attributes={
"business.coupon_code": order.coupon_code,
"business.original_amount": order.total_amount,
}
) as discount_span:
try:
discount = await self.discount_engine.calculate(order)
discount_span.set_attribute("business.discount_amount", discount.amount)
discount_span.set_attribute("business.rules_evaluated", discount.rules_count)
except DiscountError as e:
discount_span.set_status(StatusCode.ERROR, str(e))
discount_span.record_exception(e)
raise
# 不正検知の手動計装
with tracer.start_as_current_span("fraud-detection") as fraud_span:
score = await self.fraud_detector.evaluate(order)
fraud_span.set_attribute("business.fraud_score", score.value)
fraud_span.set_attribute("business.fraud_decision", score.decision)
if score.value > 0.8:
fraud_span.add_event("high-fraud-risk", attributes={
"score": score.value,
"reason": score.reason,
})
return await self._finalize_order(order, discount)
3.5 ハイブリッド戦略の適用ガイドライン
手動計装を あらゆる場所 に追加すると、かえってノイズになる。どこに手動Spanを追加するかを決める基準が重要だ。
| 手動計装を追加する基準 | 例 | 優先度 |
|---|---|---|
| コストの大きいビジネスロジック | 価格計算、割引エンジン、税額計算 | 高 |
| 外部依存とのやり取り | 独自HTTPクライアントのラッパー、レガシーAPI呼び出し | 高 |
| 条件分岐が多いロジック | 決済手段ごとの分岐、配送方法の決定 | 中 |
| バッチ/バルク処理 | 大量データの加工、ETLパイプラインの各段階 | 中 |
| 単純なCRUD操作 | 基本的なDBの読み書き | 低 (自動計装で十分) |
| ユーティリティ関数 | 文字列変換、日付フォーマット | 不要 |
4. [Takeaway 3] テールベースサンプリング: データの洪水から本当の問題だけを選び出す知的フィルタ
4.1 なぜサンプリングが必要なのか?
プロダクションのマイクロサービス環境で、すべてのリクエストのすべてのSpanを100%収集すると、どうなるだろうか?
テレメトリー量の試算例
============================================
サービス数: 30個
サービスあたり平均Span数: 5個/リクエスト
秒間リクエスト数(RPS): 10,000
Spanあたり平均サイズ: 1 KB
秒間Span数: 30 x 5 x 10,000 = 1,500,000 spans/sec
秒間データ量: 1,500,000 x 1 KB = 1.5 GB/sec
日次データ量: 1.5 GB x 86,400 = ~130 TB/day
年間ストレージ費用 (S3の場合): ~$35,000/month = ~$420,000/year
年間Datadog費用 (ingestion基準): 数十倍以上
100%の収集は非現実的だ。サンプリングは必須である。問題はどのようにサンプリングするかだ。
4.2 Head-based vs Tail-basedサンプリングの比較
| 特性 | Head-based Sampling | Tail-based Sampling |
|---|---|---|
| 決定タイミング | Trace開始時 (最初のSpan生成時) | Trace完了後 (すべてのSpan収集後) |
| 決定基準 | 確率的 (例: 10%をランダムに) | 内容ベース (エラー、レイテンシ、属性値など) |
| メモリ要求量 | ほぼなし | 高い (Trace完了を待つバッファが必要) |
| 実装場所 | SDK (アプリケーション内部) | Collector (外部パイプライン) |
| エラーTraceの保証 | 不可 (エラー発生前にドロップされうる) | 可能 (エラーTraceを100%維持できる) |
| ネットワークコスト | 低い (ドロップしたSpanは送信しない) | 高い (すべてのSpanをCollectorまで送信) |
| Collector依存性 | なし | 高い (専用Collectorインフラが必要) |
| 複雑度 | 低い | 高い |
核心的なTrade-off: Head-basedは軽いが盲目的であり、Tail-basedは知的だが重い。
4.3 Tail-basedサンプリングの中核となる前提条件: Trace-IDベースのロードバランシング
Tail-basedサンプリングが正しく動作するには、一つのTraceを構成するすべてのSpanが同一のCollectorインスタンスに到達する必要がある。そうして初めて、CollectorがTrace全体を見てサンプリングの判断を下せる。
そのために 2-tier Collectorアーキテクチャ が必要になる。
Tail-based Samplingのための2-Tier Collectorアーキテクチャ
============================================
[Service A] [Service B] [Service C] [Service D]
│ │ │ │
│ OTLP │ OTLP │ OTLP │ OTLP
▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ Tier 1: Agent Collectors │
│ (DaemonSet, 各ノードに1個) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Agent 1 │ │ Agent 2 │ │ Agent 3 │ │
│ │ (Node 1) │ │ (Node 2) │ │ (Node 3) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ │ Load Balancing Exporter │ │
│ │ (Trace IDハッシュ基準) │ │
└───────┼──────────────┼──────────────┼─────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ Tier 2: Gateway Collectors │
│ (Deployment/StatefulSet, 水平拡張) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Gateway 1 │ │ Gateway 2 │ │
│ │ │ │ │ │
│ │ Trace ID │ │ Trace ID │ │
│ │ a1xx → ここ │ │ b2xx → ここ │ │
│ │ │ │ │ │
│ │ tail_sampling │ │ tail_sampling │ │
│ │ processor │ │ processor │ │
│ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────┼──────────────────────────┘
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│ Backend │ │ Backend │
│ (Tempo) │ │ (Jaeger) │
└───────────┘ └───────────┘
4.4 OTel Collectorの設定: Tier 1 (Agent)
# otel-collector-agent.yaml
# Tier 1: 各ノードにDaemonSetとしてデプロイされるAgent Collector
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
# メモリ保護のためのリミッター (必須!)
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
# 基本属性の追加 (ノード情報など)
resource:
attributes:
- key: k8s.node.name
value: '${K8S_NODE_NAME}'
action: upsert
- key: deployment.environment
value: 'production'
action: upsert
# バッチ処理 (ネットワーク効率)
batch:
send_batch_size: 1024
send_batch_max_size: 2048
timeout: 5s
exporters:
# Trace IDベースのロードバランシング → Tier 2 Gatewayへ送信
loadbalancing:
protocol:
otlp:
tls:
insecure: true
resolver:
dns:
hostname: otel-gateway-headless.observability.svc.cluster.local
port: 4317
# MetricsとLogsは直接バックエンドへ送信 (サンプリング不要)
otlp/metrics:
endpoint: mimir.observability.svc.cluster.local:4317
tls:
insecure: true
otlp/logs:
endpoint: loki.observability.svc.cluster.local:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, resource, batch]
exporters: [loadbalancing] # TraceはGatewayへルーティング
metrics:
receivers: [otlp]
processors: [memory_limiter, resource, batch]
exporters: [otlp/metrics] # Metricsは直接送信
logs:
receivers: [otlp]
processors: [memory_limiter, resource, batch]
exporters: [otlp/logs] # Logsは直接送信
4.5 OTel Collectorの設定: Tier 2 (Gateway with Tail Sampling)
# otel-collector-gateway.yaml
# Tier 2: Tail-based Samplingを実行するGateway Collector
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
memory_limiter:
check_interval: 1s
limit_mib: 4096 # Gatewayはより多くのメモリが必要
spike_limit_mib: 1024
# ⚠️ 重要: tail_samplingの前にbatchを使ってはならない!
# batchが同じTraceのSpanを分離してしまうことがある。
# Tail-basedサンプリングのポリシー
tail_sampling:
# Trace完了を待つ時間
# システムの最大想定Trace継続時間 + ネットワークマージン
decision_wait: 30s
# 決定後に追加Spanを待つ猶予時間
num_traces: 100000 # 同時に追跡する最大Trace数
expected_new_traces_per_sec: 1000
policies:
# ポリシー1: エラーが発生したTraceは100%維持 (最優先)
- name: errors-always-keep
type: status_code
status_code:
status_codes:
- ERROR
# ポリシー2: 高レイテンシのTraceを維持 (p99以上)
- name: high-latency
type: latency
latency:
threshold_ms: 5000 # 5秒以上かかったTrace
# ポリシー3: 特定サービスのTraceは常に維持 (クリティカルサービス)
- name: critical-services
type: string_attribute
string_attribute:
key: service.name
values:
- payment-service
- order-service
enabled_regex_matching: false
# ポリシー4: プレミアムユーザーのTraceをより高い比率で維持
- name: premium-users
type: and
and:
and_sub_policy:
- name: is-premium
type: string_attribute
string_attribute:
key: business.user_tier
values: ['premium', 'enterprise']
- name: premium-rate
type: probabilistic
probabilistic:
sampling_percentage: 50 # 50%を維持
# ポリシー5: 残りの正常Traceは5%だけ維持 (コスト最適化)
- name: baseline-probabilistic
type: probabilistic
probabilistic:
sampling_percentage: 5
# tail_samplingの後にbatchを適用
batch:
send_batch_size: 2048
timeout: 10s
exporters:
otlp/tempo:
endpoint: tempo.observability.svc.cluster.local:4317
tls:
insecure: true
otlp/jaeger:
endpoint: jaeger-collector.observability.svc.cluster.local:4317
tls:
insecure: true
service:
telemetry:
metrics:
address: 0.0.0.0:8888 # Collector自体のメトリクス監視
logs:
level: info
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/tempo]
4.6 Tail Samplingのメモリサイジング計算式
Tail samplingは決定を下すまですべてのSpanをメモリに保持しなければならないため、適切なメモリ見積もりが重要だ。
メモリ要求量の計算式
============================================
Required Memory (GB) =
traces_per_second
x decision_wait_seconds
x avg_spans_per_trace
x bytes_per_span
/ 1,000,000,000
x safety_factor
計算例:
traces_per_second = 1,000
decision_wait_seconds = 30
avg_spans_per_trace = 15
bytes_per_span = 1,000 (1 KB)
safety_factor = 2.0
= 1,000 x 30 x 15 x 1,000 / 1,000,000,000 x 2.0
= 450,000,000 / 1,000,000,000 x 2.0
= 0.45 x 2.0
= 0.9 GB
→ 最小1 GB、推奨2 GBのメモリを割り当て
4.7 サンプリング戦略の選択ガイド
| シナリオ | 推奨戦略 | 根拠 |
|---|---|---|
| 導入初期、トラフィックが低い (< 1K RPS) | Head-based 100% (サンプリングなし) | 量が小さくコスト負担がない |
| 成長期、中程度のトラフィック (1K~10K RPS) | Head-based 10~50% | 単純で効果的 |
| 大規模トラフィック、エラー追跡が重要 | Tail-based (エラー100% + 正常5%) | コスト削減とエラー可視性を同時に達成 |
| 複数チーム、サービスごとに別ポリシーが必要 | Tail-basedの複合ポリシー | サービス別、ユーザー等級別の差をつけたサンプリング |
| 規制要件 (すべての取引の記録が必須) | 100%収集 + 別途アーカイブパイプライン | 規制遵守のための全数記録 |
5. [Takeaway 4] Semantic Conventions: データの標準化とコラボレーションの価値
5.1 標準のない世界の混乱
20チームがそれぞれのマイクロサービスを運用し、各チームが自由にテレメトリー属性名を決めたら、どうなるだろうか?
標準なしに各チームが自由に命名した属性
============================================
チームA (注文サービス): user_id="12345", status_code=200, method="POST"
チームB (決済サービス): userId="12345", httpStatus=200, httpMethod="POST"
チームC (在庫サービス): uid="12345", response_code=200, req_method="POST"
チームD (通知サービス): customer_id="12345", http_code=200, verb="POST"
チームE (検索サービス): user="12345", code=200, http.method="POST"
同じユーザーの同じリクエストなのに、5通りの異なる属性名が使われている。
→ サービス間の相関分析(Correlation)が不可能!
→ 統合ダッシュボードの構築が不可能!
→ 自動アラートルールの作成が不可能!
この問題を解決するのが OpenTelemetry Semantic Conventions である。
5.2 Semantic Conventionsの構造
OpenTelemetry Semantic Conventions(現在v1.40.0)は、テレメトリーデータの 標準的な属性名と意味 を定義する。主要な領域ごとの標準属性を見ていこう。
リソース属性 (Resource Attributes)
サービス自体を識別するメタデータだ。
| 属性名 | 型 | 説明 | 例 |
|---|---|---|---|
service.name | string | サービスの論理的な名前 (必須) | order-service |
service.version | string | サービスのバージョン | 2.1.0 |
service.namespace | string | サービスグループ/ネームスペース | ecommerce |
deployment.environment.name | string | デプロイ環境 | production |
host.id | string | ホストの一意な識別子 | i-0a1b2c3d4e5f6 |
host.name | string | ホスト名 | ip-10-0-1-42 |
k8s.pod.name | string | Kubernetes Pod名 | order-service-7d4f5b-x9z2k |
k8s.namespace.name | string | Kubernetesネームスペース | production |
k8s.deployment.name | string | Kubernetes Deployment名 | order-service |
HTTP属性 (Span Attributes)
HTTPリクエスト/レスポンスに対する標準属性だ。
| 属性名 | 型 | 説明 | 例 |
|---|---|---|---|
http.request.method | string | HTTPメソッド | POST |
url.full | string | URL全体 | https://api.example.com/orders |
url.path | string | URLパス | /api/orders |
http.response.status_code | int | HTTPレスポンスコード | 201 |
server.address | string | サーバーアドレス | api.example.com |
server.port | int | サーバーポート | 443 |
network.protocol.version | string | プロトコルバージョン | 2.0 |
user_agent.original | string | User-Agentヘッダー原文 | Mozilla/5.0... |
データベース属性
| 属性名 | 型 | 説明 | 例 |
|---|---|---|---|
db.system | string | データベースシステム | postgresql |
db.namespace | string | データベース名 | orders_db |
db.operation.name | string | DB操作名 | SELECT |
db.query.text | string | クエリテキスト (sanitized) | SELECT * FROM orders WHERE id = ? |
db.collection.name | string | テーブル/コレクション名 | orders |
5.3 標準化の実戦的な効果: Before vs After
Before (標準なし):
チームごとに異なる属性名 → 統合クエリが不可能
─────────────────────────────────────
Grafanaで「すべてのサービスの5xxエラー率」ダッシュボードを作るには?
Panel 1 (注文): rate({status_code=~"5.."})
Panel 2 (決済): rate({httpStatus=~"5.."})
Panel 3 (在庫): rate({response_code=~"5.."})
→ サービスごとに別のクエリが必要。新サービス追加時はダッシュボードの修正が必須。
After (Semantic Conventions適用):
すべてのチームが同じ属性名を使用 → 単一クエリで全体を照会
─────────────────────────────────────
Grafanaで「すべてのサービスの5xxエラー率」ダッシュボード:
単一クエリ: rate({http.response.status_code=~"5.."}) by (service.name)
→ すべてのサービスが自動的に含まれる。新サービス追加時も変更なし。
5.4 Cross-Signal Correlation: Logs、Metrics、Tracesの統合
Semantic Conventionsのもう一つの強力な利点が シグナル間の相関分析(Cross-Signal Correlation)だ。同じ属性名を使えば、Traceで見つけた異常をMetricで確認し、関連するLogをすぐに照会できる。
Cross-Signal Correlationのワークフロー
============================================
1. Alertの発生:
metric: http_server_request_duration_seconds{service.name="order-service"} > 5s
2. Traceで原因を追跡:
trace: service.name="order-service"
AND http.response.status_code >= 500
→ Spanでdb.operation.name="SELECT",
db.collection.name="orders" を確認
3. 関連するLogを照会:
log: service.name="order-service"
AND trace_id="abc123"
→ "Connection pool exhausted" のエラーログを発見
すべてのシグナルでservice.name、trace_idなど同じ属性名を使うため
このワークフローが自然につながる。
5.5 Semantic Conventions適用のためのCollector設定
チームごとにすでに異なる属性名を使っているなら、Collectorの attributes プロセッサで 中央で正規化 できる。
# Collectorで属性名をSemantic Conventionsに合わせて正規化
processors:
# レガシーな属性名を標準の属性名へ変換
attributes/normalize:
actions:
# HTTP属性の正規化
- key: http.method
action: upsert
from_attribute: httpMethod # チームBの属性名
- key: http.method
action: upsert
from_attribute: req_method # チームCの属性名
- key: http.method
action: upsert
from_attribute: verb # チームDの属性名
# レガシーキーの削除
- key: httpMethod
action: delete
- key: req_method
action: delete
- key: verb
action: delete
# ユーザーIDの正規化
- key: enduser.id
action: upsert
from_attribute: user_id
- key: enduser.id
action: upsert
from_attribute: userId
- key: enduser.id
action: upsert
from_attribute: uid
- key: enduser.id
action: upsert
from_attribute: customer_id
# 레거시 키 삭제
- key: user_id
action: delete
- key: userId
action: delete
- key: uid
action: delete
- key: customer_id
action: delete
# HTTP 상태 코드 정규화
- key: http.response.status_code
action: upsert
from_attribute: status_code
- key: http.response.status_code
action: upsert
from_attribute: httpStatus
- key: http.response.status_code
action: upsert
from_attribute: response_code
- key: http.response.status_code
action: upsert
from_attribute: http_code
# 필수 리소스 속성이 누락된 경우 기본값 추가
resource:
attributes:
- key: service.namespace
value: 'default'
action: insert # 이미 존재하면 덮어쓰지 않음
- key: deployment.environment.name
value: 'production'
action: insert
5.6 커스텀 비즈니스 속성 네이밍 가이드
Semantic Conventions에 정의되지 않은 비즈니스 속성을 추가할 때는 일관된 네이밍 규칙을 따라야 한다.
| 규칙 | 좋은 예 | 나쁜 예 |
|---|---|---|
| 네임스페이스를 접두사로 사용 | business.order_id | orderId |
| snake_case 사용 | business.payment_method | business.paymentMethod |
| 단위를 속성명에 포함 | business.order_total_usd | business.order_total |
불리언은 is_ 접두사 | business.is_first_order | business.first_order |
| 열거형은 소문자 | business.user_tier="premium" | business.user_tier="PREMIUM" |
6. [Takeaway 5] OTLP 전송 방식의 선택: gRPC vs HTTP
6.1 OTLP(OpenTelemetry Protocol)란?
OTLP는 OpenTelemetry가 정의한 텔레메트리 데이터 전송을 위한 표준 프로토콜이다. 현재 OTLP 스펙 1.9.0 기준으로, Traces, Metrics, Logs(그리고 최근 추가된 Profiles)를 단일 프로토콜로 전송할 수 있다. OTLP는 세 가지 전송 방식을 지원한다.
6.2 성능 비교
| 항목 | OTLP/gRPC | OTLP/HTTP (Protobuf) | OTLP/HTTP (JSON) |
|---|---|---|---|
| 기본 포트 | 4317 | 4318 | 4318 |
| 직렬화 형식 | Protobuf (바이너리) | Protobuf (바이너리) | JSON (텍스트) |
| 전송 프로토콜 | HTTP/2 (양방향 스트리밍) | HTTP/1.1 또는 HTTP/2 | HTTP/1.1 또는 HTTP/2 |
| 측정 처리량 | ~10,000-50,000 spans/sec | ~5,000-30,000 spans/sec | ~3,000-15,000 spans/sec |
| 상대 CPU 사용 | 1.0x (기준) | 1.2x | 2.5x |
| 페이로드 크기 | 가장 작음 (1.0x) | 작음 (1.0x, 동일 Protobuf) | 큼 (3~5x) |
| 연결 관리 | Connection Multiplexing | Connection per request | Connection per request |
| 헤더 압축 | HPACK (자동) | 없음 | 없음 |
| 압축 지원 | gzip, zstd | gzip, zstd | gzip, zstd |
6.3 gRPC의 장점과 적합한 시나리오
gRPC가 유리한 경우:
- 고처리량 환경: 초당 10,000 spans 이상을 생성하는 서비스
- 서비스 간 내부 통신: Kubernetes 클러스터 내부 같은 신뢰할 수 있는 네트워크
- HTTP/2 지원 인프라: 서비스 메시, 내부 로드 밸런서
- 양방향 스트리밍: 연속적인 텔레메트리 스트리밍이 필요한 경우
- 대역폭 절감이 중요한 경우: 바이너리 인코딩 + HPACK 압축
# gRPC Exporter 설정 예시 (SDK)
# 환경변수 방식
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_EXPORTER_OTLP_COMPRESSION=gzip
OTEL_EXPORTER_OTLP_TIMEOUT=10000
# Collector Receiver 설정 (gRPC)
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
max_recv_msg_size_mib: 16 # 최대 수신 메시지 크기
max_concurrent_streams: 100 # 동시 스트림 수
keepalive:
server_parameters:
max_connection_idle: 60s
max_connection_age: 300s
time: 30s
timeout: 10s
tls:
cert_file: /certs/server.crt
key_file: /certs/server.key
client_ca_file: /certs/ca.crt # mTLS
6.4 HTTP의 장점과 적합한 시나리오
HTTP가 유리한 경우:
- 방화벽/프록시 호환성: 기업 네트워크에서 HTTP/1.1만 허용하는 경우
- 브라우저 환경: 웹 프론트엔드에서 직접 텔레메트리 전송 (CORS 지원)
- 서버리스/엣지: AWS Lambda, Cloudflare Workers 등 gRPC 지원이 어려운 환경
- 디버깅: JSON 형식으로 전송하여 페이로드를 사람이 읽을 수 있음
- 범용 인프라: HTTP 로드 밸런서, CDN, 리버스 프록시 호환
# HTTP Exporter 설정 예시 (SDK)
# 환경변수 방식 (Protobuf)
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_EXPORTER_OTLP_COMPRESSION=gzip
OTEL_EXPORTER_OTLP_TIMEOUT=10000
# HTTP JSON (디버깅 용도)
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
# Collector Receiver 설정 (HTTP)
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
cors:
allowed_origins:
- 'https://*.example.com' # 브라우저 CORS 허용
allowed_headers:
- 'Content-Type'
- 'X-Custom-Header'
max_age: 7200
tls:
cert_file: /certs/server.crt
key_file: /certs/server.key
6.5 전송 방식 선택 의사결정 트리
OTLP 전송 방식 선택 의사결정 트리
============================================
시작: 텔레메트리 전송 방식을 선택해야 한다
Q1. 브라우저 또는 서버리스 환경인가?
├── YES → HTTP/Protobuf 사용
│ (gRPC는 브라우저/Lambda에서 지원 불가)
│
└── NO ──▶ Q2. 방화벽이 HTTP/2 또는 gRPC를 차단하는가?
├── YES → HTTP/Protobuf 사용
│ (HTTP/1.1로 폴백 가능)
│
└── NO ──▶ Q3. 초당 10,000 spans 이상인가?
├── YES → gRPC 사용
│ (HTTP/2 멀티플렉싱, 최고 성능)
│
└── NO ──▶ Q4. 디버깅이 주 목적인가?
├── YES → HTTP/JSON 사용
│ (사람이 읽을 수 있음)
│
└── NO → HTTP/Protobuf 사용
(가장 범용적, 무난한 선택)
6.6 하이브리드 구성: gRPC + HTTP 동시 수신
실무에서는 Collector가 gRPC와 HTTP를 동시에 수신하도록 구성하여, 다양한 클라이언트를 모두 수용하는 전략이 일반적이다.
# 하이브리드 Receiver 설정 (gRPC + HTTP 동시 수신)
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
max_recv_msg_size_mib: 16
keepalive:
server_parameters:
max_connection_idle: 60s
time: 30s
timeout: 10s
http:
endpoint: 0.0.0.0:4318
cors:
allowed_origins: ['*']
allowed_headers: ['*']
# 서비스 유형별 권장 구성
# ┌─────────────────────────┬──────────────────────────┐
# │ 서비스 유형 │ 권장 전송 방식 │
# ├─────────────────────────┼──────────────────────────┤
# │ 백엔드 (Java, Go) │ gRPC (포트 4317) │
# │ 프론트엔드 (Browser JS) │ HTTP/Protobuf (포트 4318) │
# │ 서버리스 (Lambda) │ HTTP/Protobuf (포트 4318) │
# │ 디버깅/테스트 │ HTTP/JSON (포트 4318) │
# │ IoT/Edge │ HTTP/Protobuf (포트 4318) │
# └─────────────────────────┴──────────────────────────┘
7. OTel Collector 배포 토폴로지: Agent vs Gateway
7.1 배포 패턴 개요
OpenTelemetry Collector는 세 가지 핵심 배포 패턴을 지원하며, 각각의 장단점과 적합한 시나리오가 다르다.
OTel Collector 배포 패턴 비교
============================================
Pattern 1: Agent (DaemonSet)
┌─────────────────────────────────┐
│ Kubernetes Node │
│ ┌─────────┐ ┌─────────┐ │
│ │Service A│ │Service B│ │
│ └────┬────┘ └────┬────┘ │
│ │ localhost │ │
│ ▼ ▼ │
│ ┌──────────────────────┐ │
│ │ OTel Collector │ │
│ │ (DaemonSet Pod) │ │
│ └──────────┬───────────┘ │
└─────────────┼──────────────────┘
▼
┌──────────┐
│ Backend │
└──────────┘
Pattern 2: Sidecar
┌──────────────────────────────┐
│ Application Pod │
│ ┌──────────┐ ┌───────────┐ │
│ │App │ │OTel │ │
│ │Container │→│Collector │ │
│ │ │ │(Sidecar) │ │
│ └──────────┘ └─────┬─────┘ │
└─────────────────────┼────────┘
▼
┌──────────┐
│ Backend │
└──────────┘
Pattern 3: Gateway (Deployment)
[Service A] [Service B] [Service C]
│ │ │
└────────────┼────────────┘
▼
┌──────────────────┐
│ OTel Collector │
│ (Deployment, │
│ replicas: 3+) │
│ + HPA │
└────────┬─────────┘
▼
┌──────────┐
│ Backend │
└──────────┘
7.2 배포 패턴별 상세 비교
| 특성 | Agent (DaemonSet) | Sidecar | Gateway (Deployment) |
|---|---|---|---|
| 배포 단위 | 노드당 1개 | Pod당 1개 | 클러스터당 N개 (수평 확장) |
| 리소스 격리 | 노드의 모든 Pod 공유 | Pod 전용 리소스 | 중앙 집중 |
| 장애 영향 범위 | 해당 노드의 모든 서비스 | 해당 Pod만 | 모든 서비스 (단일 장애점) |
| 설정 관리 | 노드 공통 설정 | Pod별 맞춤 설정 가능 | 중앙 집중 설정 |
| 네트워크 지연 | 최소 (localhost) | 최소 (localhost) | 네트워크 홉 존재 |
| 리소스 효율 | 높음 | 낮음 (Pod마다 중복) | 매우 높음 |
| 적합한 시나리오 | 범용, 가장 일반적 | 멀티테넌트, 보안 격리 필요 | 중앙 처리, 샘플링, 라우팅 |
7.3 Kubernetes DaemonSet 배포 예시
# otel-collector-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: otel-collector-agent
namespace: observability
labels:
app: otel-collector
component: agent
spec:
selector:
matchLabels:
app: otel-collector
component: agent
template:
metadata:
labels:
app: otel-collector
component: agent
spec:
serviceAccountName: otel-collector
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:0.120.0
args:
- '--config=/conf/otel-collector-config.yaml'
ports:
- containerPort: 4317 # gRPC
hostPort: 4317
protocol: TCP
- containerPort: 4318 # HTTP
hostPort: 4318
protocol: TCP
- containerPort: 8888 # Prometheus metrics
protocol: TCP
env:
- name: K8S_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: K8S_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: GOMEMLIMIT
value: '460MiB' # Go runtime 메모리 제한
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
volumeMounts:
- name: config
mountPath: /conf
livenessProbe:
httpGet:
path: /
port: 13133 # health_check extension
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 13133
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: config
configMap:
name: otel-agent-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-agent-config
namespace: observability
data:
otel-collector-config.yaml: |
extensions:
health_check:
endpoint: 0.0.0.0:13133
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# 노드 레벨 메트릭 수집 (호스트 메트릭)
hostmetrics:
collection_interval: 30s
scrapers:
cpu: {}
memory: {}
disk: {}
network: {}
# Kubelet 메트릭 수집
kubeletstats:
collection_interval: 30s
auth_type: "serviceAccount"
endpoint: "https://${K8S_NODE_NAME}:10250"
insecure_skip_verify: true
processors:
memory_limiter:
check_interval: 1s
limit_mib: 400
spike_limit_mib: 100
batch:
send_batch_size: 1024
timeout: 5s
resource:
attributes:
- key: k8s.node.name
value: "${K8S_NODE_NAME}"
action: upsert
# Kubernetes 메타데이터 자동 추가
k8sattributes:
auth_type: "serviceAccount"
extract:
metadata:
- k8s.pod.name
- k8s.pod.uid
- k8s.namespace.name
- k8s.deployment.name
- k8s.node.name
labels:
- tag_name: app.label.team
key: team
from: pod
annotations:
- tag_name: app.annotation.version
key: app-version
from: pod
exporters:
# Traces → Gateway (Tail Sampling을 위해)
loadbalancing:
protocol:
otlp:
tls:
insecure: true
resolver:
dns:
hostname: otel-gateway-headless.observability.svc.cluster.local
port: 4317
# Metrics → Prometheus/Mimir 직접 전송
prometheusremotewrite:
endpoint: http://mimir.observability.svc.cluster.local:9009/api/v1/push
# Logs → Loki 직접 전송
otlp/logs:
endpoint: loki.observability.svc.cluster.local:4317
tls:
insecure: true
service:
extensions: [health_check]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, resource, batch]
exporters: [loadbalancing]
metrics:
receivers: [otlp, hostmetrics, kubeletstats]
processors: [memory_limiter, k8sattributes, resource, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, resource, batch]
exporters: [otlp/logs]
7.4 Gateway Deployment 예시
# otel-collector-gateway.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector-gateway
namespace: observability
spec:
replicas: 3
selector:
matchLabels:
app: otel-collector
component: gateway
template:
metadata:
labels:
app: otel-collector
component: gateway
spec:
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:0.120.0
args:
- '--config=/conf/otel-collector-config.yaml'
ports:
- containerPort: 4317
protocol: TCP
env:
- name: GOMEMLIMIT
value: '3600MiB'
resources:
requests:
cpu: 1000m
memory: 2Gi
limits:
cpu: 4000m
memory: 4Gi
volumeMounts:
- name: config
mountPath: /conf
volumes:
- name: config
configMap:
name: otel-gateway-config
---
# Headless Service (Load Balancing Exporter의 DNS 리졸버용)
apiVersion: v1
kind: Service
metadata:
name: otel-gateway-headless
namespace: observability
spec:
clusterIP: None
selector:
app: otel-collector
component: gateway
ports:
- port: 4317
targetPort: 4317
protocol: TCP
---
# HPA (수평 자동 확장)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: otel-gateway-hpa
namespace: observability
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: otel-collector-gateway
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75
7.5 OpenTelemetry Operator를 활용한 자동화
Kubernetes 환경에서는 OpenTelemetry Operator를 사용하면 Collector의 배포와 자동 계측 주입을 선언적으로 관리할 수 있다.
# OpenTelemetryCollector CRD를 통한 선언적 배포
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel-agent
namespace: observability
spec:
mode: daemonset # daemonset | deployment | sidecar | statefulset
image: otel/opentelemetry-collector-contrib:0.120.0
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
config:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_mib: 400
batch:
send_batch_size: 1024
timeout: 5s
exporters:
otlp:
endpoint: otel-gateway.observability.svc.cluster.local:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp]
---
# 자동 계측 주입 (Java 애플리케이션에 OTel Agent 자동 설치)
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: java-instrumentation
namespace: production
spec:
exporter:
endpoint: http://otel-agent.observability.svc.cluster.local:4317
propagators:
- tracecontext
- baggage
sampler:
type: parentbased_traceidratio
argument: '0.25' # Head-based 25% (Tail sampling이 추가 필터링)
java:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
env:
- name: OTEL_JAVAAGENT_DEBUG
value: 'false'
---
# 이 annotation을 Pod/Deployment에 추가하면 자동 계측 활성화
# metadata:
# annotations:
# instrumentation.opentelemetry.io/inject-java: "java-instrumentation"
8. 옵저버빌리티 백엔드 비교
올바른 백엔드 선택은 옵저버빌리티 전략의 성패를 좌우한다. OpenTelemetry의 벤더 중립성 덕분에 백엔드를 자유롭게 선택하고 교체할 수 있다.
8.1 오픈소스 vs 상용 백엔드 비교
| 솔루션 | 유형 | Traces | Metrics | Logs | OTLP 네이티브 | 비용 모델 | 특징 |
|---|---|---|---|---|---|---|---|
| Jaeger | OSS | O | X | X | O | 무료 | CNCF 졸업 프로젝트, 경량 추적 전용 |
| Grafana Tempo | OSS | O | X | X | O | 무료 | 오브젝트 스토리지 기반, 인덱스 불필요 |
| Grafana Mimir | OSS | X | O | X | O | 무료 | Prometheus 호환 장기 저장소 |
| Grafana Loki | OSS | X | X | O | O | 무료 | 레이블 기반 로그 집계 |
| SigNoz | OSS | O | O | O | O | 무료 | 올인원 OSS 옵저버빌리티 (ClickHouse 기반) |
| Grafana Cloud | SaaS | O | O | O | O | 사용량 기반 (무료 티어 있음) | Tempo+Mimir+Loki 통합 |
| Datadog | SaaS | O | O | O | O | 호스트 + 사용량 기반 | 가장 풍부한 기능, 높은 가격 |
| New Relic | SaaS | O | O | O | O | 사용량 기반 (무료 100GB/월) | 넉넉한 무료 티어 |
| Elastic APM | OSS/SaaS | O | O | O | O | 노드 + 사용량 기반 | Elasticsearch 기반, 강력한 검색 |
| Honeycomb | SaaS | O | X | X | O | 이벤트 기반 | 고카디널리티 분석 특화 |
| AWS X-Ray | SaaS | O | X | X | O (ADOT 경유) | 사용량 기반 | AWS 네이티브 통합 |
| Dynatrace | SaaS | O | O | O | O | 호스트 기반 | AI 기반 자동 근본 원인 분석 |
8.2 백엔드 선택 기준
| 요구사항 | 권장 솔루션 | 근거 |
|---|---|---|
| 비용 최소화, 자체 운영 가능 | SigNoz 또는 Grafana Stack (Tempo+Mimir+Loki) | OSS 무료, 커뮤니티 지원 |
| 운영 부담 최소화 | Grafana Cloud 또는 New Relic | 관리형 SaaS, 무료 티어 존재 |
| 대기업, 풍부한 기능 필요 | Datadog 또는 Dynatrace | 가장 성숙한 기능셋, 엔터프라이즈 지원 |
| AWS 올인 | AWS X-Ray + CloudWatch | 네이티브 AWS 통합, IAM 연동 |
| 고카디널리티 분석 | Honeycomb | BubbleUp 등 고유 분석 기능 |
| Elasticsearch 이미 운영 중 | Elastic APM | 기존 인프라 활용 |
9. 프로덕션 도입 로드맵
9.1 단계별 도입 체크리스트
OpenTelemetry 도입은 한 번에 모든 것을 적용하는 것이 아니라, 단계적이고 점진적으로 진행해야 한다.
Phase 1: 기반 구축 (2~4주)
체크리스트
============================================
[ ] OTel Collector를 Kubernetes DaemonSet으로 배포
[ ] OTLP Receiver (gRPC + HTTP) 활성화
[ ] 백엔드 선택 및 Exporter 설정 (Tempo, Jaeger 등)
[ ] memory_limiter 프로세서 설정
[ ] health_check extension 설정
[ ] Collector 자체 메트릭 모니터링 (Prometheus scrape)
[ ] 파일럿 서비스 1~2개에 자동 계측 적용
[ ] 기본 대시보드 구축 (RED metrics: Rate, Errors, Duration)
Phase 2: 확산 및 표준화 (4~8주)
체크리스트
============================================
[ ] Semantic Conventions 가이드 작성 및 팀 교육
[ ] 레거시 속성명 정규화를 위한 attributes 프로세서 설정
[ ] 자동 계측을 전체 서비스로 확산
[ ] k8sattributes 프로세서로 Kubernetes 메타데이터 자동 부착
[ ] resource 프로세서로 필수 리소스 속성 보장
[ ] service.name, deployment.environment 등 필수 속성 검증
[ ] 알림 규칙 설정 (에러율, 지연 시간 임계치)
Phase 3: 고도화 (8~12주)
체크리스트
============================================
[ ] 비즈니스 크리티컬 로직에 수동 계측 추가 (하이브리드 전략)
[ ] W3C Baggage를 통한 비즈니스 컨텍스트 전파 도입
[ ] Tail-based sampling을 위한 2-tier Collector 아키텍처 구축
[ ] Load Balancing Exporter 설정 (Trace ID 기반)
[ ] tail_sampling 프로세서 정책 설계 및 튜닝
[ ] Cross-signal correlation 대시보드 구축
[ ] SLO(Service Level Objective) 기반 모니터링
Phase 4: 최적화 및 운영 성숙 (지속적)
체크리스트
============================================
[ ] 샘플링 정책 지속적 튜닝 (비용 vs 가시성 최적화)
[ ] Collector 리소스 사용량 모니터링 및 자동 확장(HPA) 적용
[ ] OTel Operator 도입 (자동 계측 주입 자동화)
[ ] 멀티 클러스터 / 멀티 리전 옵저버빌리티 통합
[ ] Baggage 보안 정책 (Trust Boundary 필터링)
[ ] 팀별 셀프 서비스 대시보드 가이드라인
[ ] 정기적인 옵저버빌리티 성숙도 평가
9.2 완전한 파이프라인 구성 예시
프로덕션 환경에서의 완전한 OTel Collector 파이프라인 구성 예시를 제시한다. 이 설정은 Receiver, Processor, Exporter의 전체 흐름을 보여준다.
# production-otel-collector.yaml
# 프로덕션 레벨의 완전한 Collector 파이프라인
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777 # Go pprof 프로파일링
zpages:
endpoint: 0.0.0.0:55679 # 디버깅용 zPages
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
max_recv_msg_size_mib: 16
http:
endpoint: 0.0.0.0:4318
# Prometheus 메트릭 스크래핑 (기존 Prometheus 타겟 호환)
prometheus:
config:
scrape_configs:
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
# 호스트 메트릭
hostmetrics:
collection_interval: 30s
scrapers:
cpu:
metrics:
system.cpu.utilization:
enabled: true
memory:
metrics:
system.memory.utilization:
enabled: true
disk: {}
network: {}
processors:
# 1. 메모리 보호 (항상 첫 번째)
memory_limiter:
check_interval: 1s
limit_mib: 1800
spike_limit_mib: 400
# 2. Kubernetes 메타데이터 부착
k8sattributes:
auth_type: 'serviceAccount'
passthrough: false
extract:
metadata:
- k8s.pod.name
- k8s.pod.uid
- k8s.namespace.name
- k8s.deployment.name
- k8s.statefulset.name
- k8s.daemonset.name
- k8s.node.name
labels:
- tag_name: service.team
key: team
from: pod
- tag_name: service.component
key: component
from: pod
# 3. 리소스 속성 보장
resource:
attributes:
- key: deployment.environment.name
value: 'production'
action: insert
- key: service.namespace
value: 'default'
action: insert
# 4. 속성 정규화 (Semantic Conventions)
attributes/normalize:
actions:
- key: http.request.method
action: upsert
from_attribute: http.method
- key: http.response.status_code
action: upsert
from_attribute: http.status_code
# 5. 민감 정보 제거
attributes/redact:
actions:
- key: db.query.text
action: hash # 쿼리를 해시로 대체
- key: http.request.header.authorization
action: delete
- key: http.request.header.cookie
action: delete
# 6. 불필요한 Span 필터링
filter/drop-health:
error_mode: ignore
traces:
span:
- 'attributes["http.target"] == "/health"'
- 'attributes["http.target"] == "/readyz"'
- 'attributes["http.target"] == "/livez"'
- 'attributes["http.route"] == "/metrics"'
# 7. 배치 처리
batch:
send_batch_size: 2048
send_batch_max_size: 4096
timeout: 10s
exporters:
# Traces → Grafana Tempo
otlp/tempo:
endpoint: tempo.observability.svc.cluster.local: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
# Metrics → Prometheus Remote Write (Mimir)
prometheusremotewrite:
endpoint: http://mimir.observability.svc.cluster.local:9009/api/v1/push
tls:
insecure: true
retry_on_failure:
enabled: true
# Logs → Grafana Loki
otlp/loki:
endpoint: loki.observability.svc.cluster.local:4317
tls:
insecure: true
# 디버깅용 (개발 환경에서만 활성화)
debug:
verbosity: basic
sampling_initial: 5
sampling_thereafter: 200
service:
extensions: [health_check, pprof, zpages]
telemetry:
metrics:
address: 0.0.0.0:8888
level: detailed
logs:
level: info
encoding: json
pipelines:
traces:
receivers: [otlp]
processors:
- memory_limiter
- k8sattributes
- resource
- attributes/normalize
- attributes/redact
- filter/drop-health
- batch
exporters: [otlp/tempo]
metrics:
receivers: [otlp, prometheus, hostmetrics]
processors:
- memory_limiter
- k8sattributes
- resource
- batch
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors:
- memory_limiter
- k8sattributes
- resource
- attributes/redact
- batch
exporters: [otlp/loki]
10. 결론: 옵저버빌리티 카르텔로부터의 탈출
10.1 OpenTelemetry가 바꾸는 게임의 규칙
전통적인 APM 시장은 "옵저버빌리티 카르텔"과 같은 구조였다. 한 번 특정 벤더의 에이전트를 설치하면, 데이터 형식, 쿼리 언어, 대시보드, 알림 규칙 모두가 해당 벤더에 종속된다. 벤더 교체 비용이 너무 높아 가격이 올라도 참을 수밖에 없는 구조다.
OpenTelemetry는 이 구조를 근본적으로 변화시킨다.
- 계측(Instrumentation)과 백엔드(Backend)의 분리: OTel SDK로 한 번 계측하면, 백엔드는 Collector 설정만으로 교체 가능
- 표준 와이어 프로토콜(OTLP): 어떤 벤더도 자체 프로토콜을 강요할 수 없음
- 커뮤니티 주도: CNCF의 두 번째로 활발한 프로젝트, 500+ 기여자, 주요 벤더 모두 참여
10.2 전략적 가시성의 실질적 효과
이 글에서 다룬 5가지 전략은 단순한 기술적 호기심이 아니라, 실질적인 비즈니스 가치로 이어진다.
| 전략 | 비즈니스 가치 |
|---|---|
| W3C Baggage | 멀티테넌트 격리, 사용자 등급별 차별화된 SLA, 비용 귀속 |
| 하이브리드 계측 | 비즈니스 로직 병목 30초 내 식별, MTTR(평균 복구 시간) 60% 이상 단축 |
| Tail-based Sampling | 텔레메트리 비용 80~95% 절감하면서 에러 Trace 100% 보존 |
| Semantic Conventions | 팀 간 협업 마찰 제거, 통합 대시보드 구축 시간 90% 단축 |
| OTLP 전송 최적화 | 환경별 최적 성능, 네트워크 비용 최소화 |
10.3 시작을 위한 다음 단계
- 지금 당장: 파일럿 서비스 하나에 OTel 자동 계측을 적용하고, OTel Collector를 통해 오픈소스 백엔드(Jaeger 또는 Tempo)로 데이터를 전송한다.
- 2주 이내: Semantic Conventions 가이드를 팀에 공유하고, 표준 속성명 사용을 합의한다.
- 4주 이내: 비즈니스 크리티컬 서비스에 수동 계측을 추가하여 하이브리드 전략을 검증한다.
- 8주 이내: Tail-based sampling을 도입하고 비용 최적화를 시작한다.
- 12주 이내: W3C Baggage로 비즈니스 컨텍스트 전파를 구현하여 완전한 옵저버빌리티를 달성한다.
벤더의 관측이 아닌, 우리의 관측을 되찾을 때다. OpenTelemetry는 그 여정의 가장 확실한 출발점이다.
참고 자료
- OpenTelemetry 공식 문서
- OpenTelemetry Collector 릴리스
- OTLP 스펙 1.9.0
- OpenTelemetry Semantic Conventions v1.40.0
- W3C Trace Context 표준
- W3C Baggage 표준
- OpenTelemetry Sampling 개념
- Tail Sampling Processor (Contrib)
- OpenTelemetry Operator (Kubernetes)
- OpenTelemetry Baggage API 스펙
- OpenTelemetry Kubernetes 배포 가이드
- OTel Collector 배포 패턴 (New Relic)
- OTLP gRPC vs HTTP 비교 (SigNoz)