들어가며
안녕하세요~! 오늘은 MSA 환경에서 추적을 용이하게 하기 위한, 라이브러리를 알아보도록 해볼텐데요.
과거에 Spring cloud sleuth 로 유명했었던 라이브러리 이지만, 아쉽게도 Spring Boot 2.x 버젼을 마지막으로 지원을 중단하고, Micrometer Tracing 로 옮기게 된 라이브러리입니다.
https://github.com/spring-cloud/spring-cloud-sleuth
위 링크를 들어가보면 아쉽게도 이러한 문구가 남아있죠
따라서 앞으로는 spring cloud sleuth 를 찾는 것이 아닌 micrometer tracing 를 찾아서, 개발을 진행해야 됩니다
자 그러면 micrometer tracing 에 대해 천천히 하나씩 알아보도록 할까요?
Micrometer Tracing
먼저 디테일하게 알아보기전에, 우리는 micrometer tracing 의 탄생 배경에 대해 알아볼 필요가 있습니다.
micrometer tracing 은 어떻게 하다가 나오게 되었을까요?
탄생 배경
탄생 배경에 대해서는 Software architecture 부터 거슬러 올라가야 합니다.
microservice architecture 가 대세로 자리잡고, 필요성이 두각된 만큼 많은 어려운 상황들이 있었습니다.
간단해보이는 기능 하나도 서로 다른 domain service 간에 통신을 하면서 비즈니스를 풀어나가야 했죠.
그런데 막상 운영해보니 여러 복합 관계를 갖는 domain service 간에 통신을 하면서 에러가 발생했을 때, 에러를 추적하기가 쉽지 않다는 것을 개발자들은 느끼기 시작했습니다.
결과적으로 서로 다른 domain service 간에 통신이 이루어졌을 때, 추적을 쉽게 하기 위한 도구들이 필요했고, 이를 지원하기 위해 시작되었다 라고 볼 수 있습니다.
원리
가장 기본적인 컨셉은 TraceContext 라는 개념을 정의하고 그 안에 Trace 와 Span 에 대한 데이터를 정의함으로써 지원하기 시작했는데요.
아래 그림을 보면서 하나씩 천천히 살펴보도록 해볼게요
Trace Id = X 라는 Trace Id 가 service1,2,3,4 까지 연결되어 흐르는 것을 볼 수 있고,
Span Id 는 각 service 내에 서로 다르게 정의되어 선언된 것을 볼 수 있죠.
즉, 우리는 MSA 에 첫 entry point service 에서 Trace 를 정의하고, 다른 service 까지 정의한 Trace 를 계속 전파하면서 TraceContext 를 유지시키는 것입니다.
이렇게 전파된 TraceContext 는 logging 에 쓰여서 요청에 흐름에 대한 발자취(slueth)를 남길 수 있게 되는 것이죠.
Span 은 굳이 왜 지정했는가? 라고 의문점이 들 수도 있지만, Span 은 큰 흐름을 보기보다는 현재 service 와 바로 직전의 service 간의 연결관계를 파악하기 위해 지정되었다고 볼 수 있습니다.
Parent Id 는 이전 Span Id 를 가리킴으로써 연결관계를 표현하게 되는 것이죠.
어느정도 흐름이 이해가 되셨나요? 그러면 공식 문서에 있는 설명을 한번 가져와보도록 해볼게요
구성요소 | 설명 |
Span | The basic unit of work. 즉, communication 방식에 대한 실질적인 작업 단위 입니다. ex) API 요청, gRPC 요청 |
Trace | Tree 구조로 구성된 Span 의 모음 |
Tracer | Span 의 life cycle 을 관리한다. Span 을 시작 하거나, 멈추거나 전파하거나 등 |
Trace Context | 분산 tracing 을 위해서 network 간의 전파되어야 하는 context(span id, trace id .. etc) |
Log correlation | Trace Context 를 기반으로 로그를 모아 관리하는 역할 |
위에서 설명드린 개념에 대해 전반적으로 이해를 하고 설명을 보니 조금 더 쉽게 이해가 되죠?
Trace Context 전파 방식
그러면 저희는 지금까지 micrometer tracing 에 대해서
왜 사용하고
어떤 개념으로 설계되었는지
전반적으로 이해할 수 있었는데요.
Trace Context 를 실제 communication 단계에서 어떻게 전파하는지 조금만 더 디테일하게 가보도록 해보겠습니다.
Trace Context 를 각 service 간에 서로 다른 규약으로 전송하게 되면, 모든 service 에서 Trace Context 를 파싱할 수 있도록 요청 받는 service 마다 각각 만들어줘야 되는 이른바 Parser Hell 로 갈 수 있습니다.
따라서 일반적으로는 표준(standard) 를 정해서 지키는 방식으로 정의가 되었는데요.
대표적인 방식이 b3 방식과 w3c 방식을 채택하고 있습니다.
먼저, b3 방식은 X-B3 라는 prefix 를 통해 HTTP 헤더로 정의된 TraceContext 를 정의하여 전달하는 구조입니다.
Trace Context 의 요소들을 4가지 HTTP header 로 추가해서 전파하게 되는 방식입니다.
반대로 w3c 방식은 굉장히 흥미로운데요
traceparent / tracestate 라는 2가지 헤더만 사용하여 전달하게 됩니다. traceparent 에는 trace context 정보들이 담기게 되고, tracestate 는 내가 어떤 trace vendor 를 사용했는지 남기게 됩니다.
traceparent 헤더는 -(하이픈) 구분자를 기반으로,
version, trace-id, parent-id(span), trace-flags 를 구분하여 traceContext 를 전달하고 있는 형태입니다.
따라서 조금 더 유연한 방식으로 사용할 수 있게끔 개선했다고 볼 수 있겠네요
그렇기 떄문인지, Spring Boot 2.x 버젼까지만 해도 b3 방식이었는데, Spring Boot 3.x 버젼으로 오면서 w3c 방식으로 옮기게 되었죠.
정리하면 Communicate 방식에 따라 trace context 를 전달할 수 있도록 하게 한 것이 핵심이라고 볼 수 있습니다.
코드레벨로 봐보기
많은 예제들이 문서에 있지만, 대표적으로 많이 사용되는 Brave setup 코드쪽 코드를 가볼까요?
// [Brave component] CurrentTraceContext is a Brave component that allows you to
// retrieve the current TraceContext.
ThreadLocalCurrentTraceContext braveCurrentTraceContext = ThreadLocalCurrentTraceContext.newBuilder()
.addScopeDecorator(MDCScopeDecorator.get()) // Example of Brave's
// automatic MDC setup
.build();
// [Micrometer Tracing component] A Micrometer Tracing wrapper for Brave's
// CurrentTraceContext
CurrentTraceContext bridgeContext = new BraveCurrentTraceContext(this.braveCurrentTraceContext);
// [Brave component] Tracing is the root component that allows to configure the
// tracer, handlers, context propagation etc.
Tracing tracing = Tracing.newBuilder()
.currentTraceContext(this.braveCurrentTraceContext)
.supportsJoin(false)
.traceId128Bit(true)
// For Baggage to work you need to provide a list of fields to propagate
.propagationFactory(BaggagePropagation.newFactoryBuilder(B3Propagation.FACTORY)
.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create("from_span_in_scope 1")))
.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create("from_span_in_scope 2")))
.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create("from_span")))
.build())
.sampler(Sampler.ALWAYS_SAMPLE)
.addSpanHandler(this.spanHandler)
.build();
// [Brave component] Tracer is a component that handles the life-cycle of a span
brave.Tracer braveTracer = this.tracing.tracer();
// [Micrometer Tracing component] A Micrometer Tracing wrapper for Brave's Tracer
Tracer tracer = new BraveTracer(this.braveTracer, this.bridgeContext, new BraveBaggageManager());
이리저리 많지만, 제일 중요한 요소는 바로 ThreadLocal 입니다.
TraceContext 의 가장 기본적인 동작방식은 ThreadLocal 기반으로 동작한다는 점에 있습니다. 요청이 들어온 Thread 에 TraceContext 를 유지시키고, MDCDecorator 까지 접목시켜 log 에도 남길 수 있도록 하는 것이죠.
위에서 설명드린 구성요소에 대한 설명과 클래스 설계의도는 일치하니 한번 비교해가면서 보는 것도 좋겠네요 ^-^
그리고 micrometer tracing 는 spring AOP 의존성을 가지고 있다면, AOP 기반으로 동작하게끔 하는 클래스들도 많으니 궁금하신 분들은 라이브러리 import 해서 살펴보시면 되겠습니다!
정리하며
Micrometer tracing 은 분산 서비스 아키텍쳐 혹은 microservice architecture 에서 요청에 대한 추적을 쉽게하기 위해 만들어진 프로젝트입니다.
여러분들이 MSA 기반의 환경에서 시스템을 구축하고 있다면,
원활한 운영 유지보수와 쉬운 디버깅을 위해 반드시 도입을 고려해야되는 라이브러리이니 꼭 생각해보시면 좋겠네요~!
참고링크
Micrometer tracing
Openzipkin b3-propagation
w3c-traceContext
'Developer > Spring' 카테고리의 다른 글
Spring Boot API server 성능 테스트(performance test)를 해보자(with python locust) (0) | 2024.02.12 |
---|---|
[JPA] Hibernate 에서 지원하는 Id generator 에 대해 알아보자 (1) | 2023.12.17 |
Spring 에서 Transactional 을 사용할 때 Exception 이 발생하는 상황에 주의하자 (1) | 2023.06.11 |
Spring WebFlux 에서는 어떻게 Kotlin Coroutine 을 지원하고 있을까? ( feat. Context ) (3) | 2023.05.27 |
[JPA] Hibernate 에서 Query statement caching 은 어떻게 이루어질까? (5) | 2023.01.08 |