Tản mạn về DDD trong Microservices, CQRS và Event Sourcing

Nam Vu
15 min readMay 14, 2021

Micro-services nhiều năm về trước và tới tận bây giờ nó vẫn là một hot-trend chưa có dấu hiện hạ nhiệt , nhà nhà người người đều nói về nó, và gần như micro-services đã thành default architecture trong mọi dự án. Ai cũng biết những điểm mạnh của nó, nhưng micro-services bản chất nó là ‘distributed system’ và tất nhiên đã là distributed system thì nó rất khó và phức tạp. Nếu trong quá trình thiết kế không cẩn trọng thì tất cả chúng ta sẽ dễ dàng rơi vào cái gọi là ‘micro-service madness’ (cơn điên/ác mộng mang tên Micro-services).

Để thiết kế một hệ thống Micro-services tốt thì tốt nhất chúng ta nên có những hiểu biết những khái niệm sau: DDD (Domain-driven design) CQRS (Command Query Responsibility Segregation) và Event-Sourcing.

DDD (Domain-drivent design)

Nếu bạn đang làm việc một hệ thống có nghiệp vụ (business) lớn và phức tạp và đặc biệt là nếu bạn muốn hệ thống của mình sẵn sàng (ready) cho micro-service trong tương lai, thì Domain-drivent design chính là phao cứu sinh của chúng ta. Thực tế thì DDD nó đã ở đâu đó trong giới phần mềm khoảng hơn 15 năm nay rồi, nhưng nó chỉ “nổi tiếng" và được “chú ý” nhiều khi micro-services trở nên “nở rộ".

DDD được xây dựng xung quanh một khái niệm (concept) gọi là Bounded Context, tôi không biết dịch ra như thế nào có thể gọi đó là ranh giới giữa `modules` hoặc `services`trong hệ thống theo nghiệp vụ mà nó đại diện. Có nghĩa là mỗi bounded context đại diện cho một nghiệp vụ duy nhấtkhông thể thay thế trong hệ thống, ví dụ như Order fulfilment service hay Payment service trong một hệ thống về tài chính (finance). Việc xác định đúng Context là điều đầu tiên và tối quan trọng trong một hệ thống DDD.

Một concept quan trọng khác của DDD đó chính là Ubiquitous langue, có nghĩa là một ngôn ngữ chung được sử dụng trong mỗi bounded contextbởi các thành viên tham gia dự án như Developers, Businesspeople … Mà trong đó tất cả mọi người đều hiểu rõ nó, để tìm ra một tiếng nói chung cho tất cả các thành phần tham gia dự án. Việc này nhằm tăng khả năng thấu hiểu nghiệp vụ của hệ thống, và giảm thiểu các rủi ro về giao tiếp (communicate) dẫn tới các hiểu lầm không đáng có (misunderstanding). Trong DDD tất cả mọi người trong dự án không chỉ Business-people mà kể cả Developers đều có nghĩa vụ và bổn phận phải hiểu biết và đóng góp cho nghiệp vụ (business) trong hệ thống.

Aggregate cũng là một khái niệm quan trọng ở tầng kỹ thuật (technical level) giúp cho Developer có thể ánh xạ (mapping) và mô hình hóa (object oriented) các business trong hệ thống vào một Object duy nhất, bắng cách tập hợp các entity có liên quan tới nhau trong một Bound Context, mỗi Bound Context có thể có một hoặc nhiều Aggregate tùy trường phái.
- Đặc điểm mà một Aggregate phải đảm bảo đó là tính nhất quán (consistency) có nghĩa là tất cả các entity trong nó phải được đi cùng nhau trong một transaction và đảm bảo tính nhất quán (consitency) trong mỗi thay đổi.
- Ngoài ra một Aggregate cũng phải đảm bảo những tính chất bất biến (invariants), có nghĩa là những điều kiện hoặc quy tắc mà dữ liệu trong một Aggregate phải luôn luôn tuân thủ, ví dụ một Aggregate quản lý đơn hàng (Order), và một trong những tính chất bất biến có thể là “Tổng số tiền đơn hàng phải không âm.” Nếu bất kỳ thay đổi nào làm cho tổng số tiền trở nên âm, thì trạng thái mới của Aggregate sẽ bị coi là không hợp lệ.
Điều này đặc biệt quan trọng trong các hệ thống distributed system.

Tuy nhiên chúng ta chỉ hưởng lợi mới mô hình DDD với những hệ thống có nghiệp vụ phức tạp, mà đa phần khoảng 95% các dự án phù hợp với CRUD hơn là với DDD.

DDD là một khái niệm hay ho và phức tạp, vài dòng không thể nói hết được, có thể một bài viết khác tôi sẽ nói thêm chi tiết, về cơ bản DDD là một cách tiếp cận và mô phỏng hệ thống phần mềm bên dưới giống với cách nghiệp vụ vận hành bên trên. Có nghĩa là mọi thứ của nó phải đều có ý nghĩa về mặt nghiệp vụ hơn là với mặt kỹ thuật.

Monolith

Monolith là một từ cổ chỉ một khối đá khổng lồ, trong phần mềm nó ám chỉ một hệ thống phần mềm bao gồm nhiều thành phần khác nhau được kết hợp thành một chương trình duy nhất được triển khai (deploy) trên một nền tảng duy nhất. Triển khai một ứng dụng kiểu Monolith từ lâu đã trở thành một tiêu chuẩn (standard) trong giới phần mềm. Một hệ thống kiểu Monolith nếu được module hóa và thiết kế tốt (well-designed) thì việc vận hành, bảo trì (maintain) hay nâng cấp vẫn rất tốt, không giống như các Micro-services “fan-boy” hay lấy ra để chê bai và dìm hàng Monolith. Tuy nhiên thì trong “thực chiến” đa số chúng ta thường không duy trì được thiết kế tốt khi dự án lớn dần, dẫn tới những vấn đề về việc maintain, mà trong trường hợp xấu nhất hệ thống chúng ta sẽ trở thành một quả bóng bùn (Big Ball of Mud).

Photo by Zoltan Tasi on Unsplash

Vậy thế nào là một Monolith well-designed? Làm thế nào để chúng ta tránh được Big Ball of Mud? Câu trả lời chính là DDD (domain-driven design), một hệ thống Monolith đi theo hướng design của DDD sẽ tách biệt ra các thành phần (component) hay module ra từng bounded context theo từng nghiệp vụ của nó đại diện. Nói thì dễ nhưng để làm được điều này người thiết kế hệ thống phải là người am hiểu về business của hệ thống không thua kém gì các buiness-people. Điều này cũng sẽ rất thuận lợi cho việc nếu có một lý do nào đó ta cần phân tách hệ thống Monolith ra nhiều services khác nhau (micro-services), chúng ta chỉ cần tách các bounded context ra các thành phần riêng biệt.

Tuy nhiên có một vấn đề khi chúng ta tách Monolith thành các Micro-Services đó là việc giao tiếp (communication) giữa các services. Ở hệ thống Monolith nó chỉ đơn gian là các object calls, nhưng trong Micro-Serivies thì việc này phải đi qua net-work bằng cách sử dụng các kỹ thuật như API Call, RPC hay Event Bus thông qua một khái niệm gọi là location transparency mà chúng ta sẽ quay lại và bàn thêm sau.

Microservices

Tới bây giờ Microservices không còn là khái niệm gì mới mẻ nữa, các concept như là service boundaries, asynchronous message hay application database, v.V… đã là những concept nổi tiếng và được sử dụng trong nhiều năm qua.

Nguồn ảnh: https://microservices.io/patterns/microservices.html

Rất nhiều các điểm mạnh để triển khai một hệ thống theo Microservices như là “độc lập về phát triển, triển khai và mở rộng (independent development/deployment/scale) v.V… Nhưng theo tôi thì điểm mạnh nhất của Microservices đó phù hợp với cách làm việc và giao tiếp “giữa người với người”. Về bản chất con người chỉ có thể làm việc và giao tiếp với nhau tốt nhất với một team dưới 10 người, vì thật quá khó khi làm việc trên một phần mềm có quá nhiều kỹ sư. Vì thế chúng ta cần phải làm là xây dựng mỗi nhóm độc lập trên các phần khác nhau của hệ thống, đó là tính Agility trong Agile. Do vậy không phải ngẫu nhiên một Agile team hay như một đơn vị nhỏ nhất trong quân đội (tiểu đội) cũng chỉ nên có dưới 10 người.

