Spring Cloud + MSA 애플리케이션 개발 12(장애 처리와 Microservice 분산 추적)

지원·2024년 3월 1일
0

MSA개발

목록 보기
12/15

CircuitBreaker 와 Resilience4J의 사용

Microservice 끼리 통신하기 위해서 RestTemplate , FeignClient 를 사용했었다.

  • user-service 에서 order-service 를 호출할 때, order-service 서버 안에서 문제가 발생한다면 호출 자체가 되지 않아서 오류가 발생할 것이다.
  • 하지만 MSA 는 독립적으로 해서 오류도 최소화를 시키기 위해서 사용하는 아키텍처이다.
  • 즉 order-service 에 문제가 생겼다고 하더라도 user-service 에서는 정상인 것 처럼 보여지게 하면 된다.
  • user-service 에서 해당 user 의 사용자 정보와 주문 내역을 가져오기 위해서 order-service 를 호출한다.
  • 이때 order-service 에 문제가 발생했을 때 주문 내역은 못 가져오더라도 사용자의 정보만큼만이라도 가져오면 된다.
  • 만약 order-service 가 복구가 된다면 주문내역까지 가지고 올 수 있도록 해주면 된다.

즉 복구가 된다면 이전처럼 흐름을 바꿔주면 된다.

  • 이런 역할을 하는 것이 바로 CircuitBreaker 이다.

CircuitBreaker

  • 장애가 발생하는 서비스에 반복적인 호출이 되지 못하게 차단
  • 특정 서비스가 정상적으로 동작하지 않을 경우 다른 기능으로 대체 수행 -> 장애 회피
  • client(user-service) -> CircuitBreaker -> supplier(order-service)
  • 위와 같은 흐름을 가지는데 이때 supplier 에서 응답을 하지 못해서 timeout 이 발생할 경우 CircuitBreaker 에서 가로채서 오류가 발생하지 않도록 해준다.

CircuitBreaker Closed & Open

  • Microservice 의 통신이 되기 때문에 Closed 상태이다. (정상적일때)
  • Microservice 의 통신이 되지 않을 때는 Open 상태이다.
    -> Open 일 경우에 우회해서 작업할 수 있도록 한다.

즉 연쇄적으로 연결되어 있는 Microservice에 문제가 생기더라도 해당하는 Microservice 만큼은 정상적으로 작동할 수 있도록 한다.

의존성 추가

원래는 Netflix Hystrix 의존성을 추가하지만 해당 라이브러리는 더이상 수정을 하지 않기 때문에 Resilience4j 라는 의존성을 추가하면 된다.

Reilience4j 기능

  • CircuitBreaker , ratelimiter , bulkhead , retry , timelimiter , cache

해야할 작업

  • CircuitBreaker 의 기능을 사용하기 위해서 CircuitBreakerFactory 를 DI 해서 사용하면 된다.
  • user-service 에서 getOrders() 가 order-service 와 통신하는 method 이다.
  • circuitBreaker.run() 을 이용해서 만약 오류가 발생하는 경우에는 throwable -> new ArrayList<>() 를 해서 빈 리스트라도 반환할 수 있도록 한다.

Customizer

  • Resilience4JCircuitBreakerFactory 를 사용해서 조금 더 다양한 옵션을 사용해보자. (Customizer<> 사용)
  • .failureRateThreshold() : circuitBreaker 를 열지 결정하는 확률이다.
    -> default 는 50 인데 10번중에 5번 실패이면 open 시킨다는 것
    -> 만약 4 로 하면 100중에 4번 실패하면 open 시킨다는 것
  • .waitDurationInOpenstate() : circuitBreaker 를 open 한 상태를 유지하는 지속 기간을 의미한다.
  • .slidingWindowType() : 정상적으로 되면 closed 가 되는데 그때 계속 호출했었던 결과 값을 저장하기 위해서 count 기반으로 할건지 time 기반을 할건지 결정 (디폴트는 횟수count)
  • .slidingWinodwSize() : Type 에서 설정한 크기를 구성
  • TimeLimiter 는 supplier 의 time limit 을 정하는 API
    -> 만약 4로 설정했다면 order-service 가 4초동안 응답이 없을 경우에 문제로 간주한다는 것

circuitBreaker 적용

  1. rabbitmq-server 명령어로 실행
  2. circuitBreaker-resilience4j 의존성 추가
// UserServiceImpl.getOrder()

//        List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);

// CircuitBreaker 적용 코드
// DI 해서 사용
CircuitBreakerFactory circuitBreakerFactory; 
// 문제가 생겼을 경우 throwable -> new ArrayList<>()
List<ResponseOrder> orderList =
                circuitBreaker.run(() -> orderServiceClient.getOrders(userId),
                        throwable -> new ArrayList<>());
  • 위와 같이 코드를 수정하고 나면 order-service 를 기동하지 않아도 오류가 발생하는 것이 아니라 주문내역(orders) 부분은 빈 리스트로 반환이 되면서 정상 반환이 된다.

Custom

@Configuration
public class Resilience4JConfig {

