Chuyển Đổi Từ Monolith Sang Microservices Mà Không Ngừng Giao Tính Năng
Một playbook thực tế cho việc tách service từng bước — cùng danh sách thẳng thắn những trường hợp mà việc phá vỡ monolith là một quyết định hoàn toàn sai lầm.
Mọi đề xuất chuyển đổi từ monolith sang microservices đều đi kèm một slide vẽ kiến trúc đích. Gần như không đề xuất nào đi kèm kế hoạch cho việc đội ngũ sẽ ship gì trong sáu tháng để đến được đích đó. Khoảng trống đó là nơi phần lớn các dự án chuyển đổi này hoặc bị đình trệ vô thời hạn hoặc vượt ngân sách — không phải vì kiến trúc đích sai, mà vì không ai lên kế hoạch cho việc tiếp tục ship tính năng trên một codebase đang bị cắt xẻ bên dưới.
Bắt đầu bằng việc tự hỏi liệu bạn có thực sự gặp vấn đề với monolith hay không
Trước bất kỳ kế hoạch tách service nào, cần thành thật về nguyên nhân thực sự gây ra khó khăn. Chúng tôi từng tiếp nhận những dự án chuyển đổi đang giải quyết sai vấn đề. Những triệu chứng thực sự chỉ ra vấn đề kiến trúc monolith:
- Ghép nối trong deploy — một thay đổi ở module báo cáo yêu cầu deploy lại và test lại toàn bộ ứng dụng, khiến nhịp độ release bị nghẽn bởi phần rủi ro nhất của hệ thống.
- Lệch pha về scaling — một thành phần (chẳng hạn xử lý ảnh) cần gấp 10 lần compute so với phần còn lại, nhưng bạn phải scale toàn bộ monolith chỉ để nuôi nó.
- Tranh chấp giữa các đội — nhiều đội cùng đụng vào một codebase và xung đột merge hay bug do shared-state xảy ra hàng tuần.
Những triệu chứng trông giống vấn đề monolith nhưng thường không phải: build chậm (thường khắc phục được bằng công cụ build tốt hơn hoặc modular hóa bên trong monolith), quyền sở hữu không rõ ràng (vấn đề về sơ đồ tổ chức, không phải kiến trúc), và các phàn nàn chung chung kiểu "code này lộn xộn quá" (vấn đề refactor, không phải vấn đề phân tán). Chúng tôi đã khuyên ít nhất hai khách hàng mỗi năm từ bỏ việc chuyển hoàn toàn sang microservices sau khi đào sâu vào nguyên nhân thực sự làm họ chậm lại — một monolith dạng module với ranh giới nội bộ rõ ràng đã giải quyết được vấn đề với chi phí vận hành thấp hơn nhiều.
Tách theo những đường ranh giới đã sẵn có
Mẫu hình strangler fig hiệu quả, nhưng chỉ khi bạn chọn những ứng viên tách service đã có ranh giới tương đối sạch sẽ. Cố gắng tách một service mà logic đan xen với năm module khác sẽ biến một cuộc tách kéo dài 6 tuần thành 6 tháng.
Chúng tôi tìm ba tín hiệu khi chọn service đầu tiên (và thứ hai) để tách:
- Mô hình dữ liệu có ranh giới rõ ràng — dữ liệu của module không bị truy vấn hay join trực tiếp bởi các phần không liên quan trong codebase.
- Giao diện ổn định — bề mặt công khai của module (chữ ký hàm hay các lời gọi API) gần đây không thay đổi nhiều, nghĩa là ít rủi ro cuộc chiến tách service diễn ra trên nền đất đang di chuyển.
- Nhu cầu scale hoặc release độc lập — module thực sự được lợi khi deploy và scale riêng, đây chính là điều biện minh cho chi phí vận hành của một service mới ngay từ đầu.
Gửi thông báo/email, tạo PDF hoặc báo cáo, và đánh chỉ mục tìm kiếm là những ứng viên tách sớm phổ biến vì chúng thường thỏa cả ba tiêu chí. Logic nghiệp vụ cốt lõi như xử lý đơn hàng hay quản lý tài khoản gần như không bao giờ nên tách trước — ranh giới của chúng thường vẫn còn mờ và bán kính ảnh hưởng nếu làm sai là lớn nhất.
Giữ cho lớp routing của strangler fig thật đơn giản
Lớp routing quyết định một request đi vào đường monolith cũ hay service mới nên đơn giản hết mức có thể — một feature flag hay một quy tắc router, không phải logic nghiệp vụ mới. Chúng tôi từng thấy các đội xây dựng routing thích ứng thông minh mà bản thân nó lại trở thành gánh nặng bảo trì và nguồn gốc của bug. Routing đơn giản, có thể đảo ngược luôn thắng routing thông minh trong quá trình chuyển đổi.
Giữ đồng bộ hai phiên bản của sự thật trong quá trình chuyển tiếp
Phần khó nhất của một cuộc chuyển đổi khi hệ thống vẫn đang chạy không phải là tách code, mà là tính nhất quán dữ liệu trong giai đoạn cả đường cũ và đường mới đều có thể được gọi tới. Hai cách tiếp cận hiệu quả trong thực tế:
- Ghi kép kèm đối soát (dual writes with reconciliation). Ghi vào cả database của monolith và kho dữ liệu của service mới, rồi chạy một job hàng đêm so sánh hai bên, cảnh báo khi có lệch. Cách này bắt được bug trong đường mới trước khi nó được tin tưởng là nguồn sự thật.
- Change data capture (CDC). Truyền các thay đổi từ database của monolith (qua công cụ như Debezium) vào service mới, để service mới duy trì tính nhất quán cuối cùng mà không cần ghi kép ở tầng ứng dụng. Cách này scale tốt hơn cho các bảng có khối lượng lớn nhưng thêm độ phức tạp vận hành của việc chạy một pipeline CDC.
Dù chọn cách nào, hãy xác định rõ ràng hệ thống nào là nguồn sự thật ở mỗi giai đoạn, và đừng chuyển công tắc đó cho đến khi việc đối soát chạy sạch trong một khoảng thời gian đủ dài — chúng tôi thường muốn ít nhất hai tuần liền không có lệch pha không giải thích được trước khi chuyển traffic cho bất cứ thứ gì liên quan đến tiền hay dữ liệu nhạy cảm về tuân thủ.
Bảo vệ tốc độ giao tính năng bằng cách chia đội theo hai luồng song song
Sai lầm về tổ chức giết chết tốc độ giao tính năng trong quá trình chuyển đổi là kéo toàn bộ đội vào việc tách service trong "vài sprint ngắn". Nó không bao giờ ngắn, và công việc tính năng bị đình trệ, làm xói mòn niềm tin của các bên liên quan vào chính cuộc chuyển đổi.
Thay vào đó, hãy chia đội: một cặp luân phiên đảm nhận việc tách service trong khi phần lớn còn lại tập trung giao tính năng, với cặp tách service luân phiên mỗi 2-3 sprint để kiến thức về chuyển đổi không tập trung vào một hai người rồi biến họ thành điểm nghẽn. Cách này chậm hơn về mặt thời gian thực so với dồn toàn lực, nhưng đó là khác biệt giữa một cuộc chuyển đổi hoàn thành và một cuộc chuyển đổi bị gác lại khi ban lãnh đạo hỏi tại sao không có gì mới được ship trong cả một quý.
Cân bằng đúng chỗ này — đủ tập trung để có tiến độ thực chất, nhưng không nhiều đến mức lộ trình sản phẩm bị đình trệ — là một trong những lý do phổ biến khiến khách hàng thuê một đội ngũ chuyên trách riêng cho luồng chuyển đổi, để đội ngũ nòng cốt được rảnh tay tiếp tục ship theo lộ trình hiện có.
Huy Hoàng
Kỹ sư Chính, Backend