Microservices
Cho đến thời điểm này của cuốn sách, bạn đã nắm vững toàn bộ các công cụ và mẫu hình của Domain-Driven Design khi đứng độc lập. Trong Phần IV này, chúng ta sẽ bắt đầu cuộc hành trình khám phá sự tương tác qua lại kỳ thú giữa Domain-Driven Design với các phương pháp luận và phong cách kiến trúc tiên tiến khác trong thế giới công nghệ:
- Chương 14: Microservices — Cách tận dụng các công cụ DDD để phân rã hệ thống thành các microservice hữu hiệu, giải mã bản chất thực sự của thuật ngữ "micro", và mối liên hệ với Bounded Contexts, Subdomains, Aggregates.
- Chương 15: Event-Driven Architecture — Làm rõ ranh giới và sự bổ trợ giữa kiến trúc hướng sự kiện và DDD, thiết kế hệ thống bất đồng bộ dựa trên các loại thông điệp sự kiện.
- Chương 16: Data Mesh — Ứng dụng các nguyên lý của DDD vào kiến trúc dữ liệu phân tích hiện đại, biến dữ liệu thành sản phẩm phục vụ nghiệp vụ.
Kiến trúc Microservices đã trở thành một trong những xu hướng công nghệ được bàn tán sôi nổi nhất trong thập kỷ qua. Rất nhiều tổ chức đã vội vã nhảy vào làn sóng microservices với kỳ vọng nó sẽ giải quyết mọi vấn đề về khả năng mở rộng (scalability), tăng tốc độ bàn giao phần mềm và giải phóng các đội ngũ kỹ sư khỏi sự kìm hãm của các khối monolithic đồ sộ.
Tuy nhiên, thực tế phũ phàng đã cho thấy không ít dự án chuyển đổi sang microservices đã thất bại cay đắng. Thay vì đạt được sự linh hoạt mong muốn, nhiều công ty lại nhận về một "Bãi Bùn Phân Tán Khổng Lồ" (Distributed Big Ball of Mud) — những thiết kế phần mềm thậm chí còn mong manh, chắp vá, nghẽn mạng và tốn kém chi phí vận hành hơn gấp nhiều lần so với chính khối monolith ban đầu mà họ muốn đập bỏ!
Về mặt lịch sử, microservices thường gắn liền mật thiết với Domain-Driven Design, đặc biệt là với mẫu hình Bounded Context. Nhiều người thậm chí còn sử dụng hai thuật ngữ "Bounded Context" và "Microservice" như hai từ đồng nghĩa có thể thay thế cho nhau. Nhưng liệu chúng có thực sự là một thứ hay không?
Chương này sẽ khám phá mối quan hệ bản chất giữa phương pháp luận Domain-Driven Design và mẫu hình kiến trúc Microservices. Bạn sẽ hiểu được sự tương tác giữa các mẫu hình, và quan trọng hơn cả: làm thế nào bạn có thể tận dụng DDD để thiết kế nên những hệ thống hướng microservices thực sự bền vững và hiệu quả.
Hãy bắt đầu từ những khái niệm căn bản nhất: Thế nào là một Dịch vụ (Service) và một Microservice?
Dịch Vụ (Service) Là Gì?
Theo định nghĩa tiêu chuẩn từ tổ chức OASIS (Organization for the Advancement of Structured Information Standards):
"Một dịch vụ (service) là một cơ chế cho phép truy cập vào một hoặc nhiều năng lực nghiệp vụ (capabilities), trong đó việc truy cập được cung cấp thông qua một giao diện quy định (prescribed interface)."
Giao diện quy định đó là bất kỳ cơ chế nào dùng để đưa dữ liệu vào hoặc lấy dữ liệu ra khỏi một dịch vụ. Nó có thể hoạt động đồng bộ (synchronous) — chẳng hạn như mô hình Request/Response thông qua REST API hay gRPC — hoặc hoạt động bất đồng bộ (asynchronous) — chẳng hạn như mô hình sản xuất và tiêu thụ các sự kiện (events) qua Message Broker.
Đây chính là giao diện công khai (public interface) của dịch vụ, như được minh họa trong Hình 14-1, đóng vai trò là phương tiện duy nhất để giao tiếp và tích hợp dịch vụ đó với các thành phần khác trong toàn bộ hệ thống.
Chuyên gia kiến trúc Randy Shoup đã ví giao diện của một dịch vụ giống như "cánh cửa trước" (front door) của một ngôi nhà: mọi dữ liệu đi vào hoặc đi ra khỏi dịch vụ bắt buộc phải bước qua cánh cửa trước đó.
Hơn thế nữa, giao diện công khai định nghĩa chính bản thân dịch vụ: nó thể hiện toàn bộ chức năng mà dịch vụ phơi bày ra thế giới bên ngoài. Một giao diện được thiết kế rõ ràng là đủ để mô tả trọn vẹn chức năng nghiệp vụ mà dịch vụ đó đảm nhận. Ví dụ, giao diện công khai trong Hình 14-2 mô tả tường minh các chức năng quản lý khách hàng của dịch vụ.
Microservice Là Gì?
Định nghĩa về một microservice thực ra đơn giản đến mức đáng kinh ngạc:
Vì một dịch vụ được định nghĩa bởi giao diện công khai của nó, nên một microservice là một dịch vụ sở hữu một giao diện công khai siêu nhỏ (a micro-public interface) — một "cánh cửa trước siêu nhỏ" (a micro-front door).
Việc sở hữu một giao diện công khai siêu nhỏ mang lại những lợi ích khổng lồ:
- Dễ hiểu và dễ nắm bắt: Giúp các kỹ sư thấu suốt chức năng của từng dịch vụ đơn lẻ cũng như cách thức tích hợp của nó với các thành phần xung quanh.
- Thu hẹp lý do phải thay đổi: Thu hẹp phạm vi chức năng của dịch vụ sẽ hạn chế tối đa các lý do khiến dịch vụ phải sửa đổi (Single Responsibility Principle ở cấp độ dịch vụ).
- Tự chủ cao độ (Autonomous): Khiến dịch vụ có khả năng tự chủ cao trong quá trình phát triển, kiểm thử, quản lý vận hành và mở rộng quy mô độc lập.
Định nghĩa này cũng giải thích tại sao trong kiến trúc microservices lại có nguyên tắc bắt buộc: các microservice tuyệt đối không được để lộ cơ sở dữ liệu của mình ra bên ngoài. Nếu bạn phơi bày cơ sở dữ liệu — biến nó thành một phần của cánh cửa trước — thì giao diện công khai của dịch vụ sẽ phình to ra tới mức vô tận! Hãy thử nghĩ xem: có bao nhiêu câu lệnh SQL khác nhau có thể được thực thi trên một cơ sở dữ liệu quan hệ? Vì SQL là một ngôn ngữ truy vấn cực kỳ linh hoạt, số lượng câu truy vấn có thể thực hiện là vô hạn. Do đó, microservices bắt buộc phải đóng gói (encapsulate) cơ sở dữ liệu của mình; dữ liệu chỉ có thể được truy cập thông qua một giao diện công khai nhỏ gọn, tập trung và hướng tích hợp.
Phương Thức Là Dịch Vụ: Sự Phân Rã Ngây Thơ (Method as a Service)
Khẳng định "microservice là một dịch vụ có giao diện công khai siêu nhỏ" nghe có vẻ quá đơn giản, đến mức dễ gây hiểu lầm. Nhiều người có thể suy luận ngây thơ rằng: nếu giao diện càng nhỏ càng tốt, vậy thì nếu ta giới hạn mỗi dịch vụ chỉ chứa đúng một phương thức duy nhất thì đó sẽ là những microservice "hoàn hảo" nhất quả đất?!
Hãy xem điều gì sẽ xảy ra trong thực tế nếu chúng ta áp dụng cách phân rã ngây thơ này.
Hãy xem xét một dịch vụ quản lý danh mục công việc (Backlog Management Service) như trong Hình 14-3. Giao diện công khai ban đầu của nó gồm có 8 phương thức nghiệp vụ. Bây giờ, chúng ta áp dụng quy tắc ngây thơ: "mỗi phương thức là một microservice".
Vì đây là những microservice "chuẩn chỉ", mỗi dịch vụ đều sở hữu một cơ sở dữ liệu riêng biệt. Không dịch vụ nào được phép chọc thẳng vào database của dịch vụ khác mà phải đi qua giao diện công khai. Nhưng than ôi, các phương thức này vốn dĩ thuộc về cùng một nghiệp vụ và phải phối hợp chặt chẽ với nhau: chúng phải đồng bộ hóa những thay đổi trạng thái mà mỗi dịch vụ tạo ra!
Kết quả là: để các dịch vụ có thể phối hợp được, chúng ta buộc phải phình to giao diện công khai của từng dịch vụ bằng hàng loạt các phương thức phục vụ việc đồng bộ, kiểm tra trạng thái và điều phối tích hợp. Hơn thế nữa, khi trực quan hóa luồng dữ liệu và sự tích hợp giữa các dịch vụ thu được, hệ thống trông giống hệt một "Bãi Bùn Phân Tán Khổng Lồ" (Distributed Big Ball of Mud) điển hình, như được vạch trần trong Hình 14-4!
Mượn lại hình tượng của Randy Shoup: bằng cách chia nhỏ hệ thống thành các dịch vụ quá hạt vụn (fine-grained), chúng ta quả thực đã thu nhỏ được "cửa trước" của từng dịch vụ. Thế nhưng, để thực hiện được chức năng chung của toàn bộ hệ thống, chúng ta lại phải đục thêm những "cánh cửa hậu dành riêng cho nhân viên" (staff-only entrances) khổng lồ cho mỗi dịch vụ!
Mục Tiêu Thiết Kế Hệ Thống (Design Goal)
Việc theo đuổi một quy tắc phân rã thiển cận — bắt mỗi dịch vụ chỉ chứa một phương thức — đã hoàn toàn thất bại vì hai lý do cốt tử:
- Bất khả thi về mặt kỹ thuật: Vì các dịch vụ phải phối hợp với nhau để hoàn thành nghiệp vụ, chúng ta bị buộc phải mở rộng giao diện công khai của chúng bằng hàng loạt phương thức tích hợp phức tạp.
- Thắng một trận đánh nhỏ nhưng thua cả cuộc chiến: Từng dịch vụ đơn lẻ trông có vẻ rất đơn giản, nhưng tổng thể hệ thống lại trở nên phức tạp hơn gấp nhiều bậc so với ban đầu!
Mục tiêu tối thượng của kiến trúc microservices là tạo ra một hệ thống linh hoạt và thích ứng cao. Việc chỉ chăm chăm dồn toàn bộ nỗ lực thiết kế vào việc làm cho từng thành phần đơn lẻ trở nên siêu nhỏ mà phớt lờ sự tương tác của nó với phần còn lại của hệ thống là đi ngược lại chính định nghĩa cơ bản nhất của một "hệ thống":
- "Một tập hợp các bộ phận hoặc thiết bị được kết nối với nhau và cùng vận hành cùng nhau."
- "Một tập hợp các thiết bị và chương trình máy tính được sử dụng phối hợp cho một mục đích cụ thể."
Do đó, một hệ thống không thể được cấu thành từ các thành phần hoàn toàn độc lập tách rời. Trong một hệ thống microservices chuẩn mực, dù các dịch vụ có được tách rời (decoupled) đến đâu, chúng vẫn bắt buộc phải tích hợp và giao tiếp với nhau để giải quyết bài toán nghiệp vụ chung.
Độ Phức Tạp Hệ Thống (System Complexity)
Bốn mươi năm trước, chưa hề có điện toán đám mây, chưa có những yêu cầu quy mô toàn cầu (global-scale), và cũng chẳng ai cần phải deploy hệ thống mỗi 11,7 giây một lần. Nhưng ngay từ thời điểm đó, các kỹ sư phần mềm đã phải đối mặt với bài toán muôn thuở: chế ngự độ phức tạp của hệ thống. Dù công cụ ngày xưa khác xa ngày nay, những thách thức — và quan trọng hơn cả là các giải pháp — vẫn hoàn toàn nguyên giá trị và có thể áp dụng chuẩn xác vào việc thiết kế các hệ thống microservices hiện đại.
Độ Phức Tạp Cục Bộ vs Độ Phức Tạp Toàn Cục
Trong cuốn sách kinh điển Composite/Structured Design (1978), tác giả Glenford J. Myers đã thảo luận về cách cấu trúc mã nguồn thủ tục để giảm thiểu độ phức tạp. Ngay tại trang đầu tiên của cuốn sách, ông đã viết:
"Chủ đề độ phức tạp có ý nghĩa sâu rộng hơn rất nhiều so với việc chỉ đơn thuần cố gắng giảm thiểu độ phức tạp cục bộ của từng bộ phận trong chương trình. Một loại độ phức tạp quan trọng hơn rất nhiều là độ phức tạp toàn cục (global complexity): độ phức tạp của cấu trúc tổng thể của một chương trình hoặc hệ thống (nghĩa là mức độ liên kết hoặc phụ thuộc lẫn nhau giữa các mảnh ghép chính của chương trình)."
Chiếu vào ngữ cảnh hiện đại của chúng ta:
- Độ phức tạp cục bộ (Local Complexity): Là độ phức tạp nằm bên trong nội bộ của từng microservice riêng lẻ (phụ thuộc vào thuật toán, logic nghiệp vụ, cấu trúc dữ liệu nội tại).
- Độ phức tạp toàn cục (Global Complexity): Là độ phức tạp của toàn bộ hệ thống tổng thể (được định hình bởi sự tương tác, giao thức mạng, phụ thuộc dữ liệu và số lượng kết nối giữa các microservices).
Khi thiết kế một hệ thống microservices, việc tối ưu hóa loại độ phức tạp nào là quan trọng hơn? Hãy cùng phân tích hai thái cực cực đoan:
Thật đáng ngạc nhiên, việc kéo giảm độ phức tạp toàn cục xuống mức tối thiểu (bằng 0) lại cực kỳ dễ dàng! Tất cả những gì bạn cần làm là triệt tiêu hoàn toàn mọi sự tương tác giữa các thành phần — tức là nhét toàn bộ chức năng vào trong một dịch vụ Monolithic duy nhất! Chiến lược này có thể phát huy tác dụng tốt trong một số trường hợp đơn giản. Tuy nhiên, khi hệ thống lớn lên, nó sẽ dẫn đến "Bãi bùn khổng lồ" (Big Ball of Mud) — nơi có mức độ phức tạp cục bộ cao nhất có thể tưởng tượng được.
Ở chiều ngược lại, chúng ta cũng vừa thấy điều gì sẽ xảy ra khi ta chỉ chăm chăm tối ưu độ phức tạp cục bộ (làm cho từng service thật bé) mà bỏ bê độ phức tạp toàn cục: một "Bãi bùn phân tán" còn tồi tệ hơn gấp bội. Mối quan hệ tương quan này được trực quan hóa tuyệt đẹp trong Hình 14-5.
Microservice Dưới Dạng Mô-Đun Sâu (Microservices as Deep Modules)
Một mô-đun trong một hệ thống phần mềm được định nghĩa bởi hai yếu tố cơ bản: chức năng (function) và logic triển khai (logic):
- Chức năng (Function): Là những gì mô-đun cần phải làm — chính là năng lực nghiệp vụ phơi bày ra ngoài thông qua giao diện công khai.
- Logic triển khai (Logic): Là cách thức mô-đun thực hiện chức năng đó bên trong nội bộ — chính là logic nghiệp vụ và mã nguồn thực thi.
Trong cuốn sách xuất sắc A Philosophy of Software Design, Giáo sư John Ousterhout đã đào sâu khái niệm về tính mô-đun và đề xuất một phương pháp phỏng đoán trực quan đầy sức mạnh để đánh giá thiết kế của một mô-đun: Độ Sâu (Depth).
Ousterhout đề xuất hình dung một mô-đun như một hình chữ nhật, như minh họa trong Hình 14-6:
- Cạnh trên của hình chữ nhật: Đại diện cho chức năng, hay độ phức tạp của giao diện công khai (public interface). Hình chữ nhật càng rộng thì giao diện càng phức tạp; hình chữ nhật càng hẹp thì giao diện càng nhỏ gọn, tinh giản.
- Diện tích của hình chữ nhật: Đại diện cho logic, hay toàn bộ phần triển khai chức năng được đóng gói bên trong mô-đun.
Theo mô hình này:
- Mô-đun sâu (Deep Modules): Là những mô-đun hiệu quả cao — một giao diện công khai nhỏ gọn, giản dị nhưng đóng gói bên trong một khối lượng logic nghiệp vụ phong phú và phức tạp.
- Mô-đun nông (Shallow Modules): Là những mô-đun kém hiệu quả — giao diện công khai của chúng phức tạp gần bằng hoặc bằng chính logic bên trong nó.
Hãy xem xét một ví dụ kinh điển về một mô-đun nông đến mức cực đoan:
int AddTwoNumbers(int a, int b)
{
return a + b;
}
Phương thức này là trường hợp tột cùng của một mô-đun nông: giao diện công khai (chữ ký hàm gồm hai tham số a, b) và logic bên trong (phép cộng a + b) là y hệt nhau về độ phức tạp! Việc tách một phương thức như thế này thành một dịch vụ riêng biệt không hề đóng gói được bất kỳ độ phức tạp nào, mà ngược lại, nó tạo thêm những "bộ phận chuyển động" (moving parts) thừa thãi, làm bùng nổ độ phức tạp ngẫu nhiên (accidental complexity) cho toàn bộ hệ thống.
Về bản chất, một microservice hiệu quả chính là một mô-đun sâu ở cấp độ dịch vụ vật lý: nó sở hữu một giao diện công khai siêu nhỏ gọn (micro-front door) nhưng che giấu và đóng gói trọn vẹn cả một miền logic nghiệp vụ giá trị và phức tạp bên trong.
Mức Độ Hạt Nhỏ & Chi Phí Thay Đổi (Granularity and Cost of Change)
Ngưỡng phân rã một hệ thống thành các microservice được định đoạt bởi các use case của bài toán kinh doanh.
Khi chúng ta bắt đầu phân rã một hệ thống monolithic thành các dịch vụ, chi phí để đưa ra một thay đổi (cost of change) ban đầu sẽ giảm dần. Chi phí này đạt mức tối ưu (thấp nhất) khi hệ thống được phân rã đúng ngưỡng microservices chuẩn mực — nơi các dịch vụ đều là các mô-đun sâu.
Tuy nhiên, nếu bạn tiếp tục phân rã vượt quá ngưỡng microservices, các dịch vụ sâu sẽ dần biến thành các dịch vụ nông (shallow services). Giao diện của chúng sẽ buộc phải phình to trở lại để phục vụ nhu cầu đồng bộ dữ liệu. Và lúc này, do chi phí tích hợp mạng và giao dịch phân tán tăng vọt, chi phí đưa ra thay đổi sẽ bùng nổ trở lại, biến kiến trúc tổng thể thành bãi bùn phân tán khổng lồ! Mối quan hệ này được khắc họa rõ nét trong Hình 14-7.
Domain-Driven Design & Ranh Giới Của Microservices
Cũng tương tự như microservices, rất nhiều mẫu hình của Domain-Driven Design xoay quanh khái niệm ranh giới (boundaries):
- Bounded Context: Ranh giới của một mô hình (model boundary).
- Subdomain: Ranh giới của một năng lực nghiệp vụ (business capability boundary).
- Aggregate & Value Object: Ranh giới của một giao dịch nhất quán (transactional consistency boundary).
Vậy ranh giới nào trong số các ranh giới này tương ứng chuẩn xác với khái niệm Microservice? Hãy cùng xem xét từng ranh giới một cách có hệ thống.
Bounded Contexts: Ranh Giới Của Khối Monolith Hợp Lệ Lớn Nhất
Microservices và Bounded Context có rất nhiều điểm tương đồng — nhiều đến mức người ta thường đánh đồng chúng là một. Cả hai đều là các ranh giới vật lý, đều thuộc quyền sở hữu của một đội ngũ kỹ thuật duy nhất, và đều cấm tiệt việc tồn tại các mô hình nghiệp vụ mâu thuẫn bên trong nó.
Do đó, chúng ta có thể khẳng định chắc chắn: Mọi microservice đều là một Bounded Context.
Thế nhưng, chiều ngược lại có đúng không? Liệu mọi Bounded Context có phải là một microservice hay không?
Câu trả lời dứt khoát là: KHÔNG!
Như bạn đã học ở Chương 3, nhiệm vụ cốt lõi của một Bounded Context là bảo vệ tính nhất quán của Ngôn ngữ Chung và mô hình miền. Miễn là không có các khái niệm mâu thuẫn ý nghĩa, một Bounded Context hoàn toàn có thể chứa đựng nhiều miền con (subdomains) cùng một lúc.
Hãy lấy ví dụ về một hệ thống quản lý quảng cáo. Thực thể nghiệp vụ Lead (khách hàng tiềm năng) được biểu diễn bằng hai mô hình hoàn toàn khác nhau trong ngữ cảnh Promotions và Sales. Do đó, Promotions và Sales là hai Bounded Context riêng biệt (Hình 14-8).
Giả sử trong toàn bộ hệ thống không còn mô hình nào mâu thuẫn ngoài thực thể Lead. Điều này làm cho hai Bounded Context trên có phạm vi chức năng khá rộng — mỗi context chứa nhiều miền con. Miễn là không tạo ra sự xung đột mô hình, tất cả các phương án phân rã trong Hình 14-9 đều là các Bounded Context hoàn toàn hợp lệ!
Liệu chúng ta có thể gọi hai Bounded Context khổng lồ trong Phương án 1 (Decomposition 1) là các "microservice" được không? Chắc chắn là không! Chức năng của chúng quá rộng lớn.
Vì vậy, mối quan hệ giữa Bounded Context và Microservice không mang tính đối xứng:
Mọi microservice đều là Bounded Context, nhưng không phải mọi Bounded Context đều là microservice.
Bounded Context đại diện cho ranh giới của khối monolith hợp lệ lớn nhất (the largest valid monolith). Khối monolith này không phải là một "bãi bùn khổng lồ"; nó là một thiết kế hoàn toàn lành mạnh và hợp lệ, có khả năng bảo vệ tính nhất quán của Ngôn ngữ Chung.
Vùng Ranh Giới An Toàn Của Hệ Thống (Safe Component Boundaries)
Hình 14-10 trực quan hóa mối quan hệ phân tầng tuyệt đẹp giữa Bounded Contexts và Microservices:
- Ranh giới Bounded Context: Là ranh giới rộng nhất cho phép một thành phần tồn tại mà không bị suy thoái mô hình. Nếu thiết kế ranh giới rộng hơn Bounded Context (dung nạp các mô hình mâu thuẫn vào chung một nơi), bạn sẽ tạo ra một Bãi Bùn Khổng Lồ (Big Ball of Mud).
- Ranh giới Microservice: Là ranh giới hẹp nhất hợp lệ của một dịch vụ sâu. Nếu phân rã quá trớn vượt qua ngưỡng microservice, các dịch vụ sẽ biến thành dịch vụ nông và đẩy hệ thống vào Bãi Bùn Phân Tán (Distributed Big Ball of Mud).
- Vùng An Toàn (Safe Zone): Toàn bộ khoảng không gian nằm giữa Bounded Context và Microservice là vùng thiết kế an toàn. Mọi cách phân rã nằm trong dải này đều là các lựa chọn thiết kế hợp lệ và lành mạnh!
Aggregates: Ranh Giới Hẹp Nhất Nhưng Chưa Chắc Đã Là Microservice
Trong khi Bounded Context thiết lập giới hạn cho ranh giới rộng nhất có thể, thì mẫu hình Aggregate lại làm điều ngược lại: ranh giới của một Aggregate là ranh giới hẹp nhất có thể có trong thiết kế miền. Việc cố tình xẻ nhỏ một Aggregate thành nhiều dịch vụ vật lý khác nhau là một thảm họa kiến trúc chết người (sẽ được phân tích sâu ở Phụ lục A).
Nhiều người thường vội vã lấy ranh giới của từng Aggregate để làm ranh giới cho một Microservice. Đúng là một Aggregate tự thân nó đóng gói toàn bộ các quy tắc nghiệp vụ và tính bất biến bất khả phân. Tuy nhiên, như chúng ta đã nhấn mạnh: microservices không chỉ là về từng dịch vụ đơn lẻ, mà là về sự tương tác của nó với toàn hệ thống:
- Aggregate này có thường xuyên phải nói chuyện với các Aggregate khác trong cùng miền con không?
- Nó có chia sẻ các Value Object với các Aggregate khác không?
- Mỗi khi logic của Aggregate này thay đổi, khả năng cao nó có kéo theo sự thay đổi ở các thành phần khác trong miền con không?
Mối quan hệ giữa Aggregate đó với các thực thể khác trong cùng miền con càng gắn bó khăng khít bao nhiêu, thì việc biến nó thành một dịch vụ vật lý độc lập sẽ càng biến nó thành một dịch vụ nông (shallow service) bấy nhiêu!
Dù có những trường hợp đặc biệt mà việc đưa một Aggregate thành một microservice tạo ra một thiết kế tốt (ví dụ: yêu cầu mở rộng tải độc lập cực cao), nhưng trong đại đa số trường hợp thông thường, những dịch vụ quá hạt vụn như vậy sẽ chỉ làm bùng nổ độ phức tạp toàn cục của hệ thống.
Subdomains: Ranh Giới Vàng Tự Nhiên Của Microservices
Một kỹ thuật phỏng đoán cân bằng, an toàn và tối ưu nhất để thiết kế microservices chính là: căn chỉnh ranh giới của microservices theo ranh giới của các Miền Con Nghiệp Vụ (Business Subdomains).
Như bạn đã học ở Chương 1, các miền con tương ứng trực tiếp với các năng lực nghiệp vụ hạt mịn của doanh nghiệp — đây chính là những viên gạch xây dựng giúp công ty cạnh tranh trên thương trường.
- Góc nhìn nghiệp vụ: Miền con mô tả năng lực — doanh nghiệp làm được gì — mà không cần bận tâm đến việc năng lực đó được triển khai ra sao.
- Góc nhìn kỹ thuật: Miền con đại diện cho một tập hợp các use case gắn kết chặt chẽ (coherent use cases): chúng cùng dùng chung một mô hình nghiệp vụ, làm việc trên cùng một tập dữ liệu liên quan mật thiết, và có quan hệ chức năng khăng khít (Hình 14-11).
Độ mịn tự nhiên của miền con và việc tập trung vào "Cái gì" (chức năng) thay vì "Làm thế nào" (logic) biến miền con trở thành những mô-đun sâu một cách hoàn toàn tự nhiên. Chức năng của miền con đóng gói trọn vẹn toàn bộ độ phức tạp của việc triển khai bên trong. Bản chất gắn kết của các use case đảm bảo rằng chúng ta không bị xé lẻ các thành phần phụ thuộc nhau, từ đó giữ cho giao diện công khai luôn nhỏ gọn.
Tất cả những yếu tố đó làm cho Subdomain trở thành ranh giới an toàn và tối ưu nhất để thiết kế nên các microservices.
Nén Giao Diện Công Khai Của Microservices
Bên cạnh việc định vị ranh giới dịch vụ chuẩn xác, các công cụ của Domain-Driven Design còn giúp chúng ta nén (compress) giao diện công khai của microservices, làm cho các dịch vụ trở nên "sâu hơn nữa". Dưới đây là cách hai mẫu hình Open-Host Service và Anticorruption Layer phát huy tác dụng.
Open-Host Service & Published Language
Mẫu hình Open-Host Service tách rời mô hình miền nội bộ của Bounded Context khỏi mô hình dùng để tích hợp với các thành phần khác, thông qua một Ngôn ngữ Công bố (Published Language), như minh họa trong Hình 14-12.
Việc đưa vào Published Language giúp giảm đáng kể độ phức tạp toàn cục:
- Tiến hóa tự do: Bạn có thể thoải mái tái cấu trúc mô hình triển khai nội bộ của dịch vụ mà không làm ảnh hưởng đến các bên tiêu thụ hạ nguồn, bởi vì mô hình mới chỉ cần được biên dịch ngược về Published Language hiện hành.
- Giao diện công khai tối giản: Published Language được thiết kế chuyên biệt cho nhu cầu tích hợp; nó chỉ phơi bày lượng dữ liệu tối thiểu và cấu trúc tiện lợi nhất cho bên ngoài, che giấu hoàn toàn các bảng biểu hay trường dữ liệu nội bộ không liên quan.
Việc sở hữu một giao diện công khai đơn giản hơn đặt trên cùng một khối lượng logic triển khai phong phú làm cho dịch vụ trở nên "sâu hơn" rõ rệt!
Anticorruption Layer Độc Lập (Stand-Alone ACL Service)
Mẫu hình Anticorruption Layer (ACL) hoạt động theo chiều ngược lại: nó giảm thiểu độ phức tạp khi một dịch vụ phải tích hợp với các Bounded Context khác (đặc biệt là hệ thống legacy hoặc dịch vụ bên thứ ba).
Thay vì cài đặt ACL ngay trong mã nguồn của Bounded Context tiêu thụ, chúng ta có thể triển khai ACL như một dịch vụ đứng độc lập (stand-alone service), như thể hiện trong Hình 14-13.
Dịch vụ ACL độc lập này giảm tải cả độ phức tạp cục bộ của ngữ cảnh tiêu thụ lẫn độ phức tạp toàn cục của hệ thống: độ phức tạp tích hợp được trút bỏ hoàn toàn sang dịch vụ ACL, giúp Bounded Context tiêu thụ giữ được một giao diện công khai tinh khiết, gọn gàng và không bị vấy bẩn bởi sự luộm thuộm của dịch vụ bên ngoài.
Tổng Kết Chương (Conclusion)
Về mặt lịch sử, phong cách kiến trúc Microservices gắn bó mật thiết với Domain-Driven Design — mật thiết đến mức hai thuật ngữ "microservice" và "bounded context" thường bị dùng lẫn lộn. Trong chương này, chúng ta đã làm sáng tỏ bản chất của cả hai:
- Microservice và Bounded Context không phải là một: Mọi microservice đều là một Bounded Context, nhưng không phải mọi Bounded Context đều là microservice. Bounded Context đại diện cho ranh giới rộng nhất của một khối monolith hợp lệ, trong khi Microservice đại diện cho ranh giới hẹp nhất của một dịch vụ sâu.
- Tránh xa hai thái cực bãi bùn: Thiết kế ranh giới rộng hơn Bounded Context sẽ tạo ra một Bãi Bùn Khổng Lồ (Big Ball of Mud). Ngược lại, phân rã hạt vụn quá mức vượt qua ngưỡng microservice sẽ biến các dịch vụ sâu thành dịch vụ nông, dẫn đến một Bãi Bùn Phân Tán (Distributed Big Ball of Mud).
- Bản chất của từ "Micro": Không nằm ở số dòng mã hay kích thước đội ngũ, mà nằm ở giao diện công khai siêu nhỏ (micro-public interface) — đóng gói một khối lượng logic nghiệp vụ phong phú đằng sau một "cánh cửa trước" tinh gọn.
- Subdomain là ranh giới vàng: Căn chỉnh microservices theo ranh giới của các Subdomain là kỹ thuật phỏng đoán an toàn, mạnh mẽ và tạo ra các mô-đun sâu tự nhiên nhất cho đa số các trường hợp.
Trong Chương 15: Event-Driven Architecture, chúng ta sẽ tiếp tục phân tích kiến trúc hệ thống cấp cao nhưng từ một góc nhìn mới: tích hợp bất đồng bộ thông qua các sự kiện. Bạn sẽ học cách khai thác các loại thông điệp sự kiện khác nhau để tối ưu hóa hơn nữa ranh giới của các microservices!
Bài Tập Ôn Tập & Củng Cố Tri Thức (Exercises)
Dưới đây là 4 câu hỏi trắc nghiệm chuyên sâu giúp bạn đánh giá mức độ thấu suốt kiến trúc Microservices và DDD, đi kèm đáp án và lời giải thích chính thức từ tác giả Vlad Khononov (Appendix B).
Giải thích chi tiết từ Tác giả (Appendix B): Một microservice thỏa mãn đầy đủ mọi tiêu chí của một Bounded Context: nó có ranh giới vật lý, thuộc quyền quản lý của một đội ngũ duy nhất và không dung nạp các mô hình mâu thuẫn. Tuy nhiên, một Bounded Context có thể có phạm vi rất rộng (chứa nhiều subdomain) miễn là mô hình của nó nhất quán, do đó Bounded Context là ranh giới của khối monolith hợp lệ lớn nhất chứ không nhất thiết phải là microservice.
Giải thích chi tiết từ Tác giả (Appendix B): Thuật ngữ "micro" trong microservices hoàn toàn không ám chỉ kích thước dòng mã hay quy mô đội nhóm. Nó phản ánh rằng dịch vụ phải có một giao diện công khai siêu nhỏ (micro-public interface): thu nhỏ lượng tri thức và độ phức tạp phơi bày ra ngoài, đóng gói tối đa logic vào bên trong để biến nó thành một "mô-đun sâu" (deep module).
Giải thích chi tiết từ Tác giả (Appendix B): Nếu mở rộng ranh giới vượt quá Bounded Context, bạn sẽ trộn lẫn các mô hình mâu thuẫn và tạo ra một Big Ball of Mud. Nếu phân rã quá sâu vượt qua ngưỡng Microservices, các dịch vụ sẽ trở thành các mô-đun nông (shallow modules) và tạo ra một Distributed Big Ball of Mud. Do đó, chỉ có vùng nằm giữa hai ranh giới này mới là vùng thiết kế an toàn.
Giải thích chi tiết từ Tác giả (Appendix B): Mặc dù việc biến từng Aggregate thành một microservice thường có nguy cơ tạo ra các dịch vụ nông (shallow services) do mối quan hệ khăng khít với các thực thể khác trong cùng Subdomain, vẫn có những bài toán cụ thể đòi hỏi điều này (chẳng hạn như một Aggregate có yêu cầu co giãn tải cực kỳ dị biệt hoặc yêu cầu bảo mật cô lập cấp độ vật lý). Do đó, thiết kế phụ thuộc vào ngữ cảnh miền nghiệp vụ và các yêu cầu phi chức năng cụ thể.