Microservices: Cái Complexity Không Ai Nói Trước

"Chúng ta chuyển sang microservices đi. Mọi thứ sẽ dễ hơn."
— câu nói của người chưa bao giờ debug distributed trace lúc 2 giờ sáng
Mọi slide ở conference về architecture đều có cùng một layout: monolith ở bên trái (ghi "bad"), microservices ở bên phải (ghi "good"). Netflix làm vậy. Amazon làm vậy. Chắc mình cũng nên làm theo thôi.
Nhưng có một phiên bản của câu chuyện này không bao giờ xuất hiện trong conference talk — phiên bản mà team làm được 6 tháng, mỗi lần deploy mất gấp đôi thời gian, không ai biết service nào đang gây ra latency spike, và một cái tính năng đơn giản giờ cần phối hợp 3 team và 5 PR.
Bài này về phiên bản đó.
Bạn Sẽ Học Được Gì
✅ Tại sao complexity của microservices là multiplicative, không phải additive
✅ Cái giá ẩn phải trả ngay từ ngày đầu tiên
✅ Khi nào microservices đáng — và khi nào không
✅ Những gì cần kiểm tra trước khi split monolith
Lời Quảng Cáo vs. Thực Tế
Những lợi ích của microservices là có thật. Deploy độc lập, cô lập lỗi, linh hoạt về tech stack, các team tự chủ — đây là những thứ thực sự có giá trị với đúng tổ chức.
Vấn đề là những lợi ích này đều có điều kiện. Chúng chỉ xuất hiện khi bạn đã xây xong toàn bộ hạ tầng hỗ trợ và thiết lập được cấu trúc tổ chức phù hợp. Mà cái công việc đó rất tốn kém, chậm, và gần như không bao giờ được nhắc đến trong lúc thuyết phục.
Điều bạn nghe: "Mỗi service có thể deploy độc lập."
Điều bạn không nghe: "...sau khi bạn đã build CI/CD pipeline riêng cho từng service, configure service discovery, setup container orchestration platform, đặt ra quy ước deploy, và giải quyết xong bài toán 'làm sao phối hợp breaking changes across services'."
Lợi ích nằm ở trang đầu. Complexity nằm trong phần chữ nhỏ ở cuối.
Complexity Là Multiplicative
Một monolith có một lần deploy. Một luồng log. Một database connection pool. Một chỗ để đặt breakpoint và hiểu chuyện gì đang xảy ra.
Microservices không chỉ cộng thêm complexity — chúng nhân nó lên. Với N services, bạn có:
- N lần deploy cần version, monitor, và rollback
- N × (N-1) / 2 tương tác giữa các service cần test và lý giải
- N luồng log riêng biệt cần correlate khi debug
- N cách lỗi có thể cascade vào nhau
- N database (nếu theo đúng nguyên tắc cô lập dữ liệu) cần maintain, backup, và migrate
Với 10 services, đó là 45 cặp service-to-service tiềm năng cần lý giải. Với 20 services, con số đó là 190.
N services → N² complexity surfaceĐây không phải lý thuyết. Đó là lý do vì sao các công ty lớn dùng microservices tốn không ít nhân lực chỉ để làm internal tooling và platform work — không phải tính năng cho người dùng, mà chỉ để giữ cho hệ thống còn hiểu được.
Cái Giá Phải Trả Từ Ngày Đầu
Ngay trước khi có production incident đầu tiên, microservices đã tạo ra overhead tức thì:
1. Network Bây Giờ Là Một Phần Trong Code Của Bạn
Trong monolith, một lời gọi hàm mất vài nanosecond. Trong microservices, thao tác tương tự trở thành một network call. Điều này kéo theo:
- Latency: Mỗi hop thêm vài millisecond. Một request của người dùng đi qua 5 services có thể dễ dàng cộng dồn 50-200ms chỉ từ network thuần.
- Partial failure: Service bên kia có thể chậm, down, hoặc trả về lỗi. Code của bạn phải xử lý tất cả những trường hợp này — retry logic, circuit breaker, timeout, fallback.
- Data consistency: Bạn không còn có thể gói gọn các thao tác trong một database transaction nữa. Distributed transaction (2PC, saga) phức tạp và tạo ra các kiểu lỗi mới.
// Monolith: đơn giản, transactional
async function placeOrder(userId: string, items: CartItem[]) {
await db.transaction(async (tx) => {
const order = await tx.orders.create({ userId, items });
await tx.inventory.decrement(items);
await tx.payments.charge(userId, order.total);
}); // Hoặc tất cả thành công, hoặc không có gì xảy ra
}
// Microservices: mỗi bước có thể fail độc lập
async function placeOrder(userId: string, items: CartItem[]) {
const order = await orderService.create({ userId, items });
// Nếu inventory call fail ở đây thì sao?
await inventoryService.decrement(items);
// Nếu payment fail ở đây? Order đã tạo. Inventory đã giảm.
await paymentService.charge(userId, order.total);
// Bạn giờ cần saga, compensation logic, và dead letter queue
}2. Observability Không Còn Là Tuỳ Chọn
Trong monolith, bạn có thể console.log và hiểu chuyện gì xảy ra. Trong microservices, một request bình thường của người dùng có thể đi qua 6 services trên 4 máy chủ khác nhau. Không có distributed tracing, bạn đang bay mù.
Điều này có nghĩa là bạn cần — từ ngày đầu tiên, không phải sau này:
- Distributed tracing (Jaeger, Zipkin, Datadog APM) để correlate log across services
- Centralized logging với correlation ID nhất quán
- Metric theo từng service cho từng endpoint, không chỉ metric hạ tầng
- Health check cho mọi service, kèm theo health check của các phụ thuộc
Đây là hạ tầng bạn phải xây và bảo trì. Không miễn phí, và nếu thiếu nó, debug trên production trở thành một cuộc khai quật khảo cổ.
3. Môi Trường Phát Triển Cục Bộ Trở Nên Khó Hơn
Thử dựng full microservices stack trên máy local khi cần test một tính năng. Bạn sẽ cần:
- Docker Compose với 8+ container
- Config riêng cho từng service theo từng môi trường
- Phiên bản mock của những service không thuộc phạm vi bạn
- Cách để route traffic local qua mesh
Một số team cuối cùng phải làm toàn bộ việc phát triển trên shared staging environment vì local setup quá nhức đầu. Điều này làm chậm vòng phản hồi đáng kể.
Complexity Về Mặt Tổ Chức
Conway's Law nói rằng kiến trúc phần mềm sẽ phản ánh cấu trúc giao tiếp trong tổ chức của bạn. Các team microservices thường học được điều này theo cách đau đớn.
Bài Toán "Schema Này Của Ai?"
Trong monolith, thêm một cột vào bảng database là việc một PR là xong. Trong microservices với data isolation đúng chuẩn, thêm một field vào một khái niệm chung đòi hỏi:
- Service A thêm field vào database của nó và đưa ra qua API
- Service B cập nhật phía client để đọc field mới
- Cả hai service deploy theo thứ tự cụ thể
- Chiến lược rollback nếu một trong hai lần deploy thất bại
Một tính năng trước đây là một PR, giờ cần phối hợp hai team, hai lịch deploy, và versioned API contract. Nhân điều này lên toàn bộ tổ chức và bạn có "coordination tax" — một phần đáng kể thời gian kỹ thuật không phải để xây tính năng, mà để đồng bộ qua ranh giới giữa các service.
Bài Toán "Gọi Ai Bây Giờ?"
Trong monolith, bạn tìm trong codebase và thấy hàm cần tìm. Trong microservices, khi có gì đó hỏng, bạn trước tiên cần xác định service nào chịu trách nhiệm cho hành vi đang debug. Lỗi ở API gateway? User service? Notification service? Event processor?
Điều này không hề đơn giản, và càng nhiều service thì càng rối hơn.
Khi Nào Thực Sự Đáng
Nói vậy không có nghĩa là microservices sai. Với đúng tổ chức và đúng bài toán, chúng là công cụ phù hợp. Nhưng các điều kiện đó rất quan trọng.
Microservices có ý nghĩa khi:
- Nhu cầu scale thực sự không đồng đều. Service xử lý ảnh cần tài nguyên nhiều hơn 50 lần so với auth service. Tách ra cho phép scale độc lập và tiết kiệm chi phí.
- Các team cần tự chủ thật sự. Nhiều team 20+ người không thể làm việc trong cùng một monolith mà không bị merge conflict và coordination overhead liên tục. Ranh giới service giúp các team làm chủ phần của mình.
- Có yêu cầu về compliance hoặc cô lập. Dữ liệu PII cần được cô lập vật lý. Một payment processor cần có khả năng audit độc lập.
- Bạn đã thực sự outgrow monolith. Monolith đang gây ra đau thực sự — deployment bottleneck, tường giới hạn về scale, hoặc coupling khiến thay đổi trở nên nguy hiểm.
Microservices không có ý nghĩa khi:
- Team nhỏ (dưới ~20 kỹ sư)
- Bạn chưa có monolith hoạt động tốt và được hiểu rõ
- Các service của bạn vẫn sẽ dùng chung database (đó không phải microservices — đó là distributed monolith)
- Bạn làm vì nghe nó có vẻ hiện đại hay nghe hay
Kết quả tệ nhất là distributed monolith: toàn bộ complexity của microservices, không có bất kỳ lợi ích nào. Điều này xảy ra khi team tách service nhưng vẫn giữ chúng gắn chặt với nhau qua shared database, chuỗi synchronous call, hoặc dependency deploy rối rắm.
Complexity Nên Được Kiếm, Không Được Thừa Kế
Đây là câu hỏi cốt lõi cần hỏi trước khi tách service:
Bài toán cụ thể nào mình đang giải quyết mà thay đổi kiến trúc này sẽ giải quyết được?
Không phải "microservices là best practice." Không phải "Netflix làm vậy." Một bài toán cụ thể, đo được: "Checkout service của chúng ta deploy 40 lần một ngày và mỗi lần đều bị chặn bởi thay đổi A/B testing của team marketing." Đó là bài toán thực tế mà microservices có thể giải quyết.
Nếu bạn không thể nêu tên bài toán một cách rõ ràng, bạn không giải quyết bài toán — bạn đang thừa kế complexity để đổi lấy điểm trong CV.
Cái bẫy là microservices cảm giác như tiến bộ. Bạn đang tách monolith ra. Bạn đang "hiện đại hóa." Nhưng complexity được thêm vào mà không có bài toán tương ứng được giải quyết thực ra chỉ là nợ kỹ thuật với thêm vài bước phụ.
Những Gì Cần Kiểm Tra Trước
Nếu bạn đang cân nhắc nghiêm túc về microservices, hãy xác nhận những điều này trước khi bắt đầu:
Team readiness:
- Bạn có kỹ sư từng vận hành distributed system trên production không?
- Bạn có platform team (hoặc khả năng xây một team như vậy) để chịu trách nhiệm hạ tầng dùng chung không?
- Bạn có runbook và on-call rotation có thể xử lý sự cố liên quan đến nhiều service không?
Technical readiness:
- Monolith của bạn có đủ test coverage để tách service an toàn không?
- Bạn có distributed tracing (hoặc kế hoạch thêm vào ngay lập tức) không?
- Bạn đã định nghĩa service contract và chiến lược versioning chưa?
Problem clarity:
- Phần cụ thể nào của monolith đang gây ra vấn đề?
- Bạn đã thử giải quyết bài toán đó ngay trong monolith trước chưa?
- Bạn có thể tách một service làm thử nghiệm trước khi cam kết toàn bộ không?
Strangler fig pattern — dần dần tách service từ một monolith đang chạy thay vì viết lại toàn bộ — là con đường an toàn nhất. Tách một service. Chạy trên production. Học những thứ bạn chưa biết. Rồi mới quyết định lần tách tiếp theo có đáng không.
Tóm Lại Thật Sự
Microservices là giải pháp hợp lệ cho những bài toán thực tế ở quy mô lớn. Những tổ chức hưởng lợi nhiều nhất từ chúng là những tổ chức đã vượt qua giới hạn của thứ gì đó đơn giản hơn — không phải bắt đầu từ microservices ngay từ đầu.
Complexity chúng tạo ra là có thật và tốn kém:
- Network call thay thế lời gọi hàm, với đầy đủ các kiểu lỗi đi kèm
- Observability đòi hỏi đầu tư chủ động, không phải thêm vào sau
- Môi trường phát triển cục bộ khó hơn trước khi dễ hơn
- Coordination overhead tăng theo mỗi ranh giới service mới
- Debug cần correlate across nhiều hệ thống cùng lúc
Không cái nào là không vượt qua được. Nhưng đó là công việc thực sự, bền vững, cạnh tranh với việc xây tính năng về thời gian và sự chú ý.
Câu hỏi không phải là "chúng ta có nên dùng microservices không?" Câu hỏi là: "Complexity chúng ta đang thêm vào có đang giải quyết bài toán thực sự xứng đáng với cái giá đó không?"
Nếu câu trả lời là có, microservices đáng từng chút operational overhead. Nếu câu trả lời là "chúng ta làm vì đó là thứ các team hiện đại làm" — bạn đang xây distributed system khó vận hành hơn, khó debug hơn, và khó onboard hơn, để đổi lấy architectural credibility.
Người dùng không quan tâm đến service topology của bạn. Họ quan tâm là sản phẩm có chạy được không.
Thêm về kiến trúc phần mềm:
Cách Chọn Software Architecture Phù Hợp
API Gateway: Hướng Dẫn Đầy Đủ
BFF Pattern: Backend for Frontend
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.