+9

🧩🧠 Vì sao 80% hệ Microservices thất bại? - Microservice Architecture P2

Vì sao 80% hệ Microservices thất bại?

1. Cơn sốt "Microservices" và cái bẫy của sự hợp thời

Trong phát triển phần mềm, tồn tại một định kiến: "Monolith là lỗi thời, còn Microservices là tương lai." Mỗi khi hệ thống nghẽn hay dev dẫm chân nhau lúc merge code, câu trả lời thường là: "Cần chuyển sang Microservices."

Lý thuyết này nghe cực kỳ thuyết phục vì nguyên lý "chia để trị" giúp kiểm soát độ phức tạp:

  • Các nhóm phát triển (team) làm việc độc lập, không phụ thuộc nhau.
  • Mỗi service sử dụng database riêng, tự do chọn công nghệ phù hợp.
  • Khả năng scale chọn lọc: chỉ scale service chịu tải cao để tối ưu chi phí.
  • Cô lập lỗi: Một service sập không làm hỏng toàn bộ hệ thống.

Nhưng thực tế lại khác. Có đến 80% dự án chuyển sang Microservices thất bại. Hệ thống trở nên chậm chạp và ngốn chi phí hạ tầng. Doanh nghiệp nhận về một "Distributed Monolith" (Monolith phân tán) mang đủ nhược điểm của cả hai thế giới: độ trễ cao, khó debug của vi dịch vụ kết hợp với sự ràng buộc chặt chẽ của nguyên khối.


2. Câu chuyện thực tế: Startup 10 kỹ sư và 30 Microservices

Hãy nhìn vào câu chuyện của một startup công nghệ trong lĩnh vực E-commerce mà tôi từng tư vấn.

Với tham vọng scale cực khủng từ đầu, vị CTO quyết định đi thẳng lên Microservices. Đội ngũ gồm 10 kỹ sư backend triển khai hệ thống gồm 30 services độc lập: User, Product, Cart, Order, Payment, Inventory, Shipping, Notification... Mỗi service chạy trên container riêng, kết nối qua REST API và gRPC.

Môi trường local chạy rất mượt. Nhưng thảm họa bắt đầu khi startup phải liên tục thay đổi để tìm Product-Market Fit.

Mô hình kinh doanh lúc này đổi theo ngày. Hôm nay thêm combo, ngày mai đổi cơ chế tích điểm, ngày kia hoàn tiền. Do domain boundary chưa ổn định, mỗi thay đổi đòi hỏi sửa API của hàng loạt service. Để chạy tính năng combo, nhóm phát triển phải sửa code đồng thời của Product, Cart, Order, và Promotion.

Kỹ sư rơi vào cái bẫy "deploy chéo" (distributed deployment lock). Họ không thể release độc lập từng service mà phải deploy 5 service cùng lúc theo đúng thứ tự. Nếu một service lỗi, toàn bộ hệ thống checkout sẽ bị tê liệt.

Thời gian release tính năng kéo dài từ vài giờ lên thành 2 tuần. Đội ngũ kiệt sức vì sửa API chéo, cấu hình mạng và giải quyết lỗi bất đồng bộ. Hệ thống 30 microservices đã bóp nghẹt 10 kỹ sư của startup.


3. Tại sao nó vỡ vụn khi ra Production?

Khi chạy thử ở local, mạng máy tính là hoàn hảo, băng thông vô hạn. Nhưng ngoài production, thực tế vật lý của hệ thống phân tán sẽ đè bẹp những thiết kế lý thuyết ngây thơ.

Trong Monolith, việc giao tiếp giữa các module là gọi hàm trong bộ nhớ (In-Memory Call) qua RAM với độ trễ tính bằng micro-giây và độ tin cậy 100%. Với Microservices, mọi cuộc gọi biến thành giao tiếp mạng (Network Call) qua REST hoặc gRPC.

Monolith:
[ Module Order ] ----(Gọi hàm qua RAM: < 0.1ms )----> [ Module Product ]

Microservices:
[ Service Order ] ----(Gửi gói tin qua mạng: 15ms - 50ms)----> [ Service Product ]
                           |
             (Mất gói tin, nghẽn cổng, IP thay đổi, timeout)

Mạng máy tính vốn dĩ bất ổn định. Thiết kế hệ thống phân tán với tư duy gọi đồng bộ liên hoàn như Monolith sẽ tạo ra Độ trễ mạng cộng dồn (Latency Accumulation)Sập dây chuyền (Cascading Failure).

Chưa kể, chi phí vận hành (operational cost) tăng vọt. Thay vì giám sát 1 ứng dụng, team DevOps phải quản lý hàng chục container, cấu hình bảo mật, xử lý Service Discovery, và đối mặt với bài toán dữ liệu phân tán không nhất quán.


4. Mổ xẻ 4 sai lầm chí mạng khiến Microservices thất bại

Sự thất bại của Microservices hầu hết đều bắt nguồn từ 4 sai lầm cốt lõi dưới đây:

4.1. Premature Decomposition (Tách quá sớm khi chưa hiểu rõ Domain)

Chia nhỏ hệ thống khi chưa hiểu rõ nghiệp vụ (domain) là một quyết định tự sát.

Khi bắt đầu, ranh giới nghiệp vụ rất mờ nhạt. Trong Monolith, refactor code cực kỳ rẻ: chỉ cần đổi tên class, dời thư mục, hay đổi chữ ký hàm. Compiler sẽ báo lỗi ngay nếu thiếu sót.

Với Microservices, mỗi service là một repository riêng. Refactor đòi hỏi sửa API, cập nhật tài liệu, phối hợp deploy và quản lý version. Chi phí thay đổi tăng lên gấp hàng chục lần.

4.2. Entity-Based Splitting (Chia service theo bảng Database)

Nhiều đội ngũ thường mở sơ đồ ERD ra và quyết định: "Mỗi bảng dữ liệu sẽ là một service".

Entity-based Splitting (Sai lầm):
+-----------------+      +-----------------+      +-----------------+
|  User Service   |      |  Order Service  |      | Product Service |
|  (Bảng users)   |      |  (Bảng orders)  |      | (Bảng products) |
+-----------------+      +-----------------+      +-----------------+
        ^                        |                        |
        |                        v                        |
        +------------(Gọi HTTP lấy thông tin) <-----------+

Cách chia này tạo ra các Anemic Services (Dịch vụ thiếu máu) chỉ thực hiện CRUD đơn giản lên một bảng dữ liệu mà không mang nghiệp vụ trọn vẹn.

Khi thực hiện Checkout, Order Service phải gọi HTTP sang User Service để lấy địa chỉ, gọi sang Product Service để lấy giá, gọi sang Inventory Service để kiểm tra kho. Một request của khách hàng tạo ra một chuỗi giao tiếp mạng chằng chit. Nếu một service ở cuối chuỗi phản hồi chậm, toàn bộ request checkout sẽ bị nghẽn do trễ mạng cộng dồn.

4.3. Shared Database (Cái bẫy dùng chung cơ sở dữ liệu)

Để tránh đồng bộ dữ liệu phức tạp, nhiều đội cho các service dùng chung database.

Shared Database (Anti-pattern):
+-----------------+      +-----------------+
|  Order Service  |      | Product Service |
+-----------------+      +-----------------+
        |                        |
        +-----------+------------+
                    | (Đọc/Ghi trực tiếp)
                    v
         +----------------------+
         |   Shared Database    |  <--- Điểm nghẽn duy nhất (SPOF)
         +----------------------+

Đây là phản mẫu kiến trúc (anti-pattern) phá vỡ hoàn toàn tính độc lập của Microservices:

  • Ràng buộc ẩn về mặt Schema: Khi Product Service đổi tên cột, Order Service lập tức crash ngoài production vì câu lệnh SQL của nó vẫn truy vấn cột cũ.
  • Điểm nghẽn chịu tải: Cơ sở dữ liệu dùng chung trở thành bottleneck lớn nhất. Một service chạy query nặng làm lock bảng hoặc nghẽn CPU database sẽ kéo sập toàn bộ các service khác.

4.4. Thiếu năng lực quan sát (Lack of Observability)

Vận hành Microservices mà thiếu hệ thống giám sát phân tán (Distributed Observability) giống như việc lái máy bay trong sương mù.

Trong Monolith, khi lỗi xảy ra, bạn chỉ cần SSH vào server và xem log tập trung. Nhưng trong Microservices, request lỗi có thể đi qua API Gateway, chuyển qua Service A, gọi sang Service B, ghi một event vào Kafka, rồi Service C đọc event đó và ghi vào DB. Nếu không có Correlation IDDistributed Tracing, việc tìm ra nguyên nhân lỗi ở bước nào là bất khả thi.


