MSA 장애 처리와 분산 추척

최준호·2022년 3월 21일
1

Microservice Architecture

목록 보기
29/32
post-thumbnail

👏장애 처리 (CircuitBreaker)

현재 order-serivce를 실행시키지 않고 요청했을 경우 getOrders()를 호출하는 과정에서 에러가 발생하여 user-service로부터 500 에러를 반환받는다. 어찌보면 getOrders() 메서드가 에러가 발생하면서 500이 떨어지는게 맞는거 같지만 하나의 서비스가 멈춘다고해서 모든 서비스가 정지되어버리면 안되는거 아닌가? 라는 생각도 할 수 있다. 그래서 CircuitBreaker를 활용하여 microservice 사이의 호출에서 에러가 발생했을 경우 처리 방법에 대해 학습해보려고 한다.

🔨Resilientce4J 적용

<!-- Resilientce4J -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

의존성을 추가해주고

@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService{
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder pwdEncoder;
    //private final RestTemplate restTemplate;
    private final OrderServiceClient orderServiceClient;    //FeignClient의 interface 받기
    private final Environment env;
    private final CircuitBreakerFactory circuitBreakerFactory;
    
    ...

    @Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);
        if(userEntity == null) throw new UsernameNotFoundException("user name not found!");
        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);
//        List<ResponseOrder> orderList = new ArrayList<>();    //이전에 빈 배열을 반환하던 값

        /* Using as RestTemplate */
//        String orderUrl = String.format(env.getProperty("order_service.url"), userId);
//        ResponseEntity<List<ResponseOrder>> orderListResponse =
//                restTemplate.exchange(orderUrl, HttpMethod.GET, null, new ParameterizedTypeReference<List<ResponseOrder>>() {
//                });
//        List<ResponseOrder> orderList = orderListResponse.getBody();

        /* Using as FeignClient */
        //List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);

        /* FeignClient exception handling*/
//        List<ResponseOrder> orderList = null;
//        try {
//            orderList = orderServiceClient.getOrders(userId);
//        }catch (FeignException e){
//            log.error(e.getMessage());
//        }

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

        CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitBreaker");
        
        List<ResponseOrder> orderList = circuitBreaker.run(() -> 
            orderServiceClient.getOrders(userId),
            throwable -> new ArrayList<>()  //Error가 발생할 경우 비어있는 리스트를 반환
        );

        userDto.setOrders(orderList);

        return userDto;
    }

}

코드를 다음과 같이 circuitBreaker를 사용하여 수정해주고 서버를 실행해서 다시 요청해보자

서비스 자체적 에러로 표시되기 보다 빈 배열값으로 반환해서 정상 반환하고 있는 것을 확인할 수 있다.

로그에서는 에러 로그가 찍혀나오는 것도 확인할 수 있다.

🔨CircuitBreaker를 Custom하여 사용하기

CircuitBreaker를 그대로 디폴트로 사용해도 되지만 Custom하여 요청횟수, 요청 대기시간 등 사용자가 직접 수정하여 사용하는 방법도 있다.

@Configuration
public class Resilience4JConfig {
    @Bean
    public Customizer<Resilience4JCircuitBreakerFactory> customCinfiguration(){
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .failureRateThreshold(4)                            // 100번중 4번만 실패해도 실행
                .waitDurationInOpenState(Duration.ofMillis(1000))   //1초 동안 circuitbreaker 사용
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)  //횟수를 기준
                .slidingWindowSize(2)                               //2개의 데이터를 저장
                .build();

        TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
                .timeoutDuration(Duration.ofMillis(4))  //4초 대기
                .build();

        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .timeLimiterConfig(timeLimiterConfig)
                .circuitBreakerConfig(circuitBreakerConfig)
                .build()
        );
    }
}

이런식으로 설정 파일을 생성해서 bean으로 등록해주어서 사용할수 있는데. 크게 커스텀할 일이 있지 않는 이상은 내 프로젝트에서까진 사용할까? 라는 생각이 들긴한다.

Customizer를 import할때 Resilience4J package에서 임포트해야한다!

분산 추적 (Sleuth + Zipkin)

👉Zipkin 설치

https://zipkin.io/pages/quickstart.html 홈페이지로 이동해서 자신이 설치하기 편한 방법으로 설치해주면 된다.

git clone https://github.com/openzipkin/zipkin
cd zipkin
# Build the server and also make its dependencies
./mvnw -DskipTests --also-make -pl zipkin-server clean install
# Run the server
java -jar .\zipkin-server\target\zipkin-server-2.23.17-SNAPSHOT-exec.jar

나는 curl -sSL 명령어가 듣지 않아서 그냥 git으로 설치했다. 그리고 git으로 설치하는게 훨씬 오래 걸린다!!!!ㅜ

설치된 버전에 따라 zipkin jar 파일 이름이 변경되는데 맞게 변경해주거나 명령어에 이름을 수정해주면 된다.

정상 실행되면 다음과 같은 화면이 나오고 9411 포트로 실행된다.

이러면 정상 실행된 화면이다.

👉Zipkin 설정

user-service에서 설정 진행

<!-- sleuth -->
<!-- sleuth -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- zipkin -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>

의존성을 먼저 추가하고

spring:
  zipkin:
    base-url: http://127.0.0.1:9411
    enabled: true
  sleuth:
    sampler:
      probability: 1.0  #전달할 log의 퍼센트 1.0 -- 100퍼센터

설정을 추가해준다.

        log.info("Before Call Order Service");
        CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitBreaker");
        
        List<ResponseOrder> orderList = circuitBreaker.run(() -> 
            orderServiceClient.getOrders(userId),
            throwable -> new ArrayList<>()  //Error가 발생할 경우 비어있는 리스트를 반환
        );

        log.info("After Call Order Service");

그리고 UserServiceImpl에서 feignClient를 실행하던 부분에 다음과 같이 로그를 찍어서 확인하기 쉽게 만들어준다.

order-service에서 설정 진행

<!-- sleuth -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- zipkin -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>

의존성 추가하고

spring:
  zipkin:
    base-url: http://127.0.0.1:9411
    enabled: true
  sleuth:
    sampler:
      probability: 1.0  #전달할 log의 퍼센트 1.0 -- 100퍼센터

똑같이 설정해주고

package com.example.orderservice.controller;

import com.example.orderservice.dto.OrderDto;
import com.example.orderservice.jpa.OrderEntity;
import com.example.orderservice.messagequeue.KafkaOrderProducer;
import com.example.orderservice.messagequeue.KafkaProducer;
import com.example.orderservice.service.OrderService;
import com.example.orderservice.vo.RequestOrder;
import com.example.orderservice.vo.ResponseOrder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/order-service")
@RequiredArgsConstructor
@Slf4j
public class OrderController {
    private final Environment env;
    private final OrderService orderService;
    private final KafkaProducer kafkaProducer;  //kafka producer 주입
    private final KafkaOrderProducer kafkaOrderProducer;    //주문 전송 producer 주입

    ...

    @PostMapping("/{userId}/orders")
    public ResponseEntity<ResponseOrder> createOrder(@PathVariable("userId") String userId, @RequestBody RequestOrder requestOrder){
        log.info("Before Add Order Data");

        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

        //기존의 jpa 로직
        OrderDto orderDto = mapper.map(requestOrder, OrderDto.class);
        orderDto.setUserId(userId);
        OrderDto createOrder = orderService.createOrder(orderDto);
        ResponseOrder responseOrder = mapper.map(createOrder, ResponseOrder.class);
//        OrderDto orderDto = mapper.map(requestOrder, OrderDto.class);
//        orderDto.setUserId(userId);
        //kafka 주문 로직 추가
//        orderDto.setOrderId(UUID.randomUUID().toString());
//        orderDto.setTotalPrice(requestOrder.getQty() * requestOrder.getUnitPrice());

        //kafka 메세지 전송
//        kafkaProducer.orderSend("example-catalog-topic", orderDto);
//        kafkaOrderProducer.orderSend("orders", orderDto);

//        ResponseOrder responseOrder = mapper.map(orderDto, ResponseOrder.class);

        log.info("After Added Order Data");
        return ResponseEntity.status(HttpStatus.CREATED).body(responseOrder);
    }

    @GetMapping("/{userId}/orders")
    public ResponseEntity<List<ResponseOrder>> getOrder(@PathVariable("userId") String userId){
        log.info("Before Retrieve Order Data");
        Iterable<OrderEntity> orderList = orderService.getOrderByUserId(userId);
        List<ResponseOrder> result = new ArrayList<>();

        orderList.forEach(v -> {
            result.add(new ModelMapper().map(v, ResponseOrder.class));
        });
        log.info("After Receive Order Data");
        return ResponseEntity.ok().body(result);
    }
}

서비스에도 똑같이 log를 추가해준다.

그리고 주문을 등록해보면

다음과 같이 trace id와 span id가 적혀져 나와야한다.

trace id란 zipkin에서 추적할 때 사용자의 한번의 요청 단위를 trace id라고 한다.
span id란 사용자의 한번의 요청 단위에서 내부적으로 추가로 요청되는 microservice 단위를 span id라고 한다.

그 후에 주문에 user정보를 불러오는 url로 호출하면 결과를 받고

trace id와 span id가 구별되는 것을 확인할 수 있으며 현재 trace id는 user-service에서 146472b7356d64ef 값인데 order-service에서도

동일한 값인것을 확인할 수 있다.

zipkin에서도 처리된 과정을 확인할 수 있다.

Find a trace에서 서비스 이름으로 검색도 가능하고

시간으로 검색하여 정상적으로 서비스들이 통신하고 있었는지 확인할 수도 있다.

일부러 에러를 발생시켜서 에러는 빨간색으로 표시되여 확인할 수 있다.

데이터 이력을 zipkin 서버가 저장해주고 spring boot에서 발생한 로그 데이터를 sleuth 라이브러리가 처리해준다는 것을 이해하고 넘어가면 된다.

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글