Với một hệ thống Micro-services được thiết kế tốt thì các team phụ trách mỗi services sẽ implement, deploy và run các ứng dụng của mình một cách độc lập và ít ảnh hưởng tới các team khác. Và tất nhiên nó rất tốt và phù hợp với các vấn đề về mở rộng (scalability) sau này.

Tuy nhiên như đã nói bên trên Micro-services là một hệ thống Distributed System, nó luôn đi kèm với những phức tạp trong việc vận hành, triển khai và về phía kỹ thuật (technical) lẫn nghiệp vu (business) bởi vì mỗi service không thể nào thực sự độc lập, nó luôn cần dữ liệu (data) từ services, qua cách communication với nhau dù ít hay nhiều. Các service thì có nhiều cách để giao tiếp với nhau nhưng thông thường đó là kiểu request-response sử dụng API qua HTTP hoặc RPC, hoặc gửi message theo kiểu send-and-forget theo mô hình Pub/Sub. Cũng từ đó mà nẩy sinh ra các vấn đề khác như “data consistency” trong kiến trúc Microservices.

Do đó việc xác định chuẩn xác các bounded contexts hay service boundaries và cách các services giao tiếp với nhau ngay từ đầu là một việc tối quan trọng. Bởi vì việc refactoring hay dịch chuyển (moving) các chức năng (functionality) trong Micro-services khó khăn hơn rất nhiều lần so với trong Monolith.

Các kỹ sư làm việc lâu năm với Distributed system thường nói rằng: “Nếu bạn không thể thiết kế một hệ thống Monolith well-designed (với DDD) thì hãy quên đi việc xây dựng kiến trúc Microservices.”

Có thể thấy DDD nó phù hợp và quan trọng trong Micro-services đến nhường nào, vì vậy hãy đừng nói với tôi rằng bạn đang làm việc với Microservies mà không biết gì DDD nhé :D.

Event-Driven Architecture

Như đã nói ở bên trên rằng các service luôn luôn có nhu cầu giao tiếp với các service khác, cách phù hợp nhất với việc giao tiếp này đó là gửi nhận message giữa các services theo một cách bất đồng bộ (asynchronously). Kiến trúc đó thường được gọi là Event-Drivent Architecture (EDA), trong EDA ta định nghĩa ra ba loại message:

  • Event: Nó đại diện cho một thông điệp nhắc nhở rằng đã có gì đó đã sẩy ra ở đâu đó. Đặc điểm của event là nó bất biến (immutable).
  • Command: Nó đại diện cho một hành động mà nơi gửi (publisher) muốn nơi nhận (consumer) thực hiện một cái gì đó, và kết quả của hành động sẽ được gửi lại cho sender. Đặc điểm của command là nó luôn luôn có ít nhất một nơi nhận (consumer)
  • Query: Nó đại diện cho một yêu cầu dữ liệu trả về từ nơi gửi (publisher) tới một hay nhiều nơi nhận (consumer), dữ liệu sẽ được lấy và trả về cho publisher.

Trong EDA ta sẽ sử dụng message kiểu Event nếu publisher gửi đi (emit) message mà không quan tâm tới có consumer nào lắng nghe (listening), và kể cả nếu không có consumer nào lắng nghe thì publisher vẫn sẽ thực hiện hành động của nó.
Message kiểu Command được sử dụng nếu publisher mong đợi một điều gì đó sẽ sẩy ra ở một service khác một cách bất đồng bộ (asynchronous), trước khi nó fulfilment nghiệp vụ (business task) của publisher đó, bằng cách chờ đợi kết quả hành động từ consumer trả về .
Chúng ta sẽ sử dụng message kiểu Query nếu muốn lấy thông tin từ một hoặc nhiều service khác. Dữ liệu cũng sẽ được trả về từ consumer tới publisher theo một cách bất đồng bộ.

Một ví dụ về EDA trong thực tiễn là trong một hệ thống về eCommerce có một service là Order Fulfilment, service này luôn yêu cầu đơn hàng (Order) phải được thanh toán (payment) trước khi nó thực hiện cách hành động khác. Lúc này Order Fulfilment service sẽ gửi một asynchronous command message tới Payment service, khi đó Payment service sẽ thực hiện việc thanh toán và trả về một event message chứa kết quả thành công hay thất bại. Order Fulfilment service sẽ consumer kết quả và tiếp tục những process tiếp theo (cancel order/shipping order).

