Nguyen Le Phong

Nền tảng Kiến trúc Phần mềmPhần 5/6

Monolith, Modular Monolith, Microservices: Hướng dẫn Quyết định Thành thật

Microservices là cái giá bạn trả cho quy mô tổ chức — không phải điểm khởi đầu. Hành trình không màu mè từ monolith đến modular monolith đến microservices, với các tín hiệu cho bạn biết khi nào (và liệu có nên) tách ra.

Đây là một cảnh diễn ra thường xuyên hơn mức cần thiết: một startup ba người quyết định, ngay từ ngày đầu, xây dựng microservices. Sáu tháng sau họ có mười lăm service, một Kafka cluster mà không ai hiểu hoàn toàn, và một bug đòi hỏi truy vết một request qua tám service để tái hiện. Team kiệt sức. Sản phẩm làm xong được một nửa. Và ở đâu đó trong một Slack thread có người gõ chúng ta về cơ bản đã xây một distributed monolith.

Microservices thực sự mạnh mẽ — ở quy mô phù hợp, với team phù hợp, vì lý do phù hợp. Nhưng chúng là cái giá bạn trả cho quy mô tổ chức, không phải điểm khởi đầu. Hướng dẫn này đi qua thành thật cả ba hình dạng kiến trúc — monolith, modular monolith, và microservices — và cho bạn các tín hiệu để biết khi nào (và liệu có nên) di chuyển giữa chúng.

Monolith: một deployable cai quản tất cả

Monolith là một đơn vị có thể deploy duy nhất. Một build, một binary (hoặc một tập asset đã compile), một bước deployment. Mọi tính năng, mọi module, mọi database query đều sống trong cùng một process.

Nghe có vẻ hạn chế, và bản thân từ này đã có chút âm thanh đáng xấu hổ — như thể thừa nhận bạn có một monolith là thừa nhận bạn chưa bắt kịp kỹ thuật hiện đại. Cách diễn đạt đó sai, và nó đã gây ra rất nhiều nỗi đau không cần thiết.

Monolith là mặc định đúng đắn. Lý do:

  • Lời gọi cục bộ miễn phí. Một lời gọi hàm trong một process chỉ vài nano-giây. Một lời gọi mạng giữa các service là mili-giây — và nó có thể lỗi, timeout, trả về kết quả một phần, hay mất hẳn. Bạn không chịu độ phức tạp đó cho đến khi cần.
  • Transaction dễ dàng. Một relational database cho bạn ACID transaction trên toàn bộ data model. Trong monolith, chuyển tiền từ tài khoản A sang B là một transaction. Trong microservices, nó trở thành distributed transaction hay saga — những vấn đề thực sự khó với các failure mode thực.
  • Debug đơn giản. Một process, một log stream, một stack trace. Bạn tìm ra vấn đề. Trong hệ phân tán, một hành động người dùng có thể sinh ra hàng chục operation bất đồng bộ trên nhiều service, và tương quan chúng đòi hỏi công cụ observability được xây đặc biệt.
  • Deployment đơn giản. Một artefact, một pipeline, một chiến lược rollback. Bạn có thể ship tự tin từ ngày đầu.
  • Các công ty lớn ship monolith lớn. Shopify chạy Rails monolith nhiều năm ở quy mô khổng lồ. Stack Overflow phục vụ hàng triệu request mỗi ngày từ một vài server chạy monolith. Monolith không ngăn bạn scale — nó chỉ scale khác đi.

Những nỗi đau thực sự của monolith là có thực, nhưng chúng đến muộn hơn người ta nghĩ. Thời gian build tăng khi codebase lớn — rebuild đầy đủ mất hai phút ở 50k dòng mất mười hai phút ở 500k. Ma sát merge tăng khi hai mươi kỹ sư đều sửa cùng codebase; branch sống lâu và conflict trở thành nghi lễ hàng ngày. Deployment coupling có nghĩa mọi team deploy cùng nhau — một thay đổi rủi ro của team này có thể ghìm lại release của mọi người khác.

Những nỗi đau này là có thực. Nhưng chúng là nỗi đau của thành công, và chúng đến muộn hơn nhiều so với hype gợi ý. Hầu hết team nên may mắn mà gặp được chúng.

Modular monolith: điểm ngọt mà hầu hết team bỏ qua

Modular monolith vẫn là một đơn vị deploy — nhưng bên trong được tổ chức thành các module với ranh giới được thực thi. Các module giao tiếp qua interface rõ ràng. Một module không được phép trực tiếp với tay vào nội bộ của module khác: không import class private từ anh em, không chia sẻ table database, không gọi hàm helper nội bộ qua ranh giới module.

Nghe như một kỷ luật nhỏ, nhưng nó thay đổi cuộc chơi đáng kể.

Hãy nghĩ nó như bình đồ của một ngôi nhà. Monolith không có module là căn hộ studio: một phòng lớn mở nơi mọi thứ ở khắp nơi. Modular monolith cho bạn các phòng có tường — bạn vẫn sống trong một ngôi nhà, vẫn dùng chung ống nước và cửa trước, nhưng bếp và phòng ngủ có mục đích riêng biệt rõ ràng và bạn không nấu ăn trên giường.

Các module ánh xạ tự nhiên sang bounded context từ Domain-Driven Design: module Billing, module Inventory, module Notifications. Mỗi cái sở hữu logic domain riêng và phần schema database riêng. Chúng expose public interface (service facade hay tập hàm public) và giữ mọi thứ khác private.

Lợi ích thực tế là đáng kể:

  • Lý luận độc lập. Developer làm việc trên Billing có thể hiểu và sửa nó mà không cần giữ toàn bộ codebase trong đầu.
  • Refactor an toàn. Bạn có thể viết lại nội bộ của module miễn là public interface được giữ nguyên — phần còn lại của app không hay biết.
  • Quyền sở hữu rõ ràng. Các team nhận module. Code review được tập trung. Onboard developer mới có nghĩa đưa cho họ một module, không phải codebase 200k dòng.
  • Seam sẵn sàng di trú. Nếu bạn cần trích xuất một service, ranh giới đã được vẽ sẵn. Module trở thành service. Interface trở thành API. Công việc đáng kể, nhưng là công việc đã biết, không phải phẫu thuật thám hiểm.

Modular monolith là kiến trúc mà hầu hết team bỏ qua thẳng, vội vàng từ monolith rối rắm sang microservices mà không dừng lại đây. Đó là sai lầm. Với đại đa số product team — dưới 100 kỹ sư, service chỉ vài con số, một hoặc hai deployment pipeline — một modular monolith được duy trì tốt là nơi sống hiệu quả nhất.

Microservices: thứ bạn thực sự mua

Microservices phân rã hệ thống thành các service có thể deploy độc lập. Mỗi service sở hữu process riêng, deployment pipeline riêng, và — quan trọng là — data store riêng. Các service nói chuyện với nhau qua mạng: HTTP, gRPC, message queue, event stream.

Khi microservices hoạt động tốt, chúng mang lại bốn thứ mà không gì khác có thể sánh kịp ở quy mô:

  • Deployment độc lập. Team payments có thể ship thay đổi cho payments service vào chiều thứ Ba mà không cần phối hợp với team recommendations, team tìm kiếm, hay team thông báo. Ở công ty có hàng trăm kỹ sư, điều này thay đổi tất cả — là sự khác biệt giữa deploy mỗi tuần và deploy một trăm lần mỗi ngày.
  • Scale độc lập. Nếu service ImageProcessing cần bốn mươi CPU core trong giờ cao điểm và service UserProfile cần hai, bạn có thể scale chúng riêng. Trong monolith, bạn scale toàn bộ — bạn mua bốn mươi core cho mọi tính năng dù nó có cần hay không.
  • Tự chủ của team. Mỗi service là một sản phẩm nhỏ tự chứa. Một team năm kỹ sư có thể sở hữu một service hoàn toàn: chọn ngôn ngữ, database, cadence deployment. Đây là siêu năng lực tổ chức của microservices — Định luật Conway làm việc cho bạn thay vì chống lại.
  • Cô lập lỗi. Memory leak trong RecommendationEngine làm sập RecommendationEngine, không phải checkout. Bạn có thể degraded gracefully — cửa hàng vẫn hoạt động, chỉ là sản phẩm không có gợi ý cá nhân. Trong monolith, exception chưa được bắt trong bất kỳ module nào có thể làm sập toàn bộ process.

Chú ý pattern: mọi thứ trong danh sách đó đều về quy mô tổ chứcvận hành. Đây không phải là những mối quan tâm áp dụng cho một team tám người đang xây một sản phẩm chưa tìm được product-market fit.

Bạn thực sự trả gì: thuế hệ phân tán

Microservices không miễn phí. Mọi lợi ích ở trên đều có chi phí tương ứng, và các chi phí không hề nhỏ. Đánh giá thấp chúng là cách các team kết thúc trong tình trạng kiệt sức và chậm tiến độ.

Cảnh báo distributed monolith

Kết quả tệ nhất không phải microservices hay monolith — mà là distributed monolith: các microservices ràng buộc chặt chẽ đến mức phải deploy cùng nhau, chia sẻ database, hay cùng lỗi. Bạn trả mọi chi phí của microservices mà không nhận được lợi ích nào. Điều này xảy ra khi team tách theo lớp kỹ thuật thay vì domain nghiệp vụ, hay khi service gọi nhau đồng bộ trong chuỗi dài. Nếu ServiceA chỉ có thể deploy sau ServiceB, bạn đã xây một distributed monolith.

Đây là thuế đầy đủ:

  • Lời gọi mạng thất bại. Lời gọi hàm không thể lỗi (trừ bug). Lời gọi mạng có thể lỗi, timeout, trả về giá trị cached cũ, hay thành công khi retry sau khi lần đầu đã có side effect. Bạn phải xử lý partial failure ở mọi nơi, liên tục.
  • Eventual consistency. Các service sở hữu data riêng. Giữ data nhất quán qua ranh giới service đòi hỏi thiết kế cẩn thận — pattern event-driven, saga, idempotent operation, compensating transaction. Giải thích cho product manager tại sao người dùng có thể đặt đơn hàng nhưng không ngay lập tức thấy trong lịch sử là điều khó xử.
  • Distributed transaction rất khó. Giao thức two-phase commit tồn tại và rất đau khổ. Hầu hết team dùng Saga pattern thay thế, chuyển độ phức tạp từ database sang application code. Cả hai đều phức tạp hơn đáng kể so với BEGIN; UPDATE; COMMIT;.
  • Debug là kỹ năng khác. Stack trace không còn cho bạn biết điều gì sai. Bạn cần distributed tracing (Jaeger, Zipkin, Honeycomb), correlation ID trong mọi dòng log, service mesh, và dashboard tương quan sự kiện qua các service. Xây và duy trì cơ sở hạ tầng observability này là công việc kỹ thuật thực.
  • Overhead ops nhân lên. Một service cần Dockerfile, Kubernetes deployment, ingress rule, health check endpoint, giới hạn CPU và memory, cấu hình log aggregation, và alert rule. Nhân với ba mươi service. Overhead này tăng tuyến tính theo số service; headcount kỹ thuật không tăng theo.
  • Test khó hơn. Unit test vẫn dễ. Nhưng integration test span qua ranh giới service đòi hỏi chạy nhiều service đồng thời, quản lý phiên bản và cấu hình, và xử lý test data qua nhiều database. Contract testing (Pact, v.v.) giúp nhưng thêm kỷ luật riêng cần học.

Không chi phí nào trong số này là không thể giải quyết — ngành đã có công cụ trưởng thành cho tất cả. Điểm mấu chốt là chúng là chi phí thực phải trả mỗi ngày, và không đáng trả cho đến khi lợi ích chúng mở khóa thực sự cần thiết.

Tín hiệu: khi nào nên tách (và khi nào không)

