OpenTelemetry 수집기 정리: 수신기, 처리기, 내보내기

OpenTelemetry 수집기 정리: 수신기, 처리기, 내보내기

지금까지 만든 구조에는 선이 하나뿐이었습니다. 애플리케이션이 백엔드로 직접 보내는 선이요. 서비스가 둘일 때는 아무 문제가 없죠.

그런데 스무 개가 되면 어떨까요? 백엔드 주소를 바꾸려면 스무 곳을 고쳐 배포해야 합니다. 어떤 속성을 모든 서비스에 붙이기로 했다면 역시 스무 곳이고요. 백엔드가 잠깐 죽으면 스무 개의 애플리케이션이 각자 재시도하다 메모리에 데이터를 쌓습니다. 😰 관측을 위해 넣은 코드가 서비스를 위협하는 셈이죠.

이 글에서는 그 사이에 두는 수집기(Collector)를 다루겠습니다.

파이프라인 세 조각

수집기는 텔레메트리를 받아서 가공한 뒤 내보내는 프로그램입니다. 구조가 단순해서 세 가지만 알면 됩니다.

수신기(receiver)는 데이터가 들어오는 입구입니다. OTLP로 받는 게 기본이지만 Prometheus가 긁어 가던 엔드포인트를 대신 긁어 오거나, 호스트의 CPU와 메모리를 직접 읽어 오는 수신기도 있어요.

처리기(processor)는 지나가는 데이터를 손보는 자리입니다. 속성을 붙이거나 지우고, 여러 건을 묶고, 일부만 남기고 버리는 일이 여기서 일어납니다.

내보내기(exporter)는 나가는 출구입니다. Jaeger로, Prometheus로, 상용 백엔드로 보내죠. 하나만 쓸 이유가 없어서 같은 데이터를 두 곳에 동시에 보내는 구성도 흔합니다.

이 셋을 엮은 게 파이프라인(pipeline)이고, 신호마다 따로 만듭니다.

flowchart TB
    accTitle: 수집기의 파이프라인 구조
    accDescr: 여러 애플리케이션이 OTLP로 수신기에 보내면, 처리기가 순서대로 데이터를 손본 뒤 내보내기가 여러 백엔드로 동시에 내보낸다. 신호마다 별도의 파이프라인을 만든다.

    apps["애플리케이션 여러 대"] -- "OTLP" --> recv

    subgraph col ["수집기"]
        direction TB
        recv["수신기<br/>otlp"] --> proc["처리기<br/>memory_limiter → resource → batch"]
        proc --> exp["내보내기<br/>otlp/jaeger"]
    end

    exp --> jaeger[("Jaeger")]
    exp --> other[("다른 백엔드")]

    style col fill:none,stroke:#888,stroke-width:1px

설정 파일 읽기

설정은 YAML 한 장입니다. 앞에서 설명한 세 가지를 선언하고, 맨 아래 service에서 조립합니다.

collector.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 256
  resource:
    attributes:
      - key: deployment.environment
        value: local
        action: upsert
  batch:
    timeout: 5s

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true

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

위쪽 세 블록은 부품을 정의만 할 뿐입니다. 실제로 동작하는 건 service.pipelines에 적힌 것뿐이에요. 정의해 놓고 파이프라인에 넣지 않으면 조용히 무시되니, 설정을 고쳤는데 아무 일도 안 일어난다면 여기를 먼저 봐야 합니다.

otlp/jaeger처럼 슬래시 뒤에 이름을 붙인 건 같은 종류를 여러 개 두기 위한 문법입니다. otlp/staging과 otlp/prod를 나란히 정의해 두고 파이프라인마다 다른 걸 쓸 수 있죠.

처리기는 순서가 전부다

processors에 적은 배열은 목록이 아니라 순서입니다. 데이터가 적힌 차례대로 지나가요. 그래서 같은 처리기를 써도 순서를 바꾸면 결과가 달라집니다.

권장되는 자리가 둘 있습니다. memory_limiter는 맨 앞이어야 합니다. 메모리가 위험해지면 들어오는 데이터를 거절해서 수집기가 죽는 걸 막는 역할인데, 뒤에 두면 앞선 처리기가 이미 메모리를 쓴 다음이라 늦습니다. 반대로 batch는 맨 뒤입니다. 여러 건을 묶어 한 번에 내보내는 처리기라 앞에 두면 뒤쪽 처리기가 묶음을 다시 풀어 다뤄야 하거든요.

샘플링처럼 버리는 처리기는 배치보다 앞에 둡니다. 어차피 버릴 데이터를 묶느라 메모리를 쓸 이유가 없으니까요.

띄워서 확인하기

Docker Compose로 수집기와 Jaeger를 함께 올립니다.

docker-compose.yml
services:
  collector:
    image: otel/opentelemetry-collector-contrib:0.112.0
    command: ["--config=/etc/otelcol/config.yaml"]
    volumes:
      - ./collector.yaml:/etc/otelcol/config.yaml:ro
    ports:
      - "4318:4318"
      - "4317:4317"
    depends_on:
      - jaeger

  jaeger:
    image: jaegertracing/all-in-one:1.62.0
    ports:
      - "16686:16686"

내보내기의 주소가 jaeger:4317인 게 눈에 띄는데요. 컨테이너끼리는 서비스 이름으로 서로를 찾기 때문입니다. 이 부분은 Docker Compose 네트워크에서 다룬 내용이에요.

수집기가 제대로 떴다면 마지막 줄에 준비됐다는 로그가 찍힙니다.

결과
info	otlpreceiver@v0.112.0/otlp.go:169	Starting HTTP server	{"kind": "receiver", "name": "otlp", "data_type": "traces", "endpoint": "0.0.0.0:4318"}
info	service@v0.112.0/service.go:230	Everything is ready. Begin running and processing data.

이제 애플리케이션이 보낼 주소만 바꾸면 됩니다. 코드는 그대로예요.

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 bun run orders.ts

앱이 모르는 속성이 붙는다

여기서 처리기의 쓸모가 드러납니다. 애플리케이션 코드 어디에도 deployment.environment를 설정한 곳이 없는데, Jaeger에서 스팬을 열어 보면 이 속성이 붙어 있습니다.

결과
 서비스: orders
   deployment.environment = local

resource 처리기가 지나가는 모든 데이터에 넣어 준 겁니다. 이걸 애플리케이션에서 하려면 서비스마다 환경 변수를 심고 배포해야 하는데, 수집기에서는 설정 한 줄이죠. 스테이징과 운영에 각각 다른 값을 넣고 싶다면 수집기 설정만 다르게 배포하면 되고요.

같은 이유로 지우는 일도 여기서 합니다. 실수로 이메일이나 토큰이 속성에 들어갔을 때, 스무 개 서비스를 고쳐 배포하기 전에 수집기에서 먼저 막을 수 있어요. 데이터가 백엔드에 닿기 전에 걸러지는 마지막 관문인 셈입니다.

안이 안 보일 때

수집기를 넣으면 애플리케이션과 백엔드 사이에 상자가 하나 생깁니다. 백엔드에 데이터가 안 보일 때 상자에 안 들어온 건지, 들어왔는데 처리기가 버린 건지, 나가다 실패한 건지 알아야 하죠.

이럴 때 debug 내보내기를 파이프라인에 하나 더 물립니다. 받은 데이터를 로그로 찍어 주는 내보내기예요.

collector.yaml
exporters:
  debug:
    verbosity: detailed
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [otlp/jaeger, debug] # 둘 다 적으면 같은 데이터가 양쪽으로

이제 컨테이너 로그에 지나간 스팬이 그대로 찍힙니다.

