Phán đoán những lỗi trong Microservice và làm việc cùng với chúng

Trích từ cuốn Microservices in .NET https://www.manning.com/books/microservices-in-net-core. Khi làm việc với những hệ thống phức tạp, chúng ta phải phán đoán trước những lỗi có thể xảy ra như lỗi phần cứng, phần mềm, hay dữ liệu không bình thường hoặc bị hỏng. Lấy ví dụ từ một hệ thống có chức năng thêm sản phẩm vào giỏ hàng:


Hình 1: Hệ thống microservices, có nhiều đường liên lạc giữa các service.
Dễ thấy hệ thống gần như rất nhiều user cùng đồng thời thực hiện rất nhiều action, chúng ta phải lường trước được những lỗi xảy ra như giao tiếp giữa 2 hệ thống, số lượng request bất thường… Khi có thể phán đoán những lỗi xảy ra trong khi liên lạc giữa các hệ thống, chúng ta có thể thiết kế hệ thống microservice có thể đối phó với điều đó.
Có thể chia sự cộng tác giữa các microservice thành 3 loại: Query, Command và Event. Khi xảy ra lỗi, ảnh hưởng phụ thuộc vào loại cộng tác gặp lỗi và cách để đối phó.

  • Query: Khi query bị lỗi, hệ thống sẽ không lấy được thông tin cần thiết, khi đó nếu hệ thống đối phó tốt thì ảnh hưởng đó hệ thống vẫn có thể hoạt động ổn định.
  • Command: Khi gửi một lệnh bị lỗi, bên gửi sẽ không biết được bên nhận có nhận được lệnh hay không.
  • Event.

Hệ thống Log

Khi có lỗi xảy ra chúng ta phải chắc rằng có thể biết được điều gì đang hoạt động sai và nguyên nhân gây ra lỗi. Điều đó có nghĩa là phải có hệ thống log tốt để có thể tìm ra nguyên nhân dẫn đến lỗi, một hệ thống Log trung tâm mà tất cả các microservice sẽ gửi log message đến và cho phép bạn điều tra, tìm kiếm bất cứ khi nào muốn.

Hình 2: Hệ thống central log microservice
Một hệ thống Log xử lý trung tâm là một thành phần mà tất cả các service đều sử dụng. Nhưng phải đảm bảo rằng khi hệ thống Log bị lỗi, nó sẽ không ảnh hưởng đến toàn bộ hệ thống cũng như các microservice khác, tức là chức năng sẽ không thể bị lỗi chỉ vì không gửi được log message. Vì thế việc gửi log trong hệ thống phải là “fire and forget” - có nghĩa message được gửi và sẽ quên đi, không nên đợi response từ việc gửi log message.

Mã tương quan (Correlation Tokens)

Để có thể tìm kiếm được tất cả log có liên quan đến một hoạt động cụ thể trong hệ thống, chúng ta có thể sử dụng Correlation Tokens. Một Correlation Tokens là một mã xác định được gắn vào request từ end-user khi nó đi vào hệ thống. Bất cứ khi nào một microservice gửi message về hệ thống Log, nó nên bao gồm Correlation Tokens. Hệ thống log sẽ cho phép tìm kiếm dựa trên Correlation Tokens. API gateway sẽ có nhiệm vụ tạo và truyền Correlation Tokens đó cho mỗi request đến.

Roll Forward and Roll back

Khi xảy ra lỗi trên production, chúng ta đối mặt với câu hỏi làm cách nào để fix chúng. Thường thì khi có lỗi xảy ra ngay khi deploy hệ thống, chúng ta sẽ mặc định roll back về version trước của hệ thống. Trong hệ thống microservice, version mặc định có thể rất khác biệt. Với việc tích hợp liên tục (Continuos Integration – CI), microservice sẽ được deploy rất thường xuyên và mỗi deployment nên nhanh và dễ để thực hiện. Xa hơn, microservice đủ nhỏ và đơn giản vì thế việc fix bug cũng đơn giản. Điều này mở ra khả năng tích hợp để tiến lên phía trước (roll forward) hơn là quay lại version trước (roll back).
Vậy vì sao chúng ta muốn rolling forward hơn là rolling backward? Trong một số trường hợp, việc rolling backward khá là phức tạp, cụ thể là trong trường hợp database bị thay đổi. Khi một version mới làm thay đổi database, hệ thống sẽ hoạt động phù hợp với việc database được thay đổi đó. Một khi những data đó đã ở trong database, sẽ khó khăn hơn khi rolling backward về version trước, trong trường hợp này rolling forward sẽ có thể đơn giản hơn.

Không để lan truyền lỗi

Một vài thứ hoạt động xung quanh hệ thống microservice có thể làm rối loạn những hoạt động bình thường của hệ thống, ví dụ:

  • Một trong những máy của cụm dữ liệu bị hỏng.
  • Mất kết nối đến một trong số các phần kết nối khác.
  • Nhận được những lượng truy cập bất thường.
  • Một trong số các phần kết nối khác bị sập.

Trong các trường hợp đó, hệ thống sẽ không thể tiếp tục làm việc theo cách thông thường, nhưng không có nghĩa là không thể hoạt động được, chỉ là phải ứng phó với những hoàn cảnh đó.
Khi một microservice bị lỗi, những thành phần liên quan trong hệ thống cũng có thể bị lỗi, khi đó những thành phần đó sẽ không thể query, gửi command và poll event đến những phần bị lỗi. Tại đó, lỗi sẽ lan truyền dọc toàn hệ thống và nguy cơ từ một phần hệ thống lỗi có thể dẫn đến nhiều phần khác bị lỗi.
Khi một microservice gửi một lệnh tới một microservice khác đang bị lỗi tại thời điểm đó, request đó sẽ bị lỗi. Nếu hệ thống gửi chỉ đơn giản bị lỗi trong trường hợp đó, điều này sẽ dần đến việc lan truyền lỗi trong hệ thống. Để không xảy ra điều đó, hệ thống gửi có thể hoạt động như đã gửi thành công, nhưng hệ thống có thể lưu trữ tập hợp các lệnh bị lỗi và có thể thực hiện cơ chế retry định kỳ cho những lệnh đó. Điều này không khả thi trong mọi hoàn cảnh vì cần handle được những lệnh bị lỗi, nhưng nó khả thi khi ngăn chặn việc lan truyền lỗi trong hệ thống.
Khi một microservice truy vấn bị lỗi, nó cũng có thể sử dụng cached response. Trong trường hợp hệ thống gửi có lưu trữ response cũ nhưng việc truy vấn response mới bị lỗi, nó có thể quyết định sử dụng cached response theo cách này. Tuy không phải là khả thi trong mọi hoàn cảnh nhưng nó cũng có thể ngăn chặn việc lan truyền lỗi trong hệ thống.
API gateway có thể hoạt động không tốt do những lưu lượng truy cập bất thường từ client, điều này có thể quy định số request trên giây từ client. Những request khi vượt quá cho phép sẽ không phản hồi. Khi bị ngăn số lượng request, trải nghiệm sẽ không tốt, nhưng nó vẫn có một số response. Nếu không ngăn chặn, gateway có thể bị chậm cho tất cả các request và dẫn đến lỗi toàn hệ thống.

Xử lý error/exception trong Microservice

Ngoài việc sử dụng ControllerAdvice và ExceptionHandler cho việc xử lý những Runtime Exception không ngờ đến trong hệ thống, chúng ta cũng có thể xử lý các error/exception như một phần của logic hệ thống nhằm tránh những mất khiểm soát không cần thiết và xử lý những lỗi nghiệp vụ.
Để tiếp cận gần hơn như trong ví dụ dưới đây có thể là cách tốt hơn để thực hiện điều đó:

  • Tạo một wrapper class tên Result<T>, có 2 method trong class:
    Result.create(object): Tạo ra một wrapper chứa data được trả ra.
    Result.error(object): Tạo ra một wrapper chứa một error.
  public Result get(int id) {
    if (id == 1) return Result.create(new Customer(1, "super customer"));
    else if (id == -1)
      return Result.error(new BadParameters("bad parameters"));
    else
      return Result.error(new NotFound("customer not found"));
  }
  • BadRequest và NotFound là các thực thể cho hỗ trợ cho logic nghiệp vụ nhằm mô tả thông tin của xử lý response.
  • Để xử lý kết quả chúng ta có thể sử dụng các method: isError() và getData()
  • Cuối cùng chúng ta sử dụng một kiểu generic ResponseEntity trả về một wrapper response không cần biết bên trong chứa gì.
  @GetMapping("/customer/{id}")
    public ResponseEntity<?> get(@PathVariable() int id) {
    final Result result = customerService.get(id);
    final HttpStatus status = getStatus(result);
    return new ResponseEntity<>(result.getValue(), status);
  }
  • Dựa trên loại error chúng ta có thể trả về các response với HttpStatus khác nhau.
  private HttpStatus getStatus(final Result result){
    if (result.isError()) {
      if (result.getValue() instanceof NotFound)
        return HttpStatus.NOT_FOUND;
      else
        return HttpStatus.BAD_REQUEST;
    } else return HttpStatus.OK;
  }
  • Khi đó response ứng với các error sẽ được trả về tương ứng theo ý muốn.