+7

[MSDP] - Timeout pattern (spring boot)

Resilience4j là gì?

Khi làm việc với các hệ thống phân tán, hãy luôn nhớ một điều rằng chúng ta có thể gặp phải các vấn đề về độ trễ mạng, dịch vụ từ xa không khả dụng, timeout, hay đang chạy chậm,...những sự cố này có dẫn đến các ứng dụng làm quá tải lẫn nhau và có thể ảnh hưởng đến hiệu suất tổng thể của hệ thống.

Nếu một hệ thống có khả năng phục hồi sau những sự cố như vậy sẽ tránh được sự cố domino (sự sụp đổ xếp tầng liên tiếp, sự cố này kéo theo sự cố khác) khi các dịch vụ giao tiếp với nhau. Resilience4j là một thư viện Java giúp chúng ta xây dựng các ứng dụng có khả năng phục hồi và chịu lỗi. Nó cung cấp một khuôn mẫu để viết code nhằm ngăn chặn và xử lý các vấn đề trên.

Timeout Pattern

Trong thực tế dù rất ít nhưng cũng có những lúc chúng ta gặp phải tình trạng google hoạt động không ổn định (có thể treo hoặc chậm phản hồi). Nếu chúng ta tiếp tục thử lại gửi yêu cầu vài lần (chẳng hạn nhấn F5 refresh) có thể những lần thử lại sau đó thì google lại phản hồi, đó có thể là sự cố mạng không liên tục và điều này là rất phổ biến. Trong kiến trúc Microservice khi có nhiều dịch vụ (A, B, C & D), một dịch vụ (A) có thể phụ thuộc vào dịch vụ khác (B), dịch vụ B phụ thuộc vào C, C phụ thuộc vào D. Đôi khi do một số sự cố mạng, dịch vụ D có thể không phản hồi như mong đợi. Sự chậm chạp này có thể ảnh hưởng đến các dịch vụ ở giữa - thậm chí lan truyền đến dịch vụ A và block thread trong từng dịch vụ. Thay vì chờ đợi phản hồi, Timeout Pattern là mẫu thiết kế sẽ giúp ích khi việc giao tiếp giữa các microservices có khả năng phục hồi tốt hơn trong những tình huống client phải chờ đợi phản hồi từ dịch vụ từ xa quá lâu.

Vì đây không phải là vấn đề hiếm gặp vì vậy nên xem xét các vấn đề về độ chậm, không khả dụng của dịch vụ trong khi thiết kế ứng dụng microservices bằng cách đặt thời gian chờ cho bất kỳ cuộc gọi dịch vụ từ xa nào. Có như vậy chúng ta mới có thể có các dịch vụ cốt lõi vẫn hoạt động như mong đợi ngay cả khi các dịch vụ từ xa không khả dụng.

Ưu điểm

  • Làm cho các dịch vụ cốt lõi luôn hoạt động ngay cả khi các dịch vụ phụ thuộc không khả dụng.
  • Chúng ta không muốn đợi vô thời hạn cho đến khi request phản hồi.
  • Chúng ta không muốn block bất kỳ thread nào trong chương trình.
  • Để xử lý các vấn đề liên quan đến mạng và làm cho hệ thống vẫn hoạt động bằng cách sử dụng kết quả phản hồi được lưu trong bộ nhớ cache.

Sample Application

Hãy cùng xem một ví dụ nhỏ bên dưới.

  • Chúng ta có nhiều microservices như được hiển thị ở trên.
  • product-service hoạt động như danh mục sản phẩm và chịu trách nhiệm cung cấp thông tin sản phẩm.
  • product-service phụ thuộc vào rating-service (đây là microservice lưu thông tin đánh giá sản phẩm). rating-service duy trì đánh giá và xếp hạng sản phẩm, nó rất chậm do lưu trữ lượng dữ liệu khổng lồ.
  • Bất cứ khi nào chúng ta xem chi tiết sản phẩm, product-service sẽ gửi request đến rating-service để lấy thông tin đánh giá cho sản phẩm. Ngoài ra, chúng ta có các dịch vụ khác như account-service, order-service, payment-service nhưng không liên quan đến nội dung của bài viết này.
  • product-service là dịch vụ cốt lõi mà không có dịch vụ này người dùng không thể bắt đầu quy trình đặt hàng.

Setup project

Chúng ta cần dependency sau:

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.6.1</version>
</dependency>

Chúng ta chia source thành nhiều module như sau:

Nếu người dùng cố gắng xem thông tin một sản phẩm, giả sử sản phẩm có id là 1, thì product-service gọi sang rating-service để lấy thông tin xếp hạng của sản phẩm như bên dưới.

{
    "productId": 1,
    "description": "Blood On The Dance Floor",
    "price": 12.45,
    "productRating": {
        "avgRating": 4.5,
        "reviews": [
            {
                "userFirstname": "vins",
                "userLastname": "guru",
                "productId": 1,
                "rating": 5,
                "comment": "excellent"
            },
            {
                "userFirstname": "marshall",
                "userLastname": "mathers",
                "productId": 1,
                "rating": 4,
                "comment": "decent"
            }
        ]
    }
}

Common DTO (data transfer object)

Chúng ta có một số dịch vụ sẽ dùng chung các DTO, vì vậy chúng ta sẽ tạo ra một mô-đun riêng biệt để chứa những class này. Mô-đun này sẽ chứa các lớp bên dưới.

Review

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class ReviewDto {

    private String userFirstname;
    private String userLastname;
    private int productId;
    private int rating;
    private String comment;

}

Product Rating

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class ProductRatingDto {

    private double avgRating;
    private List<ReviewDto> reviews;

}

Product

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class ProductDto {

    private int productId;
    private String description;
    private double price;
    private ProductRatingDto productRating;

}

Rating Service

Dịch vụ này có trách nhiệm lưu tất cả các đánh giá về sản phẩm. Để đơn giản hóa mọi thứ, chúng ta sẽ sử dụng kiểu dữ liệu Map thay cơ sở dữ liệu.

Service class

@Service
public class RatingService {

    private Map<Integer, ProductRatingDto> map;

    @PostConstruct
    private void init(){

        // product 1
        ProductRatingDto ratingDto1 = ProductRatingDto.of(4.5,
                List.of(
                        ReviewDto.of("vins", "guru", 1, 5, "excellent"),
                        ReviewDto.of("marshall", "mathers", 1, 4, "decent")
                )
        );

        // product 2
        ProductRatingDto ratingDto2 = ProductRatingDto.of(4,
                List.of(
                        ReviewDto.of("slim", "shady", 2, 5, "best"),
                        ReviewDto.of("fifty", "cent", 2, 3, "")
                )
        );

        // map as db
        this.map = Map.of(
                1, ratingDto1,
                2, ratingDto2
        );

    }

    public ProductRatingDto getRatingForProduct(int productId) {
        return this.map.getOrDefault(productId, new ProductRatingDto());
    }

}

Controller

Chúng ta mô phỏng thời gian phản hồi chậm là một số ngẫu nhiên với Thread.sleep

@RestController
@RequestMapping("ratings")
public class RatingController {

    @Autowired
    private RatingService ratingService;

    @GetMapping("{prodId}")
    public ProductRatingDto getRating(@PathVariable int prodId) throws InterruptedException {
        Thread.sleep(ThreadLocalRandom.current().nextInt(10, 10000));
        return this.ratingService.getRatingForProduct(prodId);
    }

}

Product Service

product-service chịu trách nhiệm cung cấp danh sách các sản phẩm dựa trên các tiêu chí tìm kiếm của người dùng. Đây là một trong những dịch vụ cốt lõi, nó cần phải hoạt động tốt và có khả năng đáp ứng ngay cả khi chịu tải cao. Giả sử nếu product-service hoạt động không tốt sẽ ảnh hưởng nặng nề đến doanh thu. Vì dịch vụ này phụ thuộc vào rating-service, chúng ta không muốn bất kỳ sự cố mạng nào hoặc sự khả dụng của rating-service ảnh hưởng đến product-service. Đây là lúc chúng ta cần đến thư viện resilience4j.

Cấu hình

Đầu tiên chúng ta tạo cấu hình cho resience4j như dưới đây. Ở đây, chúng ta đặt thời gian chờ là 3 giây. Chúng ta có thể thêm nhiều dịch vụ với thời gian chờ cụ thể.

resilience4j.timelimiter:
  instances:
    ratingService:
      timeoutDuration: 3s
      cancelRunningFuture: true
    someOtherService:
      timeoutDuration: 1s
      cancelRunningFuture: false
---
rating:
  service:
    endpoint: http://localhost:7070/ratings/

Product entity

@Data
@AllArgsConstructor(staticName = "of")
public class Product {

    private int productId;
    private String description;
    private double price;

}

Resilience4j

  • product-service hoạt động như một client của rating-service.
  • @TimeLimiter cho biết rằng resilience4j sẽ áp dụng thời gian chờ khi thực thi method.
  • name = ratingService cho biết rằng resilience4j sẽ sử dụng cấu hình ratingService trong file yaml.
  • FallbackMethod được sử dụng khi phương thức chính không thành công vì một số lý do nào đó.
@Service
public class RatingServiceClient {

    private final RestTemplate restTemplate = new RestTemplate();

    @Value("${rating.service.endpoint}")
    private String ratingService;

    @TimeLimiter(name = "ratingService", fallbackMethod = "getDefault")
    public CompletionStage<ProductRatingDto> getProductRatingDto(int productId){
        Supplier<ProductRatingDto> supplier = () ->
            this.restTemplate.getForEntity(this.ratingService + productId, ProductRatingDto.class)
                    .getBody();
        return CompletableFuture.supplyAsync(supplier);
    }

    private CompletionStage<ProductRatingDto> getDefault(int productId, Throwable throwable){
        return CompletableFuture.supplyAsync(() -> ProductRatingDto.of(0, Collections.emptyList()));
    }

}

Product service

@Service
public class ProductService {

    private Map<Integer, Product> map;

    @Autowired
    private RatingServiceClient ratingServiceClient;

    @PostConstruct
    private void init(){
        this.map = Map.of(
                1, Product.of(1, "Blood On The Dance Floor", 12.45),
                2, Product.of(2, "The Eminem Show", 12.12)
        );
    }

    public CompletionStage<ProductDto> getProductDto(int productId){
           return this.ratingServiceClient.getProductRatingDto(1)
                   .thenApply(productRatingDto -> {
                       Product product = this.map.get(productId);
                       return ProductDto.of(productId, product.getDescription(), product.getPrice(), productRatingDto);
                   });
    }

}

Controller

@RestController
@RequestMapping("product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("{productId}")
    public CompletionStage<ProductDto> getProduct(@PathVariable int productId){
        return this.productService.getProductDto(productId);
    }

}

Demo

Chạy product-servicerating-service. Sau đó truy cập đường dẫn

http://localhost:8080/product/1
  • Trường hợp 1: Khi rating-service hoạt động hoàn toàn tốt và phản hồi trong vòng 3 giây.
{
    "productId": 1,
    "description": "Blood On The Dance Floor",
    "price": 12.45,
    "productRating": {
        "avgRating": 4.5,
        "reviews": [
            {
                "userFirstname": "vins",
                "userLastname": "guru",
                "productId": 1,
                "rating": 5,
                "comment": "excellent"
            },
            {
                "userFirstname": "marshall",
                "userLastname": "mathers",
                "productId": 1,
                "rating": 4,
                "comment": "decent"
            }
        ]
    }
}
  • Trường hợp 2: Khi rating-service cần thời gian để phản hồi hơn 3 giây.
{
    "productId": 1,
    "description": "Blood On The Dance Floor",
    "price": 12.45,
    "productRating": {
        "avgRating": 0.0,
        "reviews": []
    }
}
  • Trường hợp 3: Khi rating-service down. Resilience4j gọi phương thức dự phòng khi dịch vụ không khả dụng.
{
    "productId": 1,
    "description": "Blood On The Dance Floor",
    "price": 12.45,
    "productRating": {
        "avgRating": 0.0,
        "reviews": []
    }
}

Ở đây số sao đánh giá và các comment đánh giá trống. Nhưng điều đó vẫn ổn hơn là không xem được thông tin sản phẩm vì nó không phải là thông tin quá quan trọng. Bản thân thông tin sản phẩm không có sẵn thì chúng ta sẽ gặp phải trải nghiệm người dùng không tốt và có thể ảnh hưởng đến doanh thu.

Ưu điểm

  • Theo cách tiếp cận này, chúng ta không chặn bất kỳ luồng xử lý nào (block thread) vô thời hạn trong product-service.
  • Mọi vấn đề bất ngờ xảy ra trong cuộc gọi mạng sẽ hết thời gian chờ trong vòng 3 giây.
  • Các dịch vụ cốt lõi không bị ảnh hưởng do việc hoạt động kém của các dịch vụ mà nó phụ thuộc.

Nhược điểm

  • Chúng ta vẫn block thread trong 3 giây. Nó vẫn có thể là một vấn đề đối với các ứng dụng nhận nhiều request đồng thời.

Tổng kết

Timeout Pattern là một trong những mẫu thiết kế trong kiến trúc microservice để thiết kế các microservice có khả năng phục hồi tốt hơn. Ở những bài viết sau mình sẽ đề cập đến những mẫu thiết kế khác cũng trong module Resilience4j. Hi vọng bài viết hữu ích với mọi người.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí