MySQL Schema Migrations ở quy mô lớn: Tìm hiểu chuyên sâu về gh-ost

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

Metadata Lock lúc 3 giờ sáng: Một thực tế trong môi trường Production

Bạn đang thêm một cột nullable duy nhất vào bảng orders dung lượng 200GB. Bạn mong đợi chỉ cần chờ năm phút. Thay vào đó, lệnh ALTER TABLE kích hoạt một metadata lock làm đóng băng mọi truy vấn đang đến. Chỉ trong vài giây, connection pool của bạn chạm giới hạn 5.000 session, và người dùng phải đối mặt với lỗi 504 Gateway Timeout. Dashboard giám sát của bạn không chỉ hiện màu đỏ — nó đang “la hét”.

Thay đổi schema MySQL trên các tập dữ liệu khổng lồ là những thao tác rủi ro cao. Mặc dù MySQL 8.0 đã cải thiện hỗ trợ Online DDL, nhiều thay đổi cấu trúc vẫn yêu cầu sao chép toàn bộ bảng hoặc sử dụng các bản khóa hạn chế. Nếu bạn đang xử lý hơn 2.000 lượt ghi mỗi giây, ngay cả một sự ngưng trệ ba giây cũng có thể gây ra sự cố trên toàn hệ thống. Tôi đã từng xử lý những thất bại này trong môi trường MySQL, PostgreSQL và MongoDB, và cách xử lý DDL cũ kỹ của MySQL vẫn là một trong những nguyên nhân phổ biến nhất gây ra các ‘sự cố’ kỹ thuật.

Tại sao các phương pháp Migration thông thường lại thất bại

Lệnh ALTER TABLE nguyên bản thường tạo ra một bảng tạm ẩn, sao chép mọi hàng, sau đó hoán đổi bảng cũ sang bảng mới. Cơ sở dữ liệu phải duy trì tính nhất quán nghiêm ngặt trong quá trình chuyển đổi này. Ngay cả với cờ ‘Online’, giai đoạn ‘cut-over’ cuối cùng vẫn yêu cầu một exclusive lock để đổi tên các bảng. Trên một hệ thống bận rộn, lần khóa ngắn ngủi đó sẽ làm đình trệ mọi thứ. Lượng transaction tồn đọng sau đó có thể làm sập các application server của bạn từ rất lâu trước khi cơ sở dữ liệu kịp phục hồi.

Đánh giá các chiến lược Migration

Cộng đồng đã xây dựng một số công cụ để lách qua các hành vi khóa này. Mỗi công cụ đều có những đặc điểm đánh đổi riêng.

1. Native Online DDL

Được giới thiệu từ MySQL 5.6, đây là cách ‘có sẵn’ (built-in). Nó tiện lợi nhưng thiếu minh bạch. Bạn không thể dễ dàng giới hạn mức sử dụng I/O của nó, và nếu quá trình thất bại khi đã chạy được 90%, việc rollback có thể mất hàng giờ, khiến CPU luôn ở mức tối đa trong suốt thời gian đó.

2. pt-online-schema-change (Percona Toolkit)

Đây từng là tiêu chuẩn của ngành trong một thập kỷ. Nó tạo ra một bảng bóng (shadow table) và sử dụng database triggers để đồng bộ hóa các lượt ghi mới. Tuy nhiên, trigger là con dao hai lưỡi. Chúng làm tăng đáng kể độ trễ cho mỗi lệnh INSERT hoặc UPDATE trên bảng gốc của bạn. Trong môi trường có tính đồng thời cao, chi phí này thường dẫn đến chính tình trạng deadlock mà bạn đang cố gắng tránh.

3. gh-ost (GitHub Online Schema Transformer)

GitHub đã xây dựng gh-ost để loại bỏ sự phụ thuộc vào trigger. Thay vì ép buộc cơ sở dữ liệu đồng bộ dữ liệu qua trigger, gh-ost truyền các thay đổi trực tiếp từ MySQL binary logs (binlog). Nó hoạt động giống như một bản sao (replica), âm thầm tiêu thụ luồng log và áp dụng nó vào một bảng ghost. Thiết kế ‘không dùng trigger’ này đảm bảo hiệu suất ứng dụng của bạn vẫn ổn định, ngay cả trong quá trình migration kéo dài 10 giờ.

gh-ost có phù hợp với bạn không?

Ưu điểm

  • Không ảnh hưởng đến độ trễ: Không có trigger nghĩa là không có thêm chi phí trên luồng ghi của ứng dụng.
  • Kiểm soát hoàn toàn: Bạn có thể tạm dừng migration ngay lập tức nếu thấy độ trễ replication tăng lên trên 500ms.
  • Xác thực Dry-Run: Bạn có thể kiểm tra toàn bộ logic migration trên một bản sao để đảm bảo schema hợp lệ trước khi tác động đến production.
  • Thất bại sạch sẽ: Nếu tiến trình gh-ost bị chết, cơ sở dữ liệu của bạn không bị ảnh hưởng. Không có các trigger mồ côi cần phải dọn dẹp.

Hạn chế

  • Chi phí lưu trữ: Bạn cần ít nhất gấp 2,1 lần không gian của bảng gốc. Một bảng 500GB yêu cầu hơn 1TB đĩa cứng khả dụng.
  • Yêu cầu nghiêm ngặt: Môi trường của bạn phải sử dụng định dạng binlog dạng ROW và mọi bảng đều phải có Primary Key.
  • Độ phức tạp trong vận hành: Đây là một file thực thi Go riêng biệt. Bạn cần quản lý quyền và kết nối bên ngoài SQL tiêu chuẩn.

Yêu cầu hạ tầng

An toàn là trên hết. Hãy đảm bảo cấu hình MySQL của bạn khớp với các thiết lập sau:

  1. Định dạng Binlog: log_bin phải được kích hoạt và binlog_format phải được đặt thành ROW.
  2. Chiến lược Replica: Mặc dù gh-ost có thể chạy trên primary, nhưng sẽ an toàn hơn nếu trỏ nó vào một replica. Nó đọc dữ liệu từ follower nhưng thực hiện giai đoạn cut-over cuối cùng trên primary.
  3. Quyền hạn: User thực hiện migration cần các quyền SUPER, REPLICATION CLIENT, và REPLICATION SLAVE.
# Xác minh cấu hình của bạn
SHOW VARIABLES LIKE 'binlog_format';
# Phải trả về 'ROW'

Quy trình thực hiện

Hãy cùng thêm một cột user_bio vào bảng users chứa 50 triệu hàng.

Bước 1: Cài đặt Binary

Tải xuống bản binary đã được biên dịch sẵn. Đây là một file thực thi Go độc lập.

wget https://github.com/github/gh-ost/releases/download/v1.1.6/gh-ost-binary-linux-20231207144602.tar.gz
tar -xf gh-ost-binary-linux-20231207144602.tar.gz
sudo mv gh-ost /usr/local/bin/

Bước 2: Chạy Dry Run

Bỏ qua bước này là bạn đang tự tìm rắc rối. Dry run giúp xác thực quyền, kết nối và cú pháp mà không di chuyển bất kỳ một byte dữ liệu nào.

gh-ost \
  --user="migrator" \
  --password="secret" \
  --host="db-primary.internal" \
  --allow-on-master \
  --database="prod_db" \
  --table="users" \
  --alter="ADD COLUMN user_bio TEXT" \
  --dry-run

Bước 3: Thực thi

Sau khi dry run thành công, hãy thay --dry-run bằng --execute. Luôn xác định một --panic-flag-file. Nếu có vấn đề xảy ra, chỉ cần touch file đó, và gh-ost sẽ dừng lại ngay lập tức.

gh-ost \
  --user="migrator" \
  --password="secret" \
  --host="db-primary.internal" \
  --allow-on-master \
  --database="prod_db" \
  --table="users" \
  --alter="ADD COLUMN user_bio TEXT" \
  --panic-flag-file="/tmp/ghost.panic" \
  --execute

Bước 4: Điều tiết trực tiếp (Live Throttling)

Giám sát là chìa khóa. Nếu các bản sao khác bắt đầu bị chậm (lag) do tải từ việc migration, bạn có thể điều tiết (throttle) tiến trình thông qua Unix socket của nó trong thời gian thực.

# Tạm dừng quá trình sao chép
echo throttle | nc -U /tmp/gh-ost.prod_db.users.sock

# Tiếp tục khi các bản sao đã đuổi kịp
echo no-throttle | nc -U /tmp/gh-ost.prod_db.users.sock

Bước 5: Giai đoạn Cut-over

Sau khi sao chép dữ liệu, gh-ost chuẩn bị việc hoán đổi. Nó sử dụng một phương pháp hoán đổi nguyên tử (atomic swap) hai session phức tạp để đảm bảo không có truy vấn nào bị mất. Quá trình chuyển đổi thường hoàn tất trong chưa đầy 100 mili giây.

Kinh nghiệm thực chiến

Ngay cả với những công cụ tốt nhất, việc migration trên production vẫn đòi hỏi sự kỷ luật:

  • Theo dõi dung lượng đĩa: Nếu bạn có 80GB trống trên ổ đĩa 150GB, trong khi bảng của bạn là 100GB, gh-ost sẽ làm sập cơ sở dữ liệu của bạn vì làm đầy ổ đĩa.
  • Tự động điều tiết: Sử dụng --max-lag-millis=1000. Lệnh này yêu cầu gh-ost tự động ngừng hoạt động nếu bất kỳ bản sao nào bị chậm hơn một giây.
  • Dọn dẹp sau Migration: gh-ost đổi tên bảng cũ thành dạng như _users_del. Hãy giữ nó trong 24 giờ như một mạng lưới an toàn, sau đó xóa nó để giải phóng không gian.

Thay đổi schema không nhất thiết phải là nguồn cơn của sự lo lắng. Chuyển từ các công cụ dựa trên trigger sang bộ chuyển đổi binlog như gh-ost sẽ mang lại cho bạn khả năng hiển thị và kiểm soát cần thiết để phát triển cơ sở dữ liệu mà không làm thức giấc kỹ sư đang trực on-call.

Share: