Tuần rồi mình có viết bài Không ai scale được microservices nếu chưa từng scale shared database có ông bạn (Chắc là Dev) bình luận ở bài viết trên Facebook mình nói là:
“Mình cũng gặp vấn đề tương tự và cũng đau đầu tìm phương án nào nhanh hiệu quả, chứ hệ thống lớn mà đang code không có CQRS, việc migrate sang quá risk. Bài viết chỉ nêu ra vấn đề chung chung chứ không có case cụ thể và phương án hiệu quả rõ ràng.”
Mình cũng từ Dev qua DevOps nên cũng hiểu cho bác ấy. Nay rảnh, mình xin kể case thật và phương án từng làm cho anh em. Có thể chưa phải best practice đâu, nhưng là thứ đã Survive Production thật, on-call thật.
Thực trạng: Monolith phình to, đọc ghi đập nhau vỡ đầu
Công ty cũ của mình trước đây có cái hệ thống quản lý tài khoản người dùng. To tổ bố, tất cả truy vấn từ check user info, auth, reset pass đến update profile, ghi log cùng đập vô 1 bảng users
. ORM thôi, PostgreSQL. Mỗi lần push feature mới kiểu thêm trạng thái, refactor logic auth là tỉ lệ lỗi tăng vọt vì clash schema. Việc scale đọc thì caching không cứu nổi vì data volatile, còn ghi thì conflict vô số.
Mình không có CQRS (Không biết viết trên này anh em nào là DevOps hay Sysadmin sang DevOps có hiểu không, nếu không thì anh em research phát cho hiểu nhé). Mọi request đều xử lý qua controller => service => repository => DB, kể cả những request đơn giản như “lấy info user X”. Gọi là fat service, logic đập hết vào 1 cục.
Vấn đề: Migrate sang CQRS “đúng bài” là tự sát
Câu hỏi lúc đó là:
“Bây giờ refactor toàn bộ hệ thống theo CQRS là mơ giữa ban ngày. Có cách nào adopt dần, không gãy production?”
Mình chọn hướng thủ cho chắc ăn, không đổi design tổng thể, nhưng làm 2 việc:
Phương án 1: Bóc Query ra trước bằng Read-Only Microservice
Việc đầu tiên là bóc hết các truy vấn “sạch” kiểu: get profile, list history, check status – ra một service riêng tên là user-reader
. Service này read-only, không có quyền ghi DB, chỉ query được thôi.
DB vẫn shared lúc đầu, nhưng dần dần, mình gắn thêm caching, sau đó sync qua Elastic để tránh query vào core DB. Cứ mỗi lần push feature query mới, không cho nó vô service cũ nữa, mà buộc phải đi qua user-reader
.
=> Về bản chất: mình đang “bóc query” ra khỏi command một cách thầm lặng.
Phương án 2: Bắt đầu từ những chỗ có “Write Conflict”
Khi làm feature ghi mới mà logic phức tạp, mình không patch vô monolith nữa, mà viết command handler riêng. Dữ liệu đẩy vào message queue (Kafka), và consumer thực hiện thao tác ghi có khi vào bảng riêng, có khi vào cùng bảng nhưng schema tách biệt.
Lúc đầu ghép 2 mô hình chạy song song, khá lủng củng, nhưng sau vài sprint thì pattern CQRS dần hình thành: đọc đi một chỗ, ghi đi chỗ khác.
Kết quả: Không phải “CQRS chuẩn mực” nhưng sống sót thật
- Khoảng 5 tháng (6 7 tháng khoảng khoảng đó) sau khi refactor từng phần, 70% lượng query đi qua
user-reader
, và query load giảm 60% trên DB chính. - Các đoạn ghi phức tạp (audit log, update batch,…) đều được đẩy qua message queue, dễ retry, không timeout.
- Khi có sự cố production, vì hệ thống đã tách command và query nên biết ngay thằng nào đang choke không cần soi log như mò kim đáy bể.
Chắc anh em nào làm rồi sẽ hiểu nhỉ
Nên nếu anh em nào đang đau đầu vì “muốn làm CQRS mà system cũ quá, migrate không nổi”, thì lời khuyên của mình là:
- Đừng đập bỏ, hãy chia nhỏ.
- Đừng refactor, hãy “cấy ghép”.
- Đừng lý tưởng hoá CQRS, hãy làm nó sống chung với system cũ.
Nói chung là:
- Muốn học architecture thì đọc paper là đúng,
- Muốn làm production thì phải lầy từng dòng code, từng service nhỏ.
Kết luận lại: Không ai dám migrate CQRS một phát cả, nhưng vẫn có cách làm từng bước mà không chết.
Mình chia sẻ case vậy, nếu các bác có chiến thuật hay hơn, hay từng làm khác đi, cứ góp ý. Mình học thêm.