5. Tư duy Kiến trúc: Chia theo "Bounded Context" thay vì Table

Sự khác biệt lớn nhất của một Senior Architect nằm ở cách phân ranh giới hệ thống. Thay vì nhìn vào các bảng cơ sở dữ liệu, họ sử dụng công cụ Bounded Context từ Domain-Driven Design (DDD).

Một Bounded Context xác định một ranh giới logic mà trong đó, mô hình ngôn ngữ nghiệp vụ và các thực thể dữ liệu có ý nghĩa nhất quán nhất.

Hãy lấy ví dụ về thực thể "Product" (Sản phẩm). Trong mô hình Monolith hoặc phân chia theo Entity, chúng ta chỉ có duy nhất một bảng products chứa tất cả thuộc tính: tên, giá, ảnh, số lượng kho, kích thước, nhà cung cấp, và lịch sử đánh giá.

Nhưng thực tế, thực thể "Product" mang ý nghĩa hoàn toàn khác nhau tùy thuộc vào ngữ cảnh:

  • Context Catalog (Bán hàng): Sản phẩm chỉ cần tên, mô tả, ảnh đại diện, và giá bán.
  • Context Inventory (Kho): Sản phẩm lúc này là một SKU cần quản lý về vị trí đặt trên kệ, kích thước, và cân nặng.
  • Context Shipping (Vận chuyển): Sản phẩm là một kiện hàng cần được gán nhãn địa chỉ nhận, mã vạch giao hàng.
Phân chia theo Bounded Context:
+------------------------------------------------------------+
|                        Catalog Context                     |
|  - Thể hiện: Product (Tên, Ảnh, Giá bán)                   |
+------------------------------------------------------------+
                              |
                     (Đồng bộ bất đồng bộ)
                              v
+------------------------------------------------------------+
|                       Inventory Context                    |
|  - Thể hiện: Product (SKU, Vị trí kệ, Kích thước)           |
+------------------------------------------------------------+

Thay vì tạo ra một Product Service dùng chung cho toàn bộ hệ thống (khiến service này liên tục bị sửa đổi bởi mọi team), chúng ta tách thành các service tương ứng với từng Bounded Context: Catalog ServiceInventory Service. Mỗi service quản lý một database riêng. Chúng giao tiếp bất đồng bộ qua các Event (ví dụ: khi sản phẩm mới được tạo trong Catalog, một event ProductCreated được phát ra để Inventory Service tự khởi tạo bản ghi SKU).


6. Lối thoát Kiến trúc: "Monolith First" và Chia Database trước Chia Code

Nếu bạn đang đứng trước quyết định thiết kế một hệ thống mới hoặc dịch chuyển hệ thống hiện tại, đây là những chỉ dẫn hành động thực tế từ góc nhìn của một kỹ sư dày dạn kinh nghiệm:

6.1. Nguyên lý "Monolith First"

Ngoại trừ những trường hợp đặc biệt, con đường an toàn nhất cho hầu hết mọi dự án là bắt đầu bằng một Modular Monolith (kiến trúc nguyên khối có cấu trúc module rõ ràng).

Modular Monolith:
+-------------------------------------------------------+
|                    Ứng dụng Monolith                  |
|                                                       |
|   +----------------+  In-Memory   +---------------+   |
|   |  Module Order  | ------------>| Module Product|   |
|   +----------------+              +---------------+   |
|           |                               |           |
|           v                               v           |
|     (Schema Order)                 (Schema Product)   |
+-------------------------------------------------------+
|                   Cơ sở dữ liệu chung                 |
+-------------------------------------------------------+

Hãy xây dựng toàn bộ hệ thống trong một mã nguồn duy nhất, nhưng thiết kế code thật sạch sẽ, phân chia rõ ràng các module nghiệp vụ qua các package hoặc namespace. Hãy cấm tuyệt đối việc module này gọi trực tiếp code nội bộ của module khác mà không đi qua các interface/facade đã định nghĩa.

Việc này giúp giữ tốc độ phát triển cực nhanh ở giai đoạn đầu, dễ dàng refactor ranh giới giữa các module khi nghiệp vụ thay đổi, trong khi vẫn đảm bảo cấu trúc hệ thống đủ gọn gàng để tách ra thành Microservices sau này khi cần thiết.

6.2. Tách Database trước khi tách Code

Nếu muốn kiểm tra xem hệ thống đã sẵn sàng cho Microservices hay chưa, hãy thực hiện bài kiểm tra sau: Hãy chia cơ sở dữ liệu duy nhất thành các database logic (schema) riêng biệt cho từng module ngay bên trong ứng dụng Monolith.

  • Module Order chỉ được phép đọc/ghi vào schema order_db.
  • Module Product chỉ được phép đọc/ghi vào schema product_db.
  • Nếu Module Order cần dữ liệu từ Product, nó bắt buộc phải gọi qua một interface dịch vụ trong code, tuyệt đối không được dùng lệnh JOIN SQL xuyên schema.

Nếu bạn hoàn thành bài kiểm tra này mà không làm sụp đổ các tính năng nghiệp vụ, điều đó chứng tỏ ranh giới domain đã thực sự rõ ràng và việc tách code thành các microservices chỉ còn là vấn đề kỹ thuật thuần túy.


7. Phân tích Trade-offs: Chi phí thực tế của sự tự do

Lựa chọn kiến trúc luôn là cuộc chơi của sự đánh đổi. Không có kiến trúc nào là tốt nhất, chỉ có kiến trúc phù hợp nhất với bối cảnh và nguồn lực.

Tiêu chí Kiến trúc Monolith / Modular Monolith Kiến trúc Microservices
Tốc độ phát triển ban đầu Cực nhanh. Dễ refactor, debug tập trung, deploy đơn giản. Chậm. Phải thiết kế API contract, cấu hình hạ tầng mạng phức tạp.
Độ trễ hệ thống (Latency) Thấp. Các cuộc gọi hàm diễn ra trong bộ nhớ RAM (< 1ms). Cao. Mỗi request phải đi qua nhiều hop mạng bằng HTTP/gRPC.
Tính nhất quán dữ liệu Tuyệt đối (ACID). Hỗ trợ transaction local trên database rất dễ dàng. Nhất quán cuối cùng (Eventual Consistency). Phải dùng Saga Pattern, Event Broker.
Độ phức tạp vận hành Thấp. Giám sát một ứng dụng đơn lẻ, log tập trung mặc định. Cực cao. Đòi hỏi năng lực DevOps tốt, hệ thống tracing phân tán, API Gateway.
Khả năng scale độc lập Kém. Phải scale toàn bộ ứng dụng dù chỉ nghẽn ở một module nhỏ. Cực tốt. Scale chọn lọc từng service chịu tải cao để tối ưu chi phí phần cứng.
Quy mô tổ chức phù hợp Dưới 20-30 lập trình viên. Trên 50 lập trình viên, chia thành nhiều nhóm độc lập.

8. Những trường hợp ngoại lệ: Khi nào nên đi thẳng lên Microservices?

Mặc dù quy tắc "Monolith First" là khuyến nghị chung, vẫn có những trường hợp ngoại lệ mà việc thiết kế Microservices ngay từ đầu là hợp lý:

  1. Yêu cầu đặc thù về công nghệ và tài nguyên: Hệ thống có một phần nhỏ cần xử lý các tác vụ ngốn cực nhiều CPU/GPU hoặc sử dụng các thư viện đặc thù (ví dụ: service chạy mô hình học máy bằng Python, trong khi API CRUD còn lại viết bằng Go hoặc Java). Việc tách riêng service xử lý AI này từ đầu giúp tối ưu hạ tầng.
  2. Hệ thống viết lại từ một sản phẩm đã chạy thực tế: Khi bạn xây dựng một hệ thống thế mạng cho một ứng dụng cũ đã chạy nhiều năm. Trong trường hợp này, domain nghiệp vụ và ranh giới boundary đã được chứng minh qua thời gian và hoàn toàn ổn định.

9. Key Takeaways cho Kỹ sư Thiết kế Hệ thống

  1. Microservices không giúp code chạy nhanh hơn hay dễ viết hơn. Nó là giải pháp giúp mở rộng quy mô tổ chức và hạ tầng khi Monolith đã đạt giới hạn chịu tải.
  2. Ranh giới nghiệp vụ (Bounded Context) quan trọng hơn cấu trúc bảng database. Chia service theo bảng database là con đường nhanh nhất dẫn đến kiến trúc Distributed Monolith.
  3. Database per Service là bắt buộc. Nếu vẫn dùng chung database vật lý, bạn chưa thực sự làm Microservices; bạn chỉ đang làm phức tạp hóa hệ thống Monolith của mình.
  4. Tích lũy đủ năng lực vận hành trước khi scale. Đừng bắt đầu chia nhỏ hệ thống nếu team chưa làm chủ quy trình CI/CD tự động và chưa trang bị hệ thống Distributed Tracing/Centralized Logging cơ bản.