Vậy làm sao bạn biết khi nào đã vượt ngưỡng mà microservices đáng công? Đây là những tín hiệu thực sự chỉ ra đã đến lúc:

  • Nhu cầu scale độc lập là thực và tốn kém. Bạn có thể chỉ ra một component cụ thể cần tài nguyên gấp 10 lần phần còn lại, và bạn đang trả tiền cho tài nguyên đó trên toàn bộ monolith. Tách ra sẽ tiết kiệm tiền thực hoặc cải thiện hiệu năng thực.
  • Tranh giành deploy là mãn tính. Các team thường xuyên bị chặn khỏi việc ship bởi thay đổi đang thực hiện của team khác. Chi phí phối hợp deployment chung đo được đang làm chậm bạn — không chỉ thỉnh thoảng, mà là sự khó chịu hàng tuần hay hàng ngày.
  • Số lượng team đòi hỏi vậy. Bạn có nhiều team năm hay hơn kỹ sư làm trên domain nghiệp vụ khác biệt với cadence deployment khác nhau, ưu tiên công nghệ khác nhau, và roadmap thực sự độc lập. Định luật Conway dự đoán kiến trúc bạn sẽ có — bạn có thể lên kế hoạch cho nó.
  • Một component có yêu cầu reliability hay security khác biệt cơ bản. Payments, authentication, và lưu trữ PII thường cần cô lập không phải vì lý do hiệu năng mà vì tuân thủ, giảm blast radius, và khả năng audit độc lập.
  • Một module đã thực sự tách biệt. Nó không có shared state với phần còn lại của hệ thống, chỉ giao tiếp qua event hay API được định nghĩa rõ, và một team khác sở hữu nó. Seam tổ chức đã tồn tại — biến nó thành ranh giới service là hình thức hóa điều đã thực sự đúng.

Và đây là các tín hiệu bạn đang tách quá sớm:

  • Toàn bộ tổ chức kỹ thuật của bạn dưới hai mươi người.
  • Bạn tách vì nó có vẻ scalable hơn chứ không phải vì bạn đã gặp giới hạn cụ thể.
  • Các service bạn đang lên kế hoạch sẽ cần deploy cùng nhau để hoạt động.
  • Bạn chưa thể vẽ ranh giới sạch nơi mỗi service sở hữu data riêng mà không có table chung.
  • Team bạn chưa có cơ sở hạ tầng observability (distributed tracing, centralized logging, alerting) để debug hệ phân tán.
  • Lý do chính là microservices có trong job description hay marketing copy của công ty.

Cách tách tốt: seam, strangler fig, và quyền sở hữu data

Nếu các tín hiệu ở trên chỉ ra nên tách, cách quan trọng cực kỳ. Tách tệ tạo ra distributed monolith được mô tả ở trên. Tách tốt tạo ra các service thực sự độc lập tự trả công.

Tách theo bounded context, không theo lớp kỹ thuật. Đừng tạo DatabaseService hay ValidationService — đó là mối quan tâm kỹ thuật, không phải nghiệp vụ. Tạo OrderService, BillingService, InventoryService — đơn vị năng lực nghiệp vụ với ý nghĩa rõ ràng, ổn định trong domain của bạn. Bounded context là phần domain nơi một model cụ thể áp dụng và ngôn ngữ nhất quán. Đó là nơi ranh giới service thuộc về.

Dùng strangler-fig pattern. Được đặt tên theo loại cây leo dần dần bao vây cây chủ, strangler-fig pattern cho phép bạn trích xuất service dần dần thay vì tất cả cùng lúc. Bạn dựng service mới song song với monolith, định tuyến một phần lưu lượng cụ thể sang nó, xác minh hoạt động, rồi xóa code tương ứng khỏi monolith. Monolith thu nhỏ theo thời gian; nó không bao giờ bị viết lại. Đây là an toàn hơn, có thể đảo ngược hơn, và ít có khả năng kết thúc trong migration big-bang sáu tháng không bao giờ kết thúc.

Trích xuất một service một lần. Mỗi lần trích xuất dạy bạn điều gì đó. Lần thứ hai sẽ tốt hơn lần đầu. Cố gắng trích xuất năm service đồng thời phân phối việc học và nhân rủi ro.

Quyền sở hữu data là ràng buộc không thể thương lượng. Service chia sẻ table database với service khác không phải là service — nó là module với overhead mạng thêm. Mỗi service phải sở hữu data riêng. Nếu hai service cần cùng data, một cái là thẩm quyền và cái kia lấy qua API hay đồng bộ qua event. Ràng buộc này khó thiết lập và khó duy trì, nhưng đây là thứ cho bạn khả năng deploy độc lập và cô lập lỗi mà bạn đến đây để tìm.

Dùng port để định nghĩa seam trước khi trích xuất. Nếu bạn đã theo cách tiếp cận Ports & Adapters trong modular monolith, việc trích xuất gần như cơ học: port định nghĩa API, adapter đằng sau nó trở thành service client. Logic domain không thay đổi — chỉ có cơ chế delivery. Đây là một trong những lập luận thực tế mạnh mẽ nhất để xây với port ngay từ đầu: chúng cho bạn seam sẵn sàng trích xuất miễn phí.

So sánh đối chiếu

Chiều đánh giá Monolith Modular Monolith Microservices
Đơn vị deploy Một process, một pipeline Một process, một pipeline Nhiều process, nhiều pipeline độc lập
Quyền sở hữu data Database chung; mọi code có thể chạm mọi table Database chung; module sở hữu vùng schema theo quy ước Mỗi service sở hữu database riêng; không có table chung
Failure mode Một process crash làm sập toàn bộ app Một process crash làm sập toàn bộ app Lỗi service bị cô lập; các service khác degraded gracefully
Phù hợp với team Tốt nhất cho 1–3 team; ma sát tăng theo số team Tốt nhất cho 2–8 team; module căn chỉnh với quyền sở hữu team Cần thiết cho 5+ team ship theo cadence độc lập
Chi phí vận hành Thấp: một deployment, một log stream, một bộ alert Thấp: footprint ops giống monolith thường Cao: observability, container orchestration, service mesh, CI/CD mỗi service
Mô hình transaction ACID transaction đầy đủ dễ dàng ACID transaction đầy đủ dễ dàng Saga hay eventual consistency; distributed transaction rất khó
Khi nào dùng Sản phẩm mới, team nhỏ, domain chưa biết Sản phẩm đang lớn với ranh giới domain rõ ràng, 10–100 kỹ sư Nhiều team tự chủ, domain model đã được kiểm chứng, nhu cầu scale độc lập thực

Góc nhìn thành thật theo quy mô công ty

Lời khuyên kiến trúc có xu hướng đến từ các công ty đã vượt ngưỡng mà microservices có ý nghĩa — vì đó là những công ty có engineering blog, conference talk, và tài nguyên viết post-mortem chi tiết. Điều đó tạo ra survivorship bias. Đây là bản đồ thành thật hơn:

Ba giai đoạn tiến hóa: Monolith, Modular Monolith, và Microservices hiển thị từ trái sang phải với mũi tên giữa chúng. GIAI ĐOẠN 1 GIAI ĐOẠN 2 GIAI ĐOẠN 3 Monolith Một deployable Tất cả Code một process DB chung thực thi ranh giới Modular Monolith Một deployable · module nội bộ Billing Orders Thông báo DB chung · schema riêng tách tại seam Billing Orders Thông báo DB riêng · lời gọi mạng
Ba giai đoạn kiến trúc. Giai đoạn 1: một hộp, mọi thứ cùng nhau. Giai đoạn 2: một hộp với ranh giới module nội bộ được thực thi (đường nét đứt). Giai đoạn 3: các hộp riêng biệt kết nối bằng lời gọi mạng (mũi tên nét đứt), mỗi cái có cylinder database riêng. Bạn có thể dừng ở bất kỳ giai đoạn nào — hình dạng phù hợp phụ thuộc vào team bạn, không phải vào điều gì đang thịnh hành.

Startup (1–15 kỹ sư). Xây monolith. Bạn chưa biết phần nào của hệ thống sẽ cần scale, tính năng nào sẽ sống sót, hay ranh giới domain nào là thực. Phân rã sớm khóa chặt các quyết định trước khi bạn có đủ thông tin để đưa ra chúng. Shopify, GitHub, và Basecamp đều bắt đầu là Rails monolith. Twitter cũng vậy, và họ chạy nó nhiều năm ở quy mô đáng kể trước khi phân rã dưới tải thực.

