Search
📌

Istio Distributed Tracing w/ OpenTelemetry

Category
as S/W 엔지니어
Tags
Istio
OpenTelemetry
Distributed Tracing
W3C Trace Context
traceparent
trace propagation
Created time
2024/05/18

Introduction

Istio의 distributed tracing 전반에 대해, 특히 OpenTelemetry 기반으로 논한다.

Summary

Istio는 distributed tracing의 핵심인 trace ID와 span ID를 생성 및 관리하여 header로 제공한다.
다만 Trace propagation은 application에서 수행해야 한다. 나머지는 Istio가 알아서 한다.
Istio는 OpenTelemetry가 채택한 W3C Trace Context를 지원한다. 그러나 default는 B3 propagation 이므로 별도 설정이 필요하다.

Istio 기반 distributed tracing 구조

Distributed tracing의 목적은 특정 request에 대한 관련 component 전체의 상태를 파악하는 것이다. 이를 Istio가 어떻게 이루는지 아래 3개의 components로 이루어진 transaction 예를 통해 설명한다.
gatewaydockebi로 request 보내는 것으로 시작해 dockebi-storage를 거쳐 최종 response를 받는 간단한 구조이다. 화살표 번호는 traffic 순서를 나타내고 ingress, egress는 해당 component 관점에서의 Istio proxy를 의미한다. 참고로, dockebi component에는 마치 Istio proxy가 ingress / egress의 두 개인 양 오해하기 쉬운데 이는 설명 편의 상 나눈 것으로, 단일 Istio proxy가 ingress / egress traffic 모두를 처리한다.
아래는 위 구조 하에서의 특정 trace에 대한 Grafana distributed tracing 뷰로, tracing backend로는 Jaeger를 사용하였다.
Grafana에서의 distributed tracing 뷰. Jaeger도 완전히 동일한 구성의 뷰를 제공한다.
위 그림의 붉은색, 분홍색, 초록색 막대 각각은 component에 대한 각 ingress / egress traffic의 시작과 끝 시간 및 관련 정보, 즉 span을 의미한다. 위에서부터 아래로 gateway (egress)dockebi (ingress)dockebi (egress)dockebi-storage (ingress) 가 나열되어 있다. 또한 span을 클릭하면 span 상세 정보가 나타나는데, 위에서는 gateway (egress) 의 상세 정보가 보인다.
span에는 parent span, 즉 자신 span을 생성한 span이 있기 마련인데, 위 그림 상에서는 자신의 바로 위에 위치한 span이 parent span이 된다. trace는 특정 request에 대한 end to end 여정 전체를 의미하여 parent - child 관계로 연결된 span의 모음으로 구성된다. 즉 모든 span은 특정 trace에 속한다.
Istio는 이들 trace와 span에 대한 정보를 자동 생성, 관리한다. 각 component에서의 매 request마다 span을 생성하고 전파된(propagation) trace ID가 없으면 이를 새로 생성하여 해당 span을 엮는다. 또한 span 각각에 대해 시작과 끝을 자동으로 매핑한다. 그리고 이들에 대한 ID, 즉 trace ID / span ID는 traceparent header를 통해 app에 전달한다. 단, trace ID의 전파, 즉 span의 parent - child 관계 설정은 Istio가 처리 못하여 application level에서 처리해야 하는데, 이어지는 섹션은 이에 대한 상세이다. 그 이후로 traceparent header 상세를 다룬다.
Distributed tracing 기능 실제 제공 주체는 Envoy Istio가 제공하는 대부분의 기능이 그렇지만, distributed tracing 기능 역시 사실 Envoy가 제공한다. Istio는 이에 대한 wrapper일 뿐이다.

Trace propagation