Trong EDA mỗi event nó sẽ không phản ánh ở tầng kỹ thuật (technical level) mà nó phải ánh xạ và có ý nghĩa với một hành vi của nghiệp vụ (business level) nhất định trong hệ thống. Ví dụ thay vì sử dụng event CustomerChanged để gửi thông điệp rằng khách hàng đang thay đổi địa chỉ mà chúng ta nên sử dụng event CustomerMoved, nó sẽ có ý nghĩa (meaningful) với business hơn. Do đó việc modelling event cũng phải tập chung vào hành vi (behavioural) của nghiệp vụ nhiều hơn là cấu trúc (structure) của model theo hơi hướng của Aggregate trong DDD. Điều này sẽ khiến cho ứng dụng dễ hiểu hơn rằng nó đang làm gì ở góc nhìn về business, qua đó ta thấy rằng concept của DDD lại một lần nữa phù hợp ở đây.

Eventual Consistency

Trong mọi hệ thống trong thực tế thì event bắt buộc phải đáp ứng việc đồng bộ và nhất quán (Eventual Consistency). Ví dụ như việc chuyển tiền giữa hai tài khoản của hai ngân hàng thì hai command event bắt buộc phải được thực hiện một cách đồng bộ và nhất quán, không thể tài khoản A bị trừ tiền và tài khoản B vẫn không nhận được tiền được. Trong đó mọi event được dồng bộ và nhất quán giữa một hoặc nhiều service khác nhau trong hệ thống, điều này cũng kéo theo sự phức tạp không đáng có trong hệ thống, tuy phức tạp nhưng nó là điều hiển nhiên và không thể tránh khỏi (inevitable)trong distributed system.

Eventual consistency is an inevitable concept in large-scale distributed systems like … Thanos

Bởi vì một số quyết định trong business logic luôn luôn phải đưa ra dựa trên các dữ liệu nhất quán nghiêm ngặt (data consistency) và đáng tin cậy, và kết quả các quyết định đó cũng phải được cập nhật tương ứng với các thành phần liên quan. Cũng giống như trong DDD các Aggregate luôn luôn phải nhất quán và bất biến (consistent & invariants), các entity và value object trong một Aggregate phải luôn consistent với nhau khi nghiệp vụ cần lấy ra hay thay đổi.

Command Query Responsibility Segregation (CQRS)

CQRS thực ra là một concept rất đơn giản là tách phần commandquery (đọc&ghi) ra thành hai thành phần (component) riêng biệt mục đích để tối ưu việc đọc hoặc ghi trong hệ thống.

Điểm mạnh của kiến trúc này là command chỉ tác động tới một Aggregate trong khi đó query có thể đọc và lấy một lượng lớn dữ liệu từ nhiều nguồn. Có nghĩa là ta có thể hạn chế ảnh hưởng (effect) của việc ghi dữ liệu và tối ưu hóa (optimize) truy vấn (query) cho phần đọc. Với việc tách biệt (segregation) đọc&ghi (read&write) hệ thống có thể dễ dàng sử dụng những data storage khác nhau cho việc tối ưu tốc độ (performant) cho từng loại. Mà thông thường thì việc read sẽ tốn performant hơn tác vụ write, vì vậy việc tách bạch này cũng khiến cho ta dễ dàng optimized hay scaling cho phần read side.

Tuy nhiên việc tách biệt này dẫn tới việc ta phải luôn đồng bộ dữ liệu từ write side với read side. Với việc đồng bộ kiểu bất đồng bộ (asynchronously) sẽ khiến cho dữ liệu read side gần như không thể phản ánh và đồng bộ tức thời (immediately) từ dữ liệu của write side. Việc phải làm cho đọc&ghi phải synchronous với nhau, hay hệ thống phải đảm bảo eventual consistency cũng làm cho hệ thống phức tạp hơn rất nhiều lần.

Tuy nhiên CQRS cũng giống như DDD chúng ta chỉ hưởng lợi với nó trên một vài hệ thống có nghiệp vụ phức tạp đặc thù dựa trên kiến trúc EDA, mà đa phần các hệ thống còn lại phù hợp với dạng CRUD hơn, và nó nên thực hiện theo cách đó. Đặc biệt là CQRS phần lớn chỉ phù hợp với một phần cụ thể trong hệ thống (BoundContext trong DDD) hơn là trên toàn bộ hệ thống.

CQRS sẽ khiến cho hệ thống sẽ trở nên rất phức tạp, do vậy chúng ta phải rất cẩn trọng khi sử dụng CQRS, vì nó cũng giống như Mircoservice là một hệ thống rất khó để sử dụng tốt. Nếu không thiết kế và sử lý tốt chúng ta sẽ phải trả giá đắt ngay cả khi ta có một team có năng lực về công nghệ tốt.

Event sourcing

Ý tưởng của event sourcing đã có từ lâu và cũng không mới mẻ gì, trong nội bộ hầu hết các database đều sử dụng kiến trúc event sourcing, ví dụ cơ chế Binlog trong MySQL hay Audit Log trong Oracle.

Concept của event sourcing là lưu trữ (persisted) tất cả lịch sử thay đổi của business entity và được sắp sếp theo trình tự thời gian của sự thay đổi (time-ordered sequence).

Khi trạng thái của entity bị thay đổi, một message dạng event sẽ gửi đi mục đích để lưu lại lịch sử thay đổi của entity. Ta có thể persisted sự thay đổi ngay lập tức khi event tới, hoặc snapshot định kỳ nếu entity có số lượng event lớn mà ta muốn optimize. Các hệ thống như tài chính (finance), ngân hàng (banking) hay bảo hiểm (insurance) Event Sourced thường xuyên được sử dụng, mục đích là để lưu trữ lại tất cả các thay đổi trong hệ thống để có thể xử lý sau này nếu cần.

Điểm mạnh của nó là ta có thể tái hiện (reconstructed) trạng thái của entity tại một thời điểm bất kỳ, bằng cách phát lại (replaying all) tất cả các event phát sinh tới thời điểm đó. Điều này có hữu ích trong việc tái hiện bug hoặc rollback lại trạng thái của entity nếu có lỗi xảy ra. Tuy nhiên việc replaying all này phải thực hiện với sự cẩn thận, vì rất có thể khi phát lại những command event ảnh hưởng tới các service khác, ví dụ event OrderPlaced được phát đi lại(emit) thì rất có thể event OrderFulfilled sẽ được thực hiện nhiều hơn một lần.

Event sourcing thường được sử dụng như một phần trong CQRS để đồng bộ sự thay đổi của Command với Query. Tuy nhiên không phải tất cả hệ thống đều phải có event sourced, mà nó phụ thuộc vào business của từng bounded contexthoặc theo từng nghiệp vụ của Aggregate. Các event sourcing không nên publish ra ngoài bounded context của nó để tránh những sự phụ thuộc không cần thiết giữa các bounded context.

Location Transparency

Một concept rất quan trọng khác trong việc xậy dựng một Monolith sẵn sàng cho Microservice là Location Transparency, nó sẽ giúp việc các Context định vị được vị trí Context khác mà nó cần tương tác (interacts) tới. Việc này làm hạn chế sự phụ thuộc (dependency) giữa các bounded context trong một dự án Monolith, khi mà mọi component đều được deploy chung trên một đơn vị (unit). Location Transparency giúp ích cho việc có thể chuyển đổi Monolith sang Microservice khi mà mọi service đều được deploy lên các môi trường riêng biệt, mà không cần thay đổi bất kỳ dòng code nào. Có nhiều cách để cài đặt Location Transparency như sử dụng messaging với những dữ liệu kiểu asynchronous, hoặc HTTPS/RPC với dữ liệu trao đổi kiểu synchronous.

Ví dụ trong một dự Monolith viết bằng Java là các bounded context sẽ trao đổi asynchronus với nhau qua Event Bus hoặc cài đặt Interface trên bounded context và Implement của nó sẽ nằm ở Locaiont Transparency nếu cần trao đổi kiểu synchronous.

Kết luận: các kiến trúc như Microservice, CQRS hay Event Sourcing không phải là một viên đạn bạc (silver bullet) để giải quyết tất cả các bài toán. Mà nó chỉ phù hợp cho vài hệ thống đặc thù có domain hay business phức tạp như các hệ thống banking, finance, insurance hay eCommerce. Và nó có điểm chung là đều sử dụng DDD như một cơ sở kiến trúc và lý luận trong hệ thống.

Hết!

Tham khảo:

--

--