MSAのトラブルシューティングと分散トレース


👏障害処理(Circuit Breaker)


order-serivceリクエストが現在実行されていない場合、getOrders()の呼び出し中にエラーが発生し、user-serviceから500個のエラーが返されます.ある意味でgetOrders()メソッドは間違っているようですが、500は正しいのですが、1つのサービスが停止したためにすべてのサービスを停止することはできませんか?そう思ってもいいです.そこで、Microservice間の呼び出しでエラーが発生した場合の対処法をCircuitBreakerで知りたいと思います.

🔨応用Resilientce 4 J

<!-- 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


デバッガとしてCircuitBreakerを直接使用することもできますが、リクエスト回数やリクエスト待ち時間など、ユーザーが自分で使用方法を変更することもできます.
@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をインポートする場合は、Resilience 4 Jパッケージからインポートする必要があります!

オーバーシュートトレース(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でfaignClientのセクションを実行し、ログを次のように記録して表示しやすくします.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で追跡する場合、ユーザの1回の要求単位をtrace idと呼ぶ.span idは、span idと呼ばれるユーザの1回の要求単位内部の追加要求のmicroservice単位である.

その後、ユーザー情報を注文のURLにロードすると、結果が表示されます.

trace idとspan idの違いがわかりますが、現在のtrace idはuser-serviceで146472b7356d64ef値、order-serviceでも

同じ値であることを確認できます.

zipkinでも処理の過程を確認できます.

Find a traceでサービス名を検索することもできます.

また、定期的に検索して、サービスが正常に通信しているかどうかを確認することもできます.

故意にエラーが発生した場合、エラーは赤で表示して確認することができます.
zipkinサーバによって格納されたデータ履歴を理解し、スキップし、springbootのログデータをsleuthライブラリで処理します.