본문 바로가기
삵
MariaDB

MariaDB Galera로 3노드 동기 복제 구성

열열린기술자·2026년 9월 25일·조회 1

운영하다 보면 마스터 한 대에 슬레이브 여러 대를 붙인 비동기 복제로는 아쉬운 순간이 온다. 장애가 나면 어느 슬레이브를 승격할지 사람이 판단해야 하고, 승격 전까지 쓰기가 멈춘다. 그럴 때 후보로 떠오르는 게 세 노드가 동시에 쓰기를 받는 Galera Cluster다. 예전에 비동기 복제 페일오버를 스크립트로 힘겹게 자동화하던 글을 쓴 적이 있는데, 이번엔 그 반대편에 있는 동기 복제 쪽을 정리한다.

결론부터 말하자면, Galera Cluster는 세 노드에 같은 galera.cnf를 깔고 첫 노드만 galera_new_cluster로 부트스트랩한 뒤 나머지를 순서대로 올리면 된다. 노드 수는 홀수(3, 5)로 잡아 쿼럼을 확보하고, 쓰기 부하가 크면 gcache.size와 gcs.fc_limit를 기본값 위로 올려 플로우 컨트롤로 클러스터가 멈추는 상황을 줄인다. 장애 노드는 다시 켜기만 하면 IST 또는 SST로 자동 재합류한다.

1. 개요와 핵심 용어

Galera는 MariaDB(와 MySQL)에 동기 멀티마스터 복제를 붙이는 라이브러리다. 모든 노드가 쓰기를 받고, 커밋 시점에 각 노드로 변경분(write-set)을 전파해 인증(certification)에 통과해야 커밋이 확정된다. 그래서 어느 노드에 붙어도 같은 데이터를 본다.

wsrep는 write-set replication의 약자로, MariaDB 서버와 Galera 라이브러리(libgalera_smm.so)를 잇는 인터페이스 규격이다. 설정 변수와 상태 변수가 전부 wsrep_ 접두어를 쓴다.

쿼럼(quorum)은 클러스터가 서비스를 계속할지 판단하는 과반 정족수다. 네트워크가 갈라졌을 때 노드 과반을 가진 쪽만 Primary Component로 살아남고, 나머지는 쓰기를 거부한다. 이 규칙이 split-brain, 즉 갈라진 양쪽이 각자 쓰기를 받아 데이터가 엇갈리는 상황을 막는다.

SST(State Snapshot Transfer)는 새 노드가 합류할 때 다른 노드의 데이터 전체를 통째로 복사하는 방식이고, IST(Incremental State Transfer)는 잠깐 빠졌던 노드가 그동안 밀린 write-set만 받아 따라잡는 방식이다. 밀린 양이 gcache에 남아 있으면 IST로 끝나고, 없으면 SST로 전체를 다시 받는다.

아래에서는 3노드 구성 파일 작성, 부트스트랩, 쿼럼과 split-brain 방지, 플로우 컨트롤과 gcache 튜닝, 장애 후 재합류, 부하분산 검증을 차례로 살펴본다.

2. 환경과 사전 준비

아래 예시는 RHEL 9 호환 배포판에 MariaDB 10.11 LTS를 설치한 상태를 가정한다. 버전 번호는 저장소 상태에 따라 다를 수 있다.

  • node1: 192.168.10.11
  • node2: 192.168.10.12
  • node3: 192.168.10.13

Galera는 클라이언트 포트 외에 세 포트를 더 쓴다. 세 노드가 서로 이 포트로 통신하도록 방화벽을 먼저 열어 둔다.

3306/tcp  # 클라이언트 접속
4567/tcp  # gcomm 그룹 통신(복제)
4567/udp  # 멀티캐스트(쓰는 경우)
4568/tcp  # IST
4444/tcp  # SST
# 각 노드에서
sudo firewall-cmd --permanent --add-port={3306,4567,4568,4444}/tcp
sudo firewall-cmd --permanent --add-port=4567/udp
sudo firewall-cmd --reload

