OpenTelemetry 구조

2,499 단어·5 분·원문(.md)

OpenTelemetry 이하 OTel은 클라우드 네이티브 환경에서 필수적인 관측 가능성을 위한 표준 프레임워크다.

특정 벤더 예를들어 데이터독같은 곳에 종속되지 않고 trace metric log 를 수집 처리 내보내는 통일된 방식을 제공한다.

OTel Architecture #

OTel은 단순히 데이터를 보내는 라이브러리가 아니라, 아래와 같은 계층 구조를 가진다.

API & SDK #

애플리케이션 코드 내부에서 작동하는 부분이고

  • API: 계측 instrumentation을 위한 추상적인 툴이다. 실제 동작 코드는 없고 여기서 span을 시작한다는 신호만 정의한다. 라이브러리 제작자가 사용함
  • SDK: API의 실제 구현체고 수집된 데이터를 메모리에 버퍼링하고 샘플링을 수행하며, 최종적으로 어디로 보낼지 결정한다.

Span은 otel에서 작업의 최소 단위이고 쉽게 말해서 요청 시스템에 들어와서 나갈때까지 거치는 여러 단계중 특정 시점에서 특정 시점까지 발생한 하나의 작업기록.

span은 밑에서 더 알아보고.

Data Model(Signals) #

OTel은 세 가지 핵심 신호를 처리한다

  • Traces: 요청 흐름을 추적 (span 단위)
  • Metrics: cpu 사용량, 요청횟수등 수치 데이터
  • Logs: 특정 시점에 발생한 이벤트 기록 (최근 표준화 진행중)
  • Resources: 데이터를 생성하는 주체에 대한 메타데이터 예를들면 서비스 이름, 호스트명, 컨테이너 id

OpenTelemetry Collector #

애플리케이션에서 보낸 데이터를 받아 가공한뒤 저장소로 넘겨주는 중간 서버다.

이 구조 덕분에 앱 코드를 수정하지 않고도 데이터 저장소를 바꿀 수 있다.

Span의 핵심 구성요소는 #

  • Name: 수행된 작업을 설명하는 이름 ex. GET /api/v1/users, SELECT * from orders 이런거
  • Trace ID: 전체 트랜잭션을 식별하는 고유 id로 모든 연관된 span은 동일한 tarce id를 공유
  • Span ID" 해당 span만의 고유 id
  • Parent Span ID: 이 작업을 요청한 상위 span id고 이를 통해 트리 구조를 형성ㅇ함
  • Start & End Timestamp: 작업이 시작되고 종료된 정확한 시간이다. 이 차이가 해당 작업의 latency가 된다.
  • Status: 작업의 성공 여부 unset, ok, error
  • Attributes: 필터링이나 분석을 위한 키 값 메카데이터로 http.method GET, db.system: postgresql 같은것들.
  • Events: Span 실행 도중 특정 시점에 발생한 기록이고 예를들어 캐시 적중, 재시도 시작같은 타임스탬프가 포함된 로그다.
  • Links: 다른 트레이스나 span과의 관계를 정의할때 사용한다. 예를들어 일괄 처리 작업에서 개발 작업들간의 연결

Collector의 내부 파이프라인 구조 #

Collector는 3단계 파이프라인으로 구성된다. config.yaml 파일에서 보통 관리하고

  1. Receivers: 데이터를 받는 입구다. OTLP(OTel 표준 프로토콜), Jaeger, Prometheus 등 다양한 형식을 지원한다.
  2. Processor: 데이털르 가공한다, 민감 정보를 마스킹하거나 데이터를 배치 단위로 묶거나 특정 태그를 추가한다.
  3. Exporters: 데이터를 최종 목적지까지 보낸다 prometheus, jaeger, aws cloudwatch, elasticsearch 등에게

Context Propagation #

문맥 전파를 알면 좋은데, 분산 시스템에서 중요한 개념이다.

서비스 a가 b를 호출할때 trace id를 http헤더 등에 실어 보내서 하나의 긴 트레이스로 묶어주는 역할을 한다.

OTel은 이 작업을 자동으로 처리해주는 auto-instrumentation 기능을 강력하게 지원해준다.

Example #

Collector를 실행할 때 사용하는 핵심 설정 파일이다.

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:     # 데이터를 묶어서 보내 효율 향상
  memory_limiter: # 메모리 부족 방지
    check_interval: 1s
    limit_mib: 2000

exporters:
  logging:   # 터미널에 로그 출력 (디버깅용)
    verbosity: normal
  otlp/jaeger:
    endpoint: "jaeger:4317"
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [logging, otlp/jaeger]

코드를 거의 건드리지 않고 트레이스를 추출하는 방법도 있다.

# 관련 패키지 설치
pip install opentelemetry-distro \
    opentelemetry-exporter-otlp

# 계측 설정 자동 설치 (Flask, Requests 등 라이브러리용)
opentelemetry-bootstrap -a install

# 애플리케이션 실행 (Collector로 데이터 전송)
export OTEL_SERVICE_NAME="my-awesome-service"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"

opentelemetry-instrument \
    --traces_exporter otlp \
    --metrics_exporter otlp \
    python app.py

코드내에서 수동으로 계측하는, 비즈니스 로직의 상세한 성능을 측정하고싶을때도 사용할 수 있다.

from opentelemetry import trace

# 트레이서 생성
tracer = trace.get_tracer(__name__)

def heavy_logic():
    # 'my_span'이라는 이름으로 구간 측정 시작
    with tracer.start_as_current_span("process_payment") as span:
        span.set_attribute("payment.amount", 50000) # 커스텀 메타데이터 추가
        
        # 비즈니스 로직 수행
        print("결제 처리 중...")
        
        # 에러 발생 시 기록 예시
        # span.record_exception(e)
        # span.set_status(trace.Status(trace.StatusCode.ERROR))
./otelcol --config=config.yaml # collector 상태 확인

grpcurl -plaintext localhost:4317 opentelemetry.proto.collector.trace.v1.TraceService/Export ## gRPC 엔드포인트 테스트 

env | grep OTEL # 현재 환경의 OTel 환경변수 확인

결론은 otel은 어떻게 수집할것인가에 집중하고 저장과 시각화는 다른 도구에 맡긴다.

따라서 otel을 도입하면 Collector를 어디에 띄울지 그리고 데이터를 받아줄 backend(jaeger, prometheus)를 무엇 으로 정할지가 다음 결정사항이다.

Span의 계층 구조와 전파 #

span들이 모여 하나의 trace를 이룬다.

  • root span: 요청이 가장 처음 진입할 때 생성되는 부모가 없는 span이다
  • child span: 부모 span 내부에서 발생하는 하위 작업들 db호출 api호출등의 span이다.

이 구조를 유지하기 위해 서비스간 통신 context propagation이 일어난다.

서비스 a가 서비스 b를 호출할때 http 헤더 traceparent 등에 trace id 부모 span id를 실어보내서 b가 자신의 누구의 자식인지 알게한다.

from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode

# 트레이서 가져오기
tracer = trace.get_tracer(__name__)

def process_order(order_id):
    # 'order_logic'이라는 이름의 Span 시작
    with tracer.start_as_current_span("order_logic") as span:
        # 1. 속성(Attributes) 추가: 검색 및 분석용
        span.set_attribute("order.id", order_id)
        span.set_attribute("user.region", "KR")

        try:
            # 비즈니스 로직 수행
            do_database_work()
            
            # 2. 이벤트(Events) 추가: Span 내 특정 시점 기록
            span.add_event("database_query_finished", {"rows": 42})
            
        except Exception as e:
            # 3. 상태(Status) 및 예외 기록
            span.set_status(Status(StatusCode.ERROR))
            span.record_exception(e)
            raise e

def do_database_work():
    # 중첩된 자식 Span 생성
    with tracer.start_as_current_span("db_query") as child_span:
        child_span.set_attribute("db.statement", "SELECT * FROM orders")
        # DB 작업 수행...

Span Attributes 규칙 #

OTel은 벤더 중립성을 위해 속성 이름을 미리 정의했다. 이를 지키리면 jaeger, grafana 같은 도구에서 자동으로 데이터를 인식하고 대시보드에 그려준다.

http.method, http.status_code, db.system, net.peer.name, service.name

http관련이나 db system은 알거같고 (메서드, 코드, 디비종류(mysql, redis, postgresql..,))

net.peer.name은 호출 대상 호스트, service.name 은 말그대로 서비스 네임 resource. payment-service같은거다.

OTel 데이터 전송 curl

collector가 실행중일때 http 엔드포인트로 더미 span 데이터를 보내 정상 작동을 확인할 수 있다.

# OTLP HTTP receiver (4318 포트)로 간단한 JSON 데이터 전송
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d @- <<EOF
{
 "resourceSpans": [{
   "resource": {
     "attributes": [{"key": "service.name", "value": {"stringValue": "test-manual-curl"}}]
   },
   "scopeSpans": [{
     "spans": [{
       "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
       "spanId": "00f067aa0ba902b7",
       "name": "manual-span-test",
       "kind": 1,
       "startTimeUnixNano": "$(date +%s)000000000",
       "endTimeUnixNano": "$(($(date +%s) + 1))000000000"
     }]
   }]
 }]
}
EOF

여기서 4318 서버는 OTel의 Collector 서버로 우리가 만든 서버가아니라 만들어져있는 소프트웨어 패키지 혹은 도커이미지로 띄워진 서버일거다.

다목적 데이터 중계서버가 목적이고 OTel collector는 go로 작성된 독립 실행 파일이다.

Span 설계시 주의사항 #

  1. Too many spans: 루프 안에서 매번 span을 생성하면 오버헤드가 커지니까 의미있는 비즈니스 경계에서 생성하도록하자
  2. High Cardinality: attributes 값에 order id user id같이 많은 고유값을 넣는것은 괜찮으나 span 이름 자체에 이런값을 넣으면 인덱싱 효율이 떨어진다. 이름은 정적으로 id는 속성으로
  3. Context Leak: 비동기 프로그래밍에서는 부모 span 맥락이 끊기지않도록 contextvars 등을 적절히 관리해야한다.
SRE/question/q_32.md