Mở đầu — vì sao bài này quan trọng
Trong vài năm qua, "microservices" đã trở thành một từ khóa mang tính thời thượng đến mức nhiều đội kỹ thuật cảm thấy bị áp lực phải chuyển sang kiến trúc này, chỉ vì sợ bị coi là lạc hậu. Tôi từng ngồi họp với một startup fintech ở TP.HCM, đội chỉ có 6 kỹ sư, nhưng họ đã vẽ sơ đồ 14 microservices trước khi viết dòng code nghiệp vụ nào. Sáu tháng sau, họ quay lại gộp gần hết về một khối lớn, vì không thể debug nổi một luồng thanh toán khi nó nhảy qua 5 service khác nhau.
Câu chuyện đó là lý do bài này quan trọng. Là người ra quyết định kiến trúc, bạn không được phép chọn microservices vì "ai cũng làm vậy". Bạn phải hiểu chính xác microservices là gì, nó đánh đổi (trade-off) cái gì để lấy cái gì, và quan trọng nhất — chi phí thực tế mà nó áp lên tổ chức của bạn. Netflix, Amazon, Grab hay Tiki chọn microservices không phải vì nó "tốt hơn", mà vì họ có quy mô tổ chức và bài toán vận hành mà microservices giải quyết được, đồng thời họ có đủ nguồn lực để trả cái giá đi kèm.
Bài này tập trung riêng vào microservices — đặc điểm bản chất và những đánh đổi thực tế của nó. Chúng ta sẽ không đi sâu vào monolith (Bài 16), modular monolith (Bài 18) hay service mesh (Bài 19) — những chủ đề đó có bài riêng. Ở đây, mục tiêu là để bạn nhìn thấu microservices đủ rõ để trả lời được một câu hỏi: "Tổ chức của tôi có nên trả cái giá này không?"
Khái niệm cốt lõi
Microservices là một phong cách kiến trúc trong đó ứng dụng được xây dựng như một tập hợp các dịch vụ nhỏ, độc lập, mỗi dịch vụ chạy trong tiến trình riêng và giao tiếp với nhau qua mạng. Nghe thì đơn giản, nhưng bốn đặc điểm dưới đây mới là thứ định hình toàn bộ trade-off của nó.
Mỗi service sở hữu database riêng
Đây là đặc điểm quan trọng nhất và cũng bị vi phạm nhiều nhất. Trong microservices "đúng nghĩa", mỗi service quản lý dữ liệu của riêng nó và không service nào được phép truy cập trực tiếp vào database của service khác. Nếu service Đơn hàng cần thông tin khách hàng, nó phải gọi API của service Khách hàng chứ không được SELECT thẳng vào bảng của service kia.
Lý do sâu xa là để mỗi service có thể thay đổi schema, đổi cả loại database (từ PostgreSQL sang MongoDB chẳng hạn) mà không làm vỡ service khác. Đây gọi là "database per service" và nó là ranh giới thực sự tạo nên tính độc lập.
Nhưng hãy để ý cái giá: bạn mất khả năng làm JOIN giữa dữ liệu của các service, mất luôn transaction ACID trải dài nhiều service. Một nghiệp vụ tưởng chừng đơn giản như "tạo đơn hàng và trừ tồn kho" giờ trở thành một bài toán phân tán phức tạp. (Cách giải quyết bài toán transaction phân tán này là Saga Pattern — chúng ta sẽ bàn kỹ ở Bài 25; ở đây bạn chỉ cần thấy rằng chi phí phát sinh là có thật.)
Deploy độc lập
Mỗi service có thể được build, test và triển khai riêng, không phụ thuộc vào lịch release của service khác. Đội quản lý service Thanh toán có thể deploy 20 lần một ngày, trong khi đội service Báo cáo cả tháng mới deploy một lần. Đây chính là lợi ích lớn nhất mà microservices mang lại về mặt tốc độ tổ chức: các đội không còn phải xếp hàng chờ nhau trong một "release train" khổng lồ.
Nhưng deploy độc lập chỉ thực sự có giá trị khi các service thực sự độc lập về mặt hợp đồng (contract). Nếu mỗi lần bạn đổi API của service A lại buộc phải deploy đồng thời service B, C, D — bạn đang có cái gọi là "distributed monolith", tức là monolith bị chia nhỏ về mặt vật lý nhưng vẫn dính chặt về mặt logic. Đây là kết cục tệ nhất: bạn gánh mọi chi phí của microservices mà không hưởng được lợi ích nào.
Giao tiếp qua mạng
Vì các service chạy trong tiến trình riêng, chúng phải nói chuyện với nhau qua mạng — thường là HTTP/REST, gRPC, hoặc qua hàng đợi tin nhắn (message queue). Đây là điểm khác biệt cốt tử so với monolith, nơi các module gọi nhau bằng những lời gọi hàm (function call) trong bộ nhớ.
Một function call trong monolith mất vài nanosecond, gần như không bao giờ thất bại. Một network call giữa hai service mất từ vài mili-giây đến hàng trăm mili-giây, và nó có thể thất bại vì hàng chục lý do: timeout, mất gói tin, service phía kia đang restart, DNS lỗi... Điều này có nghĩa là bạn buộc phải thiết kế cho thất bại: retry, timeout, circuit breaker (Bài 41), fallback. Trong monolith bạn không cần nghĩ đến những thứ này.
Mỗi service được sở hữu bởi một team, end-to-end
Đây là đặc điểm mang tính tổ chức nhiều hơn kỹ thuật, nhưng lại là lý do sâu xa nhất để tồn tại microservices. Theo mô hình "you build it, you run it", một team sở hữu trọn vẹn một service từ code, test, deploy cho đến vận hành và trực đêm khi có sự cố. Điều này ánh xạ trực tiếp với Định luật Conway: cấu trúc hệ thống của bạn sẽ phản chiếu cấu trúc giao tiếp của tổ chức bạn.
Hệ quả rất thực tế: nếu tổ chức bạn không có đủ các team tự chủ, được trao quyền và có kỹ năng vận hành, thì microservices sẽ không có "chủ" đúng nghĩa. Bạn sẽ có 30 service nhưng chỉ 3 team, dẫn đến việc mỗi team phải gánh 10 service mà không ai hiểu sâu service nào cả.
Bức tranh trade-off tổng thể
Nếu tóm gọn, microservices đánh đổi sự đơn giản trong phát triển để lấy sự độc lập trong vận hành và mở rộng tổ chức. Bạn nhận được: khả năng scale từng phần độc lập, cách ly lỗi (một service sập không kéo cả hệ thống sập), tự do công nghệ cho mỗi team, và tốc độ release song song. Đổi lại bạn trả bằng: độ phức tạp vận hành tăng vọt, khó debug, tính nhất quán dữ liệu trở nên khó khăn, và một lượng lớn hạ tầng phụ trợ (observability, CI/CD, service discovery, API gateway) mà bạn buộc phải xây trước khi hưởng lợi.
Tình huống thực tế
Tình huống 1 — Grab và bài toán scale từng phần
Grab, khi mở rộng từ một ứng dụng gọi xe thành siêu ứng dụng (super-app) với GrabFood, GrabPay, GrabExpress, đã đối mặt với một thực tế: các nghiệp vụ có đặc tính tải hoàn toàn khác nhau. Vào giờ cao điểm buổi tối, GrabFood có thể tăng tải gấp 10 lần, trong khi module xác thực người dùng tăng nhẹ, còn module quản lý tài xế lại có mẫu tải khác hẳn.
Nếu để tất cả trong một monolith, muốn scale GrabFood buổi tối thì phải nhân bản toàn bộ ứng dụng khổng lồ — tốn kém và lãng phí. Với microservices, họ chỉ cần scale riêng cụm service của GrabFood lên, giữ nguyên các phần khác. Ngoài ra, tính cách ly lỗi cũng cực kỳ quan trọng: nếu service khuyến mãi (promotions) gặp sự cố, người dùng vẫn phải gọi được xe. Trong monolith, một memory leak ở module khuyến mãi có thể làm sập cả tiến trình.
Bài học rút ra: Microservices tỏa sáng khi các phần của hệ thống có đặc tính scale khác nhau rõ rệt và khi cách ly lỗi giữa các nghiệp vụ là yêu cầu sống còn. Nhưng lưu ý — Grab có hàng nghìn kỹ sư và đội SRE chuyên trách. Cái giá vận hành mà họ trả được bù đắp bởi quy mô.
Tình huống 2 — Startup fintech 6 người và cái bẫy "distributed monolith"
Quay lại câu chuyện startup fintech ở TP.HCM tôi kể ở đầu bài. Họ chia thành 14 service: user-service, auth-service, wallet-service, transaction-service, notification-service, và nhiều service nhỏ khác. Vấn đề bắt đầu lộ ra khi luồng "nạp tiền vào ví" phải đi qua auth-service → wallet-service → transaction-service → notification-service.
Khi có bug, kỹ sư phải mở log của 4 service riêng biệt, cố ghép nối một request bằng cách dò timestamp — vì họ chưa kịp xây distributed tracing. Tệ hơn, các service dùng chung một số bảng và gọi chéo API của nhau chặt đến mức mỗi lần đổi một field, họ phải deploy đồng loạt 4-5 service. Họ đã tạo ra một distributed monolith: gánh mọi chi phí mạng, mọi độ phức tạp vận hành, nhưng vẫn phải deploy đồng bộ như monolith.
Với 6 kỹ sư, chi phí vận hành 14 service (mỗi cái cần CI/CD pipeline, monitoring, alert, on-call) đã ngốn hết thời gian mà lẽ ra dành cho tính năng tạo ra doanh thu. Cuối cùng họ gộp về 3 service theo ranh giới nghiệp vụ rõ ràng và tốc độ phát triển tăng gấp đôi ngay lập tức.
Bài học rút ra: Số lượng service không nên vượt quá khả năng vận hành của tổ chức. Một nguyên tắc thực dụng: nếu bạn chưa có đủ team để mỗi service có một "chủ" rõ ràng, và chưa có nền tảng observability + CI/CD trưởng thành, thì microservices sẽ hại nhiều hơn lợi. Với đội nhỏ, monolith hoặc modular monolith gần như luôn là lựa chọn đúng.
Tình huống 3 — Amazon và động lực tổ chức đằng sau microservices
Amazon là ví dụ kinh điển, và điều thú vị là động lực chính của họ mang tính tổ chức chứ không thuần túy kỹ thuật. Đầu những năm 2000, monolith của Amazon khiến hàng trăm kỹ sư dẫm chân nhau: mọi thay đổi phải qua một quy trình release chung, một bug nhỏ có thể chặn cả trăm người release. Jeff Bezos đưa ra chỉ thị nổi tiếng rằng mọi team phải giao tiếp qua service interface, và ra đời khái niệm "two-pizza team" — mỗi team đủ nhỏ để hai chiếc pizza nuôi đủ.
Kết quả là mỗi team sở hữu service của mình end-to-end, tự deploy theo nhịp riêng, tự chịu trách nhiệm vận hành. Điều này cho phép Amazon mở rộng số lượng kỹ sư mà không làm giảm tốc độ của mỗi team — thứ mà một monolith chung không bao giờ làm được ở quy mô đó.
Bài học rút ra: Lý do mạnh nhất để chọn microservices thường là để mở rộng tổ chức (organizational scaling), không phải để mở rộng kỹ thuật. Nếu bài toán của bạn là "quá nhiều kỹ sư dẫm chân nhau trên một codebase", microservices là câu trả lời hợp lý. Nếu bài toán chỉ là "hệ thống chạy chậm", thường có những cách rẻ hơn nhiều để giải quyết.
Hướng dẫn từng bước
Khi bạn phải quyết định có nên áp dụng microservices hay không, hãy đi theo trình tự sau:
Bước 1 — Xác định động lực thật sự. Viết ra một câu trả lời trung thực cho câu hỏi: "Vấn đề cụ thể nào khiến tôi cân nhắc microservices?" Nếu câu trả lời là "để scale tổ chức nhiều team" hoặc "để scale/cách ly các phần tải rất khác nhau", đó là động lực chính đáng. Nếu là "vì nó hiện đại" hoặc "để code sạch hơn" — dừng lại, vì có những cách rẻ hơn (như modular monolith) đạt được điều đó.
Bước 2 — Kiểm tra điều kiện tổ chức. Đếm số team tự chủ bạn có. Nguyên tắc thực dụng: số service không nên vượt quá số team quá nhiều lần. Nếu bạn có 3 team, đừng bắt đầu với 15 service.
Bước 3 — Đánh giá độ trưởng thành hạ tầng. Trước khi tách service đầu tiên, bạn cần sẵn sàng bốn thứ nền tảng: CI/CD tự động cho từng service, hệ thống log tập trung, distributed tracing (để lần theo một request qua nhiều service), và cơ chế theo dõi metrics + cảnh báo. Thiếu những thứ này, microservices sẽ biến việc vận hành thành cơn ác mộng.
Bước 4 — Xác định ranh giới service theo nghiệp vụ. Đừng chia service theo tầng kỹ thuật (không có "database-service" hay "controller-service"). Hãy chia theo năng lực nghiệp vụ (business capability): Đơn hàng, Thanh toán, Kho, Khách hàng. Mỗi service phải sở hữu trọn vẹn một mảng nghiệp vụ và database của nó.
Bước 5 — Thiết kế cho thất bại ngay từ đầu. Với mỗi lời gọi qua mạng, xác định rõ: timeout bao lâu, có retry không, khi service kia sập thì hành vi dự phòng (fallback) là gì. Đừng giả định mạng luôn hoạt động.
Bước 6 — Bắt đầu nhỏ và tách dần. Không cần tách hết một lần. Cách an toàn nhất thường là bắt đầu từ monolith, xác định một phần có nhu cầu độc lập rõ ràng nhất, tách ra thành service đầu tiên, học hỏi rồi mới tiếp tục. Đây là tinh thần của Strangler Fig Pattern (Bài 56).
Lỗi thường gặp & mẹo
Lỗi 1 — Tách quá sớm, quá nhiều. Đây là lỗi phổ biến nhất ở đội nhỏ và startup. Mẹo: nếu nghi ngờ, hãy bắt đầu với monolith có ranh giới module rõ ràng. Việc tách một monolith tốt thành microservices luôn dễ hơn việc gộp lại một mớ service rối rắm.
Lỗi 2 — Chia sẻ database giữa các service. Khi hai service cùng đọc/ghi vào một bảng, bạn đã phá vỡ tính độc lập và tạo ra sự phụ thuộc ngầm nguy hiểm nhất. Mẹo: mỗi service sở hữu dữ liệu của nó tuyệt đối; muốn lấy dữ liệu service khác thì gọi API hoặc lắng nghe sự kiện.
Lỗi 3 — Distributed monolith. Nếu bạn không thể deploy một service mà không deploy service khác, bạn đã thất bại. Mẹo: kiểm tra bằng câu hỏi "Tôi có thể deploy service này lúc 3h chiều thứ Sáu mà không cần đụng đến service nào khác không?" Nếu không, hãy xem lại ranh giới và hợp đồng API.
Lỗi 4 — Quên chi phí observability. Nhiều đội tách service xong mới nhận ra không debug nổi. Mẹo: coi distributed tracing và log tập trung là điều kiện tiên quyết, không phải tính năng "để sau".
Lỗi 5 — Bỏ qua network latency tích lũy. Một request đi qua 6 service, mỗi service thêm 20ms, là đã 120ms chỉ riêng cho việc "đi lại". Mẹo: hạn chế độ sâu chuỗi gọi đồng bộ; cân nhắc giao tiếp bất đồng bộ (async) khi phù hợp (Bài 21).
Mẹo chốt: Hãy nhớ câu của Martin Fowler — "bạn phải đủ cao để nhảy microservices" (you must be this tall to use microservices). Nếu tổ chức chưa vững về DevOps, tự động hóa và văn hóa vận hành, hãy hoãn lại.
Bài tập thực hành
- Phân tích động lực: Chọn một hệ thống bạn đang làm (hoặc một sản phẩm bạn dùng hằng ngày như một ứng dụng ngân hàng số). Viết ra 3 lý do có thể khiến nó nên hoặc không nên dùng microservices. Với mỗi lý do, ghi rõ đó là động lực kỹ thuật hay tổ chức.
- Vẽ ranh giới service: Cho một sàn thương mại điện tử nhỏ (đăng nhập, sản phẩm, giỏ hàng, đơn hàng, thanh toán, thông báo). Hãy đề xuất cách chia thành service theo năng lực nghiệp vụ, và chỉ ra service nào sở hữu dữ liệu nào. Sau đó liệt kê các lời gọi mạng cần có giữa chúng khi khách hàng đặt một đơn.
- Kiểm tra distributed monolith: Với thiết kế ở bài 2, hãy tìm ít nhất một trường hợp mà việc đổi một field dữ liệu có thể buộc phải deploy nhiều service. Đề xuất cách thiết kế lại hợp đồng API để tránh điều đó.
- Đánh giá độ sẵn sàng: Lập một checklist gồm 5 điều kiện tổ chức và hạ tầng mà đội của bạn cần có trước khi tách service đầu tiên. Tự chấm điểm đội mình đang ở đâu.
Tóm tắt
Microservices là một kiến trúc trong đó ứng dụng được chia thành các service nhỏ, mỗi service sở hữu database riêng, deploy độc lập, giao tiếp qua mạng và được một team sở hữu end-to-end. Đây không phải là "phiên bản tốt hơn" của monolith — nó là một sự đánh đổi có chủ đích: bạn hy sinh sự đơn giản trong phát triển và tính nhất quán dữ liệu dễ dàng để đổi lấy khả năng mở rộng tổ chức, scale từng phần độc lập và cách ly lỗi.
Ba bài học cốt lõi: (1) Động lực mạnh nhất để chọn microservices thường là mở rộng tổ chức nhiều team, không phải chỉ vấn đề kỹ thuật thuần túy. (2) Số lượng service không được vượt quá năng lực vận hành của tổ chức — startup nhỏ chọn microservices thường tự bắn vào chân mình. (3) Cạm bẫy nguy hiểm nhất là distributed monolith, nơi bạn trả mọi chi phí nhưng không hưởng được lợi ích độc lập.
Là người ra quyết định kiến trúc, câu hỏi bạn cần trả lời không phải "microservices có tốt không" mà là "tổ chức của tôi có nên trả cái giá này không, để đổi lấy đúng những lợi ích mà tôi thực sự cần?" Nếu câu trả lời chưa rõ ràng là "có", thì gần như chắc chắn câu trả lời là "chưa".