MYSQL 리플리케이션
리플리케이션
개요
"2개의 데이터베이스에 저장되는 데이터가 같고, 이 데이터가에 가해진 변경도 같으면 결과도 같다"
마스터에 일어난 변경을 연속적으로 기록해서 같은 변경을 슬레이브에 전송하며 슬레이브에서는 마스터에서 보내온 변경 이력을 연속적으로 재생한다.
MySQL에서는 이러한 리플리케이션을 실현하기 위해 아래와 같은 기능을 탑재하고 있다
- 바이너리 로그
- 슬레이브에 데이터를 전송하기 위한 마스터 스레드
- 슬레이브 I/O 스레드
- 릴레이 로그에서 데이터를 읽어 재생하기 위한 SQL 스레드
MySQL은 비동기형 리플리케이션을 구현하고 있으며 마스터의 변경이 일어난 이후 변경 로그를 슬레이브에 전송하여 재생하게 되며 이러한 일련의 과정은 약간의 시차를 두고 따라가게 된다.
바이너리 로그
- MySQL 서버에서 발생한 모든 변경을 직렬화하여 기록한 파일
- log_bin 옵션에서 지정한 명칭을 접두어로 하여 6자리 일련번호를 덧붙인 이름의 파일
- 서버에서 보관 중인 바이너리 로그의 정보는 별도의 인덱스 파일에 저장하게 된다. 인덱스 파일의 명칭은
log_bin에 지정한 파일명.index
이 된다. - 바이너리 로그 파일과 해당 로그의 인덱스 파일은 반드시 내용상 일치해야 한다.
- MySQL 5버젼까지는 바이너리 로그에 실행하는 SQL문(SBR, Statement Based Replication)이 기록되었지만, 값의 결정성 문제(uuid()와 같은 함수를 사용했을 때 값이 매번 바뀌는 것이 가장 대표적인 예)때문에 이후 버젼에서는 RBR(Row Based Replication) 기준으로 값일 기록하게 되었다. RBR의 경우 변경이 가해지는 전후 값을 기록하는 형식이다. 이러한 SBR과 RBR의 장점을 모두 취합한 형식도 존재하는데 MBR(Mixed Based Replication) 방식이다.
마스터 스레드
- 바이너리 로그를 읽어 슬레이브에 전송하는 역할을 수행
- 슬레이브가 여러 개인 경우, 슬레이브 숫자에 대응하는 숫자만큼의 마스터 스레드를 생성
슬레이브 I/O 스레드
- 마스터로부터 받은 로그를 릴레이 로그라는 로그파일에 보존하게 되는데 그 보존하는 작업을 수행하는 스레드
- 릴레이 로그에도 인덱스 파일이 존재
슬레이브 SQL 스레드
- 릴레이 로그에 기록된 갱신 내용을 읽어들여서 슬레이브에서 재생할 때 사용하는 스레드
- 처리량에 많아지게 되면 지연이 발생하게 되는데, MySQL 5.7에서는 이런 지연을 막아줄 수 있는 기능이 추가되었다.
Replication의 형태는?
- 마스터/슬레이브(Master -> Slave)
- 듀얼 마스터(Master <-> Master)
- 다단 구성(Master -> Slave -> Slave)
- 순환형(Master -> Master -> Master 순환참조)
- 1:N(1 Master - N Slaves)
- N:1(N Masters - 1 Slaves)
하나의 마스터가 가질 수 있는 슬레이브의 수를 제한할 수 없으며 위 형태들 중 가장 인기있는 형태는 1:N
의 형태(참조 부하를 줄여줄 수 있는 형태)이다.
리플리케이션 구축
마스터
server_id 설정
리플리케이션을 구성하는 모든 서버는 고유의 ID가 필요하게 되는데 이것이 바로 server_id이다. 32비트 Unsigned 정수이며 값이며, 중복 값을 절대 지정해서는 안된다. 해당 아이디 값은 바이너리 로그에도 기록되며, 해당 값은 리플리케이션 구조에서 값의 갱신 시 어떤 서버에서 발생한 변경인지 혹은 자기가 실행한 이벤트인지 확인하는 목적으로 사용하게 된다.
바이너리 로그 활성화
my.cnf
파일에 log_bin
옵션을 활성화하자
슬레이브 접속용 계정 생성
슬레이브가 여러 개 존재한다면 슬레이브의 수 만큼 계정을 만들어야 되는데 아래는 계정을 생성하는 간단한 SQL 스크립트이다.
1 | CREATE USER 'repl'@'192.0.2.0/255.255.255.0' IDENTIFIED BY 'secret'; |
슬레이브
마스터의 데이터 복제
현존하는 서버를 마스터로 하고 신규로 만들게 되는 서버를 슬레이브로하고 싶다면 마스터에서 데이터를 덤프를 받아 슬레이브에 적용시켜야 한다. 백업 시점은 마스터에서 바이너리 로그를 활성화한 이후여야 한다. mysqldump
사용 시, –master-data 옵션을 사용하여 바이너리 로그 위치 및 파일명을 가져올 수 있도록 지정할 수 있다.
server_id 설정
리플리케이션 설정
CHANGE MASTER
명령어를 사용
1 | CHANGE MASTER TO |
리플리케이션 시작
1 | START SLAVE USER = 'repl' PASSWORD = 'secret'; |
제대로 동작하는지 유무를 확인하려면 SHOW SLAVE STATUS
명령어를 사용하여 Slave_IO_Running, Slave_SQL_Running 항목의 값이 YES
인지 확인하자
GTID(Global Transaction ID)
바이너리 로그에 기록된 각각의 트랜잭션에 고유한 ID를 붙여주는 기능으로 아래와 같은 포맷을 가지게 된다
발신처 MySQL 서버의 ID: 트랜잭션 ID
발신처 서버의 ID는 최초 기동 시점에 UUID 값으로 생성되며 auto.cnf
파일 이름으로 저장된다. 기동 시 해당 파일이 존재하면 해당 파일의 값을 읽어서 해당 아이디 값을 사용하게 된다. 트랜잭션 ID의 값은 1부터 시작하여 단순히 증가하는 숫자 값이다.
GTID는 1:N 형태의 리플리케이션 구조에서 슬레이브의 자동 마스터 승격에 사용된다. 미래에 승격 될 가능성이 있는 슬레이브에는 log-bin 옵션과 log_slave_update 옵션을 써서 바이너리 로그를 출력하도록 설정되어 있어야 한다. GTID가 존재하기 때문에 슬레이브에서는 본인이 생성한 GTID와 마스터에서 생성한 GTID를 구분할 수 있는 기준이 생기기 때문에 변경에 대한 전후 관계를 뚜렷하게 기록할 수 있게 된다.
GTID의 온라인 상태에서의 활성화/비활성화
[TODO]: 활성화 과정 설명 추가
준동기 리플리케이션
개요
준동기 리플리케이션은 MySQL 5.5에서 추가된 기능으로 마스터에서 일어난 변경이 슬레이브에 확실하게 전달되었지를 보장하는 것을 말한다. 마스터에서 트랜잭션이 실행되면 COMMIT을 완료하기 전 슬레이브 바이너리 로그를 전송하게 되는 형식으로 완벽하게 동기화 되는 시점이 동일하지 않기 때문에 동기라고 보증할 수 없다. 준동기 리플리케이션은 통상의 리플리케이션과 비교하여 커밋의 오버헤드가 큰 편이고 ACK에 대한 지연이 특히 문제로 이어 질 수 있으므로 주의해야 한다고 한다.
2단계 커밋(Two-Phase-Commit)
커밋을 PREPARE와 COMMIT의 두 단계로 나눈 것을 말하는데, 만약 준동기 모델이 아닌 동기 모델에서 리플리케이션을 처리한다면 2단계 커밋 방식으로 처리해야 한다고 한다.
준동기 모델
무손실 리플리케이션
MySQL 5.6까지의 준동기 리플리케이션은 슬레이브가 ACK를 기다리는 시점이 클라이언트에게 응답을 반환하기 직전 타이밍이었다. 하지만, 이런 구조에서 마스터가 손상될 경우 마스터에만 존재하는 데이터가 있을 수 있는 치명적인 단점이 있다고 한다.
개선된 모델에서 기존 준동기 모델과 다른 점은 ACK를 기다리는 시점을 변경하는 것이다. MySQL 5.7에서는 COMMIT을 실행한 다음이 아니라 COMMIT을 실행하기 전 시점에 ACK를 기다리도록 변경했다.
무손실 모델
성능 개선
- 기존 MySQL 5.6에서는 ACK를 수신하기까지 다른 이벤트를 송신할 수 없다는 단점이 존재했다. 5.7에서는 새로운 ACK를 처리하는 새로운 스레드를 도입함으로써 이러한 문제를 해결했는데, 말그래도 ACK 스레드라고 부른다.
- ACK 스레드의 존재 여부는
performance_schema.threads
테이블에서 확인 할 수 있다.
멀티 소스 리플리케이션
- 여러 마스터로부터 동시에 변경을 받아들이는 구조로 MySQL 5.7부터 사용가능하다. 여러 마스터로부터 데이터를 수신할 때
채널
이라는 개념을 도입하여 사용하는데 이는 마스터와 통신하는 것 자체를 가리키는 용어이다. - 멀티 소스 리플리케이션에서는 여러 마스터의 갱신에서 경합이 생기지 않도록 주의해야 한다. 특히 각각의 마스터에 같은 데이터를 포함되지 않아야 되고 바이너리 로그의 포맷도 SBR이 아닌 RBR을 사용하도록 해야 한다.
- 데이터 갱신의 경우 비동기로 이뤄지기 때문에, 어떤 순간에 취한 데이터의 정합성이 반드시 맞기는 어려울 수 있다.
마스터 성능 개선
그룹 커밋
- 리플리케이션 간 데이터 동기화를 하려면 위에서 설명한 것처럼 바이너리 로그를 사용해야 하는데, 바이너리 로그를 쓰고 동기화하는 것 자체도 트랜잭션이라고 볼 수 있기 때문에 이에 대한 비용도 크다고 할 수 있다.
- MySQL 5.6부터는 이러한 바이너리 로그 트랜잭션에 대해 그룹단위로 커밋이 가능하도록 지원하기 시작했다.
[TODO]: 그룹커밋 이미지 추가
메모리 효율 개선
- MySQL 5.6까지는 마스터 스레드에서 바이너리 로그에 대한 이벤트를 읽을 때마다 이벤트 크기 맞는 버퍼를 할당(malloc)하고 슬레이브에 송신 후 메모리를 해제(free)하는 방식을 사용했다.
- 5.7에서 부터는 메모리 할당에 대한 성능 개선이 이뤄졌으며 매번 malloc과 free를 하지 않고 버퍼가 충분하면 그대로 사용하며 그렇지 않으면 다시 할당하여 사용하는 방식으로 변경했다.
- 거대한 메모리 버퍼를 사용했을 경우, 해당 버퍼의 크기를 정기적으로 검토하여 남은 버퍼를 축소하는 기능를 탑재했다.
마스터 스레드의 Mutex 경합 개선
5.6 이후 도입된 그룹 커밋(
sync_binlog = 1
)으로 크게 성능이 향상 되었지만, 마스터의 손상 시 슬레이브에 기록되지 않은 데이터가 발생할 수 있는 문제가 생기가 되었다.그룹 커밋 시 바이너리 로그를 디스크로
fsync
하기 전에 몇 개의 트랜잭션을 묶어 바이너리 로그에 쓰게 되는데, 이때fsync
가 끝나지 않아도write
가 되는 시점에 슬레이브로 로그 전송를 하게 된다.fsync
가 끝나지 않은 상태에서 마스터 서버에 손상이 발생하게 되면 재기동 후 바이너리 로그가 없어지고 이에 따라 해당 트랜잭션의 데이터는 롤백 처리가 된다. 하지만, 슬레이브로는 데이터가 전송이 되었기 때문에 슬레이브와 마스터의 부정합이 발생하게 되는 것이다.
- 위 그림에서 Tx1
5까지 하나의 그룹으로 커밋되고 있는데, 바이너리 로그가 디스크에5까지 해당하는 트랜잭션은 모두 롤백이 되게 된다, 하지만, 1fsync
를 하지 않은 상태에서 손상이 진행되면 13까지의 트랜잭션이 슬레이브로 전송이 된 상태이므로 13까지의 트랜잭션 데이터는 슬레이브에만 존재하게 되는 것이다. - 5.6에서 이러한 문제 때문에,
LOCK_log
라는 Mutex에 의해 보호받도록 수정했으나 그룹 커밋과 마스터 스레드가 Mutex를 얻기 위한 경합이 발생하면서 마스터 스레드에서 슬레이브로의 바이너리 로그 전송 효율이 떨어지게 되었다. - 5.7에서는
LOCK_log
는 그대로 남아 있지만, 사용하지 않고LOG_binlog_end_pos
라는 Mutex를 도입하여 LOCK의 입자 크기를 미세하게 조정했다. 해당 Mutex는 마스터 스레드가 읽어 들이는 것이 가능한 최후의 바이너리 로그의 위치를 관리하게 된다. - 5.6에서는 EOF까지 바이너리 로그를 읽었으나 이럴 경우
fsync
처리 되지 않은 로그까지 읽게 된다. 이에 반해, 5.7에서는 마스터 스레드가fsync
된 데이터만 읽도록 읽기 가능한 로그의 포지션을 관리하게 했다. 마스터 스레드는fsync
가 된 로그만을 슬레이브로 전송하게 됨으로서 마스터가 손상되어도 슬레이브에도 동일한 데이터만 남게 되는 것이다.
병렬 SQL 스레드
MTS의 LOGICAL_CLOCK 모드
- 5.6부터 슬레이브의 SQL 스레드를 여러 개 생성하여 바이너리 로그의 재생을 병렬로 수행할 수 있게 되었다. 이러한 병렬로 처리하는 것을 MTS(Multi-Thread-Salve)라 부른다.
- 리플리케이션 지연의 가장 큰 원인으로 SQL 스레드의 지연을 찾을 수 있는데, 이러한 지연은 마스터와 슬레이브 머신의 처리 차이에서 발생하게 된다. 마스터에서는 코어를 모두 사용하여 데이터를 갱신하는 반면, 슬레이브는 하나의 SQL 스레드가 하나하나 바이너리 로그를 재생하게 됨에 따라 이러한 지연이 발생하게 되는 것이다.
“결국 마스터 머신의 코어 개수가 많아 짐에 따라 이러한 지연은 더 크게 발생하게 되는 것이라 볼 수 있다.”
- 5.6에서는 이러한 병렬 SQL 스레드가 동일 데이터베이스에 활성화 시킬 수 없었기 때문에 대부분 이런 기능을 사용하지 않았지만, 5.7부터는 하나의 데이터베이스에도 병렬 SQL 스레드가 작업을 할수 있게 되었다.
1
2
3
4
5
6
7
8
9
10
11[mysqld]
# 병렬 스레드 개수
slave_parallel_workers=32
# 5.7 추가, 입자 크기 지정
slave_parallel_type=LOGICAL_CLOCK
# 5.7 추가, COMMIT 순서가 바뀌지 않도록 지정, 0일 경우 미보장
slave_preserve_commit_order=1
log_bin=mysql-log
log_slave_updates=1 - 병렬로 실행 시, 동일한 데이터에 대한 트랜잭션인지 유무는 매우 중요하다. 만약 동일한 데이터에 대한 트랜잭션이 아닐 경우 내부적으로는(InnoDB에서는 바이너리 로그 동기화를 위하여 2-Phase-Commit을 내부적으로 사용한다.) 2-Phase-Commit에서 PREPARE 상태가 되었다는 것인데 해당 상태 값은 결국 동일한 트랜잭션에서 발생할 수 있는 LOCK으로 인한 경합이 발생하지 않았을 경우일뿐이다.
- 마스터에서 슬레이브로 전달할 때 이러한 경합 혹은 순서에 대한 정보를 전달해줘야 하는데, 바이너리 로그에
last_committed
와sequence_number
라는 항목으로 몇 번째 트랜잭션인지 카운터 값을 표기하고 있는 것을 볼수 있을 것이다. - 마스터에서 실행한 트랜잭션과 슬레이브가 실행한 트랜잭션이 달라질 수 있음은 위에서 설명한대로 병럴 SQL스레드의 처리 방식때문에 충분하게 의심을 해볼 수 있게 되는데, 이때 유용한 옵션이 바로
slave_preserve_commit_order
이다. 명시적으로 1로 지정해줘야지 순서가 바뀌지 않게 되며 0으로 지정하게 되면 데이터 장합성을 보장받을 수 없게 되기 때문에 0으로 지정해서는 안된다.
마스터 튜닝
- LOGICAL_CLOCK 모드로 MTS를 사용하는 경우 마스터에서 그룹 커밋을 튜닝하여 보다 많은 트랜잭션이 하나의 그룹에 묶이도록 해야 한다.
binlog_group_commit_sync_delay
,binlog_group_commit_no_delay_count
옵션을 조정하여 환경과 상황에 맞는 최적화를 진행하는 것이 중요하며, 마스터의 갱신 처리 성능에만 집중하는 것이 아니라 슬레이브에서의 지연 유무도 역시 주시해야 한다.