IT/MSA

Jaeger를 활용한 분산 환경 서비스 로그 트레이싱(SpringBoot, Spring Cloud Gateway 활용)

twofootdog 2020. 7. 11. 17:43

이번 글에서는 Jaeger를 활용하여 분산 환경 내 SpringBoot로 구성된 마이크로서비스들의 로그 트레이싱하는 방법에 대해 배워볼 것이다. 이 글은 쿠버네티스 환경이 아닌 AWS EC2 환경에서 실습하는 내용이며 Jaeger도 EC2 환경에 설치해서 실습을 진행할 것이다. 만약 쿠버네티스 환경에서 구동중인 마이크로서비스를 트레이싱 하기 위해 Jaeger도 쿠버네티스 환경에 설치하는 글을 참고하려면 다음 글을 참고하길 바란다(AWS EKS에 로그 트레이싱 환경 구축하기(2) - Jaeger 설치하기)

 

 

이 글의 순서는 다음과 같다

1. Jaeger는 무엇인가?

2. Jaeger 용어 및 구성요소

3. Jaeger 설치 및 실행

4. SpringBoot 서비스 로그 트레이싱 실습

 

1. Jaeger는 무엇인가?

Jaeger는 분산 서비스 간 트랜잭션을 추적하는 오픈소스 소프트웨어로 복잡한 마이크로서비스 환경을 모니터링하는데 사용된다. 클라우드 네이티브 소프트웨어에서는 사용자가 애플리케이션에 요청을 보내면 요청에 맞는 개별의 마이크로서비스가 이에 응답하여 결과를 생성한다. 따라서 단 한번의 호출로 이와 관련된 여러개의 서로 다른 마이크로서비스가 실행될 수 있기 때문에, 에러나 속도 지연등의 문제가 발생할 경우 문제를 파악하기 위해서는 모든 연결을 추적할 수 있어야 하며, 이때 사용할 수 있는 오픈소스 소프트웨어가 바로 Jaeger이다.

Jaeger를 사용하면 다양한 마이크로서비스의 요청 경로를 추적하고, 요청 흐름을 시각적으로 확인하며 분산 트랜잭션을 모니터링하고, 성능과 대기시간을 최적화하고, 문제 해결을 위한 근본 원인 분석을 할 수 있다.

Jagger는 처음에 차량공유 서비스 기업인 Uber에서 2015년 오픈소스 프로젝트로 개발했으며, 2017년 CNCF(Cloud Native Computing Foundation) Incubation 프로제게트로 채택되었으며, 2019년에는 정식 프로젝트로 승인되었다.

 

 


2. Jaeger 용어 및 구성요소

2-1. Jaeger 용어

1. Trace : 시스템을 통하는 데이터/실행 경로. 1개 이상의 Span으로 이루어져 있다. 쉽게 설명하자면 클라이언트가 특정 기능 요청 후 응답을 리턴받을 때까지를 Trace라고 이해하면 된다. 

2. Span : Jaeger의 논리적인 작업단위. 각 Span에는 작업명. 시작시간. 기간등이 정보가 포함되어 있다. Span은 중첩되거나 순서대로 정리되어 있을 수 있다. 

 

 

2-2. Jaeger 구성요소

1. Jaeger Client : OpenTracing API로 구현되어 있다. Jaeger Client가 적용된 서비스가 호출되면, 서비스에서는 SpanID, TraceID, Baggage를 작성한 후 Request에 첨부하여 다음 서비스로 전송한다. 서비스명, 태그 및 로그와 같은 데이터는 전파되지 않고 Jaeger 백엔드로 전송하게 된다. 

2. Agent : UDP를 통해 전송 된 Span 수신을 대기하는 네트워크 데몬으로 일괄 처리하여 Collector로 전송한다. Agent는 Jaeger 적용 서비스가 존재하는 모든 호스트에 배포되어야 한다. 

3. Collector : Agent로부터 Trace를 수신하여 처리 파이프라인을 통해 유효성검사, 색인생성/변환을 수행한 후 Cassandra, ElasticSearch등 저장소에 저장한다.

4. Query : 저장소에서 Trace를 검색한 후, UI로 결과를 보여주는 서비스

5. Ingester : Kafka Topic에서 데이터를 읽은 후 Cassandra나 ElasticSearch같은 백앤드 스토리지에 기록하는 서비스

 

트레이싱 정보를 Collector에서 Storage로 바로 적재하는 구성도 : 

 

트레이싱 정보를 Collector에서 Kafka를 거쳐서 Storage로 적재하는 구성도 : 

 

 

 


3. Jaeger 설치 및 실행

그럼 이제 Jaeger를 설치해보자.

우선 실습에 사용된 서버 Spec은 다음과 같다.

Jaeger 설치 서버 : AWS EC2 Instance t2.micro Amazon Linux2

 

Jaeger를 설치하는 방법에는 Jaeger는 도커 이미지 파일로 설치하는 방법과 바이너리로 설치하는 방법이 있는데, 이 글에서는 조금 쉽게 바이너리를 설치하는 방식으로 진행해보도록 하겠다.

우선 Jaeger 공식 사이트 다운로드 페이지로(https://www.jaegertracing.io/download/) 들어가 Jaeger가 설치될 OS를 선택한 후 바이너리파일(all-in-one)을 다운받는다. 다운로드가 완료되었으면 해당 파일을 서버로 업로드 한 후 압축을 풀어보자.

$ tar -xvf jaeger-1.18.0-linux-amd64.tar.gz      # 압축 해제
$ cd jaeger-1.18.0-linux-amd64                   # 디렉토리 이동

 

압축을 풀게되면 Jaeger 디렉토리 내에 아래와 같이 총 6개의 파일이 생성된다.

jaeger-1.18.0-linux-amd64/jaeger-ingester

jaeger-1.18.0-linux-amd64/jaeger-all-in-one

jaeger-1.18.0-linux-amd64/jaeger-collector

jaeger-1.18.0-linux-amd64/jaeger-agent

jaeger-1.18.0-linux-amd64/example-hotrod

jaeger-1.18.0-linux-amd64/jaeger-query

이중 jaeger-1.18.0-linux-amd64/jaeger-all-in-one 파일이 있는데, 이 파일은 collector, agent, query 등의 jaeger 구성요소들이 결합된 하나의 실행파일이며 이 파일 하나만으로 Jaeger의 기능 전부 사용할 수 있다. 단 어디까지나 테스트이기 때문에 all-in-one을 사용하는 것이며, Jaeger를 운영환경에 적용할 때에는 all-in-one 보다는 Collector + Query + Agent 방식으로 적용하는 것이 권장되는 방식이라고 한다. 

$ ./jaeger-all-in-one

 

Jaeger가 정상적으로 실행되었으면 이제 Jaeger UI에 접속해보도록 하자. Jaeger UI는 [Jaeger 설치 서버 IP]:16686으로 접속하면 되며, 접속 전에 방화벽은 열어놓도록 하자(AWS의 경우 인바운드 정책에 16686포트 추가 필요).

Jaeger UI에 접속해보면 아래와 같은 화면이 나오게 된다. 

하지만 아직 마이크로서비스로부터 로그 트레이싱 정보를 받고 있지 않기 때문에 어떤 정보도 표시되진 않는다.

 

 


4. SpringBoot 서비스 로그 트레이싱 실습

4-1. gateway에서 마이크로서비스 호출

Jaeger 설치 및 구동이 완료되었으니, 이제 SpringBoot 서비스 로그 트레이싱을 진행해보자.

 

- Jaeger 모니터링 대상 마이크로서비스 : SpringBoot 2.3.1, JDK11, Gradle 6.4.1, webflux

- Jaeger 모니터링 대상 API 게이트웨이 : Spring Cloud Gateway, SpringBoot2.2.3, JDK11, Gradle6.4.1

 

Jaeger를 통해 로그 트레이싱을 진행할 SpringBoot 서비스는 API 게이트웨이, 마이크로서비스1(cafe서비스), 마이크로서비스2(user서비스) 이렇게 총 3개로 이루어져 있다. 서비스 구축에 대한 자세한 설명은 이전 블로그 글(https://twofootdog.tistory.com/64)을 참고하면 된다. 

서비스가 준비되었으면 Jaeger를 적용해보자.

우선 3개의 서비스에 Jaeger 모듈 의존성을 추가해준다.

 

build.gradle : 

dependencies {
    ...
    
    compile group: 'io.opentracing.contrib', name: 'opentracing-spring-jaeger-web-starter', version: '3.1.2' // jaeger
    
    ...
}

 

다음으로 3개의 서비스의 application.yml 파일에 jaeger 연동 정보를 추가해준다.

gateway-svc > application.yml :

server:
  port: 8080  # service port

spring:
  application:
    name: gateway-service
opentracing:
  jaeger:
    service-name: gateway-svc # jaeger service name
    udp-sender:
      host: 15.164.171.211 # jaeger server ip
      port: 6831           # jaeger udp port

user-svc > application.yml : 

server:
  port: 8081  # service port

spring:
  application:
    name: user-service
opentracing:
  jaeger:
    service-name: user-svc # jaeger service name
    udp-sender:
      host: 15.164.171.211 # jaeger server ip
      port: 6831           # jaeger udp port

cafe-svc > application.yml : 

server:
  port: 8082  # service port

spring:
  application:
    name: cafe-service
opentracing:
  jaeger:
    service-name: cafe-svc # jaeger service name
    udp-sender:
      host: 15.164.171.211 # jaeger server ip
      port: 6831           # jaeger udp port

 

설정이 완료되었으면 3개의 서비스를 실행시킨 후, Gateway를 통해서 서비스를 호출한 후(예를 들면 GET localhost:8080/cafe/info), 다시 Jaeger UI로 접속해보자([Jaeger 설치 서버 IP]:16686).

그러면 정상적으로 호출된 서비스 정보가 나오는 것을 확인할 수 있다.

그런데 이상한 점이 있다. 왜 gateway-svc에서 user-svc나 cafe-svc를 호출했는데도, 로그 트레이싱 정보에는 gateway-svc 정보만 보이는 것일까?(Zipkin에서는 호출된 모든 정보가 다 나왔었는데..)

gateway-svc는 spring cloud 기반이기 때문에 의존성을 한개 더 추가해줘야 한다고 한다(https://github.com/opentracing-contrib/java-spring-cloud 참고). 그러므로 아래와 같이 build.gradle 파일을 수정해주자.

 

gateway-svc > build.gradle : 

dependencies {
    ...
    
    compile group: 'io.opentracing.contrib', name: 'opentracing-spring-jaeger-web-starter', version: '3.1.2' // jaeger
    compile group: 'io.opentracing.contrib', name: 'opentracing-spring-cloud-starter', version: '0.5.5' // 추가
    
    ...
}

 

그리고 서비스를 재 실행하게 되면 아래와 같이 정상적인 결과를 확인할 수 있다.(gateway-svc -> cafe-svc 호출)

 

서비스 실행 로그를 확인해보면 아래와 같이 로그가 찍히게 된다.

위 로그의 의미는 아래와 같다(https://www.jaegertracing.io/docs/1.18/client-libraries/ 참고)

 - f135b321b92581b3 : traceId

 - f00af4802d9369a9 : spanId

 - 447cd12f49aa2978 : parent-span-id

 - 1 : Trace가 collector에 수집된 경우는 1, 수집되지 않은 경우 0

 

 

4-2. 마이크로서비스에서 마이크로서비스 호출

그럼 이제 마이크로서비스에서 마이크로서비스를 호출하는것도 테스트해보자.

호출방향은 gateway-svc -> user-svc -> cafe-svc이며, user-svc의 /user/info 호출 시 cafe-svc가 호출되게끔 구현해보자.

여기서 한가지 주의할 점은 이 글에서는 마이크로서비스가 webflux로 만들어졌기 때문에 RestTemplate 대신 webClint를 사용해야 한다.

user-svc의 UserController.java 코드를 아래와 같이 수정해보자.

user-svc > UserController.java : 

package com.example.usersvc;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/user")
public class UserController {
    private static final Logger logger = LogManager.getLogger(UserController.class);

    @Autowired
    WebClient.Builder webClientBuilder;

    @GetMapping("/info")
    public Mono<String> getUserInfo(ServerHttpRequest request, ServerHttpResponse response) {

        WebClient webClient = webClientBuilder.build();
        Mono<String> result = webClient.get().uri("http://localhost:8082/cafe/info").retrieve().bodyToMono(String.class);
        result.subscribe();


        logger.info("User MicroService Start>>>>>>>>");
        HttpHeaders headers = request.getHeaders();
        headers.forEach((k, v) -> {
            logger.info(k + " : " + v);
        });
        logger.info("User MicroService End>>>>>>>>");
        
        return Mono.just("This is User MicroService!!!!!");
    }
}

 

그리고 localhost:8080/user/info 서비스를 호출한 후 jaeger UI를 확인해보면, 아래 그림과 같이 게이트웨이에서 2개의 서비스가 순차적으로 호출되는 것을 확인할 수 있다.

 

 

4-3. 마이크로서비스에서 Jaeger에 로그 남기기

다음으로 마이크로서비스에서 Jaeger에 로그를 남겨보자.

user-svc의 UserController.java 코드를 아래와 같이 수정해보자.

user-svc > UserController.java : 

package com.example.usersvc;

import io.opentracing.Span;
import io.opentracing.Tracer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/user")
public class UserController {
    private static final Logger logger = LogManager.getLogger(UserController.class);

    @Autowired
    WebClient.Builder webClientBuilder;

    @Autowired
    private Tracer tracer;

    @GetMapping("/info")
    public Mono<String> getUserInfo(ServerHttpRequest request, ServerHttpResponse response) {

        Span parentSpan = tracer.scopeManager().activeSpan();
        Span spanPhase1 = tracer.buildSpan("spanPhase_1").asChildOf(parentSpan).start();

        WebClient webClient = webClientBuilder.build();
        Mono<String> result = webClient.get().uri("http://localhost:8082/cafe/info").retrieve().bodyToMono(String.class);
        result.subscribe();

        try {
            spanPhase1.log("User Micro Service Call. Request Header: " + request.getHeaders());  // 로그1
            spanPhase1.log("User Micro Service Call. Request Body: " + request.getBody());		// 로그2
            spanPhase1.log("User Micro Service Call. Request QueryParams: " + request.getQueryParams());// 로그3
            logger.info("User MicroService Start>>>>>>>>");
            HttpHeaders headers = request.getHeaders();
            headers.forEach((k, v) -> {
                logger.info(k + " : " + v);
            });
            logger.info("User MicroService End>>>>>>>>");
        } finally {
            spanPhase1.finish();
        }

        return Mono.just("This is User MicroService!!!!!");
    }
}

 

수정을 완료하고 서비스를 재 실행한 후 localhost:8080/user/info 서비스를 호출하여 jaeger UI를 확인해보면, 아래 그림과 같이 마이크로서비스에서 찍은 로그를 확인할 수 있다.

 

 

 

 

참고

https://www.jaegertracing.io/docs/1.18/getting-started/

 

Getting Started

Get up and running with Jaeger in your local environment

www.jaegertracing.io

https://www.redhat.com/ko/jaeger%EB%9E%80

 

Jaeger란?

Jaeger는 복잡한 마이크로서비스 환경을 모니터링하고 문제를 해결하는 데 사용되는 분산 추적 소프트웨어입니다.

www.redhat.com

http://www.vinsguru.com/spring-boot-distributed-tracing-with-jaeger/

 

Spring Boot – Distributed Tracing With Jaeger

Overview: There is a quote that ‘Troubles do not come alone and they would like to arrive in group’. So are MicroServices! They do not come alone! We will have often 10+ microservices for a small /mid size application. For large applications it could b

www.vinsguru.com

https://github.com/opentracing-contrib/java-spring-cloud

 

opentracing-contrib/java-spring-cloud

Distributed tracing for Spring Boot, Cloud and other Spring projects - opentracing-contrib/java-spring-cloud

github.com

https://blog.naver.com/PostView.nhn?blogId=alice_k106&logNo=221832024817&redirect=Dlog&widgetTypeCall=true&directAccess=false

 

192. [Kubernetes] Jaeger + Istio를 이용한 tracing의 개념 이해 및 구현

이번 포스트에서는 쿠버네티스에서 Jaeger + Istio를 활용해 서버 간 요청을 트레이싱하는 기초적인 방법...

blog.naver.com