티스토리 뷰
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 |
그림으로 표현하면 아래와 같습니다.
또한, 이러한 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 아키텍처는 아래와 같습니다.
- 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
2) Zipkin설치
아래 git repository의 README를 참조하여 설치합니다.
github.com/happyspringcloud/zipkin-helm
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 |
- Total
- Today
- Yesterday
- 디토소비
- 도파밍
- SAGA
- spotify
- 육각형인간
- 돌봄경제
- 스포티파이
- 리퀴드폴리탄
- 마이크로서비스
- 호모프롬프트
- 버라이어티가격
- 분초사회
- 애자일
- AXON
- CQRS
- API Composition
- micro service
- 스핀프로젝트
- 마이크로서비스 패턴
- 요즘남편 없던아빠
- agile
- Event Sourcing
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |