- Published on
OpenTelemetry로 마이크로서비스의 블랙박스를 해제하는 5가지 결정적 전략
- Authors
- Name
- 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)의 두 번째로 활발한 프로젝트(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)