Skip to Content
Infra & DevOpsB3와 W3C Trace Context를 함께 사용하는 이유
☁️ Infra & DevOps2023년 6월 14일

B3와 W3C Trace Context를 함께 사용하는 이유

#infra-devops#observability#distributed-tracing#opentelemetry#w3c-trace-context#zipkin

분산 추적을 설정하면서 다음 JVM 옵션을 사용했습니다.

text
JVM 옵션
-Dotel.propagators=b3,b3multi,tracecontext

연결된 서비스들이 같은 추적 헤더를 쓰지 않아 여러 전파 형식을 함께 지원하는 설정입니다. B3를 사용하는 기존 서비스와 W3C Trace Context를 사용하는 서비스 사이에서도 같은 요청의 흐름을 이어 보기 위해 필요했습니다.

저는 Zipkin을 사용하던 시절부터 분산 추적을 다뤘고, 모니터링 플랫폼을 개발하면서도 형식이 다른 시스템을 연결하는 문제를 접했습니다. 이 글에서는 그 배경과 옵션의 의미, 실제로 전파를 확인할 때 볼 항목을 정리하겠습니다.

추적 도구와 전파 형식

Google의 Dapper 논문은 대규모 서비스에서 요청 경로를 추적하는 시스템의 설계와 운영 경험을 소개합니다. 이후 Zipkin과 Jaeger 같은 도구가 확산됐지만, 요청에 추적 정보를 싣는 형식까지 모두 같았던 것은 아닙니다.

OpenTracing과 OpenCensus가 OpenTelemetry로 통합되는 등 계측 도구도 바뀌었습니다. 그렇다고 이미 운영 중인 서비스의 헤더와 에이전트가 동시에 교체되지는 않았습니다. 여러 시기에 도입한 도구가 남아 있으면 연결되는 서비스 사이의 호환성을 확인해야 합니다.

형식HTTP 헤더
B3 Singleb3
B3 MultiX-B3-TraceId, X-B3-SpanId와 샘플링 관련 헤더 등
W3C Trace Contexttraceparent, 선택적인 tracestate

추적 데이터를 저장하는 백엔드와 전파 형식도 구분할 필요가 있습니다. 같은 수집 시스템으로 보낸다고 요청 사이의 context가 자동으로 이어지는 것은 아닙니다.

수신과 송신에서 하는 일

Propagator는 들어온 헤더에서 context를 읽는 extract와, 나갈 요청에 context를 쓰는 inject를 담당합니다. Java의 otel.propagators는 사용할 propagator를 쉼표로 나열하는 설정이며, 해당 구현이 에이전트나 애플리케이션에 포함돼 있어야 합니다.

여러 형식을 함께 설정하면 각 형식으로 context를 읽고 쓸 수 있습니다. 다만 여러 헤더에 서로 다른 trace가 들어왔을 때 어떻게 선택하는지까지 단순히 “모두 지원한다”로 설명할 수는 없습니다. B3 사양은 Single 헤더를 Multi보다 우선해 읽도록 정하며, W3C까지 함께 처리할 때의 동작은 사용하는 propagator 조합과 구현도 확인해야 합니다.

실제 확인에서는 각 형식만 들어온 경우와 여러 형식이 함께 들어온 경우를 나눠 봅니다. 다음 서비스로 나가는 헤더도 확인해야 수신만 되고 송신에서 끊기는 문제를 찾을 수 있습니다.

Trace ID와 Span ID

W3C의 버전 00 헤더는 다음 형태입니다.

text
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

trace ID는 전체 추적 흐름을 식별하고, parent ID는 이 요청을 보낸 쪽의 span을 가리킵니다. 서버가 요청을 이어받아 새 span을 만들면 trace ID는 유지하고 처리 구간에 해당하는 span ID는 새로 만듭니다.

B3는 64비트 또는 128비트 trace ID를 사용하고, W3C 형식은 128비트 trace ID를 사용합니다. 두 형식을 비교할 때는 표시 길이와 정규화도 고려해야 합니다. 문자열 길이가 다르다는 이유만으로 요청이 다른지 판단하지 않습니다.

수동 전파 예시

다음은 수신 context를 부모로 연결하고 현재 span을 활성화한 뒤, 후속 요청의 헤더에 쓰는 예시입니다. 실제 HTTP 프레임워크의 계측 코드를 옮긴 것은 아닙니다. 이미 자동 계측이 처리하는 구간이라면 같은 server span을 중복으로 만들지 않아야 합니다.

