Multi-master Replication Setup — คู่มือตั้งค่า MySQL, MariaDB Galera และ PostgreSQL

Multi-master Replication คือรูปแบบการทำสำเนาฐานข้อมูลที่อนุญาตให้ทุก Node สามารถรับคำสั่ง Write ได้พร้อมกัน ต่างจาก Primary-Replica ทั่วไปที่มีเพียง Node เดียวรับ Write ข้อดีคือไม่มี Single Point of Failure สำหรับ Write Operation และกระจาย Write Load ไปยังหลาย Node ได้

บทความนี้จะอธิบายแนวคิด ข้อดี-ข้อเสีย และวิธีตั้งค่า Multi-master Replication สำหรับ MySQL (Group Replication Multi-Primary Mode), MariaDB (Galera Cluster) และ PostgreSQL (BDR — Bi-Directional Replication) พร้อมตัวอย่างการตั้งค่าจริงและการจัดการ Conflict

Multi-master vs Single-master

ก่อนเริ่มตั้งค่า ควรเข้าใจความแตกต่างระหว่างสองรูปแบบ Single-master (Primary-Replica) มี Node เดียวรับ Write ส่วน Node อื่นรับเฉพาะ Read ข้อดีคือไม่มี Write Conflict แต่เมื่อ Primary ล่มต้องรอ Failover ก่อนจึงจะ Write ได้อีกครั้ง ส่วน Multi-master ทุก Node รับทั้ง Read และ Write ได้ ไม่ต้องรอ Failover เมื่อ Node ใด Node หนึ่งล่ม แต่ต้องจัดการ Write Conflict ที่อาจเกิดขึ้นเมื่อหลาย Node แก้ไขข้อมูลเดียวกันพร้อมกัน

Multi-master เหมาะกับระบบที่ต้องการ Write Availability สูง เช่น ระบบที่ต้องทำงานข้ามหลาย Data Center หรือระบบที่ต้องการ Zero-downtime สำหรับ Write Operations อย่างไรก็ตาม ไม่ใช่ทุกระบบที่เหมาะจะใช้ Multi-master เพราะ Conflict Resolution เพิ่มความซับซ้อนและอาจทำให้ประสิทธิภาพลดลง

MySQL Group Replication — Multi-Primary Mode

MySQL Group Replication รองรับ Multi-Primary Mode ซึ่งทุก Node สามารถรับ Write ได้ ระบบใช้ Certification-based Conflict Detection ที่ตรวจสอบ Transaction ก่อน Commit ถ้า Transaction ชนกัน (แก้ไขข้อมูลแถวเดียวกัน) ระบบจะ Abort Transaction ที่มาทีหลัง

ข้อจำกัดของ Multi-Primary Mode

ก่อนเปิดใช้งานต้องทราบข้อจำกัดสำคัญหลายประการ ประการแรก ไม่รองรับ Cascading Foreign Keys (Foreign Key ที่มี ON DELETE CASCADE หรือ ON UPDATE CASCADE) เพราะอาจทำให้ Conflict Detection ทำงานไม่ถูกต้อง ประการที่สอง ไม่รองรับ Serializable Isolation Level เพราะจะทำให้เกิด Conflict บ่อยเกินไป ประการที่สาม DDL Statement (ALTER TABLE, CREATE INDEX) ต้องระวังเป็นพิเศษ เพราะ DDL ไม่ถูก Replicate แบบ Transactional อาจทำให้ Schema ไม่ Sync กัน

เปิดใช้งาน Multi-Primary Mode

ถ้ามี Group Replication แบบ Single-Primary อยู่แล้ว สามารถเปลี่ยนเป็น Multi-Primary Mode ได้ โดยต้องทำบนทุก Node

# ตรวจสอบ Mode ปัจจุบัน
SELECT @@group_replication_single_primary_mode;
# 1 = Single-Primary, 0 = Multi-Primary

# เปลี่ยนเป็น Multi-Primary Mode (ต้องหยุด Group Replication ก่อน)
STOP GROUP_REPLICATION;
SET GLOBAL group_replication_single_primary_mode = OFF;
SET GLOBAL group_replication_enforce_update_everywhere_checks = ON;
# บน Node แรก — Bootstrap ใหม่
SET GLOBAL group_replication_bootstrap_group = ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group = OFF;

# บน Node อื่น ๆ — Join Group
START GROUP_REPLICATION;

