Trong những năm gần đây, kiến trúc Microservices đã trở thành nền tảng của nhiều hệ thống phân tán hiện đại. Tuy nhiên, khi số lượng dịch vụ ngày càng tăng, mô hình giao tiếp Request-Response truyền thống bắt đầu bộc lộ nhiều hạn chế như phụ thuộc chặt chẽ giữa các dịch vụ, khó mở rộng và phức tạp trong xử lý lỗi. Event-Driven Microservices ra đời để giải quyết những vấn đề này bằng cách sử dụng các sự kiện (events) làm phương tiện giao tiếp chính giữa các dịch vụ. Bài viết này sẽ giúp bạn hiểu cách Event-Driven Microservices hoạt động, vai trò của Domain-Driven Design (DDD), Bounded Context, Event-Driven Data Communication cũng như so sánh với mô hình Request-Response để biết khi nào nên áp dụng từng kiến trúc
1. Event-Driven Microservices (Microservice hướng sự kiện)
Các vi dịch vụ (Microservices)
được xây dựng có mục đích để hoàn thành các yêu cầu kinh doanh cụ thể của tổ chức.
Một Microservice nên đủ nhỏ để có thể "nằm gọn trong đầu" của một người
và tập trung giải quyết các vấn đề nghiệp vụ chuyên biệt. Đội ngũ quản lý dịch
vụ này cũng nên là một nhóm nhỏ, thường được ví von là "đội quân hai chiếc
bánh pizza" (nhóm đủ nhỏ để ăn hết hai cái pizza).
Ở dạng đơn giản nhất, một
Microservice hướng sự kiện là một dịch vụ chuyên biệt thực hiện việc
tiêu thụ (consume) và/hoặc tạo ra (produce) các sự kiện. Điểm khác biệt cốt lõi
nằm ở cách chúng giao tiếp:
- Giao tiếp bất đồng bộ:
Thay vì phải chờ đợi phản hồi từ dịch vụ khác (kiểu đồng bộ), Event-Driven
Microservices phát hành sự kiện và tiếp tục làm việc khác mà không cần
đợi.
- Dữ liệu không bị mất đi:
Trong kiến trúc hiện đại, các sự kiện không bị hủy sau khi được tiêu thụ
mà vẫn có sẵn trong luồng sự kiện để các dịch vụ khác có thể đọc lại khi cần.
- Vòng lặp sự kiện:
Theo định nghĩa, dịch vụ này được thúc đẩy bởi một vòng lặp: nó tiêu thụ sự
kiện, xử lý và có thể tạo ra sự kiện mới.
Mô hình Microservice cơ bản
Hình trên minh họa một Microservice
hướng sự kiện điển hình với hai luồng sự kiện đầu vào và một luồng sự kiện đầu
ra. Bạn có thể hình dung dịch vụ này như một "cái máy" đang vận hành
trong một dây chuyền dữ liệu:
· Tiêu
thụ (Consume): Dịch vụ tiếp nhận (đọc) các sự kiện từ
hai luồng sự kiện đầu vào (Event streams) khác nhau.
· Kho
lưu trữ trạng thái (State store): Đây là thành phần rất
quan trọng nằm ngay bên trong dịch vụ. Microservice tự chịu trách nhiệm lưu trữ
và quản lý dữ liệu riêng của nó trong kho lưu trữ này thay vì dùng chung một cơ
sở dữ liệu lớn với các dịch vụ khác.
· Xử
lý và Phản ứng: Dựa trên logic nghiệp vụ của mình, dịch vụ
sẽ xử lý các sự kiện đầu vào, cập nhật trạng thái nội bộ và phản ứng lại với
các thông tin đó.
· Sản
xuất (Produce): Sau khi xử lý, kết quả được dịch vụ ghi
vào một luồng sự kiện đầu ra để các dịch vụ khác trong hệ thống có thể tiếp tục
sử dụng.
Lợi ích
Microservice trong hình
này hoàn toàn làm chủ quy trình của nó, từ việc đọc/ghi dữ liệu, quản lý trạng
thái cho đến khả năng tự mở rộng. Cách tiếp cận này mang lại nhiều lợi ích quan
trọng:
- Linh hoạt về công nghệ:
Mỗi dịch vụ có thể dùng ngôn ngữ lập trình hoặc loại cơ sở dữ liệu phù hợp
nhất cho công việc của nó.
- Giảm phụ thuộc (Loose coupling):
Các dịch vụ kết nối với nhau qua dữ liệu (sự kiện) chứ không phụ thuộc chặt
chẽ vào mã nguồn hay API của nhau.
Khả năng mở rộng: Từng dịch vụ riêng lẻ có thể được tăng cường sức mạnh xử lý hoặc bộ nhớ khi cần thiết mà không ảnh hưởng đến toàn bộ hệ thống.
2. Thiết kế hướng miền (Domain-Driven Design - DDD) và Ngữ cảnh giới hạn (Bounded Contexts)
Đây là những công cụ tư
duy quan trọng để xác định ranh giới cho các Microservice.
Các khái niệm cốt lõi trong DDD
Để xây dựng một hệ thống Microservice
tốt, cần hiểu rõ cấu trúc doanh nghiệp thông qua 4 khái niệm sau:
- Miền (Domain):
Đây là "không gian vấn đề" mà doanh nghiệp đang hoạt động. Nó
bao gồm tất cả các quy tắc, quy trình, ý tưởng và thuật ngữ chuyên môn mà
doanh nghiệp phải đối mặt để cung cấp giải pháp cho khách hàng.
- Miền con (Subdomain):
Một doanh nghiệp lớn thường được chia thành các phần nhỏ hơn để dễ quản
lý, như kho bãi, bán hàng, kỹ thuật.... Mỗi phần này chính là một miền con.
- Mô hình miền (Domain Model):
Đây là cách doanh nghiệp "vẽ" lại thực tế vào trong phần mềm. Mỗi
miền sẽ có ngôn ngữ riêng. Ví dụ: Từ "Khách hàng" trong bộ phận
Bán hàng có thể mang ý nghĩa khác với "Khách hàng" trong bộ phận
Hỗ trợ kỹ thuật.
- Ngữ cảnh giới hạn (Bounded Context):
Đây là khái niệm quan trọng nhất cho Microservice. Nó thiết lập một ranh
giới logic rõ ràng cho một miền con. Trong ranh giới này, các mô hình
dữ liệu, quy trình và sự kiện đều được xác định cụ thể và độc lập.
Mối quan hệ giữa Bounded Context và Microservice
Không có quy tắc cứng nhắc
về số lượng Microservice trong một Ngữ cảnh giới hạn.
- Một Bounded Context có thể chỉ cần một Microservice để hoàn thành nhiệm vụ.
- Nhưng nó cũng có thể chứa nhiều Microservice khác nhau, miễn là tất cả chúng cùng phục vụ cho mục đích chung của ngữ cảnh đó.
Tại sao ranh giới này lại quan trọng?
Hãy tổ chức dịch vụ theo
nghiệp vụ, đừng tổ chức theo công nghệ.
- Tổ chức theo nghiệp vụ (Business
Alignment): Khi các nhóm được chia theo Bounded
Context, họ có toàn quyền quyết định về mã nguồn và cơ sở dữ liệu của mình
(được gọi là Sole ownership - Quyền sở hữu duy nhất). Điều này giúp
giảm sự phụ thuộc lẫn nhau giữa các nhóm và giúp tổ chức phản ứng nhanh
hơn với các thay đổi.
- Sai lầm khi tổ chức theo công nghệ:
Nếu bạn chia nhóm theo các lớp kỹ thuật (ví dụ: nhóm chuyên làm Database,
nhóm chuyên làm App), bạn sẽ tạo ra sự phụ thuộc chéo (Cross-cutting
ownership). Khi đó, một thay đổi nhỏ ở lớp dữ liệu có thể gây ra hiệu ứng
dây chuyền làm hỏng nhiều dịch vụ khác, khiến việc bảo trì trở nên vô cùng
tốn kém và khó khăn.
Ví dụ về Tổ chức theo nghiệp vụ (Business Alignment)
Hình minh họa này trình
bày một hệ thống phân cấp logic:
- Miền (Domain - Khung lớn nhất):
Đây là toàn bộ "không gian vấn đề" mà doanh nghiệp đang giải quyết.
Trong hình, là miền Sales (Bán hàng).
- Miền con (Subdomain - Các khung bên
trong): Để dễ quản lý, miền Sales được chia
nhỏ thành các mảng nghiệp vụ chuyên biệt hơn là Orders (Đơn hàng)
và Shipping (Giao hàng). Mỗi phần này tập trung vào một tập hợp
trách nhiệm cụ thể.
- Ngữ cảnh giới hạn (Bounded Context -
Các khung màu xanh): Đây là khái niệm quan trọng nhất.
Nó thiết lập ranh giới logic rõ ràng cho các quy trình và mô hình dữ liệu.
Ví dụ: Trong mảng "Orders", chúng ta có ngữ cảnh Order-taking
(Tiếp nhận đơn) và Inventory (Kho bãi). Mỗi ngữ cảnh này có
"ngôn ngữ" và quy tắc riêng để không làm lẫn lộn với nhau.
- Vi dịch vụ (Microservice - Các khối
MS nhỏ nhất): Đây là các đơn vị thực thi mã nguồn.
Bạn sẽ thấy các khối MS nằm gọn trong các Ngữ cảnh giới hạn.
Những điểm mấu chốt:
- Mối quan hệ linh hoạt:
Một Ngữ cảnh giới hạn có thể chứa một Microservice duy nhất (như
trong phần Inventory) hoặc nhiều Microservice làm việc cùng nhau
(như trong phần Order-taking) để hoàn thành mục tiêu nghiệp vụ.
- Giao tiếp xuyên biên giới:
Các mũi tên giữa các khối MS (ngay cả khi chúng nằm ở các Ngữ cảnh khác
nhau như từ Order-taking sang Orders-to-fill) đại diện cho
luồng dữ liệu. Trong kiến trúc này, chúng giao tiếp với nhau thông qua các
luồng sự kiện (event streams).
- Giảm phụ thuộc (Loose Coupling):
Bằng cách đặt ra các ranh giới "Bounded Context", những thay đổi
bên trong một ngữ cảnh (ví dụ: thay đổi cách quản lý Kho) sẽ không làm hỏng
các ngữ cảnh khác (như Giao hàng).
Khi một nhóm sở hữu trọn
vẹn một "Bounded Context", họ có toàn quyền quyết định từ mã nguồn đến
cơ sở dữ liệu (quyền sở hữu duy nhất - sole ownership), giúp họ phản ứng
nhanh hơn với các thay đổi của thị trường mà không bị phụ thuộc vào các nhóm
khác.
Tóm lại: Việc áp dụng DDD và Bounded Context giúp chúng ta "chia để trị" một cách khoa học. Nó giúp các Microservice hướng sự kiện trở nên linh hoạt, dễ bảo trì và quan trọng nhất là luôn bám sát nhu cầu thực tế của doanh nghiệp.
3. Cấu trúc Giao tiếp
Phần này giải thích lý do
tại sao kiến trúc hướng sự kiện lại giải quyết được những bế tắc kinh điển
trong việc chia sẻ dữ liệu.
Tách rời "Hành động" và "Dữ liệu"
Microservice hướng sự kiện
hoạt động hiệu quả vì chúng tách biệt những gì một dịch vụ làm (logic) khỏi
cách nó truy cập dữ liệu. Trong các hệ thống cũ, dữ liệu và dịch vụ bị
"nhốt" chung một chỗ, khiến việc chia sẻ trở nên rất khó khăn.
Định luật Conway: Hệ thống soi gương tổ chức
Định luật Conway:
Các tổ chức thiết kế hệ thống phần mềm sẽ tạo ra những bản sao của chính cấu
trúc giao tiếp trong tổ chức đó.
- Nếu các nhóm bị chia rẽ theo ranh giới phòng ban, họ sẽ xây dựng các dịch vụ chỉ có dữ liệu trong "ngữ cảnh giới hạn" (Bounded Context) của riêng họ.
- Vấn đề là: Để kinh doanh hiệu quả, các dịch vụ này thường xuyên cần dữ liệu quan trọng nằm ở các phòng ban khác.
Hiệu ứng "Cây gậy và Củ cà rốt" (Vấn đề của hệ thống cũ)
Có một tình huống trớ
trêu khi một nhóm nhận được yêu cầu kinh doanh mới:
- Củ cà rốt (Sự lôi cuốn):
Việc nhồi nhét tính năng mới vào dịch vụ cũ hiện có rất dễ dàng vì dữ liệu
đã nằm sẵn ở đó, không cần lo về đường truyền hay bảo mật.
- Cây gậy (Sự trừng phạt):
Việc tạo ra một dịch vụ mới hoàn toàn rất khó khăn vì việc lấy dữ liệu từ
các hệ thống khác vô cùng phức tạp, chậm chạp và dễ lỗi.
Hậu quả là hệ thống cũ cứ
thế phình to ra thành một khối khổng lồ (monolith) khó kiểm soát, hoặc trở
thành một mớ "spaghetti" những kết nối chồng chéo.
Giải pháp: Dữ liệu là "Công dân hạng nhất"
Tinh thần là hãy biến dữ
liệu thành một "cấu trúc giao tiếp vững chắc" thông qua các luồng
sự kiện (event streams).
- Dữ liệu là sự thật:
Các sự kiện cũng chính là dữ liệu. Trong Event-Driven Architecture
hiện đại, sự kiện không chỉ đóng vai trò thông báo rằng một hành động đã xảy
ra mà còn là nguồn dữ liệu chính để các dịch vụ khác xây dựng trạng thái
và xử lý nghiệp vụ. Điều này giúp giảm sự phụ thuộc vào các API đồng bộ và
tạo nên khả năng mở rộng cao hơn.
- Giải phóng dữ liệu:
Thay vì dịch vụ A phải đi xin xỏ dịch vụ B, dịch vụ A chỉ cần đăng
ký (subscribe) vào luồng sự kiện mà nó cần.
- Tự chủ hoàn toàn: Mỗi Microservice giờ đây có thể tự xây dựng mô hình dữ liệu riêng từ các luồng sự kiện đó, dùng công nghệ nào tùy thích mà không cần phụ thuộc vào API hay cơ sở dữ liệu của nhóm khác.
4. Event-Driven Data Communication (Giao tiếp dữ liệu hướng sự kiện)
Sự kiện chính là "Dữ liệu"
Trong các hệ thống cũ, sự
kiện thường chỉ là các tín hiệu báo rằng "dữ liệu đã sẵn sàng ở chỗ
khác". Nhưng trong mô hình này, sự kiện chính là dữ liệu. Mỗi sự kiện
là một bản ghi bền vững về một sự thật kinh doanh đã xảy ra (ví dụ: "Đơn
hàng đã thanh toán"). Các luồng sự kiện này đóng vai trò vừa là nơi lưu trữ,
vừa là phương tiện giao tiếp giữa các dịch vụ.
Giải phóng bên sản xuất (Producer)
Trong kiến trúc hướng sự
kiện, trách nhiệm của dịch vụ tạo ra dữ liệu (Producer) được giới hạn lại:
- Chúng chỉ cần đảm bảo dữ liệu được ghi vào luồng sự kiện đúng định dạng và đúng thời điểm.
- Chúng không cần quan tâm dịch vụ khác
sẽ truy vấn dữ liệu đó như thế nào. Producer hoàn toàn
rũ bỏ được gánh nặng phải xây dựng và bảo trì các API phức tạp chỉ để phục
vụ nhu cầu tra cứu của các nhóm khác.
Trao quyền tự chủ cho bên tiêu thụ (Consumer)
Ngược lại, các dịch vụ sử
dụng dữ liệu (Consumer) giờ đây có toàn quyền chủ động:
- Tiêu thụ sự kiện theo tốc độ riêng của mình.
- Tự xây dựng mô hình dữ liệu và lớp truy vấn riêng dựa trên các sự kiện nhận được, thay vì đi "xin" hay phụ thuộc vào API của bên sản xuất.
- Dịch vụ tiêu thụ hoàn toàn tự chịu trách nhiệm về logic nghiệp vụ riêng của mình mà không làm phiền đến bên cung cấp dữ liệu.
Bản ghi có thể "phát lại" (Replayable Record)
Dữ liệu trong luồng sự kiện
không bị mất đi sau khi đọc. Nó tạo thành một luồng nhật ký liên tục và có thể
đọc lại bất cứ lúc nào. Điều này có nghĩa là bạn có thể dễ dàng thay thế một dịch
vụ cũ bằng một dịch vụ mới mà không gây ra bất kỳ tác động nào đến hệ thống hiện
tại, vì dữ liệu cốt lõi luôn có sẵn và độc lập với mã nguồn thực thi.
Kết nối trực tiếp với nhu cầu kinh doanh
5. Request-Response Microservices
Request-Response
Microservices là cách tiếp cận truyền thống trong giao
tiếp giữa các dịch vụ và nó có những hạn chế nhất định so với kiến trúc hướng sự
kiện.
Request-Response Microservices là gì?
Trong mô hình này, các dịch
vụ giao tiếp trực tiếp với nhau thông qua một yêu cầu (request) và nhận lại một
phản hồi (response). Các giao tiếp này có thể là:
- Đồng bộ (Synchronous):
Bên gửi yêu cầu sẽ dừng mọi việc lại để đợi phản hồi.
- Bất đồng bộ (Asynchronous):
Bên gửi yêu cầu tiếp tục làm việc khác và sẽ quay lại kiểm tra kết quả
sau. Điểm mấu chốt là các dịch vụ nói chuyện trực tiếp với nhau
thay vì thông qua một luồng sự kiện trung gian.
Những nhược điểm khi áp dụng ở quy mô lớn
Thành công của các công
ty lớn như Netflix hay Uber không có nghĩa là kiến trúc Request-Response của họ
không có vấn đề; thực tế, nhiều hệ thống thành công vẫn có thể sở hữu mã nguồn
"rối rắm như mì spaghetti". Một số hạn chế lớn bao gồm:
- Kết nối điểm-điểm (Point-to-point
couplings): Các dịch vụ phụ thuộc lẫn nhau tạo
thành một chuỗi dây chuyền. Điều này khiến việc theo dõi trách nhiệm của từng
dịch vụ trở nên khó khăn và việc thay đổi hệ thống trong tương lai rất tốn
kém.
- Khả năng mở rộng bị phụ thuộc
(Dependent scaling): Một dịch vụ chỉ có thể mở rộng
sức mạnh xử lý nếu tất cả các dịch vụ mà nó phụ thuộc vào cũng mở rộng được.
- Xử lý lỗi phức tạp:
Nếu một dịch vụ bị hỏng, dịch vụ gọi nó phải tự quyết định xem nên thử lại
(retry), dừng lại hay xử lý lỗi như thế nào, điều này trở nên cực kỳ khó
quản lý khi hệ thống có hàng trăm dịch vụ.
- Quản lý phiên bản API:
Rất khó để buộc tất cả client nâng cấp lên phiên bản mới cùng lúc, dẫn đến
việc phải duy trì nhiều phiên bản API cũ, gây tốn kém và phức tạp.
- Khó kiểm thử:
Việc kiểm thử tích hợp yêu cầu tất cả các dịch vụ liên quan phải hoạt động
hoàn hảo, điều này rất khó thiết lập trong môi trường thử nghiệm.
Những lợi ích không thể phủ nhận
Request-Response vẫn có
những thế mạnh riêng:
- Phù hợp với một số tác vụ nhất định:
Ví dụ như xác thực người dùng hoặc báo cáo kết quả thử nghiệm A/B.
- Tích hợp bên thứ ba:
Việc tích hợp với các giải pháp bên thứ ba hầu như luôn sử dụng cơ chế yêu
cầu-phản hồi và thường cung cấp một cơ chế giao tiếp linh hoạt, không phụ
thuộc ngôn ngữ lập trình thông qua HTTP.
- Dễ gỡ lỗi (Debug):
Việc theo dõi một thao tác chạy qua nhiều hệ thống thường dễ hơn nhờ hệ thống
log chi tiết và trực quan.
- Trải nghiệm người dùng:
Các ứng dụng web và di động chủ yếu dựa trên mô hình này để trả về kết quả
nhanh chóng cho người dùng.
- Nguồn nhân lực:
Hiện nay, tìm kiếm lập trình viên có kinh nghiệm làm Request-Response dễ
hơn so với lập trình hướng sự kiện bất đồng bộ.
Cần phải khẳng định rằng không có mô hình nào hoàn toàn tốt hơn mô hình nào. Mỗi cái đều có vị trí riêng và các nhiệm vụ phù hợp. Tuy nhiên, kiến trúc hướng sự kiện mang lại sức mạnh và sự linh hoạt mà Request-Response không thể có được. Trong thực tế, các công ty thường sử dụng kiến trúc hỗn hợp (hybrid), nơi cả hai mô hình này cùng tồn tại song song để giải quyết các bài toán kinh doanh khác nhau.
Kết luận
Event-Driven Microservices không chỉ là một cách giao tiếp mới giữa các dịch vụ mà còn là một tư duy thiết kế giúp xây dựng các hệ thống phân tán linh hoạt, dễ mở rộng và ít phụ thuộc lẫn nhau. Bằng việc coi sự kiện là nguồn dữ liệu trung tâm, mỗi Microservice có thể phát triển, triển khai và mở rộng một cách độc lập mà vẫn phối hợp hiệu quả với toàn bộ hệ thống. Tuy nhiên, Event-Driven Architecture không thay thế hoàn toàn mô hình Request-Response. Trong thực tế, phần lớn doanh nghiệp đều kết hợp cả hai để tận dụng ưu điểm của từng mô hình. Việc hiểu rõ bản chất, lợi ích và giới hạn của từng cách tiếp cận sẽ giúp bạn lựa chọn kiến trúc phù hợp với từng bài toán nghiệp vụ và xây dựng những hệ thống bền vững hơn trong dài hạn.


Comments
Post a Comment