java
ContextForwardingExample.java
import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.TextMapGetter; import java.util.Map; public class ContextForwardingExample { public static void forward( OpenTelemetry telemetry, Map<String, String> incoming, TextMapGetter<Map<String, String>> getter, Map<String, String> outgoing) { var propagator = telemetry.getPropagators().getTextMapPropagator(); Context parent = propagator.extract(Context.root(), incoming, getter); Span span = telemetry.getTracer("propagation-example") .spanBuilder("handle-request") .setSpanKind(SpanKind.SERVER) .setParent(parent) .startSpan(); try (Scope ignored = span.makeCurrent()) { propagator.inject(Context.current(), outgoing, (carrier, key, value) -> carrier.put(key, value)); // 이 범위 안에서 outgoing 헤더를 붙여 후속 요청을 보냅니다. } finally { span.end(); } } }

헤더를 읽는 getter는 HTTP 헤더 이름의 대소문자를 구분하지 않도록 구현해야 합니다. 실제 HTTP 클라이언트 호출을 계측할 때는 그 호출의 client span도 별도로 만들어질 수 있습니다.

핵심은 부모 context를 읽는 것 외에 현재 span을 활성화하는 과정도 필요하다는 점입니다. span을 생성만 하고 Context.current()에 연결하지 않으면 후속 요청에 의도한 context가 실리지 않을 수 있습니다. 처리가 끝난 뒤 scope를 닫고 span도 종료해야 합니다.

자동 계측과 메시지 큐

Java 에이전트는 지원하는 HTTP 서버·클라이언트, DB 드라이버, 메시징 라이브러리를 계측합니다. Node.js와 Python도 자동 계측 도구를 제공하지만, 설치 방식과 지원 라이브러리는 다릅니다. 사용하는 언어와 클라이언트의 지원 범위를 확인해야 합니다.

Java 에이전트의 설정 예시는 다음과 같습니다. Collector 주소와 에이전트 경로는 실행 환경에 맞춰야 합니다. 이 예시에서는 OTLP의 gRPC 프로토콜을 명시했습니다.

bash
Java 실행 예시
java \ -javaagent:/path/opentelemetry-javaagent.jar \ -Dotel.service.name=example-service \ -Dotel.exporter.otlp.protocol=grpc \ -Dotel.exporter.otlp.endpoint=http://localhost:4317 \ -Dotel.propagators=b3,b3multi,tracecontext \ -jar app.jar

메시지 큐를 거치는 경우에는 HTTP 헤더 대신 메시지 속성 등으로 context를 전달합니다. 자동 계측이 해당 클라이언트와 전파 방식을 지원하는지, 워커가 값을 읽고 처리 span에 연결하는지 확인해야 합니다.

메시지 재전달이나 여러 메시지의 묶음 처리에서는 단순한 HTTP 부모·자식 관계와 다른 모델이 필요할 수도 있습니다. 메시징 계측 규약에서는 span link도 사용하므로, 모든 비동기 작업을 무조건 하나의 직계 자식 span으로 설명하지는 않습니다.

추적 화면과 로그 연결

추적 화면은 span의 시각과 관계를 바탕으로 호출 경로를 보여 줍니다. 누락된 계측이나 샘플링이 있으면 실제 처리의 일부만 보일 수 있고, 서로 겹친 span의 시간을 단순히 더하면 전체 응답 시간과 맞지 않을 수 있습니다.

느린 구간을 찾은 뒤에는 trace ID로 로그를 모아 구체적인 상태와 예외를 확인할 수 있습니다. span ID까지 포함한 traceparent 전체를 고정 검색하면 같은 trace의 다른 span 로그가 빠질 수 있어, 요청 전체 조회와 특정 작업 조회를 구분해야 합니다.

로그 출력에 추적 필드를 넣는 과정은 애플리케이션·접근 로그 연결 글에서 따로 다룹니다.

여러 전파 형식을 함께 쓰는 설정은 호환성을 유지하는 수단입니다. 설정에 이름이 들어 있다는 사실보다, 실제 연결된 서비스와 메시지 처리 구간에서 같은 요청을 이어받는지가 확인돼야 합니다.

참고 자료

Last updated on