Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 45 — Database Migration: Online vs Offline

Operations Management for Tech Bài 45/60

Mở đầu — vì sao bài này quan trọng

Có một sự thật mà bất kỳ kỹ sư vận hành nào rồi cũng sẽ va phải: thứ khó thay đổi nhất trong một hệ thống không phải là code, mà là dữ liệu. Bạn có thể deploy lại một service chỉ trong vài giây, rollback một bản build lỗi cũng chỉ mất một câu lệnh. Nhưng khi bạn phải đổi cấu trúc bảng của một database đang phục vụ hàng triệu giao dịch, mọi thứ trở nên đáng sợ hơn nhiều — bởi vì dữ liệu là trạng thái, nó có tính lịch sử, và một sai lầm có thể khiến bạn mất dữ liệu vĩnh viễn, hoặc tệ hơn, làm sai lệch dữ liệu mà không ai phát hiện ra cho đến vài tháng sau.

Database migration — hiểu đơn giản là quá trình thay đổi cấu trúc (schema) hoặc di chuyển dữ liệu từ trạng thái này sang trạng thái khác — là một trong những kỹ năng phân biệt rõ ràng giữa một kỹ sư vận hành non tay và một người dày dạn. Người non tay chạy ALTER TABLE thẳng trên production lúc 2 giờ chiều rồi ngồi cầu nguyện. Người dày dạn biết chính xác câu lệnh đó sẽ khóa bảng bao lâu, ảnh hưởng đến những request nào, có kế hoạch rollback ra sao, và quan trọng nhất: biết khi nào nên chấp nhận downtime để làm cho đơn giản, khi nào phải làm zero-downtime cho phức tạp.

Trong bài này, chúng ta sẽ mổ xẻ hai mô hình migration cốt lõi — offline (dừng ứng dụng để migrate) và online (migrate không downtime) — hiểu rõ đánh đổi của từng cái, và học kỹ thuật kinh điển của online migration: dual-write, backfill, và switch-over. Đây là phần trọng tâm riêng của Bài 45; các chủ đề như read replica, sharding hay backup sẽ được bàn ở những bài khác.

Khái niệm cốt lõi

Migration là gì và tại sao lại khó

Khi nói đến database migration, người ta thường gộp chung hai loại thay đổi khác bản chất:

  • Schema migration — thay đổi cấu trúc: thêm/xóa cột, đổi kiểu dữ liệu, thêm index, đổi tên bảng, tách bảng.
  • Data migration — di chuyển hoặc biến đổi bản thân dữ liệu: chuyển từ database này sang database khác, chuẩn hóa lại dữ liệu, chia dữ liệu cũ vào bảng mới.
Điều làm migration khó nằm ở chỗ database phải phục vụ traffic trong lúc bạn thay đổi nó. Nếu được dừng cả hệ thống lại, migration nào cũng dễ. Cái khó là làm sao thay đổi mà người dùng không cảm nhận được sự gián đoạn.

Mô hình 1 — Offline migration

Offline migration là cách trực tiếp và đơn giản nhất: dừng ứng dụng → chạy migration → khởi động lại ứng dụng. Trong khoảng thời gian migration chạy, hệ thống không phục vụ traffic (hoặc trả về trang bảo trì).

Quy trình điển hình:

  • Bật trang maintenance ("Hệ thống đang bảo trì, vui lòng quay lại sau").
  • Dừng các service ghi vào database (hoặc chuyển sang read-only).
  • Chạy các câu lệnh migration.
  • Kiểm tra tính đúng đắn của dữ liệu.
  • Khởi động lại service với code mới.
Ưu điểm: đơn giản, dễ suy luận, dễ rollback (chỉ cần restore snapshot trước migration rồi bật lại code cũ). Không có tình trạng dữ liệu "nửa cũ nửa mới" gây nhức đầu. Rất phù hợp khi thay đổi phức tạp, khi bảng dữ liệu nhỏ, hoặc khi hệ thống được phép có cửa sổ bảo trì.

Nhược điểm: có downtime. Với hệ thống thương mại điện tử, ngân hàng, hay bất cứ dịch vụ nào tính bằng doanh thu mỗi phút, downtime là chi phí trực tiếp. Ngoài ra, nếu migration chạy lâu hơn dự kiến (điều rất hay xảy ra với bảng lớn), cửa sổ bảo trì có thể phình to ngoài kiểm soát.

Mô hình 2 — Online migration (zero-downtime)

Online migration là kỹ thuật thay đổi database mà không dừng hệ thống. Người dùng vẫn đọc/ghi bình thường trong suốt quá trình. Đây là chuẩn mực bắt buộc ở các công ty vận hành 24/7.

