Event Driven Architecture: Queue vs Log

Hệ thống nhắn tin (Messaging System) là trung tâm của hầu hết các kiến ​​trúc hướng sự kiện (Event Driven Architecture) và có rất nhiều công nghệ khác nhau. Chúng có thể được phân loại là dựa trên queue (hàng đợi) hoặc dựa trên log (nhật ký).

  • Dựa trên queue: RabbitMQ, ActiveMQ, MSMQ, AWS SQS, JMQ ...
  • Dựa trên log: Apache Kakfa, Apache Pulsar, AWS Kinesis, Azure Event Hubs ...

Mỗi hệ thống nhắn tin có các tính năng khác nhau nhưng cốt lõi là cấu trúc dữ liệu của chúng: queue hoặc log. Trong bài viết này, chúng ta sẽ tìm hiểu cấu trúc dữ liệu cơ bản ảnh hưởng như thế nào đến kiến ​​trúc hướng sự kiện (Event Driven Architecture) của bạn.

Queue

Queue về cơ bản là tạm thời trong tự nhiên. Việc đọc từ queue sẽ loại bỏ dữ liệu ra khỏi queue.

Các ứng dụng khác nhau không thể chia sẻ cùng một queue vì khi đó chúng sẽ cạnh tranh nhau để đọc các tin nhắn. Do đó chúng cần các queue của riêng chúng.

Nếu chúng ta muốn cho phép nhiều ứng dụng sử dụng tin nhắn thì chúng ta cần sử dụng mẫu pub-sub. Lúc này chúng ta không gửi tin nhắn vào queue mà là một topic. Topic là một khái niệm hơi trừu tượng và cho phép chúng ta tách việc publish khỏi consume. Cần có cơ chế định tuyến để chuyển tin nhắn từ topic đến các queue vật lý.

Một ứng dụng có thể có queue riêng và định tuyến nhiều loại sự kiện từ nhiều topic đến một queue duy nhất. Điều này cho phép các ứng dụng giữ thứ tự của các sự kiện liên quan. Những sự kiện nào nó muốn kết hợp có thể được cấu hình khác nhau cho từng ứng dụng.

Log

Log có bản chất liên tục và là tài nguyên được chia sẻ. Đọc từ log không xóa dữ liệu. Nhiều consumer có thể đọc cùng lúc các vị trí khác nhau từ cùng một log.

Không có định tuyến tin nhắn đến queue cho mỗi ứng dụng. Điều này có nghĩa là mô hình hiện dựa trên một kịch bản được chia sẻ.

Chúng ta không thể kết hợp các sự kiện khác nhau với nhau theo nhu cầu của từng ứng dụng. Chúng ta cần đưa ra quyết định về những sự kiện liên quan nào nên được nhóm lại theo topic, như một phần của kiến ​​trúc hệ thống lớn.

Điều này có nghĩa là mô hình hóa kiến ​​trúc hướng sự kiện bằng cách sử dụng nền tảng nhắn tin dựa trên queue rất khác với nền tảng dựa trên log.

Tại sao phải gửi nhiều sự kiện khác nhau vào cùng một queue / log?

Hãy tưởng tượng chúng ta có các sự kiện sau: OrderPlaced, OrderModified và OrderCancel. Người dùng có thể dễ dàng hủy đơn hàng ngay sau khi đặt hàng. Đối với mỗi sự kiện OrderCancel, chúng ta nhận được 1000 sự kiện OrderPlaced.

Nếu chúng ta có một queue hoặc log riêng cho mỗi sự kiện thì rất có khả năng chúng ta có thể xử lý các yêu cầu hủy theo thứ tự. Tất cả những gì cần làm là tồn đọng các sự kiện OrderPlaced để tích lũy trong queue / log OrderPlaced và một queue / log OrderCancel trống.

Tôi có thể nghĩ ra nhiều tình huống có thể xảy ra, ví dụ:

  • Sửa đổi đơn hàng được xử lý trước khi sự kiện tạo đơn hàng.
  • Sự kiện địa chỉ khách hàng mới được xử lý trước sự kiện khách hàng mới.
  • Các sửa đổi được xử lý không theo thứ tự khiến dữ liệu ở trạng thái không nhất quán.

Vì vậy, nếu có thể, chúng ta thực sự muốn có thể nhóm các sự kiện liên quan vào một queue / log duy nhất. Khi tất cả các sự kiện liên quan, chẳng hạn như tất cả các sự kiện liên quan đến khách hàng, đơn đặt hàng hoặc chuyến bay tồn tại theo cùng một trình tự, chúng ta nhận được sự đảm bảo về thứ tự mà không thể thực hiện được khi chúng ta lưu trữ riêng lẻ.

Sự tách biệt giữa publisher và consumer

Với các hệ thống queue hỗ trợ publish - subscribe, chúng ta có thể xuất bản lên các topic (hoặc exchange như trong RabbitMQ) và sử dụng định tuyến để chuyển các tin nhắn đến các queue vật lý không được chia sẻ giữa các ứng dụng.

Sự tách biệt giữa publisher với consumer này rất mạnh mẽ và làm cho một kiến ​​trúc dễ dàng phát triển và tùy chỉnh. Chúng ta có thể xuất bản theo các topic mà không ép buộc bất kỳ nhóm sự kiện nào. Chúng ta có thể có một topic riêng cho mỗi sự kiện và chỉ cần xuất bản mỗi sự kiện theo topic tương ứng của nó.

Định tuyến đến queue không cố định và có thể thay đổi mà không ảnh hưởng đến các ứng dụng khác. Điều này cho phép chúng ta tự do phát triển và thay đổi kiến ​​trúc của mình.

Ngoài ra, mỗi ứng dụng có thể có thứ tự tương đối của bất kỳ tập hợp sự kiện tùy ý nào, do đó tránh được nhiều điểm khó khăn có thể xảy ra trong kiến ​​trúc hướng sự kiện.

Với log, việc publish và consume gắn kết với nhau hơn về việc ai consume gì. Một publisher xuất bản cho một topic và một consumer sử dụng tất cả các tin nhắn của topic đó.

Đúng là consumer có thể đọc từ nhiều topic và đạt được điều gì đó tương tự như việc định tuyến topic  đến các queue nhưng những sự kiện đó không thể được sắp xếp tương đối với nhau như khi các sự kiện được trộn vào một queue.

Việc lựa chọn sự kiện nào để kết hợp vào một topic nhất định được thực hiện để mang lại lợi ích lớn hơn. Một khi quyết định đã được thực hiện, bất kỳ thay đổi nào đối với điều đó đều có hậu quả lâu dài vì một số log có thể lưu trữ dữ liệu trong một thời gian dài.

Chúng ta không thể thay đổi sự kiện nào sẽ chuyển đến log nào. Điều này đặt log vào một bất lợi khác biệt đối với queue về vấn đề này.

Nhưng hệ thống log tách publish khỏi consume theo cách mà queue không thể. Chúng ta không bị ràng buộc về thời gian. Consumer không còn bị ràng buộc chỉ được đọc tin nhắn một lần, kể từ thời điểm publisher gửi tin nhắn đi.

Consumer có thể quay ngược thời gian và xem các sự kiện trong quá khứ. Consumer mới có thể đọc tin nhắn từ khi ứng dụng thậm chí còn được phát triển! Đây là một cải tiến lớn so với queue.

Mở rộng quy mô và Thứ tự

Trường hợp queue có thể cho phép bạn sắp xếp thứ tự các sự kiện tùy ý, chúng có thể mất những đảm bảo đó khi bạn bắt đầu mở rộng quy mô.

Cách dễ dàng để mở rộng quy mô consumer trong queue là tạo ra những consumer cạnh tranh xử lý song song các thông điệp của một queue. Xử lý song song một chuỗi đơn có nghĩa là bạn mất thứ tự. Tôi cần nghiên cứu các tùy chọn với các công nghệ queue khác, nhưng với RabbitMQ, chúng tôi có một giải pháp.

Giải pháp là bắt chước những gì hệ thống dựa trên log làm. Kafka, Pulsar, Kinesis và Event Hubs đều có phân vùng log và bạn có thể định tuyến tin nhắn đến các phân vùng dựa trên khóa tin nhắn.

Điều này có nghĩa là tất cả các tin nhắn của một thực thể nhất định, chẳng hạn như một đơn đặt hàng, đi đến cùng một phân vùng. Chúng ta chỉ có một consumer duy nhất trên mỗi phân vùng, điều đó có nghĩa là chúng ta đảm bảo thứ tự. Điều này cho phép mở rộng quy mô và đảm bảo thứ tự.

RabbitMQ có Exchange băm nhất quán. Chúng ta có thể tạo nhiều queue và liên kết chúng với Exchange băm nhất quán và thực hiện định tuyến dựa trên băm trên khóa định tuyến, tiêu đề tin nhắn hoặc thuộc tính tin nhắn.

Giống như Kafka thực sự. Ngoại trừ việc Kafka cũng quản lý consumer và thực hiện phân vùng tự động cho consumer. Đây là điểm mà RabbitMQ không quá xuất sắc. Logic của việc gán queue không có ở đó, chúng ta phải tự phát triển nó. Nhưng đây chỉ đơn giản là một thách thức kỹ thuật, không phải là một thách thức về kiến ​​trúc. Đó là điều mà tôi sẽ xem xét thêm trong tương lai.

Nhưng có một vấn đề bổ sung. Bây giờ các sự kiện của chúng ta được phân chia thành nhiều queue, nếu chúng ta muốn duy trì thứ tự trên một tập hợp các sự kiện tùy ý, chúng ta sẽ cần các sự kiện đó sử dụng cùng một loại khóa phân vùng. Ví dụ: id đơn đặt hàng, id khách hàng, v.v. Nhưng không chắc rằng một tập hợp các sự kiện tùy ý đều có liên quan đến cùng một thực thể.

Vì vậy, cuối cùng, chúng ta không thể yêu cầu queue có thể cung cấp thứ tự trên một tập hợp các sự kiện tùy ý khi chúng ta thực hiện phân vùng. Chỉ trên các nhóm sự kiện có cùng khóa phân vùng.

Điều bạn có thể làm để tối đa hóa tập hợp các sự kiện có thể được sắp xếp cùng nhau là thêm nhiều tiêu đề tin nhắn. Ví dụ: sự kiện địa chỉ khách hàng có thể có Id khách hàng và tiêu đề tin nhắn là Id địa chỉ. Bằng cách đó, các ứng dụng khác nhau có thể bao gồm sự kiện này trong một nhóm có thứ tự theo một trong hai Id. Nó làm tăng số lượng các sự kiện có thể được nhóm lại với nhau.

Chỉ cần tính đến điều đó với Kafka chẳng hạn, chúng ta có cùng một vấn đề về khóa tin nhắn. Nếu chúng ta kết hợp năm sự kiện vào một nhật ký (chủ đề) nhất định thì chúng ta cần đảm bảo rằng tất cả chúng đều có cùng một khóa tin nhắn, ví dụ: Id Đơn hàng. Nếu không, chúng ta sẽ không có lợi ích gì khi trộn chúng với nhau.

Quyết định - Queue hay Log

Vì vậy, khi xem xét hệ thống nhắn tin tiếp theo của bạn, hãy nghĩ xem đó là hệ thống dựa trên queue hay hệ thống dựa trên log. Hãy tự hỏi bản thân xem bạn coi trọng điều gì hơn?

Queue có thể dẫn đến một kiến ​​trúc dễ phát triển và tùy chỉnh hơn cho từng ứng dụng. Mỗi ứng dụng có thể nhận được sự đảm bảo về thứ tự đối với các sự kiện mà nó cần (với những cảnh báo xung quanh việc mở rộng quy mô). Nhưng dữ liệu chỉ là thoáng qua. Một khi nó đã được sử dụng, nó sẽ biến mất. Nếu bạn cần lại, bạn sẽ cần tìm một nguồn khác.

Log sẽ buộc bạn dành thời gian để thiết kế cẩn thận nhóm các sự kiện trên mỗi log. Bạn phải đưa ra quyết định cho hệ thống tổng thể của mình, bạn không thể tối ưu hóa việc phân nhóm cho từng consumer.

Thực hiện thay đổi khó hơn vì log của bạn là tài nguyên được chia sẻ. Tin nhắn có thể được lưu trữ trong một thời gian dài và do đó, khả năng tương thích ngược sẽ là một vấn đề. Nhưng bạn sẽ nhận được những lợi ích tuyệt vời khi có dữ liệu liên tục:

  • Time travel.
  • Event sourcing.
  • Nguồn để sao chép dữ liệu sang các hệ thống khác.

Vì vậy, hy vọng tôi đã cung cấp cho bạn một cái gì đó để suy nghĩ về. Trong bài viết tiếp theo, chúng ta sẽ xây dựng một số cấu trúc liên kết dựa trên queue và dựa trên log dựa trên những gì đã được mô tả trong bài đăng này.

Event Driven Architecture: Queue vs Log - Nghiên cứu điển hình
Trong bài viết này, chúng ta sẽ tìm hiểu một số case study về hệ thống dựa trên queue (RabbitMQ) và log (Kafka) trong kiến trúc Event Driven Architecture.
Event Driven ArchitectureSoftware ArchitectureRabbitMQApache KafkaEvent Sourcing
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 - Nghiên cứu điển hình
Trung Nguyen 30/04/2021
Event Driven Architecture: Queue vs Log - Nghiên cứu điển hình

Trong bài viết này, chúng ta sẽ tìm hiểu một số case study về hệ thống dựa trên queue (RabbitMQ) và log (Kafka) trong kiến trúc Event Driven Architecture.