1. replication
Database 관점에서 replication이란 데이터베이스의 복제를 의미한다. 단순 그 시각에서의 복제 뿐만 아니라, 복제 서버로써 기존 서버와의 동기화도 되어있는 서버를 만드는 것이다.
이러한 replication 기술은 기존 서버를 master, 복제 서버를 slave 라고 부르기도 하는데, 이를 사용하는 이유는 다음과 같다.
1.high availability, data security
만약 데이터베이스에 화재가 나서 복구가 당장 시급할 때, 복제 서버 한대를 백업용으로 사용 중이었다면 손쉽게 복구 한 후 서비스를 지속가능하게 유지할 수 있다.
또한 data corruption이 일어나지 않게도 할 수 있다.
2. scale-out
기존 서버 한대에는 쓰고, 읽기가 모두 가능하고 복제 서버는 읽기만 가능하다. 데이터를 읽을 때 기존 서버한대만을 이용해서 여러 요청을 시도하면, 기존 서버에 과부하가 걸려서 그 과정이 느려질 수 있다. 하지만 복제서버를 두고 이용자 log을 확인한다든지, (analytics)
카톡을 보낼 때 쓸 때는 기존서버에 쓰고 복제 서버를 통해서 톡을 읽는 다든지 서버의 기능을 분할해서 사용하는 용도로도 사용가능하다.
3. Long-distance data distribution
먼 곳에서 데이터 베이스를 접근할 수 있도록 하기 위해 사용할 수 있다. 지리적으로 여러 곳에 데이터베이스를 분산할 때도 사용한다.
2. replication 과정
쉽게 표현하면 mysql client가 쿼리문을 입력하면, mysql server 에서 c++, c 로 구현된 mysql query 들이 복제된다.
binary_logging 옵션을 켜두면 이게 binary_log에 이 명령이 저장된다.
(이때 복제형식에는 sql query 문 자체로 복사하는 statement 옵션도 있고, 더 로우하게 변경을 저장하는 row 옵션도 있다. )
이 binary_log 에서 파일을 가져와서 replica server 에서도 그대로 데이터 변경이 일어나도록 한다.
이게 slave (replica server) 로 byte array 로 전송되기 때문에, python-mysql-replication 서비스가 이를 파싱해서 replica (slave ) server 에 변경이 일어나도록 돕는다.
byte array 를 파싱하는 과정이기 때문에, 마치 http 파싱처럼 각 데이터가 가지는 길이에 해당하는 바이트씩 끊어서 데이터를 읽어들이는 굉장히 로우한 과정이 포함되어 있다.
(hexdump 참고)
(공식 문서 참고)
3. replication 과정에서 중요한 것
복제서버를 이용할 때 중요한 것은, 실제 서버와 동기화가 잘 되어있어야 하는 점이다.
만약 실제 서버의 sql query 를 잘못 해석해서 두번 query를 실행한다든지의 오류가 발생한다면 큰 문제가 될 수 있다.
이럴 때 transaction 개념과 GTID 개념이 사용된다.
transaction은 데이터베이스 측면에서 데이터를 변경하는 하나의 작업 단위라고 말할 수 있고, 이를 사용하면 중간에 오류가 났을 때
commit 되었던 transaction log 를 참고해서 다시 시작하도록 할 수 있어서 데이터의 무결성을 지킬 수 있다.
이를 위해서 GTID라는 개념이 replication에서 사용된다. 쉽게 이야기 하면 transaction을 식별하는 고유 ID 이다.
(Global Transaction IDentifier )
4. GTID
mysql에서 GTID에 대해 분석해보았다. ( 공식문서 참고)
- GTID 라는 개념은 고유 식별자로써 master(source 라고도 한다. ) 와 복제본 (replica , slave 라고도 한다)에서 같은 값을 가진다.
transaction을 구분하는 용도이다.
- GTID를 사용할 때 각 트랜잭션은 원래 서버에서 커밋되고 모든 복제본에 의해 적용될 때 식별되고 추적될 수 있다.
소스(master)에서 커밋된 모든 트랜잭션이 복제본에서도 커밋되는 한, 둘 사이의 일관성이 보장될 수 있다.
- source에서 데이터 변경 작업이 일어나서 transaction이 커밋되면, binary_log 에 저장이 될 때 GTID 값이 할당된다. ( 데이터 읽는 작업에는 binary_log에 저장이 안된다. 변경사항이 있을 때만 저장됨. 그리고 GTID는 단조증가하는 값. )
- replica 서버에서 지정된 GTID가 있는 트랜잭션이 시작되었지만 아직 커밋이나 롤백되지 않은 경우, 동일한 GTID를 가진 transaction은 시작하려는 모든 시도가 차단된다. 서버는 동시 트랜잭션 실행을 시작하지도 않고 클라이언트에 제어권을 반환하지도 않는다. 트랜잭션의 첫 번째 시도가 커밋되거나 롤백되면 동일한 GTID에서 차단되고 있던 그 세션이 진행될 수 있다.
GTID 는 다음과 같이 표현된다.
GTID = source_id:transaction_id
source_id는 원래 서버를 식별하고, server_uuid가 이 값에 사용된다.
transaction_id: 원래 서버 즉 source_id 가 명시한 서버에서 커밋된 몇 번째 transaction을 의미하는지의 정보이다. 단순히 숫자일 때도 있지만 몇번째부터 몇번째까지의 transaction인지의 의미로 i-j 로 표현되기도 한다. 그리고 : 로 구분되면서 계속 범위가 추가될 수도 있다.
예시)
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5
(':'이전은 source_id)
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-3:11:47-49
(첫번째 ':' 이전까지는 source_id, 이후 ':'부터는 범위의 구분)
추가 ) 다른 서버의 값까지 포함할 수도 있다.
예시)
2174B383-5441-11E8-B90A-C80AA9429562:1-3, 24DA167-0C0C-11E8-8442-00059A3C7B00:1-19
(',' 로 서버 source_id가 구분됨)
<mysql.gtid_executed 테이블>
- 예기치 않게 서버가 중지되는 경우 현재 바이너리 로그 파일의 GTID 집합이 mysql.gtid_executed 테이블에 저장되지 않는다. 복제를 계속할 수 있도록 binary _log 파일의 table에 추가되어야 하는데 --skip-log-bin 설정이 되어있거나 --disable-log-bin 설정으로 인해 서버가 다시 시작될 때 binary_logging 을 비활성화한 경우, 서버가 binary log 파일에 엑세스해서 GTID를 복구할 수 없으므로 복제를 시작할 수 없다.
- mysql.gtid_executed 테이블이 모든 transaction에 대한 GTID 전체 레코드를 가지고 있지 않는다. 8.0.17이전 버전의 release 에서는 mysql.gtid_executed테이블 쿼리대신 매 commit마다 update되어있던 @@GLOBAL.gtid_executed 을 사용한다.
binary_logging이 서버에 활성화 되어있는 경우, mysql.gtid_executed 테이블은 binary log rotation 마다 압축된다. (1 - 5까지의 값이 연속적으로 있다면 1-5로 표현해두는 것이다. ) binary logging이 disable 된 경우에는 gtid_executed compression period 값을 이용해서 압축 주기를 설정해줄 수 있고, 이걸 수행하는 thread가 이 값에 따라 loop를 돌면서 압축을 수행한다.
- 8.0.17 버전 부터는 InnoDB transaction은 non-InnoDB transaction과 다른 process에 의해 적힌다. (innodb/clone_gtid_thread) 이 thread는 GTID를 group 별로 모은다음 flush 하고, compress하는 방식을 거친다.
이럴 때 non-InnoDB transaction과 혼합된 server의 경우 mysql.gtid_executed table 에 서로 다른 thread가 쓰기를 실천할 때, 압축까지 하는 과정에서 이 과정이 굉장히 느려질 수 있다. 그래서 이럴 땐 gtid_executed_compression_period 값을 0으로 두어서 압축 thread를 아예 활성화 되지 않도록 하는 것이 해결책으로 제시되어 있다.
gtid_executed_compression_period 값은 1000이 기본 값이다. 1000 transaction 마다 압축을 수행하는 것이다. server instanace가 시작했을 때 0이 아닌 값으로 세팅되어 보통 mysql.gtid_executed 테이블을 압축하기 시작한다.
8.0.17버전 이전에는 binary log rotate 시에 압축을 하고, 8.0.20버전 이후로는 thread launch에 의해 trigger 되어 compression이 시작된다. 그 사이에는 시작될 때 압축이 시작되지는 않는다.
Life cycle
(공식문서 참고)
1. 트랜잭션이 실행되고 소스에서 커밋됩니다.
이 클라이언트 트랜잭션에는 소스의 UUID와 이 서버에서 아직 사용되지 않은 가장 작은 0이 아닌 트랜잭션 시퀀스 번호로 구성된 GTID가 할당됩니다. GTID는 소스의 바이너리 로그(로그에서 트랜잭션 자체 바로 앞)에 기록됩니다. 클라이언트 트랜잭션이 바이너리 로그에 기록되지 않은 경우(예: 트랜잭션이 필터링되었거나 트랜잭션이 읽기 전용이었기 때문에) GTID가 할당되지 않습니다.
2. 트랜잭션에 GTID가 할당된 경우 트랜잭션 시작 시 바이너리 로그에 GTID를 기록하여 커밋 시 GTID가 원자적으로 유지됩니다 (Gtid_log_event로) 바이너리 로그가 회전하거나 서버가 종료될 때마다 서버는 이전 바이너리 로그 파일에 기록된 모든 트랜잭션의 GTID를 mysql.gtid_executed 테이블에 기록합니다.
3. 트랜잭션에 GTID가 할당된 경우 GTID는 시스템 gtid_executed변수( @@GLOBAL.gtid_executed)의 GTID 집합에 추가하여 비원자적으로 외부화(externalized) 됩니다(트랜잭션이 커밋된 직후). 이 GTID 집합은 커밋된 모든 GTID 트랜잭션 세트의 표현을 포함하며 복제에서 서버 상태를 나타내는 토큰으로 사용됩니다. 바이너리 로깅이 활성화된 경우(source에서 필요시 ) 시 gtid_executed 환경 변수에 있는 GTID 집합은 적용된 트랜잭션의 잔체 레코드이지만, mysql.gtid_executed 테이블은 그렇지 않습니다. 가장 최근 기록이 여전히 현재 바이너리 로그 파일에 있기 때문입니다.
4. 바이너리 로그 데이터가 복제본으로 전송되고 복제본의 릴레이 로그에 저장된 후(우리의 경우 row 형식으로 저장될 것.) 복제본은 GTID를 읽고 해당 gtid_next 시스템 변수의 값을 설정합니다. 이는 다음 트랜잭션 시에는 이 GTID 값을 이용해서 log 되어야 한다는 것을 나타냅니다. 복제본이 gtid_next 값을 session context에서 설정한다는 점이 중요합니다.
5. 복제본(replica)은 트랜잭션을 처리하기 위해 아직 gtid_next에 기록된 GTID의 소유권을 가져간 스레드가 없는지 확인합니다. 복제된 트랜잭션의 GTID를 먼저 읽고 확인함으로써 트랜잭션 자체를 처리하기 전에 복제본은 이 GTID를 가진 이전 트랜잭션이 복제본에 적용되지 않았음을 보장할 뿐만 아니라, 다른 세션이 이미 이 GTID를 읽었지만 관련된 transaction을 commit 하지 않았음을 보장합니다. 따라서 여러 클라이언트가 동일한 트랜잭션을 동시에 적용하려고 시도하는 경우, 서버는 그 중 하나만 실행되도록 하여 이를 해결합니다. 복제본의 시스템 gtid_owned 변수(@@GLOBAL.gtid_owned)는 각각의 GTID가 현재 사용중인지, 그리고 이를 소유한 thread 의 ID를 보여줍니다. GTID가 이미 사용된 경우 오류가 발생하지 않으며 자동 건너뛰기 기능을 사용하여 트랜잭션을 무시합니다.(auto-skip function)
6. GTID가 사용되지 않은 경우 복제본은 복제된 트랜잭션을 적용합니다. gtid_nex가 소스에서 이미 할당한 GTID로 설정되어 있기 때문에, 복제본은 이 트랜잭션에 대해 새 GTID를 생성하려고 시도하지 않고 대신 gtid_next에 저장된 GTID를 사용합니다
7. 복제본에서 바이너리 로깅이 활성화된 경우(우리의 경우와 동일) 트랜잭션 시작 시 바이너리 로그에 GTID를 기록하여 커밋 시 GTID가 원자적으로 유지됩니다( Gtid_log_event 로) 바이너리 로그가 회전하거나 서버가 종료될 때마다 서버는 이전 바이너리 로그 파일에 기록된 모든 트랜잭션의 GTID를 mysql.gtid_executed 테이블에 기록합니다
8. (복제본에서 이진 로깅이 비활성화된 경우 GTID는 테이블에 직접 작성하여 원자적으로 유지됩니다 mysql.gtid_executed. MySQL은 트랜잭션에 명령문을 추가하여 GTID를 테이블에 삽입합니다. MySQL 8.0부터 이 작업은 DDL 문과 DML 문에 대해 원자적입니다. 이 상황에서 mysql.gtid_executed테이블은 복제본에 적용된 트랜잭션의 전체 레코드입니다.)
9. 복제된 트랜잭션이 복제본에서 커밋된 직후, GTID는 gtid_executed 시스템 변수 (@@GLOBAL.gtid_executed) 안의 GTID 집합에 추가됨으로써 비원자적으로 외부화된다.(3과 일맥상통) source의 경우 이 GTID 집합에는 커밋된 모든 GTID 트랜잭션 세트의 표현이 포함됩니다. 복제본에서 바이너리 로깅이 비활성화된 경우 mysql.gtid_executed 테이블은 복제본에 적용된 트랜잭션의 전체 레코드이기도 합니다. 복제본에서 이진 로깅이 활성화된 경우(일부 GTID가 이진 로그에만 기록됨) gtid_executed 시스템 변수에 기록된 GTID 집합이 유일하게 완전한 레코드입니다.
해석본을 요약해보면 다음과 같다.
서버는 트랜잭션을 커밋하자마자 mysql.gtid_executed 테이블에 해당 GTID를 기록한다.
저 테이블은 모든 기록이 다 담겨있고 복제본 replica에서 서버 상테를 대변하기도 한다.
복제본 서버에서는 다음에 실행할 트랜잭션에 대해 gtid_next 라는 값에 이를 저장하고,(session context)
이 트랜잭션을 중복으로 처리하지 않도록 보장한다.
트랜잭션이 시작될 때마다 binary log에 Gtid_log_event로 GTID가 저장되고, server가 꺼지거나 binary log가 rotated 되면 mysql.gtid_executed table에 모두 불러온다.
master (source)는 mysql.gtid_executed 테이블에 모든 gtid를 가지고 있고, replica (slave)는 gtid_next로 관리하고 Gtid_log_event로 개별적으로 gtid를 일부 가지고 있다.
(복제 서버에서 binary log가 roate 되면 복제서버의 gtid는 새롭게 시작되지만, 기존 server에서는 이전 binary logging 파일을 이용해서 모든 gtid를 불러오는 것 같다.)