10. Lời kết và Open Loop

Chuyển dịch sang kiến trúc Microservices thành công đòi hỏi một sự thay đổi sâu sắc trong tư duy thiết kế hệ thống và quản lý dữ liệu. Một khi bạn đã quyết định tách database riêng cho từng service để đạt được sự độc lập hoàn toàn, bạn sẽ phải đối mặt với một thử thách cực kỳ hóc búa: Làm thế nào để thực hiện các giao dịch nghiệp vụ xuyên suốt nhiều dịch vụ (Distributed Transaction) mà không phá vỡ tính nhất quán dữ liệu, và cũng không làm giảm hiệu năng hệ thống?

Chúng ta sẽ cùng nhau đi sâu vào việc giải quyết bài toán phức tạp này trong tập tiếp theo: Tách Service theo Database: cái bẫy lớn nhất của Microservices (Episode 03).


11. Vì sao 80% thất bại không đến từ kỹ thuật, mà đến từ timing

Nhiều đội ngũ nghe đến con số "80% hệ microservices thất bại" rồi mặc định lỗi nằm ở chuyện chọn sai stack, cấu hình Kafka dở, hay Kubernetes quá khó. Nhưng trong rất nhiều trường hợp, gốc của thất bại lại nằm ở thời điểm áp dụng.

Một tổ chức chưa có platform maturity mà nhảy vào microservices quá sớm sẽ gặp đúng một vòng xoáy quen thuộc:

  • cần thêm service discovery vì IP đổi liên tục
  • cần thêm tracing vì không còn debug bằng local log được
  • cần thêm async messaging vì sync call bắt đầu gây nghẽn
  • cần thêm CI/CD nghiêm ngặt vì release chéo trở nên nguy hiểm
  • cần thêm security layer vì trust boundary không còn nằm trong một process

Mỗi lớp bổ sung đều hợp lý cục bộ. Nhưng cộng lại, chúng tạo ra một hóa đơn mà tổ chức chưa có sức trả. Từ đó hệ thống không chết vì design sai một điểm, mà chết vì tổng complexity vượt quá năng lực vận hành hiện tại.

12. Cách phân biệt "đau phát triển" với "đau kiến trúc"

Trước khi kết luận monolith là vấn đề, team nên phân biệt hai loại đau:

  • đau do codebase thiếu kỷ luật: module lẫn nhau, test yếu, release thủ công, boundary nghiệp vụ mờ
  • đau do kiến trúc thật sự chạm trần: team quá đông, workload quá lệch giữa các miền, yêu cầu scale và isolation không còn xử lý được bằng một khối duy nhất

Hai loại đau này trông giống nhau ở bề mặt vì cả hai đều gây chậm tiến độ. Nhưng cách chữa hoàn toàn khác nhau. Loại đầu cần refactor và modularize. Loại sau mới có thể cần tách service.

Một senior engineer giỏi không đo chất lượng kiến trúc bằng việc nó nhìn "enterprise" cỡ nào. Họ đo bằng việc chi phí vận hành và tốc độ học của tổ chức có đang tốt lên thật hay không.

🧭 Học theo lộ trình

TechCraft không hướng tới việc chia sẻ những mẹo kỹ thuật rời rạc.

Mục tiêu của TechCraft là xây dựng một lộ trình học giúp Developer từng bước phát triển từ người biết implement feature thành người có thể thiết kế, vận hành và mở rộng các hệ thống production.

Nếu bạn muốn tiếp tục hành trình đó, Dev Insider sẽ là điểm đến tiếp theo.

🚀 Dev Insider https://www.patreon.com/techcraft_official/posts/vi-sao-dev-ra-161163881?collection=2220113

📘 Facebook https://www.facebook.com/techcraft.official

🎥 YouTube https://www.youtube.com/@techcraft.official

🎵 TikTok https://www.tiktok.com/@techcraft.official

Hiểu hệ thống. Không chỉ framework.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí