Transactions Trên Các Microservice

Transactions Trên Các Microservice

1. Giới thiệu

Trong bài viết này, chúng ta sẽ thảo luận về các tùy chọn để triển khai giao dịch (transaction) trên nhiều dịch vụ vi mô (microservices).

Chúng tôi cũng sẽ xem xét một số giải pháp thay thế cho giao dịch trong kịch bản dịch vụ vi mô phân tán (distributed microservice).

2. Tránh giao dịch trên các dịch vụ vi mô

Giao dịch phân tán là một quá trình rất phức tạp với nhiều bộ phận chuyển động có thể bị lỗi. Ngoài ra, nếu các bộ phận này chạy trên các máy khác nhau hoặc thậm chí ở các trung tâm dữ liệu khác nhau, quá trình cam kết giao dịch có thể trở nên rất dài và không đáng tin cậy.

Điều này có thể ảnh hưởng nghiêm trọng đến trải nghiệm của người dùng và băng thông hệ thống nói chung. Vì vậy, một trong những cách tốt nhất để giải quyết vấn đề giao dịch phân tán là tránh chúng hoàn toàn.

2.1. Ví dụ về kiến trúc yêu cầu giao dịch (architecture requiring transactions)

Thông thường, một microservice được thiết kế theo cách độc lập và hữu ích khi hoạt động riêng lẻ. Nó phải có khả năng giải quyết một số tác vụ kinh doanh nguyên tử.

Nếu chúng ta có thể chia hệ thống thành các dịch vụ siêu nhỏ như vậy, rất có thể chúng ta sẽ không cần phải triển khai các giao dịch giữa chúng.

Ví dụ, hãy xem xét một hệ thống broadcast messaging giữa những users.

User microservice sẽ liên quan đến user profile (creating a new user, editing profile data etc...) với domain class sau:

@Entity
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Basic
    private String name;

    @Basic
    private String surname;

    @Basic
    private Instant lastMessageTime;
}

Message microservice sẽ liên quan đến việc broadcasting. Nó đóng gói Message entity và mọi thứ xung quanh nó:

@Entity
public class Message implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Basic
    private long userId;

    @Basic
    private String contents;

    @Basic
    private Instant messageTimestamp;

}

Mỗi microservice có cơ sở dữ liệu riêng. Lưu ý rằng chúng ta không tham chiếu đến User entity từ Message entity , vì các user class không thể truy cập được từ message microservice . Chúng ta chỉ tham chiếu đến User theo id.

Bây giờ User entity chứa trường lastMessageTime vì chúng tôi muốn hiển thị thông tin về thời gian hoạt động gần đây nhất của người dùng trong hồ sơ của họ.

Tuy nhiên, để thêm tin nhắn mới cho người dùng và cập nhật lastMessageTime của họ , giờ đây chúng ta phải triển khai giao dịch trên nhiều dịch vụ vi mô.

2.2. Phương pháp tiếp cận thay thế không có giao dịch

Chúng ta có thể thay đổi kiến ​​trúc dịch vụ vi mô và xóa trường lastMessageTime khỏi User entity .

Sau đó, chúng ta có thể hiển thị thời gian này trong hồ sơ người dùng bằng cách gửi một request riêng đến message service và tìm giá trị messageTimestamp tối đa cho tất cả tin nhắn của người dùng này.

Có lẽ, nếu Message service đang chịu tải cao hoặc thậm chí ngừng hoạt động, chúng ta sẽ không thể hiển thị thời gian của tin nhắn cuối cùng của người dùng trong hồ sơ của họ.

Nhưng điều đó có thể chấp nhận được hơn là không thực hiện được giao dịch phân tán để lưu tin nhắn chỉ vì dịch vụ vi mô của người dùng không phản hồi kịp thời.

Tất nhiên, sẽ có những tình huống phức tạp hơn khi chúng ta phải triển khai một business process trên nhiều microservices và chúng ta không muốn có sự không nhất quán giữa các microservice đó.

3. Two-Phase Commit Protocol

Two-phase commit protocol (hay 2PC) là cơ chế thực hiện giao dịch trên nhiều software components khác nhau (multiple databases, message queues etc...)

3.1. Kiến trúc của 2PC

Một trong những người tham gia quan trọng trong giao dịch phân tán là transaction coordinator. Giao dịch phân tán bao gồm hai bước:

  • Giai đoạn chuẩn bị — trong giai đoạn này, tất cả những người tham gia giao dịch chuẩn bị cam kết và thông báo cho transaction coordinator rằng họ đã sẵn sàng hoàn tất giao dịch

  • Giai đoạn cam kết hoặc hủy bỏ — trong giai đoạn này, lệnh cam kết hoặc hủy bỏ sẽ được transaction coordinator đưa ra cho tất cả những người tham gia

Vấn đề với 2PC là nó khá chậm so với thời gian vận hành một single microservice.

Việc phối hợp (coordinating) giao dịch giữa các dịch vụ vi mô, ngay cả khi chúng nằm trên cùng một mạng, thực sự có thể làm chậm hệ thống , do đó, cách tiếp cận này thường không được sử dụng trong trường hợp tải cao.

3.2. XA Standard

Tiêu chuẩn XA là thông số kỹ thuật để thực hiện các giao dịch phân tán (distributed transactions) 2PC trên các tài nguyên hỗ trợ. Bất kỳ máy chủ ứng dụng (application server) nào tuân thủ JTA (JBoss, GlassFish, v.v.) đều hỗ trợ ngay khi cài đặt.

Các tài nguyên tham gia vào giao dịch phân tán có thể là, ví dụ, hai cơ sở dữ liệu của hai microservices khác nhau.

Tuy nhiên, để tận dụng cơ chế này, các tài nguyên phải được triển khai tới một nền tảng JTA duy nhất. Điều này không phải lúc nào cũng khả thi đối với kiến ​​trúc microservices.

3.3. REST-AT Standard Draft

Một tiêu chuẩn được đề xuất khác là REST-AT đã trải qua một số quá trình phát triển của RedHat nhưng vẫn chưa thoát khỏi giai đoạn dự thảo. Tuy nhiên, nó được hỗ trợ bởi máy chủ ứng dụng WildFly ngay khi xuất xưởng.

Tiêu chuẩn này cho phép sử dụng máy chủ ứng dụng như một bộ điều phối giao dịch với REST API cụ thể để tạo và tham gia các giao dịch phân tán.

Các dịch vụ web RESTful muốn tham gia vào giao dịch hai giai đoạn cũng phải hỗ trợ một REST API cụ thể.

Thật không may, để kết nối một giao dịch phân tán với các tài nguyên cục bộ của dịch vụ vi mô, chúng ta vẫn phải triển khai các tài nguyên này tới một nền tảng JTA duy nhất hoặc giải quyết nhiệm vụ không hề đơn giản là tự mình viết cầu nối này.

4. Sự nhất quán và đền bù cuối cùng (Eventual Consistency and Compensation)

Cho đến nay, một trong những mô hình khả thi nhất để xử lý tính nhất quán trên các dịch vụ vi mô là tính nhất quán cuối cùng (eventual consistency).

Mô hình này không áp dụng các giao dịch ACID phân tán trên các dịch vụ vi mô. Thay vào đó, nó đề xuất sử dụng một số cơ chế để đảm bảo rằng hệ thống cuối cùng sẽ nhất quán tại một thời điểm nào đó trong tương lai.

4.1. Một trường hợp cho sự nhất quán cuối cùng

Ví dụ, giả sử chúng ta cần giải quyết nhiệm vụ sau:

  • Đăng ký hồ sơ người dùng

  • Thực hiện một số kiểm tra lý lịch tự động để người dùng thực sự có thể truy cập vào hệ thống

Nhiệm vụ thứ hai là đảm bảo rằng người dùng này không bị cấm khỏi máy chủ của chúng tôi vì lý do nào đó.

Nhưng có thể mất thời gian và chúng tôi muốn trích xuất nó thành một microservice riêng biệt. Sẽ không hợp lý khi để người dùng chờ đợi quá lâu chỉ để biết rằng họ đã đăng ký thành công.

Một cách để giải quyết vấn đề này là sử dụng phương pháp tiếp cận dựa trên thông điệp (message-driven) bao gồm cả bù trừ(compensation). Hãy xem xét kiến ​​trúc sau:

  • User microservice được giao nhiệm vụ đăng ký hồ sơ người dùng

  • Validation microservice được giao nhiệm vụ kiểm tra lý lịch

  • Messaging platform hỗ trợ hàng đợi liên tục

Messaging platform có thể đảm bảo rằng các message được gửi bởi các microservices được lưu lại. Sau đó, chúng sẽ được gửi vào thời điểm sau nếu người nhận hiện không có mặt.

4.2. Happy Scenario

Trong kiến ​​trúc này, một kịch bản khả quan sẽ là:

  • User service đăng ký người dùng, lưu thông tin về người dùng đó trong cơ sở dữ liệu cục bộ của nó.

  • User service đánh dấu người dùng này bằng một lá cờ. Nó có thể biểu thị rằng người dùng này vẫn chưa được xác thực và không có quyền truy cập vào toàn bộ chức năng của hệ thống.

  • Xác nhận đăng ký được gửi đến người dùng với cảnh báo rằng không phải tất cả các chức năng của hệ thống đều có thể truy cập ngay lập tức.

  • User service gửi một message đến validation service để thực hiện kiểm tra lý lịch của người dùng.

  • Validation service chạy kiểm tra lý lịch và gửi message đến user service với kết quả kiểm tra

    • Nếu kết quả là positive, user service sẽ bỏ chặn người dùng

    • Nếu kết quả là negative, user service sẽ xóa tài khoản người dùng

Sau khi chúng ta thực hiện tất cả các bước này, hệ thống sẽ ở trạng thái nhất quán. Tuy nhiên, trong một khoảng thời gian, User entity dường như ở trạng thái chưa hoàn thiện.

Bước cuối cùng, khi user service xóa tài khoản không hợp lệ, là giai đoạn bù trừ .

4.3. Failure Scenarios

Bây giờ chúng ta hãy xem xét một số tình huống thất bại:

  • Nếu validation service không thể truy cập được, thì message platform với chức năng hàng đợi liên tục của nó đảm bảo rằng validation service sẽ nhận được message này vào một thời điểm sau đó.

  • Giả sử message platform bị lỗi, sau đó user service sẽ cố gắng gửi lại message vào một thời điểm sau, ví dụ, bằng cách xử lý hàng loạt theo lịch trình (scheduled batch-processing) đối với tất cả người dùng chưa được xác thực.

  • Nếu validation service nhận được message, xác thực người dùng nhưng không thể gửi lại câu trả lời do message platform bị lỗi, validation service cũng sẽ thử gửi lại message vào một thời điểm sau đó.

  • Nếu một trong các message bị mất hoặc một số lỗi khác xảy ra, user service sẽ tìm thấy tất cả người dùng chưa được xác thực bằng cách xử lý hàng loạt theo lịch trình và gửi lại yêu cầu xác thực.

Ngay cả khi một số message được gửi đi nhiều lần, điều này cũng không ảnh hưởng đến tính nhất quán của dữ liệu trong cơ sở dữ liệu của các microservice.

Bằng cách cân nhắc cẩn thận tất cả các kịch bản lỗi có thể xảy ra, chúng ta có thể đảm bảo rằng hệ thống của mình sẽ đáp ứng các điều kiện về tính nhất quán cuối cùng. Đồng thời, chúng ta không cần phải xử lý các giao dịch phân tán tốn kém.

Nhưng chúng ta phải nhận thức rằng việc đảm bảo tính nhất quán cuối cùng là một nhiệm vụ phức tạp. Không có giải pháp duy nhất cho mọi trường hợp.

5. Kết luận

Trong bài viết này, chúng tôi đã thảo luận về một số cơ chế triển khai giao dịch trên các microservices.

Và chúng tôi cũng đã khám phá một số giải pháp thay thế cho kiểu giao dịch này ngay từ đầu.

Nguồn sưu tầm.