실전 프로젝트/프로젝트 과정

실전 프로젝트 19 - 분산 추적

구너드 2023. 8. 26. 23:16

분산 추적

 

여러 마이크로서비스가 함께 구성하는 애플리케이션 간 상호 작용을 모니터링하고 분석하는 기술. 마이크로서비스 환경에서는 애플리케이션이 서로 통신하는 여러 독립적인 서비스로 구성된다. 이러한 아키텍처는 확장성과 유연성과 같은 이점을 제공하지만, 서로 다른 서비스 간 상호 작용 및 전체 성능에 미치는 영향을 이해하는 데 복잡성을 증가시킨다. 분산 추적은 요청이 다양한 마이크로서비스를 통과하는 과정을 시각화하여 개발자와 운영 팀이 전체 시스템의 동작과 성능을 더 잘 이해할 수 있도록 한다. 분산추적을 통해 특정 요청이 어떠한 마이크로서비스를 거쳐가는지에 대한 요청흐름, 각 서비스 및 네트워크 통신에 얼마나 많은 시간이 소요되는 지에 대한 지연분석, 특정 요청이 어떠한 부분에서 오류를 발생시키는지 확인할 수 있는 오류분석, 서로 다른 마이크로 서비스들 간의 종속성을 확인할 수 있는 의존성 매핑 등을 확인할 수 있다.

분산 추적을 구현하려면 일반적으로 각 마이크로서비스의 코드를 계측하여 추적 정보를 생성하고 전파해야 한다. 추적은 스팬의 모음으로 구성되며, 각 스팬은 서비스 내의 작업 또는 작업 세그먼트를 나타낸다. 스팬은 서로 연결되어 요청이 마이크로서비스를 통해 이동하는 전체 과정을 나타내는 추적을 형성한다.

 

이번 프로젝트에서 MSA를 설계한 이상, 서로 다른 마이크로서비스들 간의 연결 흐름을 알기 위해서는 분산 추적이 꼭 필요하다고 생각이 들었고 기존 서비스 아키텍처에 포함되어 있지 않았기 때문에 추가적인 과제로 내가 맡아보겠다고 팀원들에게 이야기했다. 염두에 두고 있던 것은 zipkin과 sleuth를 이용하여 각 마이크로서비스 간의 흐름을 시각화하는 것이었다.


초반에 계획한 대로 Sleuth와 Zipkin을 이용하여 분산추적을 구현해보려고 했으나, 스프링 3,x 부터는 Sleuth가 지원되지 않음을 알게 되었다. 따라서 Sleuth 프로젝트를 넘겨받은 micrometer를 이용하게 되었고 이를 기반으로 코드를 작성하게 되었다.

 

    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'io.micrometer:micrometer-tracing-bridge-brave'
    implementation 'io.zipkin.reporter2:zipkin-reporter-brave'

의존성 설정

 

@Slf4j
@Component
@Aspect
public class LogAspect {


    @Pointcut("execution(* com.example.auctionserver.auction..*(..))")
    public void all() {}

    @Before("all()")
    public void logAll(JoinPoint joinPoint) {

        log.info("[{}]" , joinPoint.getSignature().getName());
    }
}

로그를 일일이 찍기 보다는 모놀리스에서 구현했던 AOP를 이용해서 작성하기 위해 AOP에 대한 의존성도 추가를 진행했다. 이를 위해 추가적으로 logging 디렉토리를 만들어 도메인 계층에 해당 로그들이 적용될 수 있게끔 설정하였다.

실제 호출되는 클래스의 메서드 이름을 로그에서 표현하기 위해 Joinpoint를 사용했고 요청이 들어가기 전에 해당 요청에 제대로 도착했는지 확인하기 위해서 @Before를 적용했다.

 

management:
  tracing:
    sampling:
      probability: 1.0
    propagation:
      consume: b3
      produce: b3_multi
  zipkin:
    tracing:
      endpoint: ${ZIPKIN_BASE_URL}
      
      
logging:
  pattern:
    level: "%5p [%X{traceId:-},  %X{spanId:-}]"

Zipkin의 url, 로그 표현 방식 등 다양한 설정들을 작성한 yml

 

tracing - 추적과 관련된 구성을 포괄하는 루트 섹션.
sampling - 추적에서 샘플링은 특정 추적이 기록되어야 하는지 여부를 결정하는 과정을 의미. 모든 추적을 기록하는 것은 자원을 많이 사용.

probability -  추적을 캡처할 확률을 나타냅니다. 이 경우에는 1.0으로 설정되어 있으므로 추적은 항상 캡처되며, 샘플링이 비활성화. 1.0보다 작은 값으로 설정하면 일부 추적만 캡처.
propagation - 분산 추적에서 전파는 추적 컨텍스트가 한 서비스에서 다른 서비스로 전달되는 방식을 의미하며, 추적 시스템이 여러 서비스 간의 전체 추적을 연결.

consume  - 요청이 시스템에 들어올 때 추적 컨텍스트가 어떤 형식으로 소비되어야 하는지를 지정 (예: b3 형식).

produce 필드는 요청이 시스템을 떠날 때 추적 컨텍스트가 어떤 형식으로 전파되어야 하는지를 지정 (예: b3_multi 형식). 

B3 - 추적 컨텍스트를 간단하고 간결하게 표현하는 방법. 세 가지 주요 구성 요소로 구성된다:
Trace ID: 추적의 전역적으로 고유한 식별자.
Span ID: 개별 스팬의 고유한 식별자. (추적 내의 단일 작업).
Sampling Flags: 추적과 스팬이 샘플링(캡처)되어야 하는지 여부를 나타내는 플래그.

 

추적 컨텍스트는 일반적으로 서비스 간 HTTP 요청의 헤더로 전달되는데, .

X-B3-TraceId: <추적 ID>
X-B3-SpanId: <스팬 ID>
X-B3-Sampled: <샘플링 플래그>

와 같은 형식으로 전달된다

B3 Multi - b3형식의 확장으로, 더 포괄적인 추적 컨텍스트 전파를 위해 사용된다. 기본 필드 외에도 다양한 맥락 정보를 전달할 수 있는 선택적 필드를 포함한다. 이러한 필드에는 상위 스팬 ID, 디버깅용 플래그 등이 포함될 수 있다. b3 multi는 분산 시스템의 디버깅과 분석을 위해 더 풍부한 정보를 제공한다. 형식은 구현과 추적 시스템의 요구에 따라 기본 b3 필드를 넘어 추가 필드를 포함할 수 있다.


Auction 도메인의 입찰 서비스에서 Member 도메인을 FeignClient를 이용하여 호출하기 때문에 gateway-auction-member로 넘어가는 추적을 확인해보고자 했다. 하지만 실제로 gateway에 입찰을 요청한 결과 gateway에서 auction은 동일한 TraceId가 넘어가지만 member에서는 별개의 TraceId가 생성되어 로그가 찍히는 것을 확인할 수 있었다. 이러한 문제는 분산추적을 고려한 의미가 퇴색되기 때문에 꼭 해결을 해야했고 해당 문제가 어디서 발생했는지 고민해본 결과 일반적인 http 요청으로 들어오게 되면 동일한 TraceId로 동기화가 되지만, Feign Client는 그렇지 않다는 결론을 내릴 수 있었다. 따라서 Feign Client부분에서 TraceId를 동기화해주는 코드가 필요하다고 생각했고 이러한 자료를 찾아본 결과

 

//의존성 추가
implementation 'io.github.openfeign:feign-micrometer:12.4'


@Configuration
public class FeignMicrometerConfig {

    @Bean
    public Capability capability(final MeterRegistry registry) {
        return new MicrometerCapability(registry);
    }
}

해당 코드를 작성할 수 있었다. 요청이 들어온 TraceId를 FignClient로 호출되는 서비스에 넘겨주어 동일한 TraceId를 갖도록 동기화해주는 코드로 추가적인 의존성을 설정해주면 작성할 수 있다. 이 후 테스트를 통해 로컬의 Zipkin 페이지에 접속한 결과 TraceId가 동기화 되어 제대로 된 분산추적이 되고 있음을 확인할 수 있었다.


여러 마이크로서비스간에 분산 추적은 실제 오류와 병목현상이 발생하는 지점을 확실하게 알기 위해서 꼭 가져가야할 시스템이라고 생각이 들었다. 다만 오류의 내용에 대해서 확인할 수 있을 뿐 해당 오류가 클라이언트에게 직접적으로 전파되는 것은 막을 수 없었는데 이러한 부분은 circuitbreak 패턴으로 잘 알려진 장애격리 디자인 패턴을 도입해야하는 필요성을 느낄 수 있었다. 프로젝트의 기본 틀과 성능적인 개선이 이루어졌다면 이후에 장애 격리에 대해서도 시도를 해봐야겠다.