여기서 한 번 걸리기 쉽다. 4444나 4568이 막혀 있으면 노드는 클러스터 멤버로는 보이는데 SST/IST 단계에서 멈춘다. 로그에 WSREP: Failed to prepare for 같은 줄이 뜨면 방화벽부터 의심한다. SELinux가 Enforcing이면 비표준 포트 사용 시 별도 정책이 필요하니, 우선 getenforce로 상태를 확인해 둔다.

3. galera.cnf 작성

세 노드에 거의 같은 파일을 넣는다. RHEL 계열은 /etc/my.cnf.d/galera.cnf에 둔다. 노드마다 다른 건 wsrep_node_address와 wsrep_node_name 두 줄뿐이다.

[galera]
wsrep_on               = ON
wsrep_provider         = /usr/lib64/libgalera_smm.so
wsrep_cluster_name     = sarc_galera
wsrep_cluster_address  = gcomm://192.168.10.11,192.168.10.12,192.168.10.13

# 노드별로 다르게
wsrep_node_address     = 192.168.10.11
wsrep_node_name        = node1

# Galera 필수 조건
binlog_format          = ROW
default_storage_engine = InnoDB
innodb_autoinc_lock_mode = 2

# SST 방식
wsrep_sst_method       = mariabackup
wsrep_sst_auth         = "sst_user:StrongPass!"

# 병렬 적용 스레드
wsrep_slave_threads    = 4

# 클라이언트가 모든 IP로 붙도록
bind_address           = 0.0.0.0

세 옵션은 협상 대상이 아니다. Galera는 InnoDB만 지원하고, 행 기반 바이너리 로그(binlog_format=ROW)를 요구하며, 여러 노드가 동시에 자동 증가 값을 쓰기 때문에 innodb_autoinc_lock_mode=2가 있어야 값 충돌을 피한다. 이 셋 중 하나라도 빠지면 노드가 아예 뜨지 않거나 뜨더라도 데이터가 어긋난다.

wsrep_sst_method의 기본값은 rsync다. 소규모 테스트라면 그대로 써도 되지만, 데이터가 크면 SST 동안 공여 노드(donor)가 읽기 잠금으로 묶이는 시간이 길어진다. 그래서 운영에서는 잠금 부담이 적은 mariabackup을 권한다. 대신 mariadb-backup 패키지가 설치돼 있어야 하고, SST 전용 계정이 필요하다.

# 부트스트랩 노드에서 한 번만 (합류 노드로 자동 복제됨)
CREATE USER 'sst_user'@'localhost' IDENTIFIED BY 'StrongPass!';
GRANT RELOAD, PROCESS, LOCK TABLES, BINLOG MONITOR, REPLICA MONITOR ON *.* TO 'sst_user'@'localhost';
FLUSH PRIVILEGES;

4. 부트스트랩과 노드 합류

클러스터는 반드시 한 노드만 새로 시작해야 한다. 기존 멤버가 하나도 없는 상태에서 galera_new_cluster로 첫 노드를 띄우고, 그 뒤에 나머지를 평소처럼 시작한다. 이 순서를 지키는 게 핵심이다.

# node1 에서만
sudo galera_new_cluster

# 뜬 뒤 크기 확인
mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size'"
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 1     |
+--------------------+-------+

이제 node2, node3을 일반 방식으로 시작하면 각자 wsrep_cluster_address에 적힌 멤버를 찾아 합류한다.

# node2, node3 각각
sudo systemctl start mariadb

세 노드가 다 붙으면 어느 노드에서 조회해도 크기가 3으로 나온다.

+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 3     |
+--------------------+-------+

전체 재시작 후에는 절대 두 노드를 동시에 galera_new_cluster로 띄우지 않는다. 그렇게 하면 서로 다른 클러스터 UUID를 가진 두 클러스터가 생겨 데이터가 갈린다. 마지막까지 살아 있던 노드를 부트스트랩 노드로 삼는 게 원칙이고, 어느 노드가 가장 최신인지는 각 노드의 grastate.dat 파일에서 seqno 값으로 판단한다.

5. 쿼럼과 split-brain 방지

클러스터가 살아 있는지, 즉 Primary Component에 속하는지는 다음 상태 변수로 확인한다.

MariaDB [(none)]> SHOW GLOBAL STATUS WHERE Variable_name IN
    -> ('wsrep_cluster_status','wsrep_ready','wsrep_local_state_comment');
+---------------------------+---------+
| Variable_name             | Value   |
+---------------------------+---------+
| wsrep_cluster_status      | Primary |
| wsrep_local_state_comment | Synced  |
| wsrep_ready               | ON      |
+---------------------------+---------+

wsrep_cluster_status가 Primary이고 wsrep_ready가 ON이면 쓰기를 받는다. 네트워크가 끊겨 한 노드가 과반에서 밀려나면 그 노드는 non-Primary로 바뀌고, 쿼리를 던지면 아래 에러로 거부한다.

ERROR 1047 (08S01): WSREP has not yet prepared node for application use

이게 split-brain을 막는 동작이다. 3노드에서 1노드가 고립되면 2대 있는 쪽만 과반이라 계속 쓰기를 받고, 고립된 1대는 스스로 쓰기를 멈춘다. 양쪽이 각자 쓰다가 나중에 충돌하는 사태가 원천적으로 안 생긴다.

그래서 노드 수는 홀수로 잡는다. 2노드 구성은 한 대만 죽어도 남은 한 대가 과반(2 중 2)을 못 채워 같이 멈춰 버린다. 노드를 2대만 둘 사정이면 갈라레라 아비트레이터(garbd)를 세 번째 투표자로 붙여 홀수를 맞춘다.

데이터센터를 둘로 나눠 배치하는 경우엔 pc.weight로 특정 노드에 가중치를 줘서, 어느 쪽이 갈라졌을 때 주 사이트가 살아남도록 조정할 수 있다. 기본값은 노드당 1이다. 운영 판단이 바뀌지 않는 한 기본값을 건드릴 필요는 없다.

6. 플로우 컨트롤과 gcache 튜닝

동기 복제라 한 노드가 write-set 적용을 못 따라가면 클러스터 전체가 그 노드를 기다린다. 이때 느린 노드가 "잠깐 멈춰라" 신호를 보내 전체 쓰기를 잠시 세우는 장치가 플로우 컨트롤이다. 적용 대기 큐가 gcs.fc_limit을 넘으면 발동한다.

기본값은 gcs.fc_limit=16, gcs.fc_factor=1.0이다. 쓰기가 몰리는 환경에서 이 값이 낮으면 클러스터가 자주 멈춰 처리량이 떨어진다. 멈춘 시간 비율은 아래로 본다.

MariaDB [(none)]> SHOW GLOBAL STATUS LIKE 'wsrep_flow_control_paused';
+---------------------------+----------+
| Variable_name             | Value    |
+---------------------------+----------+
| wsrep_flow_control_paused | 0.000000 |
+---------------------------+----------+

이 값은 0에 가까워야 좋다. 마지막 상태 조회 이후 클러스터가 플로우 컨트롤로 멈춰 있던 시간 비율인데, 0.1을 넘어가면(전체의 10% 이상 멈춤) 튜닝이 필요하다는 신호다. wsrep_flow_control_sent가 계속 늘어나는 노드가 병목이다. 대응은 두 갈래다. 느린 노드의 wsrep_slave_threads를 올려 적용을 빠르게 하거나, gcs.fc_limit을 키워 멈추는 빈도를 낮춘다.

wsrep_provider_options = "gcache.size=2G; gcs.fc_limit=64; gcs.fc_factor=0.8"

gcache.size는 각 노드가 최근 write-set을 담아 두는 링 버퍼의 크기다. 기본값 128M은 작다. 이 버퍼가 크면 장애 노드가 돌아왔을 때 밀린 분량이 아직 버퍼에 남아 있어 무거운 SST 대신 가벼운 IST로 끝날 확률이 높아진다. 노드가 빠져 있을 만한 시간 동안 쌓이는 쓰기 양을 가늠해서, 디스크 여유를 보고 수 GB 수준으로 잡는다. 정답 숫자는 환경에 따라 다르니 재합류가 자꾸 SST로 떨어지면 더 키운다.

튜닝값을 galera.cnf에 넣고 나면 노드를 재시작해야 반영된다. 한 노드씩 롤링으로 재시작해서 쿼럼을 유지한다.

7. 노드 장애 후 SST/IST 재합류

node3을 강제로 내려 장애를 흉내 낸 뒤 다시 올려 본다.

# node3 정지
sudo systemctl stop mariadb

# 남은 노드에서 크기 확인 -> 2로 줄고 서비스는 계속
mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size'"
+--------------------+-------+
| wsrep_cluster_size | 2     |
+--------------------+-------+

node3을 다시 시작하면 재합류가 일어난다. 어떤 방식으로 붙었는지는 로그에서 확인한다.

sudo systemctl start mariadb
sudo journalctl -u mariadb -n 40 --no-pager

밀린 write-set이 다른 노드의 gcache에 남아 있으면 IST로 붙고, 로그에 이런 줄이 보인다.

WSREP: Receiving IST: 1842 writesets, seqnos 91032-92874
WSREP: IST received: :92874
WSREP: Member 2.0 (node3) synced with group.

반대로 노드가 오래 빠져 있어 gcache에서 밀려났으면 SST로 전체를 다시 받는다. 이때는 공여 노드가 정해지고 mariabackup이 도는 로그가 나온다.

WSREP: Node 2.0 (node3) requested state transfer from '*any*'. Selected 0.0 (node1)(SYNCED) as donor.
WSREP: Running: 'wsrep_sst_mariabackup ... '
WSREP: SST received: :93551

둘 다 마지막에 synced with group이 뜨고 wsrep_local_state_comment가 Synced가 되면 재합류가 끝난 것이다. 사람이 개입할 일은 없다. 만약 SST가 반복 실패하면 SST 계정 권한이나 4444 포트, wsrep_sst_auth 값을 다시 확인한다.

8. 부하분산 검증

세 노드가 모두 쓰기를 받으니 앞단에 로드밸런서를 두고 나눠 보낸다. HAProxy로 라운드로빈을 걸되, 죽은 노드로 트래픽이 가지 않게 헬스 체크가 중요하다. Galera는 mysqld_exporter 없이도 상태를 HTTP로 뱉는 clustercheck 스크립트를 제공한다(xinetd로 9200 포트에 물린다).

backend galera_write
    balance roundrobin
    option httpchk GET /
    server node1 192.168.10.11:3306 check port 9200 inter 2s rise 2 fall 3
    server node2 192.168.10.12:3306 check port 9200 inter 2s rise 2 fall 3
    server node3 192.168.10.13:3306 check port 9200 inter 2s rise 2 fall 3

분산이 실제로 되는지는 각 노드의 커밋 카운터로 본다. 부하를 주면서 세 노드의 wsrep_local_commits가 고르게 올라가면 정상이다.

for h in 11 12 13; do
  echo -n "node$h: "
  mysql -uroot -p'****' -h 192.168.10.$h \
    -e "SHOW GLOBAL STATUS LIKE 'wsrep_local_commits'" -sN
done

여기서 한 가지 함정이 있다. 여러 노드에 동시에 쓰기를 뿌리면 같은 행을 서로 다른 노드에서 고치다가 인증 단계에서 충돌이 나 한쪽 트랜잭션이 데드락으로 롤백된다(ERROR 1213). wsrep_local_cert_failures가 눈에 띄게 늘면 이 현상이다. 그래서 현장에서는 읽기는 세 노드로 분산하되 쓰기는 한 노드로 몰아주는 구성을 자주 택한다. 멀티마스터가 가능하다는 것과 모든 쓰기를 아무 노드에나 뿌려도 된다는 건 다른 이야기다.

MariaDB [(none)]> SHOW GLOBAL STATUS LIKE 'wsrep_local_cert_failures';
+---------------------------+-------+
| wsrep_local_cert_failures | 0     |
+---------------------------+-------+

9. 결론

Galera Cluster는 같은 galera.cnf를 세 노드에 배포하고 첫 노드만 galera_new_cluster로 띄우면 나머지는 알아서 붙는다. 홀수 노드로 쿼럼을 확보하면 split-brain은 클러스터가 스스로 막아 준다. 운영에서 손이 가는 지점은 두 곳이다. 쓰기가 몰릴 때 gcache.size와 gcs.fc_limit을 기본값 위로 올려 플로우 컨트롤로 멈추는 시간을 줄이는 것, 그리고 로드밸런서에 제대로 된 헬스 체크를 걸어 죽은 노드를 빼는 것이다.

동기 복제라 쓰기 지연이 비동기보다 늘고, 여러 노드 동시 쓰기는 인증 충돌을 부른다. 읽기는 분산, 쓰기는 단일 노드로 시작해서 충돌 지표를 보며 넓혀 가는 편을 권한다.

자주 묻는 질문

Galera Cluster는 노드를 몇 대로 구성해야 하나?

쿼럼(과반) 확보를 위해 홀수로 잡는다. 3노드가 표준이며 한 대가 죽어도 남은 2대가 과반이라 서비스가 계속된다. 2노드 구성은 한 대만 죽어도 과반을 못 채워 같이 멈추므로, 부득이하면 갈라레라 아비트레이터(garbd)를 세 번째 투표자로 붙여 홀수를 맞춘다.

SST와 IST는 언제 각각 발생하나?

노드가 재합류할 때 밀린 write-set이 다른 노드의 gcache 링 버퍼에 아직 남아 있으면 밀린 분량만 받는 IST로 끝난다. 노드가 오래 빠져 있어 버퍼에서 밀려났으면 데이터 전체를 다시 받는 SST가 돈다. gcache.size를 키우면 IST로 끝날 확률이 높아진다.

split-brain은 어떻게 방지되나?

네트워크가 갈라지면 노드 과반을 가진 쪽만 Primary Component로 남아 쓰기를 받고, 과반에 못 미치는 쪽은 non-Primary가 되어 쓰기를 거부한다(ERROR 1047). 양쪽이 각자 쓰다 충돌하는 상황이 구조적으로 생기지 않는다. 노드를 홀수로 두는 것이 전제다.

wsrep_flow_control_paused 값이 높으면 어떻게 대응하나?

이 값은 클러스터가 플로우 컨트롤로 멈춰 있던 시간 비율이며 0에 가까워야 한다. 0.1을 넘으면 wsrep_flow_control_sent가 큰 노드가 병목이다. 그 노드의 wsrep_slave_threads를 올려 적용 속도를 높이거나, gcs.fc_limit(기본 16)을 키워 멈추는 빈도를 낮춘다.

세 노드에 쓰기를 모두 분산해도 되나?

기술적으로는 가능하지만 같은 행을 여러 노드에서 동시에 고치면 인증 단계에서 충돌해 한쪽이 데드락으로 롤백된다(ERROR 1213). wsrep_local_cert_failures가 늘면 이 현상이다. 읽기는 세 노드로 분산하고 쓰기는 한 노드로 몰아주는 구성으로 시작하는 것을 권한다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.