Trái tim của online migration là nguyên tắc expand–contract (còn gọi là parallel change): bạn không thay đổi trực tiếp từ trạng thái A sang B, mà đi qua một trạng thái trung gian tương thích ngược, nơi cả cấu trúc cũ và mới cùng tồn tại. Ba kỹ thuật cốt lõi:

  • Dual-write (ghi kép): trong giai đoạn chuyển tiếp, ứng dụng ghi dữ liệu vào cả vị trí cũ lẫn vị trí mới. Nhờ đó dữ liệu mới luôn được cập nhật, sẵn sàng cho lúc switch-over.
  • Backfill (lấp dữ liệu cũ): dual-write chỉ xử lý dữ liệu ghi từ thời điểm bật nó trở đi. Còn hàng triệu bản ghi cũ thì sao? Bạn phải chạy một job nền để sao chép/biến đổi dữ liệu lịch sử từ cấu trúc cũ sang mới, theo từng lô nhỏ (batch), để không làm nghẽn database.
  • Switch-over (chuyển đổi): khi dữ liệu mới đã đầy đủ và được kiểm chứng khớp với dữ liệu cũ, bạn chuyển đường đọc (read path) sang cấu trúc mới, rồi cuối cùng tắt việc ghi vào cấu trúc cũ và dọn dẹp.
Ưu điểm: không downtime. Nhược điểm: phức tạp hơn nhiều lần, phải viết thêm code dual-write, có giai đoạn dữ liệu tồn tại ở hai nơi (rủi ro lệch dữ liệu), và cần công cụ để so sánh, xác thực. Quy trình có thể kéo dài nhiều ngày đến nhiều tuần.

Trường hợp đặc biệt: đổi schema trên bảng lớn

Ngay cả khi không đổi database, chỉ một câu ALTER TABLE trên bảng vài chục triệu dòng cũng có thể khóa bảng hàng chục phút với MySQL đời cũ. Đây là lý do tồn tại các công cụ online schema change như pt-online-schema-change (Percona), gh-ost (GitHub) cho MySQL, hay pg_repack cho PostgreSQL. Chúng tạo một bảng "bóng" (shadow table) với cấu trúc mới, copy dữ liệu sang dần dần, đồng bộ thay đổi mới qua trigger hoặc binlog, rồi tráo tên bảng ở phút chót. Về bản chất, đó chính là dual-write + backfill + switch-over được đóng gói sẵn.

Tình huống thực tế

Ví dụ 1 — Sàn TMĐT đổi kiểu cột order_id giữa mùa sale

Một sàn thương mại điện tử tại TP.HCM (giả định, gọi là ShopViet) có bảng orders với khoảng 80 triệu dòng. Cột order_id ban đầu để kiểu INT (giới hạn ~2,1 tỷ). Team dự báo rằng với đà tăng trưởng, trong 8 tháng nữa số ID sẽ tràn khỏi INT — một quả bom hẹn giờ. Họ cần đổi sang BIGINT.

Ban đầu team định làm offline: bật maintenance lúc 2 giờ sáng Chủ nhật, chạy ALTER TABLE orders MODIFY order_id BIGINT. Nhưng khi test trên bản sao staging, câu lệnh này khóa bảng và chạy mất hơn 40 phút vì MySQL phải rebuild lại toàn bộ bảng. 40 phút downtime cho một sàn có đơn hàng liên tục là không chấp nhận được, đặc biệt gần đợt sale 12/12.

Họ chuyển sang dùng gh-ost. Công cụ tạo bảng bóng _orders_gho với order_id BIGINT, đọc binlog để bắt mọi thay đổi realtime, copy dữ liệu theo từng lô 1000 dòng với throttle tự động khi tải database cao. Sau khoảng 3 giờ chạy nền (không downtime), khi bảng bóng đã đồng bộ, gh-ost thực hiện cú tráo bảng nguyên tử chỉ trong dưới 1 giây.

Bài học: downtime dự kiến trên giấy tờ thường khác xa thực tế trên bảng lớn. Luôn đo thời gian migration trên bản sao production trước. Và với thay đổi schema nặng trên bảng lớn, công cụ online schema change gần như luôn thắng cách chạy ALTER trực tiếp.

Ví dụ 2 — Startup fintech tách bảng users bằng dual-write

Một startup fintech ở Singapore phục vụ khách Đông Nam Á cần tách bảng users khổng lồ (thông tin đăng nhập + hồ sơ KYC + cài đặt) thành ba bảng riêng để dễ phân quyền và tối ưu. Đây là data migration đúng nghĩa, không thể làm bằng một câu ALTER. Hệ thống chạy 24/7, không có cửa sổ bảo trì.