# ตรวจสอบสถานะ — ทุก Node ควรเป็น PRIMARY
SELECT MEMBER_HOST, MEMBER_ROLE, MEMBER_STATE
FROM performance_schema.replication_group_members;

ตั้งค่าใน Configuration File

เพื่อให้ค่าคงอยู่หลัง Restart ต้องเพิ่มการตั้งค่าใน Configuration File ด้วย

# เพิ่มใน /etc/mysql/mysql.conf.d/mysqld.cnf ของทุก Node
[mysqld]
# Group Replication — Multi-Primary Mode
group_replication_single_primary_mode = OFF
group_replication_enforce_update_everywhere_checks = ON

# แนะนำสำหรับ Multi-Primary
transaction_write_set_extraction = XXHASH64
binlog_transaction_dependency_tracking = WRITESET

ทดสอบ Multi-Primary Write

# บน Node 1 — สร้างตารางทดสอบ
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    price DECIMAL(10,2),
    updated_by VARCHAR(50)
);

# บน Node 1 — เขียนข้อมูล
INSERT INTO testdb.products (name, price, updated_by)
VALUES ('Product A', 100.00, 'node1');

# บน Node 2 — เขียนข้อมูลพร้อมกัน (คนละแถว)
INSERT INTO testdb.products (name, price, updated_by)
VALUES ('Product B', 200.00, 'node2');

# บน Node 3 — ตรวจสอบว่าข้อมูลจากทุก Node มาครบ
SELECT * FROM testdb.products;

ทดสอบ Conflict Detection

# ทดสอบ Write Conflict
# บน Node 1 — เริ่ม Transaction
BEGIN;
UPDATE testdb.products SET price = 150.00 WHERE id = 1;

# บน Node 2 — แก้ไขแถวเดียวกันพร้อมกัน
BEGIN;
UPDATE testdb.products SET price = 180.00 WHERE id = 1;
COMMIT;
# Transaction บน Node 2 จะ Commit สำเร็จ (มาถึง Certification ก่อน)

# กลับไปบน Node 1
COMMIT;
# ERROR 1180: Got error 149 during COMMIT
# Transaction ถูก Abort เพราะ Conflict
# แอปพลิเคชันต้อง Retry Transaction ใหม่

MariaDB Galera Cluster

Galera Cluster เป็น Multi-master Solution สำหรับ MariaDB (และ MySQL) ที่ใช้ Synchronous Replication ทำให้ทุก Node มีข้อมูลเหมือนกันตลอดเวลา ข้อดีของ Galera คือ Replication เป็น Synchronous จริง ๆ ไม่มี Replication Lag, ทุก Node เป็น Master รับทั้ง Read และ Write และ Node ที่เพิ่มเข้ามาจะ Sync ข้อมูลอัตโนมัติ

ติดตั้ง MariaDB Galera Cluster

# ติดตั้ง MariaDB บนทุก Node (3 Node)
sudo apt update
sudo apt install -y mariadb-server galera-4

# หยุด MariaDB ก่อนตั้งค่า
sudo systemctl stop mariadb

ตั้งค่า Galera บน Node 1

# สร้างไฟล์ Configuration
sudo nano /etc/mysql/mariadb.conf.d/galera.cnf
[mysqld]
binlog_format = ROW
default_storage_engine = InnoDB
innodb_autoinc_lock_mode = 2
innodb_flush_log_at_trx_commit = 0
innodb_buffer_pool_size = 1G

# Galera Settings
wsrep_on = ON
wsrep_provider = /usr/lib/galera/libgalera_smm.so
wsrep_cluster_name = "my_galera_cluster"
wsrep_cluster_address = "gcomm://10.0.0.1,10.0.0.2,10.0.0.3"
wsrep_node_address = "10.0.0.1"
wsrep_node_name = "node1"
wsrep_sst_method = mariabackup
wsrep_sst_auth = "sst_user:SSTPassword123!"

# Tuning
wsrep_slave_threads = 4
wsrep_retry_autocommit = 3

Node 2 และ 3 ใช้ Configuration เดียวกัน เปลี่ยนเฉพาะ wsrep_node_address และ wsrep_node_name ให้ตรงกับ IP และชื่อของแต่ละ Node

สร้าง SST User และ Bootstrap Cluster

# Bootstrap Cluster จาก Node 1 (ทำครั้งแรกครั้งเดียว)
sudo galera_new_cluster

# ตรวจสอบว่า Cluster เริ่มต้นแล้ว
sudo mariadb -e "SHOW STATUS LIKE 'wsrep_cluster_size';"
# wsrep_cluster_size = 1

# สร้าง SST User สำหรับ State Snapshot Transfer
sudo mariadb -e "
  CREATE USER 'sst_user'@'localhost' IDENTIFIED BY 'SSTPassword123!';
  GRANT RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT ON *.* TO 'sst_user'@'localhost';
  FLUSH PRIVILEGES;"
# บน Node 2 และ 3 — เริ่ม MariaDB เพื่อ Join Cluster
sudo systemctl start mariadb

# ตรวจสอบจำนวน Node ใน Cluster
sudo mariadb -e "SHOW STATUS LIKE 'wsrep_cluster_size';"
# wsrep_cluster_size = 3 (หลัง Node ทุกตัว Join)

# ตรวจสอบสถานะ Cluster โดยละเอียด
sudo mariadb -e "SHOW STATUS LIKE 'wsrep_%';" | grep -E 'cluster_size|cluster_status|connected|ready|state_comment'

ทดสอบ Galera Multi-master

# บน Node 1
CREATE DATABASE galera_test;
USE galera_test;
CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    item VARCHAR(100),
    amount DECIMAL(10,2),
    created_on VARCHAR(20)
) ENGINE=InnoDB;

INSERT INTO orders (item, amount, created_on) VALUES ('Order from Node1', 500, 'node1');

# บน Node 2
INSERT INTO galera_test.orders (item, amount, created_on) VALUES ('Order from Node2', 750, 'node2');

# บน Node 3
INSERT INTO galera_test.orders (item, amount, created_on) VALUES ('Order from Node3', 300, 'node3');

# ตรวจสอบบน Node ใดก็ได้ — ต้องเห็นข้อมูลจากทุก Node
SELECT * FROM galera_test.orders;

จัดการ Conflict ใน Galera

Galera ใช้ Optimistic Locking — ปล่อยให้ Transaction ทำงานก่อน แล้วตรวจสอบ Conflict ตอน Commit ถ้า Transaction สองตัวแก้ไขแถวเดียวกันพร้อมกัน Transaction ที่ Certify ทีหลังจะถูก Rollback และแอปพลิเคชันจะได้รับ Deadlock Error (Error 1213) ต้อง Retry Transaction เอง

# ตรวจสอบ Conflict ที่เกิดขึ้น
SHOW STATUS LIKE 'wsrep_local_cert_failures';
# จำนวน Transaction ที่ถูก Abort เพราะ Conflict

SHOW STATUS LIKE 'wsrep_local_bf_aborts';
# จำนวน Transaction ที่ถูก Abort โดย Brute Force (Transaction จาก Node อื่นมาถึงก่อน)

# ตั้งค่า Auto-retry ใน Configuration
# wsrep_retry_autocommit = 3 (Retry อัตโนมัติ 3 ครั้งสำหรับ Autocommit Transaction)

PostgreSQL Multi-master ด้วย BDR

PostgreSQL ไม่มี Built-in Multi-master แต่ใช้ Extension อย่าง BDR (Bi-Directional Replication) ของ EDB (EnterpriseDB) หรือ pgEdge ซึ่งเป็น Open Source Alternative สำหรับตัวอย่างนี้จะแสดงแนวคิดการตั้งค่า Logical Replication แบบ Bi-directional ซึ่งเป็นพื้นฐานของ Multi-master

ตั้งค่า Logical Replication แบบ Bi-directional

# แก้ไข postgresql.conf บนทั้ง Node 1 และ Node 2
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
max_worker_processes = 10
max_logical_replication_workers = 4

# Restart PostgreSQL
sudo systemctl restart postgresql
# บน Node 1 — สร้างตารางและ Publication
CREATE TABLE shared_data (
    id SERIAL PRIMARY KEY,
    content TEXT,
    source_node VARCHAR(20) DEFAULT 'node1',
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE PUBLICATION pub_node1 FOR TABLE shared_data;

# สร้าง Replication User
CREATE USER repl_user WITH REPLICATION PASSWORD 'ReplPass123!';
GRANT ALL ON shared_data TO repl_user;
# บน Node 2 — สร้างตารางเดียวกันและ Publication
CREATE TABLE shared_data (
    id SERIAL PRIMARY KEY,
    content TEXT,
    source_node VARCHAR(20) DEFAULT 'node2',
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE PUBLICATION pub_node2 FOR TABLE shared_data;

CREATE USER repl_user WITH REPLICATION PASSWORD 'ReplPass123!';
GRANT ALL ON shared_data TO repl_user;
# บน Node 1 — Subscribe ไปยัง Node 2
CREATE SUBSCRIPTION sub_from_node2
  CONNECTION 'host=10.0.0.2 port=5432 dbname=myapp user=repl_user password=ReplPass123!'
  PUBLICATION pub_node2
  WITH (copy_data = false, origin = none);

# บน Node 2 — Subscribe ไปยัง Node 1
CREATE SUBSCRIPTION sub_from_node1
  CONNECTION 'host=10.0.0.1 port=5432 dbname=myapp user=repl_user password=ReplPass123!'
  PUBLICATION pub_node1
  WITH (copy_data = false, origin = none);

ตัวเลือก origin = none (PostgreSQL 16 ขึ้นไป) ป้องกันไม่ให้ข้อมูลที่ Replicate มาจาก Node หนึ่งถูกส่งกลับไปยัง Node ต้นทาง ซึ่งป้องกัน Infinite Loop ได้

ข้อจำกัดและการจัดการ Conflict

PostgreSQL Logical Replication ไม่มี Built-in Conflict Detection เหมือน MySQL Group Replication หรือ Galera ดังนั้นต้องออกแบบแอปพลิเคชันให้หลีกเลี่ยง Conflict เช่น แบ่ง ID Range ให้แต่ละ Node ใช้คนละช่วง (Node 1 ใช้เลขคี่ Node 2 ใช้เลขคู่) หรือใช้ UUID แทน Auto-increment เพื่อป้องกัน Primary Key Conflict และกำหนดให้แต่ละ Node รับผิดชอบ Write เฉพาะข้อมูลบางส่วน (Partitioned Write)

# ตัวอย่าง: แบ่ง Sequence ให้คนละ Node
# Node 1
CREATE SEQUENCE shared_data_id_seq START 1 INCREMENT 2;  -- เลขคี่
ALTER TABLE shared_data ALTER COLUMN id SET DEFAULT nextval('shared_data_id_seq');

# Node 2
CREATE SEQUENCE shared_data_id_seq START 2 INCREMENT 2;  -- เลขคู่
ALTER TABLE shared_data ALTER COLUMN id SET DEFAULT nextval('shared_data_id_seq');

# หรือใช้ UUID แทน
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE shared_data_v2 (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    content TEXT,
    source_node VARCHAR(20),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

แนวทางจัดการ Conflict ในระดับแอปพลิเคชัน

ไม่ว่าจะใช้ Multi-master แบบใด แอปพลิเคชันต้องพร้อมรับมือกับ Conflict ที่อาจเกิดขึ้น แนวทางหลัก ๆ มีดังนี้

แนวทางแรกคือ Retry Logic ที่เมื่อได้รับ Error จาก Conflict ให้ Retry Transaction ใหม่ ควรใช้ Exponential Backoff เพื่อลดโอกาสชนซ้ำ แนวทางที่สองคือ Last-Write-Wins ที่ใช้ Timestamp เปรียบเทียบ ค่าที่เขียนล่าสุดจะถูกเก็บ เหมาะกับข้อมูลที่ไม่จำเป็นต้องรวม (merge) แนวทางที่สามคือ Merge Logic ที่เขียน Logic รวมข้อมูลจากหลาย Node เข้าด้วยกัน เหมาะกับข้อมูลที่สามารถรวมได้ เช่น การนับจำนวน (Counter) และแนวทางที่สี่คือ Partitioned Write ที่กำหนดให้แต่ละ Node รับผิดชอบ Write เฉพาะข้อมูลบางกลุ่ม เป็นวิธีที่ปลอดภัยที่สุดเพราะไม่เกิด Conflict เลย

# ตัวอย่าง Retry Logic ใน Python
import time
import random
import mysql.connector

def execute_with_retry(conn, sql, params, max_retries=3):
    for attempt in range(max_retries):
        try:
            cursor = conn.cursor()
            cursor.execute(sql, params)
            conn.commit()
            return True
        except mysql.connector.Error as e:
            if e.errno in (1213, 1180):  # Deadlock / Conflict
                conn.rollback()
                wait = (2 ** attempt) + random.uniform(0, 1)
                print(f"Conflict detected, retry #{attempt+1} after {wait:.1f}s")
                time.sleep(wait)
            else:
                raise
    return False

Monitoring Multi-master Cluster

การ Monitor เป็นสิ่งสำคัญสำหรับ Multi-master Cluster เพราะต้องตรวจสอบทั้งสถานะ Node และ Conflict Rate ที่เกิดขึ้น

# MySQL Group Replication Multi-Primary
# ตรวจว่าทุก Node เป็น PRIMARY
SELECT MEMBER_HOST, MEMBER_ROLE, MEMBER_STATE
FROM performance_schema.replication_group_members;

# ตรวจ Conflict Count
SELECT COUNT_TRANSACTIONS_CHECKED, COUNT_CONFLICTS_DETECTED
FROM performance_schema.replication_group_member_stats
WHERE MEMBER_ID = @@server_uuid;

# Galera Cluster
# ตรวจสถานะ Cluster
SHOW STATUS LIKE 'wsrep_cluster_status';    -- Primary = ปกติ
SHOW STATUS LIKE 'wsrep_cluster_size';      -- จำนวน Node
SHOW STATUS LIKE 'wsrep_local_state_comment';  -- Synced = ปกติ

# ตรวจ Conflict
SHOW STATUS LIKE 'wsrep_local_cert_failures';
SHOW STATUS LIKE 'wsrep_local_bf_aborts';

# ตรวจ Flow Control (ถ้า Node ช้าเกิน Cluster จะชะลอ Write)
SHOW STATUS LIKE 'wsrep_flow_control_paused';  -- ค่าควรน้อยกว่า 0.01

ข้อควรระวังสำหรับ Multi-master

การใช้งาน Multi-master มีหลายจุดที่ต้องระวัง ประการแรก Auto-increment Conflict ถ้าไม่ตั้งค่า auto_increment_increment และ auto_increment_offset อาจเกิด Duplicate Key ต้องตั้งค่าให้ Node แต่ละตัวใช้ ID คนละชุด ประการที่สอง DDL Operations ควรทำจาก Node เดียวเท่านั้นและทำนอกเวลาที่มี Traffic สูง เพราะ DDL อาจทำให้ Cluster หยุดชั่วคราว ประการที่สาม Network Partition ถ้า Node ขาดการเชื่อมต่อจะเกิด Split-brain ได้ ต้องมีกลไก Quorum ป้องกัน ประการที่สี่ Large Transactions ควรหลีกเลี่ยง Transaction ที่แก้ไขข้อมูลจำนวนมากในครั้งเดียว เพราะต้อง Certify ทุกแถวที่แก้ไข อาจทำให้ Cluster ช้าลง

# ตั้งค่า Auto-increment สำหรับ Multi-master (3 Node)
# Node 1
[mysqld]
auto_increment_increment = 3
auto_increment_offset = 1
# Node 1 จะได้ ID: 1, 4, 7, 10, ...

# Node 2
[mysqld]
auto_increment_increment = 3
auto_increment_offset = 2
# Node 2 จะได้ ID: 2, 5, 8, 11, ...

# Node 3
[mysqld]
auto_increment_increment = 3
auto_increment_offset = 3
# Node 3 จะได้ ID: 3, 6, 9, 12, ...

สรุป

Multi-master Replication เป็นเครื่องมือที่ทรงพลังสำหรับระบบที่ต้องการ Write Availability สูง MySQL Group Replication Multi-Primary Mode ตั้งค่าได้ง่ายสำหรับผู้ที่ใช้ MySQL อยู่แล้ว MariaDB Galera Cluster ให้ Synchronous Replication ที่แน่นอนและมีความเสถียรสูง ส่วน PostgreSQL ใช้ Logical Replication แบบ Bi-directional ได้แต่ต้องจัดการ Conflict เอง สิ่งสำคัญที่สุดคือการออกแบบแอปพลิเคชันให้รองรับ Conflict ตั้งแต่แรก ไม่ว่าจะเป็น Retry Logic, Partitioned Write หรือ Conflict-free Data Structure

แนะนำบริการ DE

การรัน Multi-master Cluster ต้องการเซิร์ฟเวอร์อย่างน้อย 3 เครื่องที่มี Network Latency ต่ำระหว่างกัน เพราะ Synchronous Replication จะช้าลงตาม Latency ระหว่าง Node Cloud VPS ของ DE รองรับ Private Network ระหว่าง VPS ด้วย Bandwidth สูง เหมาะสำหรับ Galera Cluster หรือ Group Replication ที่ต้องการ Low Latency Communication

สำหรับโปรเจกต์ที่ไม่ต้องการจัดการเซิร์ฟเวอร์เอง Cloud Hosting ของ DE เป็นทางเลือกที่สะดวกพร้อม Managed Infrastructure และระบบ High Availability ให้ใช้งานได้ทันที