Scale-up (15–80 kỹ sư, đang lớn). Đây là nơi modular monolith đáng công. Bạn đã có đủ kỹ sư đến mức tăng trưởng không kiểm soát trong một codebase duy nhất gây ma sát thực, nhưng bạn vẫn deploy như một team và overhead của microservices đầy đủ sẽ giết chết velocity. Đầu tư vào ranh giới module, quyền sở hữu team, và interface sạch. Giữ tùy chọn trích xuất service, nhưng chưa thực thi trừ khi nhu cầu scale cụ thể bắt buộc bàn tay.

Enterprise / tổ chức lớn (80+ kỹ sư, nhiều team tự chủ). Microservices có chọn lọc có ý nghĩa ở đây — nhưng có chọn lọc là từ khóa. Các kiến trúc quy mô lớn hiệu quả nhất không phải là monolith thuần túy hay biển service giống nhau: chúng là sự pha trộn suy nghĩ kỹ. Vài service lõi cho các năng lực mang tải nhiều nhất, domain-critical nhất, modular monolith cho phần middle vận hành, và vài service chuyên biệt nơi scale độc lập hay tuân thủ thực sự đòi hỏi. Amazon không phân rã mọi thứ cùng lúc; họ xác định các seam dưới tải và trích xuất từng cái một.

Bài học thực từ Netflix và Amazon

Khi Netflix và Amazon mô tả hành trình microservices của họ, phần được trích dẫn là sơ đồ kiến trúc với hàng trăm service. Phần bị bỏ qua là họ bắt đầu là monolith, chạy chúng cho đến khi nỗi đau không thể phủ nhận, rồi đầu tư nhiều năm và nỗ lực kỹ thuật khổng lồ vào quá trình chuyển đổi — bao gồm xây phần lớn công cụ hiện làm microservices khả thi. Họ đang nói với bạn nơi họ đến, không phải nơi bạn nên bắt đầu.

Những điều cốt lõi cần nhớ

  • Monolith là mặc định đúng đắn. Đơn giản để xây, dễ debug, transaction tầm thường. Những nỗi đau đến muộn hơn hype gợi ý và chỉ ở quy mô thực.
  • Modular monolith là tùy chọn được dùng ít nhất. Ranh giới được thực thi giữa các module nội bộ cho bạn sự rõ ràng team, refactor an toàn, và seam sẵn sàng trích xuất — mà không có bất kỳ độ phức tạp hệ phân tán nào.
  • Microservices là thuế, không phải tính năng. Bạn trả bằng lỗi mạng, eventual consistency, distributed tracing, và overhead ops. Lợi nhuận — deployment độc lập, scale độc lập, tự chủ team — là thực nhưng chỉ ở quy mô đòi hỏi vậy.
  • Distributed monolith là kết quả tệ nhất. Service ràng buộc chặt phải deploy cùng nhau cho bạn mọi chi phí của microservices mà không có lợi ích nào. Tách theo bounded context, không theo lớp kỹ thuật.
  • Quyền sở hữu data là ràng buộc cứng. Service chia sẻ database với service khác không phải là service. Mỗi service phải sở hữu data riêng; nhất quán qua ranh giới đòi hỏi thiết kế rõ ràng.
  • Dùng strangler-fig pattern để tách dần dần. Trích xuất một bounded context mỗi lần. Mỗi lần trích xuất dạy bạn điều gì đó và có thể đảo ngược. Đừng bao giờ làm decomposition big-bang.
  • Kiến trúc phù hợp với quy mô team, không phải khát vọng. Startup: monolith. Scale-up: modular monolith. Team tự chủ lớn: microservices có chọn lọc nơi thực sự cần thiết.

Quyết định kiến trúc lan rộng về phía trước theo thời gian và cộng dồn. Câu hỏi tiếp theo nhiều team đối mặt sau khi quyết định tách là phải làm gì về frontend — liệu có phục vụ một UI thống nhất hay phân rã nó nữa. Câu hỏi đó được khám phá trong Micro-Frontends: Khi Nào, Tại Sao, và Chi Phí Thực.