Được viết bởi Sayan Moitra, Associate Solutions Architect và Sangram Sonawane, Senior Solutions Architect
Bài viết gốc.
Kiến trúc Microservice là kiểu kiến trúc trong đó ứng dụng được cấu thành từ một tập hợp các Service được nối kết một cách lỏng lẻo (Loosely-coupled) và có thể triển khai một cách độc lập với nhau. Mỗi Service phải giao tiếp với các Service khác để trao đổi Message và thực hiện các hoạt động nghiệp vụ. Việc đảm bảo độ tin cậy của các Message trong khi luôn giữ cho các Service được nối kết lỏng lẻo là một điều tối quan trọng trong việc xây dựng các hệ thống mạnh mẽ và có tính mở rộng cao.
Bài viết này sẽ trình bày về cách sử dụng Amazon DynamoDB, một dịch vụ cơ sở dữ liệu key-value NoSQL phi máy chủ được quản lý hoàn toàn bởi AWS, cùng với đó là Amazon EventBridge, một dịch vụ phi máy chủ khác về Event Bus. Ta sẽ ứng dụng hai dịch vụ ấy để triển khai hệ thống trao đổi Message cho các Microservice sử dụng Transactional Outbox pattern.
Các hoạt động nghiệp vụ có thể trải rộng trên nhiều hệ thống và cơ sở dữ liệu để duy trì tính nhất quán và tính đồng bộ giữa chúng. Một cách tiếp cận thường thấy với các kiến trúc hay hệ thống phân tán trong đó dữ liệu được nhân bản trên nhiều vị trí hay nhiều thành phần, là Dual Write(Ghi kép). Trong các kịch bản sử dụng Dual Write, khi một thao tác write được thực hiện trên một hệ thống hay một cơ sở dữ liệu, thì cùng một dữ liệu được Write hoặc sự kiện Write sẽ được thực hiện trên một hệ thống khác trong thời gian thực hoặc tiệm cận thời gian thực. Việc đó sẽ đảm bảo hai hệ thống sẽ có cùng dữ liệu và giảm thiểu sự không nhất quán giữa cả hai.
Dual Write cũng tạo ra một thách thức về tính toàn vẹn dữ liệu giữa các hệ thống phân tán. Việc thất bại trong khi cập nhật cơ sở dữ liệu hay gửi Event đến một hệ thống khác sẽ dẫn đến mất dữ liệu và để ứng dụng ở một trạng thái không nhất quán. Một cách tiếp cận để vượt qua vấn đề này là kết hợp Dual Write và Transactional Outbox Pattern.
Các thách thức của Dual Write
Ta hãy cùng xem xét một trường hợp về một ứng dụng đặt thức ăn online và hình dung trong đầu về các điểm bất lợi của Dual Write. Khi người dùng submit một Order, Order Service sẽ cập nhật trạng thái của Order trong cơ sở dữ liệu, cùng với đó là gửi một Event về việc cập nhật trạng thái Order đến hai Service notify_restaurant và order_tracking một cách bất đồng bộ thông qua Message Bus. Sau khi cập nhật trạng thái Order thành công trong cơ sở dữ liệu, Order Service tạo ra một Event mới trong Message Bus. Tại đây, ta có thể nói rằng, Order Service thực hiện Dual Write trong việc cập nhật cơ sở dữ liệu và publish Event để các Service khác có thể nhận được.
Cách tiếp cận này sẽ chạy ổn cho đến khi một số vấn đề xảy ra trong việc publish Event vào Message Bus. Việc publish Event có thể bị thất bại do nhiều lý do như lỗi mạng hay Message Bus ngưng hoạt động. Khi thất bại, notify_restaurant và order_tracking sẽ không thể nhận được Event khi cập nhật trạng thái Order, khiến hệ thống rơi vào trạng thái không nhất quán. Do đó, triển khai Transactional Outbox Pattern cùng với Dual Write sẽ đảm bảo việc gửi Message một cách tin cậy giữa các hệ thống khi cơ sở dữ liệu được cập nhật.
Hình minh hoạ sau thể hiện sơ đồ tuần tự của ứng dụng đặt thức ăn online cùng với đó là các thách thức của Dual Write:
Tổng quan về Transactional Outbox Pattern
Trong Transactional Outbox Pattern, một cơ sở dữ liệu khác được tạo ra để lưu trữ các Message gửi đi (Outgoing Message). Trong ví dụ về ứng dụng đặt thức ăn, cập nhật cơ sở dữ liệu với chi tiết đơn hàng và lưu trữ thông tin Event vào Outbox Table trở thành một giao dịch nguyên tử(Atomic Transaction).
Giao dịch sẽ được tính là thành công khi việc ghi vào cơ sở dữ liệu Order và Outbox Table thành công. Bất kì một lỗi nào trong việc cập nhật Outbox Table đều sẽ roll back Transaction. Một tiến trình độc lập khác sẽ đọc các Event trong Outbox Table và publish các Event vào Message Bus. Một khi Event đã sẵn sàng trong Message Bus, nó có thể được đọc bởi hai Service notify_restaurant và order_tracking. Việc kết hợp Transactional Outbox Pattern và Dual Write có thể đảm bảo sự nhất quán dữ liệu trên khắp hệ thống cùng với đó là độ tin cậy của việc chuyển phát các Message trong ngữ cảnh giao dịch(Transactional Context).
Hình minh hoạ dưới đây thể hiện sơ đồ tuần tự cho ví dụ ứng dụng đặt thức ăn với Transactional Outbox Pattern để đảm bảo độ tin cậy khi chuyển phát các Message.
Triển khai Transactional Outbox Pattern
DynamoDB có một tính năng được gọi là DynamoDB Streams để ghi lại trình tự sửa đổi cấp Item Iitem-level) theo thứ tự thời gian trong các Table và lưu trữ chúng vào Log trong khoảng thời gian lên đến 24 giờ. Ứng dụng có thể truy cập vào Log và xem các Item khi chúng xuất hiện và khi được thay đổi gần như theo thời gian thực.
Bất cứ khi nào ứng dụng tạo, chỉnh sửa hay xoá các Item trong một Table, DynamoDB Stream sẽ ghi một bản ghi vào Stream với khoá là khoá chính của Item được thay đổi. Một bản ghi bao gồm thông tin về dữ liệu được thay đổi đối với một Item trong DynamoDB Table. DynamoDB Streams sẽ tạo ra các bản ghi gần như theo thời gian thực và có thể được đọc để xử lý thông qua nội dung của chúng. Sử dụng tính năng này sẽ loại bỏ sự cần thiết của việc phải duy trì một Outbox Table và giảm đi chi phí vận hành và quản lý.
EventBridge Pipes kết nối Event Producer với Consumer với các lựa chọn như Transform, Filter hay Enrich các Message. EventBridge Pipes có thể tích hợp với DynamoDB Streams để đọc các Event khi thay đổi dữ liệu mà không cần viết bất kỳ dòng code nào, việc tạo ra và duy trì một tiến trình độc lập để đọc Stream là không cần thiết. EventBridge Pipes đồng thời cũng hỗ trợ cơ chế Retry, bất kì Event bị thất bại nào có thể được điều hướng đến Dead-Letter Queue(DLQ) nhằm mục đích tái xử lý hoặc phân tích sau.
EventBridge Pipes sẽ thăm dò(Poll) các phân đoạn(Shard) trong DynamoDB Streams để tìm các bản ghi và gọi các Pipe bất cứ khi nào có bản ghi sẵn sàng. Bạn có thể cấu hình EventBridge để đọc các bản ghi trong DynamoDB khi nó đã tập hợp đủ một Batch có kích thước nhất định hoặc Batch Window bị hết hạn. Pipe sẽ duy trì thứ tự của các bản ghi được lấy từ Stream khi gửi nó đến đích. Bạn có thể tuỳ ý Filter hoặc Enhance các bản ghi này trước khi gửi chúng đến nơi để xử lý.
Ví dụ tổng quan
Biểu đồ sau minh hoạ cho việc triển khi Transactional Outbox Pattern với DynamoDB Streams và EventBridge Pipe. Amazon API Gateway được sử dụng để kích hoạt cho DynamoDB vận hành thông qua một POST Request. Sự thay đổi của DynamoDB sẽ kích hoạt một EventBridge Event Bus thông qua Amazon EventBridge Pipes. Event Bus sau đó sẽ gọi Lambda Function qua SQS Queue, tuỳ thuộc vào Filter được áp dụng.
- Trong sơ đồ trên, Amazon API Gateway tạo POST Request đến DynamoDB Table để update dữ liệu. Amazon API Gateway hỗ trợ các thao tác CRUD cho Amazon DynamoDB mà không cần đến một lớp tính toán riêng biệt nhằm mục đích gọi đến cơ sở dữ liệu.
- DynamoDB được bật cho Table, ghi lại trình tự thay đổi cấp độ Item theo thứ tự thời gian.
- EventBridge Pipes tích hợp với DynamoDB Streams để ghi lại các Event , sau đó có thể tuỳ ý Filter và Enrich dữ liệu trước khi gửi đến đích. Trong ví dụ trên, Event được gửi đến Amazon EventBridge, thứ đóng vai trò như một Event Bus. Ta có thể thay thế EventBridge bằng bất cứ dịch vụ nào được liệt kê tại đây. DLQ có thể được cấu hình để xử lý các Event bị lỗi với việc phân tích và tái xử lý.
- Consumer lắng nghe Event Bus để nhận các Message. Bạn cũng có thể mở rộng ra và chuyển Message cho nhiều Consumer sử dụng Filter. Bạn cũng có thể cấu hình DLQ để xử lý lỗi.
Chuẩn bị
- AWS SAM CLI, phiên bản 1.85.0 hoặc cao hơn
- Python 3.10
Triển khai ứng dụng mẫu
- Clone source code về máy:
| git clone https://github.com/aws-samples/amazon-eventbridge-pipes-dynamodb-stream-transactional-outbox.git |
- Đi đến thư mục gốc và chạy các lệnh sau:
| cd amazon-eventbridge-pipes-dynamodb-stream-transactional-outbox sam buildsam deploy –guided |
- Nhập tên bạn muốn để tạo Stack. Trong quá trình triển khai, chọn Default cho tất cả các bước bổ sung:
- Ứng dụng đã được triển khai.
Kiểm thử ứng dụng
Khi hệ thống đã triển khai thành công, ta sẽ được cung cấp Amazon API Gateway URL, ta có thể test sử dụng URL này. Để test ứng dụng, bạn có thể sử dụng PostMan để tạo một POST Request đến API Gateway prod URL
Hoặc sử dụng lệnh curl:
| curl -s –header “Content-Type: application/json” \ –request POST \ –data ‘{“Status”:”Created”}’ \ <API_ENDPOINT> |
Ta sẽ được kết quả như sau:
Để đảm bảo Order Details đã được cập nhật, sử dụng lệnh sau để scan DynamoDB Table:
| aws dynamodb scan \ –table-name <DynamoDB Table Name> |
Xử lý lỗi
DynamoDB Streams lưu lại trình tự thay đổi cấp Item theo thứ tự thời gian trong DynamoDB Table và giữ thông tin đó trong Log tồn tại trong khoảng thời gian lên đến 24 giờ. Nếu EventBridge không sẵn sàng để đọc DynamoDB Streams do cấu hình không đầy đủ, tất cả bản ghi đều sẽ khả dụng trong Log trong 24 giờ. Khi EventBridge được tái tích hợp, nó sẽ lấy toàn bộ những bản ghi chưa được chuyển đi trong vòng 24 giờ ấy và xử lý. Với vấn đề tích hợp giữa EventBridge Pipes và ứng dụng đích, các Message lỗi có thể được gửi vào DLQ để tái xử lý sau đó.
Dọn dẹp tài nguyên
Để dọn dẹp tài nguyên đã tạo, chạy lệnh sau, trả lời “y” cho toàn bộ câu hỏi:
| sam delete –stack-name <stack_name> |
Tổng kết
Giao tiếp một cách tin cậy giữa các Service là một yếu tố quan trọng khi thiết kế kiến trúc Microservice, đặc biệt là với việc sử dụng Dual Write. Việc kết hợp giữa Transactional Outbox Pattern và Dual Write là một giải pháp mạnh mẽ để cải thiện độ tin cậy khi phân phối các Message.
Bài viết này trình bày một Pattern về kiến trúc để giải quyết các bất lợi của Dual Write bằng cách kết hợp với Transactional Outbox Pattern sử dụng DynamoDB và EventBridge Pipes. Giải pháp này cung cấp hướng tiếp cận no-code với các Service được AWS quản lý, giảm bớt chi phí vận hành và quản lý.
Truy cập ServerlessLand để tìm hiểu thêm về Serverless.
Tìm hiểu thêm về DynamoDB: