Cảnh báo PagerDuty lúc 2:14 sáng: Cơn ác mộng Split-Brain
Điện thoại của tôi rung bần bật trên bàn cạnh giường ngủ lúc 2:14 sáng. PagerDuty bắn liên tiếp ba cảnh báo trong vòng ba mươi giây: cơ sở dữ liệu thanh toán chính của chúng tôi đột ngột gặp sự cố kernel panic. Nó rớt khỏi mạng nội bộ, khiến 14 microservice API đồng loạt ném lỗi HTTP 500 trên khắp các vùng US-East và EU-West thay vì kích hoạt database circuit breaker để tự phục hồi kết nối. Trong khi đó, replica dự phòng chỉ ngồi im chờ dữ liệu ghi – những transaction không bao giờ tới.
Tôi lồm cồm bò dậy mở máy tính, bật terminal và nhìn chằm chằm vào các tọa độ replication kiểu cũ. Với mô hình file-and-position truyền thống, việc nâng cấp replica này lên làm master đồng nghĩa với việc phải xác định chính xác tên log file—mysql-bin.000412—và chính xác byte offset—4829104—nơi replica đã dừng lại. Chúng tôi vừa phải đảm bảo không làm mất bất kỳ transaction nào đang xử lý dở dang, vừa phải trỏ lại thủ công hai read replica còn lại.
Giữa cơn hoảng loạn và áp lực nghẹt thở, một kỹ sư on-call đã gõ nhầm một chữ số trong phần offset. Chỉ trong vòng bốn phút, một nửa số secondary replica bị crash với lỗi Error 1062: Duplicate entry, nửa còn lại bị treo cứng, và kênh hỗ trợ khách hàng ghi nhận 42 giao dịch bị trừ tiền trùng lặp. Chúng tôi mất trọn hai giờ đồng hồ đầy căng thẳng chỉ để gỡ mớ bòng bong sai lệch dữ liệu đó.
Việc phụ thuộc vào việc tính toán file và offset thủ công trong lúc xảy ra sự cố là mối đe dọa trực tiếp đến tính toàn vẹn của dữ liệu. Bạn cần một cơ chế theo dõi định danh transaction trên toàn hệ thống để quá trình failover trở nên tất định thay vì một trò chơi may rủi đoán mò.
Nguyên nhân gốc rễ: Vì sao tọa độ Binary Log truyền thống dễ gãy dưới áp lực
Replication truyền thống trong MySQL dựa vào các tọa độ: mỗi replica sẽ theo dõi một tên file binlog cụ thể (chẳng hạn như binlog.000102) và một vị trí byte bên trong file đó (như 712). Dù về mặt lý thuyết thì rất đơn giản, nhưng cơ chế này sẽ đổ vỡ ngay khoảnh khắc phần cứng gặp sự cố bất ngờ.
Mỗi instance MySQL tự ghi một luồng binary log độc lập của riêng mình. Khi primary bị crash, tên file và offset cục bộ của nó hoàn toàn không khớp với những gì được tạo ra trên các replica phía dưới. Hãy tưởng tượng một mô hình gồm Primary P, Replica A và Replica B.
Nếu P chết, bạn không thể ra lệnh cho Replica B: “Hãy tiếp tục đồng bộ từ Replica A tại offset 5512”. Thứ tự đánh số binlog của Replica A chẳng liên quan gì đến luồng của primary ban đầu cả. Bạn buộc phải kiểm tra transaction header bằng tay, tính toán offset giữa cả hai node và cầu nguyện rằng phép tính của mình không sai dưới áp lực. Trong các cụm lớn hơn với bốn hoặc năm replica, một sự cố duy nhất thường kéo theo hàng giờ điều tra và khôi phục thủ công.
So sánh chiến lược Replication: File/Offset vs. GTID
Trước khi chuẩn hóa cụm cơ sở dữ liệu production quy mô hơn 40 node, chúng tôi đã đánh giá kỹ lưỡng sự đánh đổi khi vận hành giữa tọa độ truyền thống và Global Transaction Identifiers (GTID).
1. File and Position Replication (Truyền thống)
- Cách theo dõi thay đổi: Dựa vào tên file và byte offset tường minh (ví dụ:
MASTER_LOG_FILE='binlog.000004', MASTER_LOG_POS=157). - Độ phức tạp khi failover: Cực kỳ mệt mỏi. Việc nâng cấp standby đòi hỏi phải so sánh log tỉ mỉ hoặc viết các bash script mong manh để chuyển đổi vị trí giữa các server.
- Rủi ro lệch dữ liệu: Rất lớn. Tiếp tục đồng bộ từ sai vị trí byte có thể âm thầm bỏ sót dòng dữ liệu hoặc dừng replication do lỗi duplicate key.
- Gánh nặng vận hành: Cao. Cần tính toán lại thủ công mỗi khi thay thế hoặc thăng cấp một instance.
2. Global Transaction Identifiers (GTID)
- Cách theo dõi thay đổi: Gắn nhãn cho mỗi transaction đã commit bằng một UUID bất biến trên toàn cluster, theo định dạng
source_id:transaction_id(ví dụ:3E11FA47-71CA-11E1-9E33-C80AA9429562:23). - Độ phức tạp khi failover: Tối thiểu. Các replica lưu giữ một sổ cái chính xác về các transaction đã thực thi. Việc thiết lập
SOURCE_AUTO_POSITION = 1sẽ tự động yêu cầu source mới tìm các phần còn thiếu và chỉ truyền đúng phần chênh lệch đó. - Rủi ro lệch dữ liệu: Gần như bằng không. Replication engine tự kiểm tra ranh giới transaction và tính lũy đẳng (idempotency) ngay ở tầng storage engine.
- Gánh nặng vận hành: Thấp. Tích hợp mượt mà với các công cụ điều phối như Orchestrator và ProxySQL, đồng thời giúp việc thăng cấp thủ công trở nên cực kỳ đơn giản.
Triển khai: Các bước thiết lập GTID Master-Slave
Dưới đây là quy trình triển khai chuẩn mà chúng tôi áp dụng trên các máy chủ Ubuntu 22.04 LTS chạy MySQL 8.0 để đảm bảo khả năng failover ổn định.
Bước 1: Cấu hình Primary (Source) Server
Mở file cấu hình của node primary, tại đường dẫn /etc/mysql/mysql.conf.d/mysqld.cnf:
[mysqld]
server-id = 101
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
gtid_mode = ON
enforce_gtid_consistency = ON
log_replica_updates = ON
binlog_checksum = CRC32
Khởi động lại MySQL daemon để kích hoạt hỗ trợ GTID:
sudo systemctl restart mysql
Tiếp theo, tạo một tài khoản replication với các quyền tối thiểu cần thiết trên primary:
CREATE USER 'repl_user'@'192.168.1.%' IDENTIFIED BY 'StrongAuthPass_2026!';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl_user'@'192.168.1.%';
FLUSH PRIVILEGES;
Bước 2: Cấu hình Replica (Target) Server
Trên standby replica, chỉnh sửa file /etc/mysql/mysql.conf.d/mysqld.cnf. Đảm bảo rằng server-id của nó là duy nhất trong toàn bộ hạ tầng:
[mysqld]
server-id = 102
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
gtid_mode = ON
enforce_gtid_consistency = ON
log_replica_updates = ON
read_only = ON
super_read_only = ON
binlog_checksum = CRC32
Bật read_only và super_read_only để chặn toàn bộ dữ liệu ghi từ phía ứng dụng cũng như từ các user không có quyền SUPER hoặc SYSTEM_VARIABLES_ADMIN. Khởi động lại replica:
sudo systemctl restart mysql
Bước 3: Tạo bản Snapshot sạch kèm trạng thái GTID
Để khởi tạo replica mà không làm gián đoạn lưu lượng truy cập production, hãy tạo một snapshot nhất quán bằng mysqldump hoặc giải pháp MySQL Hot Backup với Percona XtraBackup. Cờ --source-data=2 (hoặc --master-data=2 trên các phiên bản MySQL cũ hơn) sẽ gắn tập hợp GTID đã thực thi của primary trực tiếp vào phần header của file SQL:
mysqldump -u root -p \
--all-databases \
--single-transaction \
--quick \
--triggers \
--routines \
--source-data=2 \
> /tmp/primary_dump.sql
Chuyển bản snapshot sang replica qua mạng nội bộ và tiến hành restore:
scp /tmp/primary_dump.sql [email protected]:/tmp/
mysql -u root -p < /tmp/primary_dump.sql
Khi import dữ liệu liên hệ thống hoặc tạo fixture test từ log CSV thô, tôi rất hay gặp vấn đề về encoding. Để chuyển đổi dữ liệu nhanh chóng ngay tại máy, các công cụ chạy trên trình duyệt như toolcraft.app/vi/tools/data/csv-to-json sẽ xử lý file hoàn toàn ở client-side mà không gửi chuỗi dữ liệu production nhạy cảm qua bất kỳ API bên thứ ba nào. Nhờ vậy, tôi đỡ phải mất công viết các script Python dùng một lần trên staging server.
Bước 4: Kết nối Replica bằng cơ chế Auto-Positioning
Vì GTID theo dõi trực tiếp các transaction nên bạn không cần phải truyền tên log file hay byte offset nữa. Đăng nhập vào MySQL trên standby node và trỏ nó về primary:
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '192.168.1.101',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl_user',
SOURCE_PASSWORD = 'StrongAuthPass_2026!',
SOURCE_AUTO_POSITION = 1;
START REPLICA;
Kiểm tra trạng thái của tiến trình replication:
SHOW REPLICA STATUS\G
Xác minh xem cả hai thread đã chạy chưa và các tập hợp GTID đã được đồng bộ hay chưa:
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 0
Retrieved_Gtid_Set: 3e11fa47-71ca-11e1-9e33-c80aa9429562:1-450
Executed_Gtid_Set: 3e11fa47-71ca-11e1-9e33-c80aa9429562:1-450
Quy trình Failover an toàn trên Production: Xử lý khi Primary gặp sự cố
Khi máy chủ primary gặp sự cố, hãy thực hiện theo đúng trình tự sau để nâng cấp replica một cách an toàn mà không mất dữ liệu.
Bước 1: Cô lập Primary bị lỗi
Đầu tiên, hãy cô lập (fence) node bị crash. Bạn cần ngăn chặn tuyệt đối việc node này nhận dữ liệu ghi nếu nó bất ngờ tự khởi động lại giữa lúc đang xử lý sự cố. Hãy thu hồi địa chỉ IP ảo (VIP), cập nhật lại security group hoặc tắt máy chủ thông qua API của nhà cung cấp cloud.
Bước 2: Áp dụng toàn bộ Transaction còn sót lại trên Replica
Đăng nhập vào replica (192.168.1.102). Dừng thread I/O để chặn các gói tin dở dang, nhưng vẫn để thread SQL chạy để xử lý sạch hàng đợi trong relay log cục bộ:
STOP REPLICA IO_THREAD;
SELECT WAIT_FOR_EXECUTED_GTID_SET(@@global.gtid_retrieved);
STOP REPLICA;
Lưu ý rằng hàm WAIT_FOR_EXECUTED_GTID_SET cần theo dõi @@global.gtid_retrieved ở đây. Lệnh này sẽ block cho đến khi thread SQL thực thi xong toàn bộ các transaction đã được kéo về trước đó bởi thread I/O.
Bước 3: Thăng cấp Standby Node thành Primary
Xóa sạch metadata replication cũ và gỡ bỏ khóa read-only để mở lại quyền đọc-ghi:
RESET REPLICA ALL;
SET GLOBAL super_read_only = OFF;
SET GLOBAL read_only = OFF;
Tiếp theo, cập nhật lại tầng kết nối của bạn—dù là đổi CNAME của DNS, chuyển IP ảo (VIP) hay cập nhật backend pool trên ProxySQL—để điều hướng lưu lượng của ứng dụng về 192.168.1.102.
Bước 4: Trỏ các Replica còn lại sang Primary mới
Các read replica còn sống có thể dễ dàng trỏ về primary mới vừa được thăng cấp chỉ với ba câu lệnh đơn giản. Trên từng node còn lại:
STOP REPLICA;
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '192.168.1.102',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl_user',
SOURCE_PASSWORD = 'StrongAuthPass_2026!',
SOURCE_AUTO_POSITION = 1;
START REPLICA;
Replica sẽ gửi sổ cái GTID cục bộ của nó cho primary mới. Primary sẽ tự nhận biết các transaction còn thiếu, stream sang và tiếp tục quá trình replication. Không cần tính offset, không cần tính toán thủ công và tuyệt đối không còn phải đoán mò.

