Software Architecture: Tách Monolith Thành Kiến Trúc Phân Tán
Posted Date:
“Làm sao ăn hết con voi? Câu trả lời cổ điển: ăn từng miếng. Nhưng trong kiến trúc phần mềm, nếu ăn không khéo, con voi đó sẽ đè bạn chết trước khi bạn xong bữa.”
🧠 Khi Sysops Squad được “bật đèn xanh”
Sau khi Addison và Austen được cấp trên gật đầu cho phép chuyển sang kiến trúc phân tán, họ mới nhận ra thử thách thật sự bắt đầu:
“Cái app này to như con voi, biết bắt đầu từ đâu bây giờ?” – Addison than.
Austen cười:
“Thì ăn từng miếng chứ sao.”
Nhưng Addison không cười nổi:
“Còn dữ liệu thì sao? Cắt mỗi phần code không giải quyết được gì hết. Phải tách database ra nữa, mà vậy thì toang.”
Cuối cùng, họ tìm đến Logan – kiến trúc sư trưởng – để hỏi. Logan nghe xong chỉ lắc đầu:
“Cách các cậu tính làm đó gọi là Elephant Migration Anti-Pattern. Cứ ‘ăn từng miếng’ là dễ biến nguyên cái monolith thành một đống microservices dính nhau – hay nói thẳng là distributed monolith.”
Câu nói này làm hai người tỉnh hẳn. Addison hỏi:
“Vậy nên bắt đầu kiểu nào?”
Logan chỉ mỉm cười:
“Có hai hướng chính: tactical forking và component-based decomposition. Chọn cách nào, còn tùy codebase của các cậu là ‘rác’ tới đâu.”
Kiến trúc tách rời – không chỉ là “vì sao”, mà là “làm sao”
Trong chương trước, ta nói về modularity – tại sao nên chia nhỏ hệ thống. Giờ là lúc bàn về decomposition – làm sao để chia mà không vỡ.
Tách một hệ thống lớn, cũ, dính đầy lịch sử code và ràng buộc dữ liệu, là một công việc vừa kỹ thuật vừa tâm lý. Nó cần thời gian, phương pháp, và quan trọng nhất: phải biết hệ thống có tách nổi không.
🔍 Trước khi chạm dao kéo: codebase của bạn có “cắt được” không?
Không phải hệ thống nào cũng tách ra được. Một số codebase đã thành “Big Ball of Mud” – cục bùn khổng lồ không còn ranh giới, nơi mọi class gọi mọi thứ.
Hệ thống nào event handler nói chuyện thẳng với database, thiếu layer, thiếu pattern – đó là “bùn”.
Nếu bạn đang trong hoàn cảnh đó, hãy tạm quên microservices đi. Việc đầu tiên là đo độ bền và độ rối của hệ thống.
Câu hỏi quan trọng mà kiến trúc sư phải trả lời là liệu cơ sở mã này có thể cứu vãn được không? Nói cách khác, liệu nó có phải là ứng cử viên cho các mẫu phân rã, hay một cách tiếp cận khác phù hợp hơn?
Có ba chỉ số có thể giúp:
Coupling (Afferent / Efferent) – Ai phụ thuộc vào ai?
Abstractness và Instability – Code quá trừu tượng hay quá dính?
Distance from Main Sequence – Tổng quan độ cân bằng giữa hai yếu tố trên.
⚙️ Afferent và Efferent Coupling – đọc mối quan hệ trong code
Từ năm 1979, Edward Yourdon và Larry Constantine đã định nghĩa hai khái niệm này:
Afferent coupling (Ca) – số lượng thành phần phụ thuộc vào module hiện tại (inbound).
Efferent coupling (Ce) – số lượng thành phần mà module hiện tại phụ thuộc vào (outbound).
Lưu ý giá trị của chỉ hai thước đo này khi thay đổi cấu trúc của một hệ thống. Ví dụ: khi phân tích một monolith thành một kiến trúc phân tán, kiến trúc sư sẽ tìm thấy các lớp được chia sẻ như Địa chỉ. Khi xây dựng một khối đơn khối, các nhà phát triển thường sử dụng lại các khái niệm cốt lõi như Address, nhưng khi tách khối monolith ra, kiến trúc sư phải xác định có bao nhiêu phần khác của hệ thống sử dụng tài sản chung này.
Hầu như mọi nền tảng đều có các công cụ cho phép kiến trúc sư phân tích các đặc điểm liên kết của mã để hỗ trợ việc tái cấu trúc, di chuyển hoặc hiểu cơ sở mã. Có nhiều công cụ dành cho các nền tảng khác nhau cung cấp chế độ xem ma trận về các mối quan hệ lớp và/hoặc thành phần.
Ví dụ:
Class Address được cả đống module dùng chung trong monolith. Khi tách ra, bạn phải tính: có bao nhiêu chỗ đang gọi Address? Nếu nhiều quá, cắt nó ra sẽ như kéo cả mớ dây điện.
Hầu hết IDE hoặc plugin hiện đại (như JDepend trong Eclipse) đều có công cụ vẽ ma trận phụ thuộc để bạn nhìn rõ luồng coupling – kiểu bản đồ codebase. Cái này nên làm đầu tiên, để biết mình đang đứng ở đâu.
📈 Abstractness và Instability – hai mặt của đồng xu
Robert C. Martin (Uncle Bob) đưa ra hai chỉ số giúp đánh giá “tính cân bằng” trong codebase:
1. Abstractness (A)
Đo tỷ lệ giữa các thành phần trừu tượng (interface, abstract class) và cụ thể (implementation).
A = Số phần tử abstract / (abstract + concrete)
Nếu A gần 0 → code cứng nhắc, khó mở rộng.
Nếu A gần 1 → quá trừu tượng, người mới đọc không hiểu nổi.
2. Instability (I)
Đo độ “mong manh” của module khi có thay đổi:
I = Ce / (Ce + Ca)
I càng cao → càng dễ gãy khi ai đó đổi code ở module khác.
Một module có I = 1 nghĩa là nó phụ thuộc lung tung mà không ai phụ thuộc lại → cực kỳ không ổn định.
🎯 Distance from Main Sequence – nhìn tổng thể qua đồ thị
Khi vẽ Abstractness (A) trên trục hoành và Instability (I) trên trục tung, một đường chéo lý tưởng sẽ chạy từ (0,1) đến (1,0). Mỗi component của bạn nên nằm gần đường này – nghĩa là cân bằng giữa trừu tượng và ổn định.
Công thức đo:
D = |A + I - 1|
Gần 0 → cân bằng tốt.
Xa 0 → rơi vào hai vùng chết:
Zone of Pain: quá cứng, quá dính, khó thay đổi.
Zone of Uselessness: quá trừu tượng, khó hiểu, không thực tế.
Khi nhiều module rơi vào hai vùng này, hãy dừng nghĩ đến decomposition – vì codebase đó chưa đủ “chín” để cắt.
🪚 Chọn cách cắt: Component-based hay Tactical Forking?
Định nghĩa một thành phần là một khối xây dựng của ứng dụng, có vai trò và trách nhiệm được xác định rõ ràng trong hệ thống và một tập hợp các thao tác được xác định rõ ràng. Các thành phần trong hầu hết các ứng dụng được thể hiện thông qua không gian tên (namespace) hoặc cấu trúc thư mục và được triển khai thông qua các tệp thành phần
Khi đã biết hệ thống có thể tách, giờ là lúc chọn đường đi.
Component-based decomposition
Phù hợp khi codebase có cấu trúc, có component rõ ràng. Mục tiêu: tách từng khối logic hoàn chỉnh ra thành service độc lập.
Tactical forking
Phù hợp khi code là một đống hỗn độn – không rõ module, không phân tầng. Mục tiêu: sao chép nguyên app, rồi xóa bớt cái không cần, thay vì trích xuất.
⚙️ Component-Based Decomposition – tách có phương pháp
Đây là cách tách chuẩn chỉnh hơn. Ta xác định các component logic (qua namespace, thư mục, hoặc domain), sau đó gom nhóm và refactor dần.
Lưu ý: “Tách theo component” chứ không “tách theo class”. Class chỉ là vật liệu, component mới là viên gạch.
Ví dụ: thư mục penultimate/ss/ticket/assign là một component Ticket Assign có namespace rõ ràng. Từ đây, mỗi component có thể trở thành một domain service trong kiến trúc service-based – bước đệm trước khi đi tới microservices.
Lợi ích của hướng này:
Có thể triển khai dần dần, không cần chia database ngay.
Dễ kiểm soát phạm vi thay đổi.
Không cần thay đổi cấu trúc tổ chức hay pipeline ngay lập tức.
Nếu mục tiêu cuối cùng là microservices, service-based architecture là trạm dừng trung gian hợp lý: chia nhỏ vừa đủ, nhưng vẫn kiểm soát được.
🧱 Tactical Forking (Fausto De La Torre) – cắt nhanh là “lãnh” hậu quả
Khi codebase đã thành bùn, việc “refactor đẹp” là bất khả thi. Lúc này, Tactical Forking là lựa chọn thực tế.
Ý tưởng đơn giản:
Clone nguyên monolith cho từng nhóm.
Mỗi nhóm chỉ giữ phần mình cần, xóa sạch phần còn lại.
Tiếp tục refactor nhẹ để giữ cho app đó chạy được độc lập.
Ưu điểm:
Làm nhanh, không cần phân tích sâu.
Dễ làm hơn “tách từng miếng”, vì chỉ cần đảm bảo còn lại phần chạy được.
Nhược điểm:
Tạo ra nhiều mã thừa, code trùng.
Rủi ro khi sửa lỗi – phải sửa ở nhiều bản sao.
Không giải quyết triệt để vấn đề coupling.
Tactical forking giống như đục tảng đá: bạn không chạm khắc tinh tế, chỉ cắt bỏ phần thừa cho tới khi thấy hình dáng mong muốn.
🔄 So sánh nhanh
Tiêu chí
Component-based
Tactical Forking
Tốc độ triển khai
Chậm, có kế hoạch
Nhanh, ít phân tích
Độ sạch code sau tách
Cao hơn, dễ bảo trì
Vừa vừa, còn code thừa
Rủi ro bug lặp
Thấp
Cao
Phù hợp với
Codebase có cấu trúc
Big ball of mud
Tác động đội ngũ
Làm chung, hợp tác
Chia nhóm, dễ tách rời
💬 Câu chuyện của Sysops Squad: chọn hướng đi
Khi đo lại các chỉ số, Addison nhận ra codebase của Sysops Squad còn khá ổn – nhiều phần nằm trên “main sequence”. Austen muốn thử tactical forking, ví như “điêu khắc từ khối đá nguyên khối”. Addison phản bác:
“Cậu định vừa code vừa làm Michelangelo hả? Tụi mình mà forking kiểu đó là lỗi copy-paste khắp nơi luôn.”
Cuối cùng, cả hai đồng ý chọn component-based decomposition, vì:
Họ có ranh giới component rõ ràng.
Muốn giảm lỗi trùng và dễ bảo trì hơn.
Có thể triển khai dần mà không dừng hệ thống.
Họ ghi lại quyết định trong ADR #3 – Migration Using the Component-Based Decomposition Approach:
“Tách theo component tuy lâu hơn, nhưng kiểm soát được. Fork nhanh mà không hiểu, chỉ biến rác thành rác nhỏ hơn.”
Kết
Chia nhỏ hệ thống không khó, chỉ khó ở chỗ biết khi nào dừng. Không phải cứ microservices là hiện đại, cũng không phải cứ giữ monolith là lỗi thời.
Quan trọng là:
Biết hệ thống của mình “cắt được tới đâu”.
Có số liệu, có lý do rõ ràng.
Và quan trọng hơn hết, đừng để dự án tách monolith biến thành cơn ác mộng phân tán.
“Tách hệ thống không phải để khoe mình hiện đại – mà để nó sống lâu thêm vài mùa update.”
Để lại một bình luận
Bạn phải đăng nhập để gửi bình luận.