Tối ưu CI/CD cho Hệ thống Microservices - Bài 7 - Quản lý Database Migration để đảm bảo Tương thích ngược (Backward Compatibility)
I. Tại sao Migration truyền thống lại "phá nát" Zero-Downtime?
Hãy nhớ lại Bài 6 về Blue-Green hoặc Canary Deployment: Version cũ (V1) và Version mới (V2) bắt buộc phải chạy song song trong một khoảng thời gian.
Giả sử ở V2, bạn quyết định đổi tên cột user_name thành full_name trong bảng users. Nếu bạn chạy file migration này ngay khi bắt đầu deploy, điều gì sẽ xảy ra?
-
Database cập nhật tên cột thành
full_name. -
Version V2 (đang nhận 5% traffic Canary) chạy trơn tru vì đã được code để đọc cột
full_name. -
Version V1 (đang gánh 95% traffic thực tế) lập tức sụp đổ hàng loạt với lỗi
Unknown column 'user_name', vì nó vẫn đang tìm kiếm cột cũ. Toàn bộ hệ thống downtime ngay lập tức.
Rõ ràng, trong microservices, bạn không bao giờ được phép thực hiện các thao tác phá hủy (Destructive Changes) như DROP COLUMN, RENAME COLUMN, hay ALTER COLUMN (đổi kiểu dữ liệu) trong một lần deploy duy nhất.
II. Giải pháp chuẩn mực: Mẫu thiết kế Expand & Contract (Mở rộng và Thu hẹp)
Để giữ cho cả V1 và V2 cùng sống hòa bình trên một Database, chúng ta phải chia một thao tác thay đổi Schema (ví dụ đổi tên cột) thành nhiều lần release khác nhau theo chiến lược Expand & Contract.
Giai đoạn 1: Expand (Mở rộng - Thêm mới, không xóa cũ)
-
Database: Tạo một migration để thêm cột mới
full_name. Tuyệt đối không xóa hay đổi tên cộtuser_namecũ. -
Code (Release V2): Cập nhật code sao cho:
-
Khi đọc dữ liệu: Ưu tiên đọc từ
full_name, nếu null thì fallback về đọcuser_name. -
Khi ghi dữ liệu: Ghi đồng thời vào cả 2 cột
user_namevàfull_name(Double-write).
-
-
Kết quả: V1 vẫn đọc/ghi bình thường vào
user_name. V2 xử lý được cả hai. Không ai bị lỗi.
Giai đoạn 2: Migrate Data (Đồng bộ dữ liệu nền)
- Chạy một background job (hoặc query SQL) để copy toàn bộ dữ liệu cũ từ
user_namesangfull_namecho các bản ghi cũ chưa được cập nhật.
Giai đoạn 3: Contract (Thu hẹp - Loại bỏ cái cũ)
-
Khi bạn đã chắc chắn 100% traffic đã chuyển sang V2 (cụm Blue đã bị hủy, Canary đã lên 100%), và không còn bất kỳ request nào gọi vào V1 nữa.
-
Code (Release V3): Xóa bỏ logic Double-write, code bây giờ chỉ thuần túy đọc/ghi vào
full_name. -
Database: Chạy một file migration cuối cùng để chính thức
DROP COLUMN user_name.
Quy trình này nghe có vẻ cồng kềnh, nhưng nó là cách duy nhất để hệ thống core (như tính tiền, vận đơn) chạy 24/7 mà không bao giờ bị gián đoạn.
III. Tích hợp Migration vào Pipeline CI/CD như thế nào?
Một sai lầm rất phổ biến là cấu hình cho ứng dụng tự động chạy lệnh migrate mỗi khi khởi động (ví dụ: gài npm run db:migrate vào CMD của Dockerfile). Khi áp dụng auto-scaling, 5 containers cùng khởi động sẽ tạo ra tình trạng Race Condition (tranh chấp) hoặc Lock Database, làm nghẽn toàn bộ kết nối.
Thực hành tốt nhất (Best Practice) trong CI/CD:
-
Tách Migration thành một Step độc lập trong Pipeline: Đặt bước chạy migration (như dùng Flyway, Liquibase, hoặc lệnh
migratecủa Laravel/Go) nằm ở khâu CD, TRƯỚC bước kích hoạt deploy code mới. -
Sử dụng CI/CD Runner để chạy: Chính hệ thống CI/CD (GitLab Runner/Bitbucket) sẽ là nơi thực thi câu lệnh SQL thay vì để container của ứng dụng tự chạy.
-
Bảo vệ bằng Backward Compatibility: Vì bạn đã áp dụng Expand & Contract, việc DB chạy migration trước hoàn toàn không làm ảnh hưởng đến ứng dụng V1 đang chạy trên production.
IV. Tổng kết bài học
Luật bất thành văn trong CI/CD Microservices: "Database thay đổi trước, Code thay đổi sau. Code thay đổi phải tương thích với Database cũ ít nhất 1 phiên bản". Nắm vững được tư duy này, bạn có thể tự tin deploy hàng chục lần mỗi ngày mà không sợ làm "cháy rạp".
All Rights Reserved