Cấu hình MySQL Active-Active Replication: Đồng bộ dữ liệu hai chiều và xử lý xung đột ghi đồng thời

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Quick Start — Setup Master-Master trong 10 phút

Mình sẽ demo với 2 server Ubuntu 22.04, cả hai chạy MySQL 8.0. Thay địa chỉ IP theo môi trường của bạn:

  • Node1: 192.168.1.10
  • Node2: 192.168.1.20

Bước 1: Tạo replication user trên cả hai node

Chạy lệnh sau trên Node1:

CREATE USER 'repl_user'@'192.168.1.20' IDENTIFIED BY 'StrongPassword123!';
GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'192.168.1.20';
FLUSH PRIVILEGES;

Tương tự trên Node2 — chỉ đổi IP thành 192.168.1.10:

CREATE USER 'repl_user'@'192.168.1.10' IDENTIFIED BY 'StrongPassword123!';
GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'192.168.1.10';
FLUSH PRIVILEGES;

Bước 2: Cấu hình my.cnf

Trên Node1 (/etc/mysql/mysql.conf.d/mysqld.cnf):

[mysqld]
server-id                = 1
log_bin                  = /var/log/mysql/mysql-bin.log
binlog_format            = ROW
relay_log                = /var/log/mysql/relay-bin.log
log_slave_updates        = ON
auto_increment_increment = 2
auto_increment_offset    = 1

Trên Node2 — chỉ khác server-idauto_increment_offset:

[mysqld]
server-id                = 2
log_bin                  = /var/log/mysql/mysql-bin.log
binlog_format            = ROW
relay_log                = /var/log/mysql/relay-bin.log
log_slave_updates        = ON
auto_increment_increment = 2
auto_increment_offset    = 2
sudo systemctl restart mysql

Bước 3: Lấy binlog position và kích hoạt replication

Trên Node1, lock bảng tạm thời để lấy vị trí binlog chính xác:

FLUSH TABLES WITH READ LOCK;
SHOW MASTER STATUS;

Kết quả trả về kiểu như này — ghi lại FilePosition:

+------------------+----------+
| File             | Position |
+------------------+----------+
| mysql-bin.000001 |      154 |
+------------------+----------+

Sau đó cấu hình Node2 trỏ về Node1:

-- Chạy trên Node2
CHANGE MASTER TO
  MASTER_HOST     = '192.168.1.10',
  MASTER_USER     = 'repl_user',
  MASTER_PASSWORD = 'StrongPassword123!',
  MASTER_LOG_FILE = 'mysql-bin.000001',
  MASTER_LOG_POS  = 154;

START SLAVE;
SHOW SLAVE STATUS\G

Lặp lại chiều ngược lại — lấy binlog position của Node2, rồi cấu hình Node1 trỏ về Node2. Nhớ unlock bảng trước:

-- Trên Node1: unlock sau khi đã lấy position của Node2
UNLOCK TABLES;

-- Cấu hình Node1 làm slave của Node2
CHANGE MASTER TO
  MASTER_HOST     = '192.168.1.20',
  MASTER_USER     = 'repl_user',
  MASTER_PASSWORD = 'StrongPassword123!',
  MASTER_LOG_FILE = 'mysql-bin.000001',  -- position lấy từ Node2
  MASTER_LOG_POS  = 154;

START SLAVE;

Kiểm tra trạng thái — hai dòng này phải đều là Yes:

SHOW SLAVE STATUS\G
-- Slave_IO_Running: Yes
-- Slave_SQL_Running: Yes

Hiểu rõ cơ chế bên dưới

Về bản chất, Master-Master chỉ là hai quan hệ Master-Slave chạy ngược chiều nhau. Node1 đồng thời là master (ghi binlog) và slave (nhận binlog từ Node2). Node2 cũng vậy. Ghi vào node nào, node kia đều nhận được — không phân biệt.

Tại sao auto_increment_increment và auto_increment_offset lại quan trọng?

Phần này dễ bỏ qua nhất — và gây đau đầu nhất nếu thiếu. Không cấu hình hai tham số này, cả hai node cùng sinh AUTO_INCREMENT bắt đầu từ 1. Node1 tạo ID=1, Node2 cũng tạo ID=1, replication dừng ngay với lỗi duplicate key.

Với auto_increment_increment=2:

  • Node1 (offset=1) sinh: 1, 3, 5, 7, 9…
  • Node2 (offset=2) sinh: 2, 4, 6, 8, 10…

Hai dãy số không bao giờ đụng nhau. Mở rộng lên 3 node? Đặt increment=3, offset lần lượt 1, 2, 3.

Nâng cao — Xử lý xung đột ghi đồng thời

Write conflict là bài toán khó nhất của Active-Active. Giả sử Node1 và Node2 cùng lúc UPDATE một row:

-- Node1 chạy
UPDATE products SET stock = 10 WHERE id = 100;

-- Node2 chạy CÙNG LÚC
UPDATE products SET stock = 5 WHERE id = 100;

Node nào replicate muộn hơn sẽ thắng. Hành vi này gọi là last-write-wins — không có lỗi, không có rollback, dữ liệu âm thầm bị ghi đè. MySQL không can thiệp.

Phát hiện replication đang bị lỗi

# Kiểm tra nhanh trên cả hai node
mysql -u root -p -e "SHOW SLAVE STATUS\G" | grep -E "Slave_SQL_Running|Last_Error|Seconds_Behind"

Thấy Slave_SQL_Running: No? Replication đang dừng. Đọc Last_Error để xác định nguyên nhân, rồi bỏ qua lỗi hiện tại nếu cần:

-- Bỏ qua lỗi hiện tại và tiếp tục (dùng cẩn thận)
STOP SLAVE;
SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1;
START SLAVE;

Cấu hình skip duplicate key error tự động

Muốn bỏ qua lỗi duplicate key (error code 1062) thay vì để replication tắt hẳn, thêm vào my.cnf:

[mysqld]
# Chỉ dùng khi bạn hiểu rõ trade-off — dữ liệu có thể không nhất quán
slave_skip_errors = 1062

Giải pháp kiến trúc: tránh conflict từ gốc

Database production của mình chạy MySQL 8.0, khoảng 50GB data. Cách xử lý conflict không phải detect-and-fix — mà là routing thông minh ngay từ đầu. Cụ thể: kết hợp ProxySQL để route toàn bộ write vào một node active duy nhất. Node còn lại chỉ nhận read. Khi node active fail, chuyển write sang node kia trong vài giây. Đơn giản hơn nhiều so với xử lý conflict sau khi xảy ra.

Các pattern phổ biến khác để tránh conflict:

  • Phân chia theo tenant/region: User A luôn ghi vào Node1, User B luôn ghi vào Node2. Không bao giờ hai node cùng ghi vào một row.
  • Application-level locking: Dùng Redis SETNX để acquire distributed lock trước khi ghi, đảm bảo chỉ 1 writer tại một thời điểm.

Tips thực tế

Dùng GTID cho dễ quản lý hơn

MySQL 5.6+ có GTID (Global Transaction Identifier) — thay thế hoàn toàn việc lưu binlog file + position thủ công. Failover và recovery dễ hơn hẳn:

[mysqld]
gtid_mode                = ON
enforce_gtid_consistency = ON

Khi dùng GTID, bỏ hẳn MASTER_LOG_FILEMASTER_LOG_POS:

CHANGE MASTER TO
  MASTER_HOST          = '192.168.1.10',
  MASTER_USER          = 'repl_user',
  MASTER_PASSWORD      = 'StrongPassword123!',
  MASTER_AUTO_POSITION = 1;  -- MySQL tự tìm position phù hợp

Monitor replication lag liên tục

# Xem lag real-time, cập nhật mỗi 5 giây
watch -n 5 'mysql -u root -p -e "SHOW SLAVE STATUS\G" | grep Seconds_Behind_Master'

Seconds_Behind_Master tăng liên tục là dấu hiệu slave không theo kịp master. Nguyên nhân phổ biến: disk I/O nghẽn, CPU yếu, hoặc quá nhiều write đồng thời — lag vượt 30 giây thường cần điều tra ngay.

Checklist trước khi lên production

  • ✅ Cả hai node cùng phiên bản MySQL
  • binlog_format = ROW — không dùng STATEMENT vì dễ gây inconsistency với hàm như NOW(), RAND()
  • ✅ Đã cấu hình auto_increment_incrementauto_increment_offset khác nhau giữa các node
  • ✅ Firewall cho phép port 3306 giữa hai node theo cả hai chiều
  • ✅ Có monitoring alert khi Slave_SQL_Running = No
  • ✅ Có runbook xử lý split-brain khi network giữa hai node bị đứt

Khi nào không nên dùng Master-Master?

Master-Master không phải lúc nào cũng là lựa chọn đúng. Tránh dùng khi:

  • Ứng dụng thường xuyên UPDATE cùng row từ nhiều node — conflict sẽ liên tục
  • Cần strong consistency — Active-Active là eventually consistent theo bản chất
  • Team chưa có kinh nghiệm với replication — troubleshoot Master-Master khó hơn Master-Slave nhiều lần

Trong những trường hợp đó, MySQL Group Replication hoặc Galera Cluster phù hợp hơn. Cả hai có built-in conflict detection và resolution. Đánh đổi: latency ghi cao hơn một chút vì cần synchronous consensus giữa các node.

Share: