soobook
KUBERNETES

OpenTelemetry

Collector 파이프라인으로 트레이스, 메트릭, 로그를 한 규격으로 모아 Kubernetes에서 흘려보내는 법

앱은 OTel로 한 번만 계측하고, Collector가 그걸 받아 원하는 백엔드로 내보낸다.

Intro

관측성 스택을 꾸리다 보면 짜증나는게 있다.

Datadog을 쓰면 Datadog 에이전트와 라이브러리를, New Relic으로 갈아타면 그걸 전부 걷어내고 New Relic 것으로 바꿔치기한다.

Metric은 Prometheus client, Log는 Fluent Bit, Trace는 Jaeger client 등등…

OpenTelemetry(OTel)는 우리 삶을 더 나아지게 해준다.

앱은 OTel API로 트레이스, 메트릭, 로그를 한 번 계측하고, 데이터는 표준 프로토콜(OTLP)로 흘려보낸다.

어느 백엔드로 보낼지는 코드가 아니라 Collector 설정에서 정한다.

백엔드를 갈아도 Application Code를 안 건드리게 만든다.

Three Signals

OTel이 다루는 신호는 넷이다.

  • traces
  • metrics
  • logs
  • baggage (Context 정보)

트레이스는 요청이 시스템을 지나간 경로, 메트릭은 런타임에 측정한 수치, 로그는 사건의 기록이다.

OTel SDK는 로그 레코드에 Trace ID와 Span ID를 자동으로 박아, 어떤 로그가 어떤 트레이스의 어떤 span에서 나왔는지 이어 준다. 로그를 보다가 그 요청의 전체 트레이스로 바로 건너뛸 수 있다는 뜻이다.

이 통일을 가능케하는게 semantic conventions다. 속성 이름을 표준화한 규약으로, http.request.methodk8s.pod.name처럼 “무엇을 어떤 키로 적을지”를 고정한다.

Trace and Span

트레이스는 같은 trace_id를 공유하는 span의 묶음이다.

span은 그 묶음의 기본 단위로, 하나의 작업 구간을 나타낸다.

span은 parent_id로 부모를 가리키고, 부모가 없는(parent_id가 빈) span이 root span이다.

이 부모-자식 관계가 쌓이면 하나의 트레이스가 span 트리가 된다.

span이 담는 필드는 이름, 부모 span ID, 시작/종료 타임스탬프, span context, 속성, span 이벤트, span 링크, 상태다.

이 중 span context는 span마다 붙는 불변 객체로 Trace ID, Span ID, Trace Flags, Trace State를 담는다.

경계를 넘는 방식이 context propagation이다.

서비스 A가 서비스 B를 호출할 때, A는 자기 trace ID와 span ID를 요청에 실어 보내고, B는 같은 트레이스 안에서 A의 span을 부모로 삼는 새 span을 만든다.

OTel의 기본 전파 규격은 W3C Trace Context이고, HTTP 헤더 traceparent에 이렇게 실린다.

traceparent: 00-a0892f3577b34da6a3ce929d0e0e4736-f03067aa0ba902b7-01
             ─┬ ────────────┬─────────────────── ───────┬──────── ─┬
           version       trace-id                    parent-id   flags

이 헤더 하나로 여러 서비스에 흩어진 span이 하나의 트레이스로 묶인다. 계측 라이브러리가 보통 이 주입과 추출을 알아서 처리하므로, 대개는 규격이 W3C Trace Context라는 것만 알면 된다.

The Collector

OpenTelemetry Collector는 텔레메트리를 받고, 가공하고, 내보내는 벤더 중립 구현이다.

하나의 코드베이스이자 하나의 바이너리로, 여러 벤더의 에이전트를 각각 돌리고 유지보수할 필요를 없앤다.

여기서 말하는 Agent 는 수집을 해주는 컴포넌트를 말한다. Backend 는 저장하는 컴포넌트를 말한다.

Jaeger, Prometheus, Fluent Bit 같은 오픈소스 포맷을 받아 여러 백엔드로 동시에 내보낼 수 있고, 트레이스, 메트릭, 로그를 함께 다룬다.

Collector의 내부는 파이프라인이다. 데이터는 세 종류의 컴포넌트를 지난다.

  • receiver: 데이터를 받는 입구. otlp receiver는 OTLP로 들어오는 앱 텔레메트리를 받고, kubeletstatshostmetrics처럼 스스로 긁어오는 receiver도 있다.
  • processor: 받은 데이터를 가공하는 중간 단계. 배치 묶기, 속성 부착, 필터링, 샘플링이 여기서 일어난다.
  • exporter: 데이터를 내보내는 출구. Loki로, Tempo로, Prometheus로, 혹은 다음 Collector로 보낸다.

이 셋을 연결(connector)과 확장(extension)이 보조한다. 신호별 파이프라인(traces, metrics, logs)마다 receiver, processor, exporter를 각각 엮어 구성한다.

OTel이 권하는 기본형은 서비스 곁에 Collector를 하나 두는 것이다.

앱은 데이터를 로컬 Collector에 빠르게 넘기고 자기 일로 돌아가고, 재시도, 배치, 암호화, 민감정보 필터링 같은 뒤처리는 Collector가 맡는다.

Agent and Gateway

Kubernetes에서 Collector를 놓는 방식은 크게 둘이고, 보통 둘을 겹쳐 쓴다.

agent 패턴은 Collector를 앱과 같은 호스트에 둔다. DaemonSet으로 노드마다 하나씩 띄우거나 sidecar로 파드에 붙인다. 앱 SDK는 이 로컬 Collector로 OTLP를 쏜다. 시작이 간단하고 앱과 Collector가 1:1로 깔끔하게 매핑되지만, 팀과 인프라가 커지면 확장성과 유연성이 떨어진다.

gateway 패턴은 Collector 여럿을 독립 서비스로 세운다. 보통 Deployment로 띄워 클러스터(또는 DC, Region)당 OTLP 엔드포인트 하나를 노출한다. 앱이나 agent Collector가 그 엔드포인트로 데이터를 모아 보낸다. 자격증명을 중앙에서 관리하고(관심사 분리), 필터링같은 정책을 한곳에서 거는 게 장점인 대신 운영 복잡도, 지연, 자원 사용이 늘어난다.

노드마다 뜬 agent Collector(DaemonSet)가 파드 로그와 kubelet 지표를 모아 gateway Collector(Deployment)로 넘기고, gateway가 정책을 한곳에서 건 뒤 트레이스는 Tempo, 메트릭은 Prometheus, 로그는 Loki로 내보내는 2계층 파이프라인

보통은 둘을 같이 쓴다.

노드마다 agent가 로컬에서 로그를 tail하고 kubelet을 긁어 자기 노드 데이터를 모으고, 그걸 gateway로 넘기면 gateway가 정책을 걸어 백엔드로 내보낸다.

한 가지 함정은 특정 Collector에서 처리해야 하는 작업이다.

대표적인 사례가 tail sampling이다.

tail sampling 은 트레이스를 끝까지 본 뒤 샘플링 여부를 정하는 방식를 말한다.

한 트레이스의 span들이 여러 gateway 인스턴스로 흩어지면 판단이 불가능하다.

그래서 Layer 를 나누는데, L1은 load-balancing exporter로 trace ID를 기준(routing_key)으로 삼아 같은 트레이스의 모든 span을 같은 L2 Collector로 몰아주고, L2에서 tail sampling을 건다.

메트릭에서도 비슷한 이유로 single-writer 원칙을 지켜야 한다.

하나의 메트릭 스트림은 유일한 writer 하나만 가져야, 여러 소스가 같은 시계열을 덮어써 Prometheus에서 중복/역순 샘플이 생기는 걸 막는다.

Kubernetes Components

클러스터 관측을 위해 OTel이 특히 중요하다고 짚는 컴포넌트가 있다.

컴포넌트종류하는 일
otlpreceiver앱이 보낸 트레이스/메트릭/로그를 OTLP로 수신
kubeletstatsreceiverkubelet(포트 10250)을 긁어 파드/노드 메트릭 수집
filelogreceiver/var/log/pods/*/*/*.log를 tail해 컨테이너 로그 수집
hostmetricsreceiver노드의 CPU, 메모리, 디스크 등 호스트 메트릭 수집
k8sattributesprocessor파드를 찾아 텔레메트리에 Kubernetes 메타데이터 부착

이 중 k8sattributes processor가 K8s 파이프라인의 핵심이다. Kubernetes API로 파드를 찾아, 들어온 span, 메트릭, 로그에 k8s.namespace.name, k8s.pod.name, k8s.pod.uid, k8s.pod.start_time, k8s.deployment.name, k8s.node.name 같은 속성을 붙인다. 이 부착 덕에 앱이 만든 신호와 클러스터가 만든 신호가 같은 파드, 같은 네임스페이스로 이어진다. 앱 텔레메트리를 받는 Collector라면 사실상 필수다.

filelog receiver 설정은 만만치 않다. 여러 operator를 엮어 컨테이너 로그를 파싱하고, 호스트의 로그 볼륨을 Collector에 마운트해야 한다. 그리고 k8sattributes, kubeletstats, k8scluster처럼 Kubernetes API를 호출하는 컴포넌트는 Collector의 서비스 계정에 ClusterRole로 RBAC 권한을 줘야 한다. 이 잡일을 덜어주려고 OpenTelemetry Collector Helm 차트가 kubernetesAttributes, kubeletMetrics, logsCollection, clusterMetrics 같은 preset을 제공한다.

References