    @Bean
    public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfiguration() {

        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .failureRateThreshold(4)
                .waitDurationInOpenState(Duration.ofMillis(1000))
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(2) // 2번의 카운트가 저장된다.
                .build();

        TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
                .timeoutDuration(Duration.ofSeconds(4))
                .build();

        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .timeLimiterConfig(timeLimiterConfig)
                .circuitBreakerConfig(circuitBreakerConfig)
                .build()
        );
    }
}
  • 일반적으로 CircuitBreaker 를 DI 받아서 사용해도 되지만 위에 코드 처럼 Custom 해서 사용할 수 있다.
  • Resilience4JCircuitBreakerFactory 를 반환하면 되는데 이떄 필요한 config 들을 만들고 넣어준 후 반환하면 된다.
  • CircuitBreakerFactory 를 DI 하면 Custom 한게 있다면 Custom 한 것을 사용하기 때문에 따로 또 추가되는 코드는 없다.

Custom 하더라도 똑같이 동작하고, 만약 order-service 서버가 정상적으로 기동되고 있다면 주문내역(orders) 부분은 정상적으로 출력이 된다.

분산 추적의 개요 Zipkin 서버 설치

더이상 연쇄적인 문제를 발생하지 않도록 CircuitBreaker 를 배워봤다.

여러개의 서비스가 연결되어 있고 필요한 정보는 RestTemplate , FeignClient 을 사용해서 데이터를 공유하고 전달할 수 있었다.

하나의 서비스가 시작이 되고 끝이 날 때 여러 Microservice 가 연결이 될 수 있기 떄문에 누가 누구를 거치고 어디가 문제가 되었는지 확인하는 것이 중요하다.

그런것을 확인하고 log 로 남기는 것이 Zipkin 이라고 생각하면 된다.

Zipkin

  • Twitter 에서 사용하는 분산 환경의 Timing 데이터 수집 , 추적 시스템 (오픈소스)
  • 분산 환경에서의 시스템 병목 현상 파악
  • Collector , Query Service , Databasem WebUI 로 구성
  • Span
    -> 하나의 요청에 사용되는 작업의 단위 (Unique)
  • Trace
    -> 트리 구조로 이뤄진 Span 셋
    -> 하나의 요청에 대한 같은 Trace ID 발급

Spring Cloud Sleuth

  • 스프링 부트 애플리케이션을 Zipkin 과 연동해주는 것
  • Zipkin 에 전달을 해주는 역할
  • 요청 값에 따른 Trace ID , Span ID 부여
  • Trace 와 Span ids 를 로그에 추가 가능

SPring Cloud Sleuth + Zipkin

  • ServiceA -> ServiceB -> ServiceC , ServiceD
  1. A -> B -> C
  • A -> B 일 때 Trace ID : A , Span ID : A (처음에는 같은 걸로 할당)
  • B -> C 일 때 Trace ID : A , Span ID : B
  1. A -> B -> D
  • A -> B 일 떄 Trace ID : Q , Span ID : V
  • B -> D 일 때 Trace ID : Q , Span ID : W

정리하면 새로운 요청이 있을 때 Trace 는 같지만 Span 은 달라진다.

terminal 에서 zipkin 서버를 설치

  • java -jar zipkin.jar 로 집킨 서버 실행할 수 있다. (9411 Port)

Spring Cloud Sleuth + Zipkin

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-tracing-bridge-brave</artifactId>
        </dependency>
        <dependency>
            <groupId>io.zipkin.reporter2</groupId>
            <artifactId>zipkin-reporter-brave</artifactId>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-micrometer</artifactId>
        </dependency>
  • 의존성 추가
  • Sleuth 가 Deprecated 가 됐기 떄문에 micometer 로 대신 사용한다.

Sleuth Deprecated

  • Spring Boot 3 버전 이후로 slueth 가 지원이 안 된다.

Trace ID and Span ID

  • user-service 에서 getOrder() 로 실행하면 Log 에 TraceID , SpanID 가 나온다.
  • user-service 와 order-service 의 TraceID 는 같지만 SpanID 는 다른것을 확인할 수 있다.

Zipkin 서버에 가서 TraceID 를 입력해서 조회할 수 있고, service 의 이름을 검색해서 추적하거나 Dependencies 로도 추적할 수 있다.

만약 order-service 에서 장애(예외)가 발생했다면 똑같이 user-service 와 order-service 의 TraceID 는 서로 같지만 SpanID 는 다르다.

장애가 발생했을 때도 어디서 발생했고 log 가 무엇인지 dashboard 로 확인할 수 있다.

management:
  tracing:
    sampling:
      probability: 1.0
    propagation:
      type: b3
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans
      
 logging:
  pattern:
    level: "%5p [%X{traceId:-},%X{spanId:-}]"
 spring:
   cloud:
    openfeign:
      micrometer:
        enabled: true
  • order-service 뿐만 아니라 user-service 에도 yml 파일을 수정하고 의존성도 추가해야한다.
  • sleuth 가 deprecated 가 되면서 사용할 수 없고 여러가지 정보를 추가해야한다.
  • 어차피 Zipkin 서버가 떠있다면 traceId 와 spanId 는 나오기 떄문에 문제 없다.

서버를 기동하고 POSTMAN 으로 주문 , 주문확인의 로직을 거치면 TracdID 와 SpanID 가 정상적으로 나오는 것을 확인할 수 있다.

  • TraceID 는 같고 , SpanID 는 달라지는것도 확인했다.
  • localhost:9411 로 접속하면 zipkin 의 대시보드가 나오고 여기서 TracdID 로 검색하면 로그를 추적할 수 있고 serviceName 으로도 검색할 수 있고 Dependencies 로도 검색할 수 있다.

참고자료

profile
덕업일치

0개의 댓글