결과
info	ResourceSpans #0
Resource attributes:
     -> service.name: Str(orders)
     -> deployment.environment: Str(local)
Span #0
    Trace ID       : 2216decd98dcd48ce1fb82e304372a93
    Parent ID      : 3e08d183942f0e59
    ID             : b14d8f48e5f708ac
    Name           : db.query
    Kind           : Internal
    Status code    : Unset
Attributes:
     -> db.system: Str(postgresql)
     -> db.statement: Str(SELECT * FROM orders WHERE id = $1)

여기 찍힌 모습은 처리기를 전부 통과한 뒤의 상태입니다. deployment.environment가 이미 붙어 있죠. 그래서 이 출력에 데이터가 보이는데 백엔드에 없다면 문제는 내보내기 쪽이고, 여기에도 없다면 수신기나 처리기를 봐야 한다는 걸 바로 알 수 있습니다.

verbosity: detailed는 모든 속성을 찍어서 양이 많으니 문제를 볼 때만 켭니다. 평소에는 basic으로 두거나 아예 빼 두는 게 좋아요.

두 가지 배포 형태

수집기를 어디에 둘지는 크게 둘로 갈립니다.

에이전트(agent) 형태는 애플리케이션 바로 옆에 두는 방식입니다. 쿠버네티스라면 노드마다 하나씩 띄우거나 파드에 사이드카로 붙이죠. 네트워크를 거의 타지 않아 애플리케이션이 보내는 부담이 작고, 호스트 이름이나 파드 정보처럼 그 자리에서만 알 수 있는 정보를 붙일 수 있습니다.

게이트웨이(gateway) 형태는 클러스터 안에 몇 대를 두고 모두가 그리로 보내는 방식입니다. 설정이 한곳에 모이고, 꼬리 기반 샘플링처럼 추적 전체를 봐야 판단할 수 있는 처리를 하려면 이 형태여야 해요. 한 추적의 스팬이 여러 에이전트로 흩어지면 전체를 볼 수 없으니까요.

둘 중 하나를 고르는 것보다 둘 다 쓰는 구성이 흔합니다. 에이전트가 받아 호스트 정보를 붙여 게이트웨이로 넘기고, 게이트웨이가 샘플링과 백엔드 분배를 맡는 식이죠.

꼭 필요한가

수집기를 두면 층이 하나 늘고, 그 층도 관리 대상이 됩니다. 죽으면 텔레메트리가 끊기고, 용량을 잘못 잡으면 데이터를 흘리죠.

그래서 처음부터 넣을 필요는 없습니다. OpenTelemetry 정리에서 본 것처럼 SDK에서 백엔드로 바로 보내도 잘 돌아가고, 서비스가 서넛일 때는 그게 더 단순합니다.

넣을 때가 오는 신호는 대체로 이렇습니다. 백엔드 설정을 바꾸려고 여러 저장소에 PR을 올리고 있거나, 모든 서비스에 같은 속성을 붙이라는 요구가 반복되거나, 백엔드 장애가 애플리케이션 메모리에 영향을 주기 시작할 때요. 하나라도 해당되면 그때 두면 됩니다.

마치며

수집기는 수신기로 받고, 처리기를 순서대로 통과시키고, 내보내기로 보내는 프로그램입니다. 설정에서 정의와 조립이 나뉘어 있어서 service.pipelines에 넣은 것만 동작하고, 처리기는 memory_limiter를 앞에 batch를 뒤에 두는 게 기본이에요. 애플리케이션을 건드리지 않고 속성을 붙이거나 지울 수 있다는 게 가장 큰 실익이고요.

여기까지 추적만 흘려보냈는데 파이프라인은 신호마다 만든다고 했죠. 다음 글에서는 지표 파이프라인을 붙이면서 계측기 종류와 Prometheus 연결을 다루겠습니다.

수신기와 처리기의 전체 목록은 OpenTelemetry 공식 문서의 Collector에서 확인할 수 있습니다.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord