-
야, 너두 분산 추적 할 수 있어DevOps, 클라우드/Observability & Monitoring 2024. 11. 24. 21:59
개요
내가 직접 구축하고 운영했던 모니터링 시스템이 있다.
메트릭과 트레이스를 애플리케이션 단에서부터 직접 수집, 시각화하고 최적화나 대응까지 이어지는 하나의 라이프사이클이 있었다.
하지만 이 흐름을 정리한 적은 없었다.
그래서 우선 분산 추적 시스템에 관해서만 가볍게 정리한다.
집계
OpenTelemetry
OpenTelemetry(콜렉터)는 마이크로 서비스 환경에서 필요한 분산 추적, 메트릭 및 로깅 정보 수집을 도와주는 중계기 혹은 에이전트다.
수집된 애플리케이션 모니터링 데이터는 콜렉터를 통해서 Jaeger나 Prometheus와 같은 저장소를 포함한 플랫폼으로 이동한다.
OpenTelemetry(콜렉터)는 수집, 처리 및 전송 을 담당하고, 이에 대한 저장-쿼리는 연결된 플랫폼에 위임한다.
스팬 메트릭(Spanmetric)
추적 데이터의 경우, 트레이스와 스팬에 대한 처리 지연시간(Latency)이 함께 기록된다.
이런 지연시간은 시계열 데이터로 사용될 수 있는데, 이를 위한 파이프라인이 필요하다.
# example :: otel-collector config connectors: spanmetrics: histogram: explicit: buckets: [5ms, 10ms, 15ms, 30ms, 50ms, 100ms, 300ms, 500ms, 1s] resource_metrics_cache_size: 1000 # default is 1000 metrics_flush_interval : 10s # default is 15s namespace: default resource_metrics_key_attributes: - service.name - telemetry.sdk.language - telemetry.sdk.name # ... service: extensions: [pprof, zpages, health_check, sigv4auth] pipelines: metrics: receivers: [otlp, spanmetrics] processors: [batch] exporters: [otlphttp/prometheus] traces: receivers: [otlp] processors: [batch] exporters: [otlp, spanmetrics]
위의 OTel-collector 구성 예시를 살펴보자.
service 필드에는 metrics와 traces 파이프라인이 정의되어 있다.
traces 파이프라인을 통해 집계된 추적 데이터은 다시 metrics 파이프라인의 receiver로 전달된다.
이렇게 스팬메트릭이 집계되는데, 이때 connectors가 동작을 하게 된다.
나 같은 경우는 실시간 레이턴시와 레이턴시 분포(중위값, P95 지표 등)를 보고자 했다.
이를 위해서는 connectors에 스팬메트릭을 히스토그램으로 집계하고자 버킷을 정의했다.
여기 정의된 버킷은 LE(Lower or Equal than). 즉, 기준보다 작거나 같은 값을 모두 기록한다.
예를 들어, 4ms의 레이턴시를 가지는 트레이스는 5ms보다 큰 버킷에 모두 들어간다.
1s의 레이턴시를 가지는 트레이스는 +inf라는 디폴트 버킷에만 들어가게 된다.
이렇게 스팬메트릭을 처리-집계해서 분포를 볼 수 있게 된다.
추적 (Trace)
Jaeger
Jaeger는 오픈소스 분산 추적 도구로, 초기 구축에 사용했다.
실시간으로 집계되는 데이터를 저장하고 Jaeger-ui를 사용한다면 시각화된 트레이스-스팬과 서비스 현황을 볼 수 있다.
위에 하늘색 큰 박스로 가린 것이 모두 개별적인 트레이스다.
Jaeger 같은 경우에는 스팬 스토리지로 메모리, badger와 같은 내장 저장소나 카산드라, 엘라스틱서치를 쓸 수 있다.
또는, 카프라로 트레이스를 전파시켜서 저장시킬 수도 있다.
Tempo와 로깅 통합
템포(Tempo) 역시는 Jaeger와 같은 분산 트레이싱 도구 다.
Grafana를 만든 Grafana-Labs에서 개발했으며, LGTM 스택이라고 불리는 모니터링 스택에서 T에 해당한다.
Loki(로깅), Grafana(시각화), Tempo(분산추적), Mimir(메트릭)
Jaeger와의 차이점을 살펴보면 다음과 같다.
- Tempo의 경우, 자체 UI가 존재하지 않고 Grafana와 통합되어 시각화를 제공한다.
- Jaeger는 KV 스토리지(Badger, 카산드라 ...) 나 ElasticSearch 등을 저장소로 사용하지만, Tempo는 버킷을 저장소로 사용한다.
Tempo는 로깅 도구(ElasticSearch, OpenSearch, Loki 등)와 분산 트레이싱 데이터를 연결할 수 있다.
- “이 로그는 A 트레이싱 에서 생성된 로그야” 라는 것을 바로 인지할 수 있게 돕는 다.
이벤트 및 오류기록
모든 데이터를 스팬으로 기록하면 좋겠지만, 스팬을 생성하고 추적 데이터에 연결하는 것 또한 비용이다.
우리는 모니터링 수단에 애플리케이션이나 시스템에 과한 부하를 주는 것을 원치 않는다.
그렇기에, 스팬 자체를 기록하는 것보다 특정 상황에서 이를 이벤트 로그로 기록하는 것을 고려해볼 수 있다.
이에 대한 명확한 기준이 없다면, 스스로 기준을 잡아서 이벤트 로그로 기록할지 혹은 스팬으로 기록할지를 결정해 볼 수 있다.
아래의 예시들은 스팬으로 기록할지, 이벤트 로그로 기록할지를 결정한 완전 주관적인 기준이다.
- 비동기 혹은 메인 루프 밖에서 동작하는가? (별도 프로세스 혹은 스레드 등등)
- 멀티 스레딩의 경우, 분리된 스레드가 무거운 작업을 한다면 스팬을 기록해 볼 수 있음.
- 이외에 가벼운 작업의 경우, 이벤트 로깅이 타당할 수 있음.
- 이벤트 루프의 경우, 분리된 비동기 루프가 무거운 작업을 한다면 스팬을 기록해 볼 수 있음.
- 멀티 프로세싱의 경우, 스팬을 기록하는 것이 타당할 수 있음.
- 서비스가 분리되어 있는가?
- 분리된 서비스는 필연적으로 스팬을 전파하고 새로운 자식 스팬을 기록-연결해야 추적 데이터에서 확인 가능함.
- 여러 곳으로 요청을 전파하는 시점인 경우, 스팬을 기록하는 것이 타당할 수 있음.
async execute( params: any span?: Span, ) { // ... try { // ... } catch (e) { if (span) { span.recordException(e); } return []; } }
단순한 Info 레벨의 이벤트 로그 외에도, 예외 처리 역시 스팬에 기록할 수 있다.
예외 처리를 스팬에 기록한다면, 오류가 발생한 요청 흐름을 트레이스 하나로 디버깅 할 수 있게 된다.
Sentry와 같은 SaaS가 오류를 기록하고 알림을 발생시키는 흐름도 유사하다.
트레이스 전파
Trace: API 요청 (Trace ID: ABCD) └── Span 1: 프론트엔드 요청 전송 (Span ID: 1) └── Span 2: 백엔드 서비스 A 처리 (Span ID: 2) └── Span 3: 서비스 B 호출 (Span ID: 3) └── Span 4: 데이터베이스 쿼리 실행 (Span ID: 4)
시스템은 하나의 함수나 애플리케이션으로 동작하지 않는다.
여러 애플리케이션이 유기적으로 연결되고 각자의 역할을 수행하면서 하나의 시스템이자 서비스를 구축한다.
우리는 이 시스템 내의 흐름을 확인하고 싶다.
단순하게 애플리케이션 하나에서 발생하는 이벤트가 아니라, 이 이벤트로부터 어떤 흐름이 발생하고 전파되는지 보고싶다.
HTTP나 gRPC와 같은 HTTP 기반의 프로토콜은 헤더를 가지고 있다.
이 헤더에는 KV 형식으로 값을 넣을 수 있다. 우리는 이 곳에 부모의 스팬 ID 값을 기록한다.
HTTP 헤더 / gRPC 메타데이터
setTraceParent(span: Span, metadata: Metadata) { const traceContext = span.spanContext(); const traceFlagsHex = traceContext.traceFlags.toString(16).padStart(2, '0'); const traceparent = `00-${traceContext.traceId}-${traceContext.spanId}-${traceFlagsHex}`; metadata.set('traceparent', traceparent); }
KV 형식에 담겨야 하는 것은 W3C Trace Context 표준의 traceparent 헤더이다.
각 자리가 의미하는 것을 알아보면 다음 표와 같다.
- {version}-{trace-id}-{span-id}-{trace-flags}
이름 설명 예시 version W3C Trace Context 표준의 버전 00 trace-id 해당 트레이스 자체 ID span-id 해당 트레이스 내 현재 부모 Span의 ID (기록 당시의 Span id) trace-flags 해당 트레이스를 샘플링 할지 결정하는 플래그 00(x), 01 (o) 우리는 이렇게 traceparent의 값을 구성하고, 헤더에 담아서 다음 서비스로 전파한다.
Baggage
OpenTelemetry에서 Baggage는 컨텍스트 상에 함께 존재하는 정보이다.
Baggage는 KV 저장소이고 컨텍스트와 함께 원하는 데이터를 전파하도록 돕는다.
Baggage를 사용하면 데이터를 서비스와 프로세스 간에 전달할 수 있으며, 이 데이터는 해당 서비스에서 트레이스, 메트릭 또는 로그에 추가하여 사용할 수 있다.
특징 Baggage Attributes 목적 요청의 전역적인 컨텍스트를 공유 및 전파 특정 스팬(Span)과 연관된 추적 정보를 기록 전파 범위 서비스 간 전파 (분산 컨텍스트와 함께 전달) 현재 스팬 내에서만 유효, 다른 스팬으로 전파되지 않음 저장 위치 분산 컨텍스트의 일부로 전역에 저장됨 개별 스팬에 로컬로 저장됨 데이터 크기 제한 네트워크 전파 시 제한 (예: 헤더 크기) 제한 없음 (추적 데이터에 포함되므로 수집에만 영향을 줌) 유즈케이스 요청 전체의 흐름에서 공유해야 하는 정보를 저장 특정 스팬의 속성이나 상세 정보를 기록 const baggage = api.propagation.createBaggage({ 'tenant-id': { value: '12345' }, 'region': { value: 'ap-northeast-2' } }); api.context.setBaggage(baggage);
Baggage의 경우, 요청 흐름에서 계속 전파되는 KV 정보를 가진다.
span.setAttribute('http.method', 'POST'); span.setAttribute('db.statement', 'SELECT * FROM users');
이와 반대로, Attribute는 단일 스팬에서만 기록되는 KV 정보를 가진다.
Sampling
트레이싱은 분산 시스템 상에서 서비스 간의 요청 흐름을 관찰할 수 있도록 돕는 도구다.
그렇지만, 대부분의 요청 흐름이 정상이라면 모든 트레이싱 데이터를 기록할 필요는 없고, 유효한 트레이싱만 기록하는 샘플링이 필요하다.
공식 문서에서는 샘플링을 고려하는 기준을 다음과 같이 잡고 있다.
더보기⚠️ 데이터 양이 적거나 사전에 집계할 수 있다면 샘플링하지 않는 것이 적합하다.
- 초당 1000개 이상의 트레이스를 생성하는 경우
- 트레이스 데이터 양이 많아 처리 비용이 크게 증가할 때 샘플링이 유용하다.
- 대부분의 트레이스 데이터가 정상적인 트래픽을 나타내며, 데이터에 큰 변화가 없는 경우
- 정상 트래픽에서 얻는 정보가 제한적일 때, 데이터 일부만 저장해도 충분할 수 있다.
- 오류나 높은 지연 시간과 같은 문제가 있는 데이터를 기준으로 판단할 수 있는 경우
- 특정 조건(예: 오류, 높은 지연 시간)이 발생한 데이터를 우선적으로 수집할 때 샘플링을 적용할 수 있다.
- 오류나 지연 시간 이외에도 특정 데이터를 판단할 수 있는 도메인별 기준이 있는 경우
- 도메인에 특화된 규칙(예: 비정상적인 사용자 행동)을 기반으로 샘플링 여부를 결정할 수 있다.
- 데이터를 샘플링할지 버릴지 결정하는 공통적인 규칙을 설명할 수 있는 경우
- 예측 가능한 방식으로 데이터를 선별할 수 있다면 샘플링을 효율적으로 활용할 수 있다.
- 서비스별로 샘플링 비율을 다르게 적용할 수 있는 경우
- 트래픽이 많은 서비스와 적은 서비스에 서로 다른 샘플링 전략을 적용할 수 있다면 더욱 효과적이다.
- 샘플링되지 않은 데이터를 저비용 스토리지 시스템으로 라우팅할 수 있는 경우
- 선택적으로 저장하지 않은 데이터를 저렴한 저장소로 전송해 나중에 필요할 때 사용할 수 있는 방법이 있다면 샘플링이 유용하다.
Head Sampling vs Tail Sampling
우리는 샘플링 여부를 트레이스 생성-시작 시점 혹은 종료 시점에 결정할 수 있다.
- Head Sampling의 경우, 명확한 판단 이전에 샘플링이 적용되므로, 정확도가 떨어질 수 있으나 빠르고 가볍게 처리할 수 있다.
- Tail Sampling의 경우, 데이터를 일시적으로 저장하고 처리 완료 후 샘플링이 적용되므로, 추가 처리 시간이 필요하나 정확한 처리가 가능하다.
이러한 샘플링 기준은 아래와 같이 적용할 수 있다.
- OTel 콜렉터의 processor에서 tail-sampling을 적용하는 예시는 다음과 같다.
processors: tail_sampling: policies: - name: error-policy # 특정 오류 상태를 가진 트레이스만 샘플링 latency: threshold_ms: 500 - name: endpoint-policy # 특정 endpoint의 트레이스만 샘플링 attributes: include: - key: http.target value: "/endpoint"
traceparent 헤더에 존재하는 trace-flags 역시 샘플링에 영향을 미친다.
- 이 값이 01인 경우, 샘플링을 수행한다.
- 샘플링되지 않는 경우(00), 추적데이터에 기록되지 않는다.
끝으로
Observability 스택과 분산 추적 시스템을 구축하고 운용하면서 생각했던 트레이스에 대한 기본적인 내용 만을 다뤘다.
물론 실제 상용 환경에서는 더 고려할 것이 많다.
요기서는 메트릭에 대한 것을 세부적으로 다루진 않았는데, 나중에 기회가 되면 다뤄보겠다.
'DevOps, 클라우드 > Observability & Monitoring' 카테고리의 다른 글
[Prometheus] Histogram에 대한 이해 (0) 2024.08.08