Họ áp dụng đúng quy trình expand–contract kéo dài 10 ngày:

  • Ngày 1: deploy code dual-write — mọi thao tác cập nhật user vừa ghi vào bảng users cũ, vừa ghi vào ba bảng mới. Read vẫn đọc từ bảng cũ.
  • Ngày 2–5: chạy backfill job, copy 12 triệu user cũ sang cấu trúc mới theo lô 5000 dòng, có sleep giữa các lô để không đè database.
  • Ngày 6: chạy job so sánh (reconciliation), đối chiếu từng bản ghi cũ và mới. Phát hiện 0,3% bản ghi lệch do một số user đăng ký từ luồng cũ chưa qua dual-write — họ fix và backfill lại phần lệch.
  • Ngày 7–8: chuyển 5% read traffic sang bảng mới (dark read + so sánh kết quả), tăng dần lên 100%.
  • Ngày 9: tắt dual-write, chỉ còn ghi vào bảng mới.
  • Ngày 10: đổi tên bảng users cũ thành users_deprecated, giữ 30 ngày như bản backup sống trước khi drop.
Bài học: online migration không phải một sự kiện, mà là một quy trình nhiều pha có kiểm chứng ở mỗi bước. Bước reconciliation là bước cứu mạng — nếu không có nó, họ đã chuyển sang bảng mới với 0,3% dữ liệu sai mà không hề hay biết.

Ví dụ 3 — Cái giá của việc bỏ qua rollback plan

Một team tại một công ty logistics ở Hà Nội cần thêm cột delivery_zone vào bảng shipments và migrate dữ liệu dựa trên địa chỉ. Họ tự tin làm online, viết dual-write, chạy backfill. Nhưng backfill job có bug: nó phân loại sai zone cho các đơn ở khu vực giáp ranh, và họ đã switch-over đọc sang cột mới trước khi kiểm chứng kỹ.

Hậu quả: trong 6 tiếng, khoảng 4.000 đơn bị định tuyến sai kho, gây trễ giao hàng. Tệ hơn, họ không giữ lại giá trị cũ để đối chiếu, và không có cách rollback nhanh vì code mới đã phụ thuộc cột mới. Họ phải viết gấp một job sửa dữ liệu và mất gần một ngày dọn dẹp.

Bài học: switch-over phải là bước cuối cùng, chỉ thực hiện sau khi đã xác thực dữ liệu mới đúng. Và luôn phải trả lời được câu hỏi "nếu sai thì rollback thế nào?" trước khi bắt đầu, chứ không phải khi đang cháy nhà.

Hướng dẫn từng bước

Đây là quy trình chuẩn để tiếp cận bất kỳ migration nào:

  • Phân loại thay đổi. Đây là schema migration đơn giản (thêm cột nullable, thêm index) hay là thay đổi nặng (đổi kiểu cột, tách bảng, đổi database)? Thay đổi nhỏ tương thích ngược thì đơn giản; thay đổi lớn cần expand–contract.
  • Quyết định offline hay online. Trả lời ba câu hỏi: Hệ thống có được phép downtime không? Bảng lớn cỡ nào? Thay đổi có tương thích ngược không? Nếu được phép downtime ngắn và bảng nhỏ → chọn offline cho nhẹ đầu. Nếu không được downtime hoặc bảng lớn → online.
  • Đo lường trên bản sao production. Chạy thử migration trên staging có dữ liệu giống production về kích thước. Đo thời gian, đo mức khóa bảng. Đừng bao giờ đoán mò con số này.
  • Với online: triển khai expand. Deploy code dual-write (ghi cả cũ lẫn mới) hoặc dùng công cụ online schema change. Đảm bảo code mới vẫn đọc từ cấu trúc cũ.
  • Backfill dữ liệu lịch sử. Chạy job theo lô nhỏ, có throttle/sleep giữa các lô để tránh đè tải. Ghi log tiến độ để có thể resume nếu job dừng giữa chừng.
  • Reconcile — đối chiếu. So sánh dữ liệu cũ và mới, đếm số dòng, kiểm mẫu ngẫu nhiên hoặc đối chiếu toàn bộ. Chỉ đi tiếp khi tỷ lệ khớp đạt ngưỡng chấp nhận (lý tưởng là 100%).
  • Switch-over dần dần. Chuyển read path sang cấu trúc mới theo phần trăm tăng dần (canary), quan sát lỗi và metric. Nếu có vấn đề, chuyển ngược lại ngay.
  • Contract — dọn dẹp. Tắt dual-write, ngưng ghi vào cấu trúc cũ. Giữ cấu trúc cũ thêm một thời gian như lưới an toàn, rồi mới xóa hẳn.
  • Chuẩn bị rollback ở mọi bước. Trước mỗi giai đoạn, viết rõ nếu hỏng thì lùi lại thế nào. Snapshot database trước khi bắt đầu là bắt buộc.

Lỗi thường gặp & mẹo

  • Chạy ALTER TABLE trực tiếp trên bảng lớn ở production. Đây là sai lầm kinh điển gây khóa bảng và outage. Với bảng lớn, luôn dùng công cụ online schema change (gh-ost, pt-online-schema-change, pg_repack).
  • Backfill "một phát ăn ngay". Cố copy hàng chục triệu dòng trong một transaction sẽ làm nổ transaction log, khóa hàng loạt và có thể sập database. Luôn chia lô nhỏ và throttle.
  • Switch-over trước khi xác thực. Như Ví dụ 3, chuyển đọc sang dữ liệu mới khi chưa reconcile là công thức cho thảm họa thầm lặng. Reconcile trước, switch-over sau.
  • Quên xử lý ghi trong lúc backfill. Nếu bạn backfill mà không bật dual-write, mọi thay đổi diễn ra trong lúc backfill sẽ bị mất ở cấu trúc mới. Dual-write phải bật trước backfill.
  • Không có kế hoạch rollback. Nhiều team chỉ nghĩ đến kịch bản thành công. Migration là nơi kịch bản thất bại phải được viết ra trước.
  • Xóa cấu trúc cũ quá sớm. Đừng drop bảng/cột cũ ngay sau switch-over. Giữ lại vài ngày đến vài tuần làm lưới an toàn — nó gần như miễn phí mà cứu bạn khi phát hiện lỗi muộn.
  • Mẹo — tương thích ngược là chìa khóa. Luôn thiết kế mỗi bước sao cho code cũ và code mới đều chạy được với schema hiện tại. Đó là lý do ta thêm cột nullable trước, rồi mới điền dữ liệu, rồi mới bắt buộc NOT NULL ở bước sau — chứ không làm tất cả một lúc.
  • Mẹo — feature flag cho switch-over. Dùng cờ để bật/tắt việc đọc từ cấu trúc mới. Nếu có sự cố, lật cờ về là rollback tức thì mà không cần deploy lại.

Bài tập thực hành

  • Phân loại quyết định. Cho ba tình huống: (a) thêm cột phone_verified kiểu boolean nullable vào bảng users 500 nghìn dòng; (b) đổi kiểu cột amount từ INT sang DECIMAL trên bảng transactions 200 triệu dòng của một ví điện tử chạy 24/7; (c) thêm index vào bảng logs 5 triệu dòng cho phép bảo trì 10 phút ban đêm. Với mỗi tình huống, quyết định offline hay online và giải thích lý do.
  • Thiết kế quy trình dual-write. Viết ra các pha (theo dạng dòng thời gian như Ví dụ 2) để tách trường address dạng text trong bảng customers thành một bảng addresses riêng, cho hệ thống không được downtime. Chỉ rõ ở pha nào bật dual-write, backfill, reconcile, switch-over.
  • Tìm lỗi. Một đồng nghiệp đề xuất: "Mình chạy backfill copy dữ liệu sang bảng mới trong 2 ngày, xong rồi mới deploy dual-write." Chỉ ra sai sót trong thứ tự này và giải thích hậu quả.
  • Viết checklist rollback. Cho một migration online đổi kiểu cột bằng gh-ost, liệt kê 4–5 bước cần chuẩn bị để có thể rollback an toàn ở bất kỳ giai đoạn nào.

Tóm tắt

Database migration là bài toán thay đổi thứ khó thay đổi nhất trong hệ thống — dữ liệu có trạng thái. Có hai mô hình cốt lõi: offline (dừng ứng dụng để migrate — đơn giản, dễ rollback, nhưng có downtime, hợp với bảng nhỏ và hệ thống cho phép bảo trì) và online (zero-downtime — phức tạp nhưng bắt buộc với hệ thống 24/7).

Trái tim của online migration là nguyên tắc expand–contract: đi qua trạng thái trung gian tương thích ngược bằng ba kỹ thuật dual-write → backfill → switch-over, với bước reconcile (đối chiếu dữ liệu) là chốt an toàn không được bỏ qua. Với thay đổi schema nặng trên bảng lớn, hãy dùng công cụ online schema change thay vì ALTER trực tiếp.

Ba nguyên tắc để nhớ suốt đời: luôn đo migration trên bản sao production trước; luôn xác thực dữ liệu trước khi switch-over; và luôn có kế hoạch rollback được viết ra trước khi bắt đầu. Migration không phải chỗ để thử vận may — nó là chỗ để kỷ luật và sự chuẩn bị kỹ lưỡng lên tiếng.