Event Driven Architecture: Queue vs Log - Nghiên cứu điển hình

Trong bài viết trước, chúng ta đã tìm hiểu thứ tự sự kiện tương đối và sự tách biệt giữa các nhà xuất bản và người tiêu dùng giữa những thứ khác.

Event Driven Architecture: Queue vs Log
Trong hướng dẫn này, chúng tôi sẽ so sánh hệ thống tin nhắn dựa trên queue và dựa trên log trong kiến trúc hệ thống hướng sự kiện (event driven architecture)

Trong bài viết này, chúng ta tìm hiểu một số mẫu kiến ​​trúc dựa các khái niệm đó. Chúng ta sẽ tìm hiểu các khả năng mô hình hóa khác nhau mà chúng ta có với RabbitMQ đại diện cho hệ thống dựa trên queue và Kafka đại diện cho hệ thống dựa trên log.

Kiến trúc mẫu của chúng ta có bốn dịch vụ tương tác thông qua các sự kiện.

Logic kiến trúc sự kiện
Hình 1. Logic kiến trúc sự kiện

Sales service và Inventory service đưa ra ba sự kiện:

  • order.placed
  • order.modified
  • order.cancelled

Billing service sử dụng ba sự kiện đó và tính phí khách hàng hoặc hoàn lại tiền cho khách hàng, nó đưa ra 3 sự kiện:

  • order.billed
  • modification.billed
  • order.refunded

Fulfillment service sử dụng các sự kiện:

  • order.placed
  • order.modified
  • order.cancelled
  • order.billed
  • modification.billed

Khi nhận được sự kiện order.placed, lô hàng bắt đầu được chuẩn bị. Chỉ khi nhận được sự kiện order.billed thì lô hàng mới được chuyển đi.

Nếu nhận được sự kiện order.modification và lô hàng vẫn chưa được vận chuyển, thì nó có thể được sửa đổi, nếu không, một lô hàng mới sẽ bắt đầu được chuẩn bị. Chỉ khi một sửa đổi đã được lập hóa đơn thì lô hàng mới được vận chuyển.

Nếu nhận được sự kiện order.cancelled và lô hàng vẫn chưa được vận chuyển, thì đơn hàng đó sẽ bị hủy và các mặt hàng được trả về kho.

Notifications service chỉ gửi email cho khách hàng về các sự kiện quan trọng nhất định. Hiện tại đó là:

  • order.placed
  • order.shipped
  • order.refunded

Cấu trúc liên kết dựa trên queue

Với cấu trúc liên kết dựa trên queue publish - subscribe, mỗi publisher xuất bản tới một topic. Chúng ta có thể có một topic cho mỗi sự kiện. Trong trường hợp của RabbitMQ, chúng ta có Exchange. Chúng ta có thể tạo Exchange fanout cho mỗi sự kiện và điều này bắt chước các topic mà bạn nhận được với các hệ thống pub-sub dựa trên queue khác.

Chúng ta thực sự muốn các sự kiện liên quan được sắp xếp tương đối với nhau, vì vậy mỗi ứng dụng sẽ có một queue duy nhất mà nó sẽ liên kết với các exchange sự kiện mà nó cần.

Exchange tới queue
Hình 2. Exchange tới queue

Bằng cách định tuyến các sự kiện nó cần đến một queue duy nhất, chúng ta đảm bảo mỗi ứng dụng sử dụng các sự kiện đó theo đúng thứ tự tương đối. Nếu chúng ta định tuyến các sự kiện đến một queue riêng biệt cho mỗi sự kiện, chúng ta có thể dễ dàng xử lý một sự kiện order.cancelled trước một sự kiện order.placed bất cứ khi nào order.placed tới sau một chút.

Hình 3. Cấu trúc liên kết RabbitMQ với exchange fanout cho mỗi sự kiện, queue đơn cho mỗi ứng dụng

Như chúng ta đã thấy trong bài viết trước, điều này gây ra khó khăn khi mở rộng. Nếu chúng ta muốn đảm bảo thứ tự xử lý, chúng tôi có thể sử dụng một queue duy nhất và một consumer duy nhất của queue đó.

Khi một consumer là không đủ, thì chúng ta có thể tạo ra những consumer cạnh tranh cho queue, nhưng lúc này sự đảm bảo thứ tự xử lý của chúng ta sẽ bị mất.

Vì vậy, chúng ta cần phân vùng queue của mình để có được sự đảm bảo về mở rộng và thứ tự. Chúng ta có thể sử dụng Exchange băm nhất quán cho việc này.

Hiện tại, chúng ta có queue fulfillment liên kết trực tiếp với năm exchange mà nó cần.

Hình 4. Fulfillment consumer liên kết queue của mình trực tiếp với các exchange sự kiện
Hình 4. Fulfillment consumer liên kết queue của mình trực tiếp với các exchange sự kiện

Chúng ta thay đổi điều này thành việc có năm queue ràng buộc với một Exchange băm nhất quán mà bản thân nó liên kết với năm sàn exchange.

Hình 5. Fulfillment consume liên kết một Exchange băm nhất quán với các exchange sự kiện mà nó muốn sử dụng, mở rộng queue của nó thành 5 queue được phân vùng và có một consumer duy nhất cho mỗi queue. Tin nhắn có tiêu đề OrderId được sử dụng để định tuyến đến các queue được phân vùng.
Hình 5. Fulfillment consume liên kết một Exchange băm nhất quán với các exchange sự kiện mà nó muốn sử dụng, mở rộng queue của nó thành 5 queue được phân vùng và có một consumer duy nhất cho mỗi queue. Tin nhắn có tiêu đề OrderId được sử dụng để định tuyến đến các queue được phân vùng.

Chúng ta có thể sử dụng cấu trúc liên kết này cho từng consumer cần mở rộng quy mô trong khi duy trì thứ tự tương đối chính xác của các sự kiện khác nhau.

Cách chúng ta khớp các consumer với queue nằm ngoài phạm vi của bài viết này. Nhưng bạn có thể tạo một cái gì đó động bằng cách sử dụng Consul hoặc ZooKeeper để giúp đảm bảo việc chỉ định queue chính xác. Hoặc bạn có thể thực hiện một cái gì đó tĩnh, sử dụng hệ thống cấu hình và đường dẫn triển khai của bạn.

Cấu trúc liên kết dựa trên log

Chúng ta không còn có sự linh hoạt của kiến ​​trúc tách rời mà chúng ta đã có với RabbitMQ, nơi chúng ta publish cho các exchange sự kiện và để các ứng dụng quyết định cách chúng muốn kết hợp các sự kiện trong queue của chúng.

Với Kafka, topic của chúng ta là cấu trúc dữ liệu vật lý được chia sẻ bởi các publisher và consumer. Chúng ta phải suy nghĩ kỹ và chọn cấu trúc liên kết của chúng ta tốt. Thay đổi nó sau này có thể rất khó.

Chúng ta có thể sử dụng một Orders topic, đảm bảo thứ tự tương đối của tất cả các sự kiện liên quan đến một đơn đặt hàng nhất định. Hoặc chúng ta có thể có ba topic liên quan đến đơn đặt hàng, thanh toán và vận chuyển. Hoặc một topic cho mỗi sự kiện. Tôi nghĩ rằng một topic cho mỗi sự kiện là một lựa chọn không tốt do việc xử lý các sự kiện liên quan không theo thứ tự.

Hình 6. Một topic duy nhất cho tất cả các sự kiện liên quan đến đơn hàng. Consumer đọc tất cả các tin nhắn và bỏ qua các sự kiện mà họ không quan tâm.
Hình 6. Một topic duy nhất cho tất cả các sự kiện liên quan đến đơn hàng. Consumer đọc tất cả các tin nhắn và bỏ qua các sự kiện mà họ không quan tâm.

Với cấu trúc liên kết này, chúng ta được đảm bảo thứ tự tất cả các sự kiện khác nhau liên quan đến đơn đặt hàng. Bởi vì đây là Kafka, chúng ta cũng đạt được khả năng mở rộng và thứ tự, nếu các publisher đều sử dụng phương pháp phân vùng bằng khóa tin nhắn để chọn phân vùng. Trong trường hợp này, id tin nhắn sẽ là OrderId hoặc thậm chí là CustomerId.

Có thể bạn đang nghĩ rằng thật lãng phí khi mỗi consumer đọc những tin nhắn mà chúng không quan tâm. Ngay cả khi mỗi consumer chỉ bỏ qua những sự kiện mà chúng không quan tâm.

Billing service thậm chí buộc phải đọc những sự kiện mà chính nó đã xuất bản! Nhưng tính đúng đắn không phải là điều tốt đẹp cần có. Tính đúng đắn là một đặc tính mà tất cả chúng ta nên ghi nhớ cho dù đó là thiết kế kiến ​​trúc hay viết một chức năng duy nhất. Ngay cả khi chúng ta quyết định tối ưu hóa cho một thuộc tính khác, chúng ta nên làm như vậy một cách có ý thức, biết rằng chúng ta hy sinh tính đúng đắn cho một cân nhắc khác quan trọng hơn.

Khi topic được chia sẻ này có thể có vấn đề nếu bạn gặp phải tình huống trong đó một hoặc hai sự kiện chiếm ưu thế, với khối lượng lớn. Consumer chỉ quan tâm đến một sự kiện duy nhất có lượng tin nhắn thấp buộc phải mở rộng quy mô để có thể đọc các sự kiện đặt hàng. Trong trường hợp này, chúng ta có thể quyết định nới lỏng nhóm các sự kiện liên quan như chúng ta sẽ thấy trong Tùy chọn 2.

Tùy chọn 2 - Nhiều topic, group nhỏ

Trong cấu trúc liên kết này, chúng ta vẫn nhóm các sự kiện liên quan, nhưng nhóm không phải là toàn cục cho tất cả các sự kiện liên quan đến đơn hàng.

Hình 7. Sử dụng nhiều topic tạo thành các nhóm nhỏ hơn của các sự kiện liên quan
Hình 7. Sử dụng nhiều topic tạo thành các nhóm nhỏ hơn của các sự kiện liên quan

Mặc dù chúng ta không nhận được đầy đủ thứ tự của tất cả các sự kiện liên quan đến đơn đặt hàng, nhưng chúng ta vẫn duy trì thứ tự các sự kiện tương đối mạnh mẽ. Chúng ta cũng giảm bớt yêu cầu đối với consumer chỉ cần một sự kiện có khối lượng thấp.

Một điều cuối cùng cần đề cập là Tùy chọn 3, một topic cho mỗi sự kiện. Điều này cũng có thể là tối ưu trong đường ống phân tích dữ liệu. Hệ thống phân tích dữ liệu có xu hướng có ít yêu cầu nghiêm ngặt hơn về thứ tự sự kiện.

So sánh các cấu trúc liên kết

Với Kafka, cho dù bạn có chọn lựa chọn 1 hay 2 hay một lần nữa, bạn vẫn đang kết hợp các publisher và consumer của mình vào cùng một chủ đề. Nếu bạn muốn thay đổi chủ đề của mình sau này, đó có thể là một bài toán khó khăn.

Đặc biệt nếu bạn sử dụng nguồn cung ứng sự kiện hoặc bạn sử dụng các chủ đề của mình để sao chép dữ liệu sang các hệ thống khác. Vì vậy, mặc dù bạn có thể bảo đảm thứ tự và mở rộng quy mô, nhưng kiến ​​trúc của bạn khó phát triển hơn và khó tối ưu hóa cho bất kỳ ứng dụng nhất định nào.

Với RabbitMQ, chúng ta có khả năng phân tách tốt hơn và có thể thay đổi cấu trúc liên kết của chúng ta tương đối dễ dàng. Chúng ta sẽ cần một exchange cho mỗi sự kiện, điều này bắt chước khái niệm topic pub-sub một cách độc đáo.

Tuy nhiên, chúng ta cũng có thể thay đổi kiến ​​trúc backend. Số lượng và loại consumer, cách chúng ta mở rộng quy mô consumer. Cách consumer nhóm các sự kiện liên quan. Tất cả những điều này có thể được thực hiện độc lập cho mỗi ứng dụng của consumer mà không ảnh hưởng đến kiến ​​trúc hệ thống.

Nhưng đừng quên những đặc điểm nổi bật của Kafka: khả năng mở rộng và lưu trữ tin nhắn. Bạn có thể thấy RabbitMQ không đủ ổn định cho quy mô mà bạn hoạt động.

Bạn có thể muốn sử dụng mô hình tìm nguồn cung ứng sự kiện để sao chép các thay đổi trạng thái trên nhiều ứng dụng, nền tảng phân tích dữ liệu. Có thể xem lại tin nhắn trong 24 giờ do một lỗi đã được đưa vào consumer là một khả năng thực sự tốt cần có.

Vì vậy, như mọi khi, tất cả đều là về sự đánh đổi và kiến ​​trúc sư cần đánh giá sự đánh đổi dựa trên tình huống cụ thể của chúng.

Event Driven ArchitectureSoftware ArchitectureRabbitMQApache Kafka
Bài Viết Liên Quan:
Tạo Event Driven Architecture với Apache Kafka bằng .NET: Azure Event Hubs
Trung Nguyen 01/05/2021
Tạo Event Driven Architecture với Apache Kafka bằng .NET: Azure Event Hubs

Trong bài viết này, chúng ta sẽ mở rộng ứng dụng của mình sử dụng Event Hubs để nhắn tin thay thế cho Kafka.

Tạo Event Driven Architecture với Apache Kafka bằng .NET: Event Consumer
Trung Nguyen 01/05/2021
Tạo Event Driven Architecture với Apache Kafka bằng .NET: Event Consumer

Trong bài viết này, chúng ta sẽ tìm hiểu cách tạo consumer xử lý tin nhắn sử dụng Consumer API của Kafka bằng .NET.

Tạo Event Driven Architecture với Apache Kafka bằng .NET: Event Producer
Trung Nguyen 01/05/2021
Tạo Event Driven Architecture với Apache Kafka bằng .NET: Event Producer

Trong bài viết này, chúng ta sẽ tìm hiểu cách chuẩn bị môi trường cục bộ để phát triển và xuất bản tin nhắn tới Kafka bằng .NET.

Event Driven Architecture: Queue vs Log
Trung Nguyen 30/04/2021
Event Driven Architecture: Queue vs Log

Trong hướng dẫn này, chúng tôi sẽ so sánh hệ thống tin nhắn dựa trên queue và dựa trên log trong kiến trúc hệ thống hướng sự kiện (event driven architecture)