클라우드 네이티브 애플리케이션 부트캠프 알림

티스토리 뷰

1. Sleuth와 Zipkin 이해

1) WHY ?

마이크로서비스로 큰 서비스를 잘게 쪼개어 개발하게 되면 자연스럽게 마이크로서비스간에 연결이 많아지고 복잡하게 됩니다.

예를 들어 고객 '홍길동'이 2021-02-01 13:33:33에 피자 3개를 주문했다고 가정해 봅시다.

그 주문이 처리되기 위해서는 아래와 같이 여러가지 마이크로서비스들이 서로 호출하게 됩니다.

마이크로서비스
(consumer)
마이크로서비스
(producer)
API
주문접수 주문등록 /order/register/{order id}
주문등록 고객체크 /customer/validate/{customer id}
주문접수 결제 /pay/{order id}
주문접수 조리요청 /restaurant/{order id}
조리요청 배달요청 /deliver/{order id}

주문 처리가 되지 않거나 매우 느려진다면 어느 지점이 문제인지 빠르게 찾을 수 있어야 합니다.

 

Sleuth는 분산된 마이크로서비스간에 트래픽의 흐름을 추적(Tracing)할 수 있도록

Trace기록을 로그에 자동 삽입해 줍니다.

 

다시말해,

Sleuth와 Zipkin이 필요한 이유는 분산된 마이크로서비스간의 트래픽을 추적하여 문제를 사전에 방지하거나 해결하기 위해서입니다.

 

2) HOW ?

어떻게 하면 연관된 트래픽의 흐름을 추적할 수 있을까요 ?

동일한 트랙잰션에 해당하는 트래픽들에 동일한 TraceID를 부여하면 됩니다.

위 주문 트랜잭션에서 모든 트래픽이 동일한 Trace ID를 갖고 있다면 쉽게 추적할 수 있을것입니다.

Sleuth를 적용한 후 Log4j, Logback, SLF4J(Simple Logging Facade for Java)등을 사용하여 로깅하면, 

자동으로 로그에 Service명, Trace ID, Span ID가 삽입됩니다.

아래는 Sleuth에 의해 로그에 자동으로 Trace정보가 주입된 예제입니다.

service명이 hystrix-consumer이고 Trace ID가 7d84c8618a10307f이며 Span ID는 e25a40b90acb4e5b입니다.

Span ID는 각 트래픽의 고유 ID입니다.

2021-02-02 11:58:58.969  INFO [hystrix-consumer,7d84c8618a10307f,e25a40b90acb4e5b,true] 1 --- [nio-8003-exec-7] com.springcloud.CafeController           : ### Received: /delay/pass

2021-02-02 11:58:58.990  INFO [hystrix-consumer,7d84c8618a10307f,e25a40b90acb4e5b,true] 1 --- [nio-8003-exec-7] com.springcloud.CafeController           : ### Sent: [Americano, Latte, Mocha]

위 주문 트랜잭션의 예를 든다면 아래와 같이 부여됩니다.

마이크로서비스
(consumer)
마이크로서비스
(producer)
API Trace ID Span ID
주문접수 주문등록 /order/register/{order id} 1000 1000
주문등록 고객체크 /customer/validate/{customer id} 1000 1100
주문접수 결제 /pay/{order id} 1000 1200
주문접수 조리요청 /restaurant/{order id} 1000 1300
조리요청 배달요청 /deliver/{order id} 1000 1400

그림으로 표현하면 아래와 같습니다.

그림출처:https://bcho.tistory.com/1243

또한, 이러한 Tracing정보에는 4가지 종류의 timestamp가 있어 소요된 시간까지 측정할 수 있습니다.

CS(Client Start) -> SR(Server Received) => SS(Server Sent) => CR(Client Received)

 

이러한 Trace정보를 Zipkin과 같은 분산 트랜잭션 추적 시스템으로 송부하면 그래픽하게 트래픽의 흐름을 볼 수 있습니다.

Trace정보를 Zipkin에 송부하기 위해서는 Zipkin client를 적용해야 합니다.

 

참고) Sleuth는 형사, 탐정이라는 뜻입니다. 트래픽을 추적하는 형사라는 의미인에서 붙여진것 같습니다.

Zipkin은 성서에서 '새'를 의미하는 Zipporah에서 유래한것 같습니다. (www.ancestry.com/name-origin?surname=zipkin)

 

2. Zipkin 설치

1) Zipkin 이해

Zipkin은 분산 트랜잭션 추적을 위한 오픈소스소프트웨어입니다. 트위터에서 제공하였습니다.

비슷한 제품으로는 Jaeger가 있습니다.

Zipkin 아키텍처는 아래와 같습니다.

그림출처:https://twofootdog.tistory.com/65

- Zipkin client library: 각 어플리케이션에 설치되어 Zipkin collector로 Trace정보를 송부함

- Collector: Trace정보 수집기

- Storge: In-memory(테스트 목적), 소규모는 MySQL, 운영환경에는 ElasticSearch나 Cassandra를 사용

- API(Query Service): Web UI의 요청을 받아 Storage를 검색하여 결과를 리턴

- Web UI: 대시보드 UI 제공

 

실습에서 사용할 zipkin서버는 helm chart로 설치하며, cassandra DB를 사용합니다.

dockder, jar로 zipkin을 설치하는 방법은 zipkin.io를 참조하세요.

EalsticSearch를 DB로 사용하고, kibana를 대시보드로 사용하려면 아래 링크를 참조하세요 .

twofootdog.tistory.com/66?category=903234

 

Zipkin과 ElasticSearch, Kibana 연동하기

이번 글에서는 Zipkin에서 수집한 트레이싱 정보를 In-Memory에 저장하지 않고, ElasticSearch에 저장한 후 Kibana를 통해서 확인하는 방법에 대해서 실습해 볼 것이다. 이 글의 순서는 다음과 같다. 1. 사

twofootdog.tistory.com

 

2) Zipkin설치

아래 git repository의 README를 참조하여 설치합니다.

github.com/happyspringcloud/zipkin-helm

 

happyspringcloud/zipkin-helm

Contribute to happyspringcloud/zipkin-helm development by creating an account on GitHub.

github.com

 

 

3. Sleuth와 Zipkin 실습

 

1) sleuth, zipkin dependency 추가

zuul, webhook, consumer, HystrixConsumer, HystrixProducer 어플리케이션의 pom.xml에  추가 합니다.

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-sleuth</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-zipkin</artifactId>
		</dependency>	

 

2) sleuth, zipkin configuration

configmng의 zuul-common.yaml, webhook-common.yaml, consumer-common.yaml, hystrix-consumer-common.yaml, hystrix-producer-common.yaml에 아래 설정을 추가합니다.

probability는 트래픽의 몇%를 zipkin으로 보낼것인지를 정의합니다. 0.5면 50%만 보내는것입니다.

spring:
  sleuth: 
    sampler:
      probability: 1.0
  zipkin:
    base-url: http://zipkin:9411

zipkin.base-url은 zipkin service의 서비스명과 포트입니다.

 

3) Zuul서버에 Log추가

zuul server

zuul server는 filter를 추가하고 그 filter안에서 Logging합니다.

PreFilter.java

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

@Component
public class PreFilter extends ZuulFilter {
	private final Logger log = LoggerFactory.getLogger(getClass());
	
    private static final int FILTER_ORDER = 1;
    private static final boolean SHOULD_FILTER = true;
    private static final String PRE_FILTER_TYPE = "pre";

    @Override
    public String filterType() {
        return PRE_FILTER_TYPE;
    }

    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        
        log.info("### Request Method : " + request.getMethod());
        log.info("### Request URL : " + request.getRequestURL().toString());
        return null;
    }
}

 

RouteFilter.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;

@Component
public class RouteFilter extends ZuulFilter {
	private final Logger log = LoggerFactory.getLogger(getClass());

	private static final int FILTER_ORDER = 1;
	private static final boolean SHOULD_FILTER = true;
	private static final String PRE_FILTER_TYPE = "route";

	@Override
	public String filterType() {
		return PRE_FILTER_TYPE;
	}

	@Override
	public int filterOrder() {
		return FILTER_ORDER;
	}

	@Override
	public boolean shouldFilter() {
		return SHOULD_FILTER;
	}

	@Override
	public Object run() throws ZuulException {
		log.info("### Route Filter");

		return null;
	}
}

 

PostFilter.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;

@Component
public class PostFilter extends ZuulFilter {
	private final Logger log = LoggerFactory.getLogger(getClass());
	
    private static final int FILTER_ORDER = 1;
    private static final boolean SHOULD_FILTER = true;
    private static final String PRE_FILTER_TYPE = "post";

    @Override
    public String filterType() {
        return PRE_FILTER_TYPE;
    }

    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    @Override
    public Object run() throws ZuulException {
    	log.info("### Post Filter");
        return null;
    }
}

 

ErrorFilter.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

public class ErrorFilter extends ZuulFilter {
	 
    private final Logger log = LoggerFactory.getLogger(getClass());
    
    @Override
    public String filterType() {
        return "error";
    }
 
    @Override
    public int filterOrder() {
        return 1;
    }
 
    @Override
    public boolean shouldFilter() {
        return RequestContext.getCurrentContext().getThrowable() != null;
    }
 
    @Override
    public Object run() {
        Throwable throwable = RequestContext.getCurrentContext().getThrowable();
        log.error("Exception was thrown in filters: ", throwable);
        return null;
    }
}

 

CustomErrorController.java

import java.util.HashMap;
import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CustomErrorController implements ErrorController {
 
    private static final String ERROR_PATH = "/error";
 
    @Override
    public String getErrorPath() {
        return ERROR_PATH;
    }
    
    @RequestMapping(ERROR_PATH)
    public Map<String, String> handleError(HttpServletRequest request, HttpServletResponse response) {
        Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        HttpStatus httpStatus = HttpStatus.valueOf(Integer.valueOf(status.toString()));
        Map<String, String> errorMsg = new HashMap<>();
        errorMsg.put("code", status.toString());
        errorMsg.put("msg", httpStatus.getReasonPhrase());
        return errorMsg;
    }
 
}

 

git repository에 push하고 재배포합니다.

 

4) backend service에 Log추가

webhook, consumer, HystrixConsumer, HystrixProducer 어플리케이션에 Log를 추가합니다.

webhook-Controller.java

public class Controller {
	private final Logger log = LoggerFactory.getLogger(getClass());

	...
   
    public String echo(@PathVariable String message) {
    	log.info("### Received: webhook > /greeting/"+message);
    	log.info("### Sent: "+greeting + "=>" + message);
        return greeting + " => " + message;
    } 
}

consumer-Controller.java

@RestController
@RefreshScope
public class Controller {
	private final Logger log = Logger.getLogger(getClass());
	
	...
    
	public String greeting(@PathVariable String message) {
		log.info("### Received: /greeting/"+message);
		
		...
        
		log.info("### Sent: "+"[" + baseUrl + "] " + response.getBody());
		return "[" + baseUrl + "] " + response.getBody();

	}
    
	...	

	public List<String> testHystrix(@PathVariable String param) {
		log.info("### Received: /hystrix/"+param);
		
		...
        
		log.info("### Sent: "+response.getBody());
		return response.getBody();
	}
	
    ...
    
	public String testHystrix2(@PathVariable String param) {
		log.info("### Received: /delay/"+param);

		...
        
		String msg = "I'm Working !";
		log.info("### Sent: "+msg);
		return msg;
	}
}

 

HystrixConsumer-CafeController.java

@RestController
public class CafeController {
	private final Logger log = LoggerFactory.getLogger(getClass());
	
    ...
	
    public List<String> getCoffees(@PathVariable String param) {
		log.info("### Received: /delay/"+param);
		
		List<String> list = cafeService.getCoffees(param);
		
		log.info("### Sent: " + list.toString());
		return list;
	}
}

HystrixProducer-Controller.java

public class Controller {
	private final Logger log = LoggerFactory.getLogger(getClass());
	
	@GetMapping("/coffees/{param}")
	public List<String> getConffees(@PathVariable String param) {
		log.info("### Received: /coffees/"+param);
		
		...
        
		List<String> list = Arrays.asList("Americano", "Latte", "Mocha");
		log.info("### Sent: "+list.toString());
		
		return list;
	}
}

 

각 앱의 소스를 git repository에 push하고 재배포합니다.

 

5) zipkin dashboard에서 확인

- zuul 서버 콘솔을 띄우고, 웹브라우저에서 http://<zuul ingress host>/consumer/hystrix/pass 을 오픈합니다.

k logs -f zuul-0

아래와 같은 로그가 나올겁입니다. Trace ID를 복사합니다.

- zipkin dashboard를 브라우저에서 오픈합니다.

zipkin dashboard의 주소는 k get ing로 확인합니다.

위에서 복사한 Trace ID를 우측 상단의 검색박스에 붙여넣고 검색합니다.

zuul -> consumer -> hystrix-consumer -> hystrix-producer로 연결되는것을 볼 수 있습니다.

각 서비스 사에에 hystrix가 있는 이유는 HystrixCommand를 적용했기 때문입니다.

그래서 총 7단계의 트래픽이 발생합니다. 각 트래픽에는 고유의 Span ID가 부여됩니다.

각 트래픽을 클릭하면 우측에 자세한 정보가 나옵니다.

 

아래와 같이 조건을 지정하여 트래픽을 검색할 수 있습니다.

 

6) kibana에서 확인

kibana에서도 Trace ID를 이용하여 로그를 통합 검색할 수 있습니다.

kibana설치는 아래 링크를 참조하세요.

happycloud-lee.tistory.com/202?category=832243

 

Kibana > Discover를 클릭하고 좌측 상단의 검색박스에 Trace ID를 입력하고 검색합니다.

시간 오름차순으로 소트하면 시간순으로 각 마이크로서비스의 로그를 볼 수 있습니다.

 

'Micro Service > mSVC개발' 카테고리의 다른 글

[SC12] Spring Cloud Gateway 란 ?  (6) 2021.02.14
[SC11] Spring Boot Actuator 이란 ?  (0) 2021.02.14
[SC09] Spring Cloud Hystrix 란 ?  (0) 2021.02.14
[SC08] Spring Cloud Ribbon 이란 ?  (0) 2021.02.14
[SC07] Spring Cloud Zuul 이란 ?  (0) 2021.02.14
댓글

클라우드 네이티브 애플리케이션 부트캠프 알림