이 글은 인프런 Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) 강의를 듣고 쓴 글입니다.
Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) - 인프런 | 강의
Spring framework의 Spring Cloud 제품군을 이용하여 마이크로서비스 애플리케이션을 개발해 보는 과정입니다. Cloud Native Application으로써의 Spring Cloud를 어떻게 사용하는지, 구성을 어떻게 하는지에 대해
www.inflearn.com
이번 섹션에서는 장애 처리와 Mircorservice 분산 추적에 대해 공부할 것이다.
장애 처리 - Circuit Breaker
만약 어떤 요청에 대해 많은 서비스들이 연쇄적인 통신을 하다가 어딘가에서 오류가 난다면, 요청은 멈추고 500 에러를 반환할 것이다.
우리는 Circuit Breaker를 통해 이러한 오류가 지속적으로 발생하면 오류가 나는 곳으로 요청하지 않고 다른 대체 요청을 할 수 있게 해줄 것이다.
예를 들면, Order Serivce에 문제가 있을 때 Order Service와 통신을 하지 않고 Order 목록을 빈 리스트로 반환해주는 것이다.
즉, CircuitBreaker 는 장애가 발생하는 서비스에 반복적인 호출이 되지 못하게 차단하고 다른 기능으로 대체 수행해서 장애를 회피하는 서비스이다.
자세히
Circuit Breaker Pattern은 2007년 발행된 Release it 이라는 소프트웨어 설계 및 방법에 대한 책에서 나온 패턴이다. 클라이언트에서의 장애를 방지하고 실패할 수 있는 요청을 계속 시도하는 것을 방지한다.
회로는 닫히면 전기가 흐르고 열리면 전기가 흐르지 않게 된다. Circuit Breaker는 이러한 모델링을 기반으로 Open, Closed, Half Open 의 3개 상태를 가진 Finite State Machine 으로 동작하게 된다.
- Closed 상태에서 실패율 혹은 느린 요청 비율이 어떤 임계치를 넘어가면 Open 상태로 바뀌어 원래 요청을 거부하고 대체 요청을 하게 된다.
- Open 상태에서 정해진 시간만큼 흐르게 되면 Half Open 상태로 바뀌게 된다.
- Half Open 상태에서는 정상/비정상을 테스트하기 위해 일부 요청만 허용한다. 실패율 혹은 느린 요청 비율이 임계치보다 높은지, 낮은지를 통해 Closed 혹은 Open 상태로 전이된다.
- 이 상태들을 저장할 때, 원형 배열 모양의 슬라이딩 윈도우 (Sliding Window)를 사용하게 된다. 카운트 기반 슬라이딩 윈도우는 새 요청이 기록되면, 시간 기반 슬라이딩 윈도우는 특정 에포크 초의 결과가 기록되면 업데이트된다.
- 우리가 사용할 Resilience4j 에서 실패율의 기본값은 50퍼센트, Open과 Half Open 기간의 기본값은 60초, Half Open 의 테스트 요청 횟수의 기본값은 10회, 슬라이딩 윈도우 크기의 기본값은 100이다.
이러한 서비스를 제공하는 라이브러리 중 Spring Cloud Netflix Hystrix 가 있는데, Netflix Ribbon, Zuul 과 같이 Deprecated 되었다. 따라서 Resilience4j 라는 라이브러리를 사용할 것이다.
적용
Circuit Breaker를 실습해보기 위해 User Service에서 사용자 정보 조회를 할 때, Order Service에서 주문 조회를 하는 부분을 수정해볼 것이다.
Resilience4j 라이브러리를 User Service에 추가하자.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
그리고 UserServiceImpl 클래스에서 다음을 프로퍼티에 추가해 기본적으로 제공되는 Circuit Breaker Bean을 사용해보자.
private final CircuitBreakerFactory circuitBreakerFactory;
이제 getUserByUserId() 메소드에서 주문 목록을 조회해오는 부분을 FeignClient 로 했는데, 해당 부분을 모두 주석처리하고 다음 코드를 삽입한다.
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitbreaker");
List<ResponseOrder> ordersList = circuitBreaker.run(() -> orderServiceClient.getOrders(userId),
throwable -> new ArrayList<>());
userDto.setOrders(ordersList);
return userDto;
getOrders() 메소드가 오류가 나면, 즉 throwable 되면 빈 리스트를 반환하게 한다.
실제로 Order Service를 기동하지 않고 User Service만 기동하고 사용자 정보 조회를 해보자. (물론 회원가입과 로그인은 하고 나서 해야 한다.)
500 오류가 나지 않고 빈 리스트로 나온다.
그리고 콘솔창에서는 오류가 마구 뿜어져 나오고 있다. 하지만 클라이언트에 오류 메시지를 반환하지 않고 빈 리스트를 반환해준 것이다.
Circuit Breaker Bean 커스터마이징
기본으로 제공되는 설정 말고 여러가지 설정들을 직접 해줄 수도 있다. 다음과 같은 클래스를 config 패키지에 만들자.
@Configuration
public class Resilience4JConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfiguration() {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(4)
.waitDurationInOpenState(Duration.ofMillis(100000))
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(2)
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(4))
.build();
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build());
}
}
최종 반환값에 넘겨주는 것이 timeLimiterConfig, circuitBreakerConfig 2개가 있다.
- timeLimiterConfig : timeoutDuration을 4초로, 즉 4초가 지나면 느린 요청으로 간주한다.
- circuitBreakerConfig : 실패율은 4%, Open 상태는 100초간 지속, 슬라이딩 윈도우는 카운트 기반, 윈도우 크기는 2로 한다. Open 상태를 100초 지속하는 것은 테스트하기 위해서이고, 보통 이렇게 길게 하지 않는다.
이렇게 설정해주면 아까의 코드를 수정하지 않아도 설정된 값이 적용되어 Bean 으로 등록되게 된다.
이러면 Open 상태가 100초가 지속되게 되는데, 잘 적용됐는지 테스트해보자.
(스샷으로 보여주긴 너무 양이 많고,,, 그냥 설명으로)
이렇게 설정하고 사용자 정보 조회 요청을 마구 시도하다 보면, 어느 순간 Order Service에 요청을 하지 않는다는 것을 콘솔창을 통해 알 수 있다. 이 상태는 100초간 지속되고, 100초 후에는 Half Open 상태로 들어가게 된다.
초반에 마구 시도해야 Open 상태로 가는 이유는, 상태 전이에 필요한 최소 요청 횟수가 있기 때문이다. 이 횟수는 기본값이 100이다. 즉, 내 경우 100회를 시도하고 4프로의 실패율을 보였을 경우 Open 상태로 들어가고 100초동안 지속된다.
분산 추적 - Zipkin, Micrometer
Zipkin
Zipkin 이란 대표적으로 Twitter에서 사용하는 분산 환경의 Timing 데이터를 수집하고 추적하는 오픈소스 시스템이다.
알아야 하는 용어로는 크게 두 가지가 있다.
- Trace : Client에서 하나의 요청이 실행되면 Trace가 생성되어 Trace ID 가 발급되고, Microservice 들 내에서 많은 요청을 거쳐 Client에 응답되어 Trace가 끝나기까지 존재한다. Span을 발급하고 관리한다.
- Span : Tracer가 Microservice 내의 요청을 처리할 때마다 Span을 발급한다. 즉 하나의 요청에 사용되는 작업의 단위이다. Span은 Parent, Child 구조를 가져 Tree 형태를 띤다. 이 Set를 Trace라 한다.
더 자세한 것은 검색해보기를...
이제 Zipkin을 설치해보자.
원하는 경로에 가서 다음 curl 명령어를 통해 jar 파일을 깔아보자.
curl -sSL https://zipkin.io/quickstart.sh | bash -s
이제 해당 jar 파일을 다음과 같이 실행한다.
java -jar zipkin.jar
Zipkin의 문양과 실행된다.
이제 localhost:9411로 실행된 Zipkin 서버에 접속할 수 있다. 여기서 Run Query를 실행하면 기본적으로 추적을 실행할 수 있다.
Micrometer (for Tracing)
방금 Zipkin 은 분산 추적을 하긴 하지만, 직접 Trace와 Span을 관리하는 것은 아니다. 스프링 부트 서비스와 Zipkin을 이어주고 각 Trace와 Span에 ID를 부여하는 역할은 Micrometer가 한다.
스프링 부트 2.X 버전은 이 역할을 Spring Cloud Sleuth가 했다. 하지만 스프링 부트 3.X 버전에는 호환되지 않는다. 이와 관련된 모든 기능은 Spring Cloud와 분리되어 Micrometer로 이관되었다.
2016년에 Spring Cloud 팀은 많은 개발자에게 도움이 될 수 있는 추적 라이브러리를 만들었습니다.
그것은 Spring Cloud Sleuth
라고 불렸습니다 . Spring 팀은 추적이 Spring Cloud에서 분리될 수 있다는 것을 깨닫고 본질적으로 Spring Cloud Sleuth의 Spring 독립적 사본인 Micrometer Tracing 프로젝트를 만들었습니다. Micrometer Tracing은 2022년 11월에 1.0.0 GA 릴리스를 출시했으며 그 이후로 꾸준히 개선되고 있습니다.
출처 : https://micrometer.io/docs/tracing#_micrometer_tracing_examples
지금부터의 모든 내용들은 Micrometer와 Spring Boot 공식문서를 참조했다.
https://micrometer.io/docs/tracing
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.micrometer-tracing
Spring Boot Reference Documentation
This section goes into more detail about how you should use Spring Boot. It covers topics such as build systems, auto-configuration, and how to run your applications. We also cover some Spring Boot best practices. Although there is nothing particularly spe
docs.spring.io
먼저 pom.xml에 다음을 추가한다. 여기에는 안 썼지만 실습을 위해 Spring Cloud dependencies도 있는 것이 정상이다.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bom</artifactId>
<version>${micrometer-tracing.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
또한 다음 3개의 종속성을 추가한다.
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>
- io.micrometer:micrometer-tracing-bridge-otel - Micrometer Observation API를 OpenTelemetry에 연결한다.
- io.opentelemetry:opentelemetry-exporter-zipkin - Zipkin에 추적을 보고한다.
또한 분산 추적 시 모든 요청이 추적되면 너무 양이 많아져 기본적으로 10%만 저장되도록 되어있다. 100% 모두 저장하기 위해 다음 설정을 추가한다.
management:
tracing:
enabled: true
sampling:
probability: 1.0
logging:
pattern:
level: "%5p[${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
이제 로그가 나올 수 있도록 설정했으니 로그를 띄우는 코드를 추가하자.
User Service 에서 UserServiceImpl 클래스에서 다음 처럼 로그를 띄우자.
/* Circuit Breaker */
log.info("Before call orders microservice");
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitbreaker");
List<ResponseOrder> ordersList = circuitBreaker.run(() -> orderServiceClient.getOrders(userId),
throwable -> new ArrayList<>());
log.info("After call orders microservice");
Order Service에도 주문 생성, 조회 시 로그가 생성되도록 다음과 같이 추가한다.
log.info("Before add orders data");
ModelMapper mapper = new ModelMapper(); //원래 코드
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); //원래 코드
...
log.info("After added orders data");
log.info("Before retrieved orders data");
Iterable<OrderEntity> orderList = orderService.getOrdersByUserId(userId); //원래 코드
...
log.info("After retrieved orders data");
분산 추적 테스트
우선 회원가입, 로그인은 하고, 주문 생성부터 해보자.
주문을 하니까 사진과 같이 원하는 대로 주문이 생성되는 로그가 뜨면서, 초록색으로 괄호 안에 Trace Id와 Span Id가 뜬다.
Trace Id는 a5a7...8f 이고, Span Id는 728a...1a 이다.
localhost:9411 에서 Run Query를 눌러 분산 추적을 해보자.
방금 주문 생성한 내역이 뜬 것을 볼 수 있다.
아까 파악한 Trace Id와 Span Id 값이 동일하다. 해당 내역이 잘 뜬 것을 볼 수 있다.
이제 사용자 정보 조회도 해보자.
여기서 발생한 문제가 있다. 현재 마이크로서비스 간에 통신은 Feign Client로 하고 있다. 이 때, User Service가 Order Service에 통신을 하면 Trace Id 가 달라지는 문제가 생겼다. 즉 두 서비스의 동일한 요청에 대해 다른 Trace Id를 발급하는 것이다.
다음 글을 참고하여 의존성을 추가하였다.
https://github.com/OpenFeign/feign/issues/1893
Different Trace ID for same request in two services · Issue #1893 · OpenFeign/feign
Hi, I am trying to achieve distributed tracing using micrometer and for inter service communication I'm using Feign Client. But when I try to call Service A and Service A is making a internal call ...
github.com
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
사실 어떤 원리로 이 문제가 해결되는지는 열심히 공부해봤지만 아직 모르겠다. 아는 분이 있으면 댓글을 달아주세요!!
다시 사용자 정보 조회를 해보자.
User Service의 로그이다.
첫 줄과 마지막 줄만 일단 보자. Trace Id는 cb8로 시작하고, 이 User Service를 처음 호출한 Span Id는 574로 시작한다.
마찬가지로 Trace Id는 cb8로 시작하고, Span Id는 f94로 시작한다.
Trace Id는 같게, Span Id는 각각 다르게 잘 나오는 것을 볼 수 있다.
그럼 이 Trace Id를 Zipkin에 검색해서 정보를 알아보자.
화면에 나오는 한 줄 한 줄이 하나의 Span이다. 시간이 드는 모든 동작을 Span으로 분류하고 Id를 발급하기 때문에, User Service와 Order Service 호출 시에 총 2개만 Span이 발생될 거라는 예상과는 다르다.
화면에서 Order Service의 Span Id는 f94로 시작하는, 아까 로그 창의 내용과 같은 것을 볼 수 있다.
오류 발생
그럼 오류도 일부러 발생시키고 Zipkin 서버를 보자.
Order Service에서 요청이 들어오면 바로 Exception 을 발생시킬 수 있도록 다음과 같이 추가한다.
try {
Thread.sleep(1000);
throw new Exception("장애 발생");
} catch (InterruptedException ex) {
log.warn(ex.getMessage());
}
이제 사용자 정보 조회를 하면 오류가 발생한다. 그 후에 Zipkin에 가서 해당 Trace를 검색해보자.
오류가 발생했다는 빨간색 느낌표와 함께 의도한 오류 메시지도 나온다.
이번엔 Dependencies 탭에 가서 Order Service 를 눌러보자.
User Service와 Order Service의 관계를 나타내주고, 그 사이에 점들이 이동하는 것으로 통신이 어떻게 이루어지고 있는지 색깔, 속도 등을 활용해 시각적으로 보여주고 있다. 복잡한 Microservice들이 있다고 할 때, 이런 시각적인 툴은 굉장한 도움이 된다.
'[MSA] Spring Cloud로 개발하는 마이크로서비스 애플리케이션' 카테고리의 다른 글
섹션 12 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (1) | 2023.09.02 |
---|---|
섹션 11 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (0) | 2023.08.24 |
섹션 10 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (0) | 2023.08.18 |
섹션 9 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (0) | 2023.08.11 |
섹션 8 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (0) | 2023.08.11 |