Hướng dẫn sử dụng gh-ost để thực hiện Online Schema Migration trên MySQL: Nâng cấp cấu trúc bảng lớn mà không gây khóa dữ liệu và gián đoạn dịch vụ

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Khi câu lệnh ALTER TABLE trở thành ác mộng

Hồi đầu xây dựng hệ thống, bảng orders chỉ có vài nghìn dòng. ALTER TABLE chạy xong trong vài giây, không ai quan tâm. Nhưng rồi hệ thống lớn dần — bảng đó giờ có 50 triệu rows, kích thước 80GB. Hôm nay PM yêu cầu thêm cột payment_method.

Bạn chạy thử trên staging: ALTER TABLE orders ADD COLUMN payment_method VARCHAR(50). Câu lệnh chạy 45 phút, và trong suốt thời gian đó, mọi INSERT/UPDATE vào bảng orders đều bị block hoàn toàn. Trên production, điều đó có nghĩa là service down 45 phút — khách hàng không đặt được hàng, revenue về 0.

gh-ost ra đời để xử lý đúng cái tình huống này.

gh-ost là gì và cơ chế hoạt động

gh-ost (GitHub Online Schema Transmogrifier) là công cụ GitHub tự phát triển để ALTER TABLE trên MySQL mà không gây lock. Thay vì chạy ALTER trực tiếp, gh-ost làm theo 4 bước:

  1. Tạo một bảng mới (ghost table) với schema mong muốn
  2. Copy dữ liệu từ bảng gốc sang bảng ghost theo từng batch nhỏ
  3. Trong lúc copy, theo dõi mọi thay đổi trên bảng gốc qua MySQL binary log và áp lên bảng ghost ngay lập tức
  4. Khi đã đồng bộ hoàn toàn, hoán đổi hai bảng (cutover) — toàn bộ bước này chỉ mất vài giây

Tại sao dùng binlog thay vì trigger?

Trước đây nhiều người dùng pt-online-schema-change (Percona Toolkit) — tool này dùng trigger để bắt các thay đổi. Vấn đề: trigger tạo overhead lớn trên write-heavy table, và trên hệ thống tải cao thì đôi khi còn gây deadlock. gh-ost bỏ hẳn trigger, đọc binlog trực tiếp như một MySQL replica giả. Kết quả là:

  • Không có trigger nào tồn tại trên bảng gốc — overhead thấp hơn nhiều
  • Có thể throttle tốc độ copy theo thời gian thực mà không dừng toàn bộ quá trình
  • Có thể pause và resume migration giữa chừng bằng cách tạo/xóa một file
  • Có thể kiểm tra trước trên replica mà không đụng đến production

Mình đã từng dùng pt-osc trên bảng transaction 30 triệu rows — trigger overhead đẩy load server lên đến mức phải throttle liên tục, mất gần 8 tiếng mới xong. Cùng quy mô đó với gh-ost, load tăng kiểm soát được và chạy xong trong 3 tiếng.

Cài đặt gh-ost

gh-ost là binary Go duy nhất, không phụ thuộc gì thêm:

# Download binary mới nhất
wget https://github.com/github/gh-ost/releases/latest/download/gh-ost-binary-linux-amd64.tar.gz
tar -xzf gh-ost-binary-linux-amd64.tar.gz
chmod +x gh-ost
sudo mv gh-ost /usr/local/bin/

# Kiểm tra
gh-ost --version

MySQL cũng cần bật binlog ở format ROW:

-- Kiểm tra
SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'log_bin';

-- Nếu chưa đúng, thêm vào /etc/mysql/my.cnf rồi restart MySQL
-- [mysqld]
-- log_bin = ON
-- binlog_format = ROW

Thực hành: migrate bảng thật trên production

Giả sử bảng user_activities có 60 triệu rows, bạn cần thêm cột và một composite index.

Bước 1: Dry-run để kiểm tra kết nối và quyền

Luôn chạy không có --execute trước. gh-ost sẽ kiểm tra mọi thứ nhưng không thực sự thay đổi gì:

gh-ost \
  --host="localhost" \
  --port=3306 \
  --user="migration_user" \
  --password="your_password" \
  --database="myapp" \
  --table="user_activities" \
  --alter="ADD COLUMN device_type VARCHAR(20), ADD INDEX idx_user_created (user_id, created_at)" \
  --allow-on-master \
  --chunk-size=1000 \
  --max-load="Threads_running=25" \
  --critical-load="Threads_running=50" \
  --verbose

Bước 2: Chạy migration thật

Thêm --execute và một số flag để kiểm soát tốt hơn:

gh-ost \
  --host="localhost" \
  --port=3306 \
  --user="migration_user" \
  --password="your_password" \
  --database="myapp" \
  --table="user_activities" \
  --alter="ADD COLUMN device_type VARCHAR(20), ADD INDEX idx_user_created (user_id, created_at)" \
  --allow-on-master \
  --chunk-size=2000 \
  --max-load="Threads_running=25" \
  --critical-load="Threads_running=50" \
  --default-retries=120 \
  --panic-flag-file=/tmp/gh-ost.panic \
  --postpone-cut-over-flag-file=/tmp/gh-ost.postpone \
  --verbose \
  --execute 2>&1 | tee /var/log/gh-ost-migration.log

Bước 3: Kiểm soát migration đang chạy

Khác với pt-osc hay nhiều tool migration khác, gh-ost không yêu cầu kill process để điều chỉnh. Toàn bộ kiểm soát thực hiện qua Unix socket hoặc file hệ thống:

# Xem tiến độ realtime qua Unix socket
echo "status" | nc -U /tmp/gh-ost.myapp.user_activities.sock
# Output: Copy: 24500000/60000000 40.8%; Applied: 125000; Backlog: 0/1000; Time: 2h34m; ETA: 3h41m

# Trì hoãn cutover: gh-ost vẫn copy và đọc binlog, nhưng chưa swap bảng
touch /tmp/gh-ost.postpone

# Cho phép cutover tiến hành
rm /tmp/gh-ost.postpone

# Giảm chunk size khi thấy load tăng
echo "chunk-size=500" | nc -U /tmp/gh-ost.myapp.user_activities.sock

# Emergency stop ngay lập tức
touch /tmp/gh-ost.panic

Flag --postpone-cut-over-flag-file đặc biệt hữu ích: khi file đó tồn tại, gh-ost sẽ hoàn thành copy dữ liệu nhưng chưa swap bảng. Bạn chờ đến 2-3 giờ sáng (traffic thấp nhất), xóa file đó — cutover xảy ra trong vài giây. Cách này cho phép bạn chủ động chọn thời điểm cutover, thay vì để gh-ost tự quyết ngay khi copy xong.

Quyền MySQL cần thiết

GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER,
      INDEX, SUPER, REPLICATION SLAVE, REPLICATION CLIENT
ON *.* TO 'migration_user'@'localhost' IDENTIFIED BY 'your_password';
FLUSH PRIVILEGES;

Những lỗi hay gặp và cách xử lý

Foreign key constraints

Chạy dry-run sẽ thấy ngay nếu bảng có foreign key — gh-ost không hỗ trợ và dừng lại trước khi làm gì. Cách xử lý phổ biến nhất: drop FK trước, chạy migration, recreate FK sau khi cutover xong. Nếu không thể drop FK tạm thời (vì các team khác đang depend vào constraint đó), xem xét application-level migration thay thế.

Binlog bị purge quá sớm

Vấn đề ít ai chú ý: gh-ost đọc binlog liên tục suốt quá trình migration. Nếu MySQL purge binlog trước khi gh-ost kịp đọc (do expire_logs_days quá thấp), migration fail giữa chừng. Trước khi chạy migration dài, tăng retention:

-- MySQL 8.0+
SET GLOBAL binlog_expire_logs_seconds = 604800;  -- 7 ngày

-- MySQL 5.7
SET GLOBAL expire_logs_days = 7;

Không đủ disk space

Bảng ghost chiếm disk xấp xỉ bảng gốc — cần tối thiểu 1.2× không gian còn trống trước khi bắt đầu. Kiểm tra nhanh:

df -h /var/lib/mysql

Khi nào nên dùng gh-ost?

gh-ost giải quyết đúng một bài toán: ALTER TABLE trên bảng lớn trong khi service vẫn chạy. Nếu bảng dưới 1-2 triệu rows, ALTER TABLE thông thường chạy đủ nhanh trong maintenance window ngắn — không cần thêm độ phức tạp.

Nhưng nếu bảng vài chục triệu rows trở lên, service 24/7, hoặc cần thêm index lớn — gh-ost là lựa chọn đáng tin cậy nhất cho MySQL. Hồi migrate database 100GB từ MySQL sang PostgreSQL, mình mất 3 ngày planning và 1 ngày thực thi — phần lớn thời gian planning là xác định thứ tự migrate các bảng phụ thuộc nhau. gh-ost không giải quyết được bài toán dependency giữa các bảng — cái đó bạn vẫn phải tự plan. Nhưng vấn đề lock thì nó xử lý gọn.

Một lưu ý: gh-ost chỉ hỗ trợ MySQL (và MariaDB với một số hạn chế). Nếu đang dùng PostgreSQL, cần tìm tool khác như pg_repack hoặc xử lý qua application-level migration với zero-downtime deployment.

Share: