목표
MariaDB가 두 서버에 존재한다. 하나는 Master로, 하나는 Slave의 역할을 맡는다.
이때, Slave는 Backup서버로서 Master의 장애 발생 시 I/O작업을 대체할 것이며, 이후 기존 Master와 Slave의 역할이 서로 교환되도록 하고 싶다. 그리고 ProxySQL이나 MHA같은 미들웨어가 이들을 중재하지 않을 때를 가정하여 수동으로 이 둘의 역할을 교환할 것이다.
GTID 복제방식을 이용할 건데, MySQL와 MariaDB의 명령어는 차이가 있어 참고자료를 찾는다면 공식문서를 활용하는 게 가장 좋은 방법이다.
설명의 편의를 위해 최초의 MasterDB 서버를 PC1, SlaveDB서버를 PC2로 칭한다.
Master/Slave 설정
✔️ PC1, PC2 내의 필요한 파일 경로 생성
우선 로그 저장을 위한 별도의 파일 공간과 이후 데이터 동기화에 필요한 파일 경로를 생성시킨다
$ mkdir -p /etc/mariadb/log/mysql-bin //로그 저장을 위한 경로 생성
$ mkdir -p /etc/mariadb/backup //파일 백업을 위한 경로 생성
✔️ 각 서버의 Config 설정
PC1의 mariaDB conf 설정
$ vi /etc/mysql/my.cnf
.. 생략 ..
[mysqld]
server_id = 1
log-bin = /etc/mariadb/log/mysql-bin
log-basename = PC1
binlog-format = ROW
sync_binlog = 1
expire-logs-days = 7
# read_only = ON // slave는 활성화, master는 비활성화
.. 생략 ..
추가해준 설정값들을 떼어 살펴보면 이렇다.
server_id =1
log-basename = PC1
서버의 고유 ID와 Binlog 파일의 이름을 지정해주었다.
이 두 값은 PC1, PC2가 각각 고유한 이름으로 지정해주는데, ID는 서로 달라야 혼동 없이 Master와 Slave를 설정하고 데이터를 주고받을 수 있게 한다. 더불어 Binlog의 이름도 달리 설정해줌으로써 헷갈림을 방지해준다.
log-bin = /etc/mariadb/log/mysql-bin
참고로, 여기서의 log는 Binary Log를 의미한다. 데이터베이스에서 어떤 데이터가 변경되었는지를 기록한다.
🫠 Binary log가 복제에서 어떻게 쓰이는 거지?
- Master가 데이터 변경 작업 (Insert, update, delete 등)을 Binlog에 기록한다.
- slave가 Binlog를 잃어 동일 작업을 수행하여 데이터 동기화를 유지한다
- Slave는 자신이 읽은 Binlog 파일의 이름과 위치(offset)을 기억해놓는다
- 복제가 실행될 때 해당 위치에 이어서 로그를 읽고 트랜잭션을 재실행한다
binlog-format = ROW
binglog는 3가지의 기록형식이 있는데 STATEMENT, ROW, MIXED 중에 ROW를 선택한다
🫠 각 기록 형식은 어떤 차이가 있는 거지?
질문에 답하기 위해, 두 형식의 예시를 가져왔다.
// STATEMENT
UPDATE users SET name = 'John' WHERE id =1;
// ROW
Table: students
Before: {id:1, score: 90}
After: {id:1, score: 99}
STATEMENT는 보는 바와 같이 쿼리 기반 로그로서, 실행된 SQL 쿼리를 기록한다. 로그 크기 자체는 비교적 작은데, 쿼리가 실행된 환경에 따라 결과가 달라질 수 있다. 그에 비해 ROW는 변경된 내용의 전후를 기록한다. 그러다 보니 로그 크기는 비교적 큰 편인데, Slave가 기록된 데이터를 그대로 반영되도록 하기 때문에 데이터 정합성을 보장한다.
여기서 그대로 반영한다는 의미는 Query를 통한 Insert가 아니라 MySQL/MariaDB의 스토리지 엔진 API를 통해 반영이 이루어지는 것이다.
🫠 왜 STATEMENT 이용 시 결과가 달라질 수 있을까
‘쿼리가 실행된 환경에 따라 결과가 달라진다’ 라는 말을 이해하려면, 다음과 같은 배경을 알고 있어야 한다.
Database의 Replication은 기본값이라면 ‘비동기적’으로 수행된다. 물론 MasterDB가 BinLog 내에 기록되는 내용 자체는 순차적이지만, Slave가 이를 읽고 처리하는 것이 비동기로 수행되는 것이다. 즉 이러한 동작이 진행되는데,
- master는 binlog에 기록한 즉시 클라이언트에게 알린다
- slave는 해당 로그에 대한 처리를 시작하며, 시간이 걸린다
- Master에서 새로운 트랜잭션이 발생한다
- Slave가 master의 binlog를 읽고 처리하는 속도를 따라가지 못해 상태가 어긋난다
그러니 STATEMENT 기록 형식을쓰는 상황이라면 Query가 한 건씩 기록되어 있는 형태인 와중에 Master엔 데이터가 10가 있는데 Slave는 8개까지만 있는 상황이되었다고 치자. 여기서 update-where- 같은 쿼리가 들어가면 최종적으로 일부만 업데이트가 되니 정합성 문제가 나타날 수 있는 것이다.
더불어 쿼리로 Random()함수를 쓴다고 생각하면, Random값은 실행시점마다 달라질테니 정합성이 깨지는 원인 중 하나가 된다.
sync_binlog=1
다음으로 넘어가면, 이 설정은 binlog를 디스크에 동기화하는 빈도를 결정한다.
: 이 값이 0이라면?
binlog를 OS의 파일 시스템 캐시에만 기록하고 디스크로의 동기화는 OS에게 맡긴다. 캐시를 주로 이용하니 가장 빠른데, 서비스가 발생하면 OS 캐시에 저장된 Binlog 데이터는 손실될 것이다.
: 그러나 이 값을 1로 둔다면?
각 트랜잭션이 Commit되면 Binlog를 디스크에 동기화하도록 만들었다. 그렇다면 동기화 작업이 빈번하니 느릴 순 있으나, 데이터 안전성은 가장 좋다고 볼 수 있다.
: 2, 3, 4..로 둔다면?
N개의 트랜잭션 만큼 커밋된 이후 한 번씩 디스크에 동기화하는 빈도 수를 취할 수 있다.
남은 하나 expire-logs-days는 로그 자동삭제 기간이니 패스
설정을 다 했다면 mysql을 재 실행하여 conf를 적용시켜준다.
위의 설정으로 PC1에 해줄 것은 끝났다. 적용을 위해 mariadb를 재시작해준다.
/etc/init.d/mariadb restart
pf -ef | grep mariadb // 정상 실행 확인
✔️ PC2의 CONF 설정
.. 생략 ..
[mysqld]
server_id = 2
log-bin = /etc/mariadb/log/mysql-bin
log-basename = PC1
binlog-format = ROW
sync_binlog = 1
expire-logs-days = 7
read_only = ON // slave는 활성화, master는 비활성화
.. 생략 ..
systemctl stop mariadb
systemctl start mariadb
pf -ef | grep mariadb // 정상 실행 확인
PC1과 다른 점은 read_only부분이다. PC2의 Database는 Slave로서, 쓰기작업은 일어나지않고 백업 용도로만 활용되도록 한다.
✔️ PC1의 복제계정 생성 및 Lock 제어
$ mysql -u root -p
> grant replication slave on *.* to 'repl'@'{PC2 IP}' identified by '{password}'
> flush privileges;
> flush table with read lock;
> select @@GLOBAL.gtid_current_pos // 여러번 수행하여 변경 내용이 없어야 함
PC2가 PC1에 접근할 수 있도록 미리 계정을 만들어 놓는다
더불어서 이제 GTID 기반 복제방식을 위해 데이터 변경이 없도록 lock을 걸며, current_pos를 확인한다
(단, 이 방식의 lock은 ‘현재 세션’에 대해서만 쓰기작업이 없도록 하는 것이며 새로운 세션에 대한 행위는 막지 못한다. Mysql는 다른 방법으로도 막을 수 있지만, Mariadb에서는 root 권한 사용자의 쓰기 작업을 막기 위한 다른 방법은 제공되지 않는 것으로 보였다. )
✔️ PC1 → PC2 데이터 동기화
GTID 기반 복제방식을 취할 거긴한데, 그전에 두 데이터베이스는 초기 데이터가 완벽히 일치해야한다. 혹은 둘 다 새로운 DB라든지. scp혹은 Filezila와 같은 FTP 전송 프로그램을 활용하면 된다.
// PC1에서
$ mysqldump --all-databases --master-data=2 -u root -p > /etc/mariadb/backup/backup.sql //-master-data 옵션은 binlog 정보를 주석으로 저장한다는 의미.
$ scp /etc/mariadb/backup/backup.sql {계정}@{PC2}:/etc/mariadb/backup/backup.sql
// PC2에서
$ mysql -u root -p < /etc/mariadb/backup/backup.sql
// pc1의 DB내에서
> unlock tables;
Dump를 수행 한 후, PC1에서는 걸어뒀던 Lock을 해제해준다.
✔️ PC2의 복제 마스터 설정
mysql -u root -p
> reset slave all;
> SET global gtid_slave_pos = '';
> change master to master_host = '{PC1 IP}',
master_port = 3306,
master_user = 'repl',
master_password='{password}',
master_user_gitd=slave_pos;
> start slave
> show slave status\\G;
맨 앞의 reset, SET global gtid_slave_pos는 slave_pos방식의 복제를 하기 위한 사전 설정이다.
원래는 current_pos를 통해 마스터 DB 기준으로 설정하는 방식도 있지만, 지금은 사람 기준으로 눈에보이는 정확한 포지션을 Slave에게 직접 지정하여 Slave가 알고 있는 값 기준으로 복제를 진행하도록 만들었다.
show slave status\G 결과로 IO_Running과 SQL_Running 상태가 YES로 확인된다면 잘 설정된 것이다.
Slave DB를 I/O 수행하도록 만들기
만약 Master/slave를 연결하는 Proxy를 두었다면 그 Proxy 역할의 미들웨어가 자동으로 죽은 DB를 감지하여 클러스터링 할 수 있을텐데, 상황이 여의치않아 그런 미들웨어를 제공할 수 없었고 따라서 수동으로 해야한다면 다음과 같이 할 수 있다.
MasterDB가 장애가 일어나 죽었다면, Backup을 담당하던 slave에 I/O를 허용하면 된다. 그리고 사용중인 application에게 이 slave DB의 IP를 가리키도록 하면 끝난다.
[mysqld]
server_id = 2
log-bin = /etc/mariadb/log/mysql-bin
log-basename = PC2
binlog-format = ROW
sync_binlog = 1
expire-logs-days = 7
# read_only = ON // slave는 활성화, master는 비활성화
즉, DB는 기존의 설정값에서 read_only 만 비활성화 재실행하면 적용된다.
Slave DB를 Master로, MasterDB를 Slave로
이전에 했던 작업을 입장만 바꿔 설정하면 된다. 다만, masterDB였었던 PC1은 데이터를 초기화해줄 필요가 있다.
✔️ PC1의 conf파일 수정
raad_only = ON
이제 이 친구가 읽기전용의 백업이 될테니 해당 설정을 해주고 DB를 재실행해준다.
✔️ PC2의 복제계정 생성 및 Lock제어
$ /etc/mariadb/bin/mysql -u root -p
> grant replication slave on *.* to 'repl'@'{PC1 IP}' identified by '{password}'
> flush privileges;
> flush table with read lock;
> select @@GLOBAL.gtid_current_pos // 여러번 수행하여 변경 내용이 없어야 함
✔️ PC2 → PC1 데이터 동기화
// PC2에서
$ mysqldump --all-databases --master-data=2 -u root -p > /etc/mariadb/backup/backup.sql //-master-data 옵션은 binlog 정보를 주석으로 저장한다는 의미.
$ scp /etc/mariadb/backup/backup.sql {계정}@{PC1}:/etc/mariadb/backup/backup.sql
// PC1에서
$ /etc/mariadb/bin/mysql -u root -p < /etc/mariadb/backup/backup.sql
// pc2의 DB내에서
> unlock tables;
Dump를 수행 한 후, PC2에서는 걸어뒀던 Lock을 해제해준다.
✔️ PC2의 복제 마스터 설정
$ mysql -u root -p
> reset slave all;
> SET global gtid_slave_pos = '';
> change master to master_host = '{PC2 IP}',
master_port = 3306,
master_user = 'repl',
master_password='{password}',
master_user_gitd=slave_pos;
> start slave
> show slave status\\G;
show slave status\G 결과로 IO_Running과 SQL_Running 상태가 YES로 확인된다면 잘 설정된 것이다.