trace ID의 전파, 즉 span의 parent - child 관계를 설정 못하면 각 component의 상태가 어느 request에 해당하는지를 알 수가 없기에 distributed tracing이 불가능하다. 아쉽게도 Istio는 이를 처리 못하기에 application level의 변경이 필요하다(그러나 간단하다). 아래 링크는 이에 대한 Istio의 공식 설명이다.
trace ID 전파라 함은 단순히 ingress의 trace header를 egress의 trace header에 넣는 것, 즉 ingress와 egress와의 매핑에 불과하기에, 얼핏보면 이들 traffic 모두에 관여하는 Istio가 처리 못할 이유가 없어 보인다. 하지만 문제(또는 문제 중 하나)는 response로 처리 완료 되기 전의 여러 ingress가 발생한 경우로, 이 경우 Istio는 egress에 어느 ingress를 매핑할지 알 방법이 없다.
놀랍게도 OpenTelemetry의 일부 언어(e.g. PHP)는 이게 가능한데, 이는 Istio와는 달리 application process 내부에서 처리하기에, ingress / egress traffic mapping 판단을 위한 더 많은 정보를 활용 가능하기 때문으로 보인다(예컨데, code injection을 사용하여, egress 호출 시점에 context로 제공된 ingress와 egress를 매핑할 수 있겠다).
Istio slack channel에서의 관련 Q&A

Application에서의 trace propagation 처리 방법

Trace propagation 처리 로직은 매우 간단하다. egress 로직이 없다면 처리할 것이 없고, 있을 경우 ingress로 받은 request header 중 x-request-idtraceparent 를 egress request header에 그대로 넣는 것 뿐이다. 나머지는 Istio가 알아서 한다(’까보지는 않았지만 아마도’ egress의 Istio Proxy는, trace ID를 새로 생성하는 대신 traceparent header 내 trace ID와 egress span을 엮을 것이다).
여기서 x-request-id 는 Istio(Envoy)가 내부적으로 사용하는 header로 log와 trace 처리를 위해 사용한다. 여기서는 그냥 traceparent 와 함께 전달해야하는 header 정도로 생각하면 된다.
위 링크인 Istio의 공식 가이드에 설명과 예제가 잘 나와있지만, B3, Datadog 등의 여기서는 불필요한 header와 함께 B3 기반 library를 사용하기에 바로 참고하기 어렵다. 그리고 OpenTelemetry SDK로 대체한다 해도, Trace propagation의 단순한 로직 대비 사용 방법이 상당히 복잡한데, trace propagation이 SDK가 제공할 전체 기능의 일부에 불과하다는 것을 고려한다면 이해 못할 바는 아니다.
아래는 OpenTelemetry SDK 대신 단순히 trace propagation 로직에만 집중한 custom python sample 코드이다. tracestate, baggage 란 header도 전파 대상으로 넣고 traceparentbaggage 사용 코드도 함께 넣었는데, 이 역시 W3C Trace Context 에서 설명한다.
참고로 하기 코드의 traceparent 에서 추출되는 span ID는 표준 상 parent span ID이긴 하나 수신 app 측의 Istio proxy가 생성하므로, 해당 app 내에서 별도 span 생성 없이 사용해도 의미론 상 문제가 없다.
... # request에서 추출한 전파대상 headers와 app 정의의 context(baggage)를 함께 전달 requests.get("http://some_upstream_url", headers=forward_headers({"isProduction": False})) --- import urllib.parse from flask import request BAGGAGE = "baggage" # ingress request에서 forwarding용 header 생성(w/ app 정의의 context 추가) def forward_headers(baggage_additional: dict = {}) -> dict: return { **{ h: val for h in ["X-Request-Id", "traceparent", "tracestate"] if (val := request.headers.get(h)) }, BAGGAGE: urllib.parse.urlencode( { **baggage(), # definition은 ingress측 코드 참조 **baggage_additional, } ).replace("&", ","), } ...
Python
복사
송신 측의 trace, baggage propagation 예제
# 추출한 trace ID, span ID, app 사용 trace_id, parent_id = trace_span() is_production = baggage()["isProduction"] --- # trace ID, parent ID 추출 def trace_span() -> Tuple[str, str]: if not (traceparent := request.headers.get("Traceparent")): return None, None tokens = traceparent.split("-") return tokens[1], tokens[2] # baggage 추출 def baggage() -> dict: return { k: v[0] for k, v in urllib.parse.parse_qs( request.headers.get(BAGGAGE), separator="," ).items() }
Python
복사
수신 측의 trace ID, span ID, baggage 추출 및 사용 예제

W3C Trace Context

Istio(Envoy)가 default로 사용하는 distributed tracing header 형식은 B3 propagation이다. 하지만 OpenTelemetry가 채택한 형식, 즉 W3C Trace Context 역시 지원한다.
W3C TraceContext는, 4개 또는 그 이상의 header를 사용하는 B3 propagation와는 달리 traceparenttracestate 두 개가 전부이다.
이들은 다수의 분산 추적 시스템 운용을 고려한 것으로 traceparent는 공통 요소를, tracestate는 각 시스템 별 특화 사항을 표현한다. 따라서 분산 추적 시스템이 단일할 경우 traceparent 만 고려하면 된다.
추가로 baggage 란 header를 이들과 함께 전파하면 application 정의 context도 service 간 공유 가능하므로 상당히 유용하다. 이 역시 OpenTelemetry 표준이다.

traceparent header

traceparent header의 단일 token 내에 trace ID와 span ID 모두를 표현하며 이들 이외에도 version과 flag도 함께 한다. 아래는 traceparent header 예이다.
# {Header Key Name}: {version-format}-{trace-id}-{parent-id}-{trace-flags} traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
YAML
복사
version-format: 1 byte인 2 hex characters로 표현한다. format 버전을 나타내어 현 규격인 버전 00 은 나머지 {trace-id}-{parent-id}-{trace-flags} format을 사용한다. invalid 값은 ff 이다.
trace-id: trace의 ID를 나타낸다. 16 byte인 32 hex characters로 표현한다. invalid 값은 00000000000000000000000000000000 이다.
parent-id: parent span을 의미한다. 8 byte인 16 hex characters로 표현한다. invalid 값은 0000000000000000 이다.
trace-flags: 현재 규격에서는 단일 bit만, 소위 sampled(sampling decision)의 의미로 사용되어, 해당 trace를 기록해야 하는지(01) 또는 아닌지(00)를 나타낸다. 1 byte인 2 hex characters로 표현한다.

tracestate header

다수의 분산 추적 시스템을 운용할 때 사용하는 header로 traceparent header와 함께 사용되어, 각 시스템에 특화된 정보를 저장한다. comma(,) separator로 구분되는 key/value 구조로 시스템 별로 자유롭게 표현한다.
# comma(,) separator로 구분되는 key/value 구조 tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE
YAML
복사

baggage header

W3C Trace Context이 아닌 별도의 W3C 표준(24.05.18 현재 Working Draft)이지만, trace와 함께 유용하게 사용될 수 있기에 OpenTelemetry는 traceparent, tracestate header와 함께 전달되는 header로 정의한다.
위 표준 문서에서 보이듯이 baggage는 특정 request에 해당하는 application 정의 context이다. 아래는 baggage header의 예로, tracestate header와 마찬가지로 comma(,) separator로 구분되는 단순한 key/value 구조이다.
# serId="alice", serverNode="DF 28", isProduction=false baggage: userId=alice,serverNode=DF%2028,isProduction=false
YAML
복사

Istio의 OpenTelemetry tracing 설정

Global Mesh Config와 Telemetry Istio resource의 두 가지 설정이 필요하다.

Global Mesh Config 설정

먼저 Global Mesh config인데, OpenTelemetry tracing의 trace backend를 설정한다. Istio proxy는 생성한 trace를 해당 설정의 주소로 보낸다. 참고로, Jaeger 역시 OTLP를 지원하므로 직접 Jaeger로 보낼 수도 있다.
... extensionProviders: - name: otelTrace opentelemetry: service: otel-otlp-collector.cluster.svc.cluster.local # trace를 전송할 service domain port: 4317 # trace를 전송할 service port ...
YAML
복사

Telemetry Istio resource 설정

B3 propagation 대신 OpenTelemetry를 사용하도록 하는 설정이다. 아래에서는 namespace에 istio root namespace(istio-system)을 지정함으로 istio 전역적으로 적용되도록 하였다. 참고로 Telemetry Istio resource는 MLT(Metric, Log, Trace)에 대한 처리 방법 지정에 사용한다.
apiVersion: telemetry.istio.io/v1alpha1 kind: Telemetry metadata: name: mesh-default namespace: istio-system # 전역적으로 적용 spec: tracing: - providers: - name: otelTrace # global mesh config에서 지정한 식별자
YAML
복사