Skip to content
난이도 문제은행 5

복제 토폴로지와 고급 설정 2 ​

복제(replication)로 레플리카(replica)를 운영하면 세 가지를 풀어야 한다. 소스(source)의 동시 쓰기를 빨리 따라잡는 멀티 스레드 복제(multithreaded replication), 죽었다 살아나도 적용 위치를 잃지 않는 크래시 세이프 복제(crash-safe replication), 그리고 복제 지연(replication lag) 때문에 방금 쓴 데이터가 레플리카에서 안 보이는 문제다.

멀티 스레드 복제 ​

소스에서는 여러 세션이 동시에 트랜잭션(transaction)을 커밋(commit)하는데, 레플리카가 이를 한 스레드로 하나씩 적용하면 소스가 바쁠 때 계속 뒤처진다. 멀티 스레드 복제는 적용을 여러 스레드로 나눠 이 격차를 줄인다.

  • 작업 스레드 수는 replica_parallel_workers가 정한다. 1 이상이면 코디네이터 스레드(coordinator thread) 하나가 릴레이 로그(relay log)를 순서대로 읽어 작업 스레드(worker thread)들에게 트랜잭션을 나눠 준다. MySQL 8.4의 기본값은 4라서 레플리카는 기본으로 멀티 스레드로 적용한다. 0(예전 방식의 단일 적용 스레드)은 폐기 예정이므로, 하나씩 적용하려면 1로 둔다.

무엇을 동시에 적용해도 되는가가 핵심이다. 같은 행을 고친 두 트랜잭션을 순서를 바꿔 적용하면 결과가 달라진다. 그래서 소스는 바이너리 로그(binary log)에 트랜잭션 사이의 의존 관계 정보를 함께 적고, 레플리카는 이를 보고 서로 무관한 트랜잭션만 동시에 적용한다. 이 방식이 기본 병렬 정책 LOGICAL_CLOCK이고, DB 단위로 나누는 옛 정책 DATABASE는 폐기 예정이다.

MySQL 8.4의 소스는 이 의존 관계를 항상 쓰기 집합(writeset, 바꾼 행 키의 모음) 방식으로 계산한다. 예전 기본 방식은 커밋 시점이 겹친 트랜잭션끼리만 동시에 적용했지만, 쓰기 집합 방식은 바꾼 행이 겹치지 않으면 커밋 시각이 떨어져 있어도 동시에 적용한다. 8.0에서는 binlog_transaction_dependency_tracking=WRITESET으로 골라야 했지만, 8.4에서는 이 변수가 제거되고 기본 동작이 됐다.

동시에 적용해도 커밋은 replica_preserve_commit_order(기본 ON)에 따라 원래 순서대로 하므로, 레플리카에서 읽는 쪽은 소스에 한 번도 없었던 중간 상태를 보지 않는다. 키가 없는 테이블은 바꾼 행을 구분하지 못해 의존 관계를 좁히지 못하고, 레플리카가 행을 찾으려 테이블을 훑어야 해 느리다. 그래서 매뉴얼은 멀티 스레드 복제 전에 모든 테이블에 PK(Primary Key, 기본 키)를 두라고 안내한다.

크래시 세이프 복제 ​

크래시 세이프 복제란 레플리카가 죽었다 재시작해도 이미 적용한 것은 다시 하지 않고, 빠진 것은 빠짐없이 이어서 적용하는 상태를 말한다. 그러려면 "데이터에 반영한 것"과 "어디까지 적용했다는 기록"이 항상 일치해야 한다.

MySQL 8.4는 적용 위치를 mysql 스키마의 InnoDB 테이블에 두고 적용하는 트랜잭션과 같은 트랜잭션 안에서 갱신하므로, 둘이 함께 커밋되거나 함께 취소되어 어긋나지 않는다. 예전의 파일 저장 방식은 따로 갱신되어 어긋날 수 있었고 8.4에서 제거됐다.

매뉴얼은 GTID(Global Transaction Identifier, 전역 트랜잭션 식별자)와 함께 다음 레플리카 설정을 권장한다.

설정하는 일
GTID + SOURCE_AUTO_POSITION=1재시작 때 빠진 트랜잭션을 GTID로 찾아 받는다
relay_log_recovery=ON재시작 때 기존 릴레이 로그를 버리고 적용 위치부터 다시 받는다(기본 OFF)
sync_relay_log=1받은 트랜잭션마다 릴레이 로그를 디스크에 기록한다(기본 10000). 가장 안전하고 가장 느리다
innodb_flush_log_at_trx_commit=1커밋마다 리두 로그(redo log)를 디스크에 기록한다(기본값)

소스 쪽은 커밋마다 바이너리 로그를 디스크에 기록하는 sync_binlog=1(기본값)과 innodb_flush_log_at_trx_commit=1을 함께 두어야 장애 뒤에도 바이너리 로그와 데이터가 일치한다.

복제 지연 재기 ​

복제 지연은 소스에서 커밋된 변경이 레플리카에 적용되기까지 걸리는 시간이다. 흔한 원인은 적용이 소스의 쓰기 속도를 못 따라가는 것으로, 수백만 행을 한 번에 바꾸는 작업, PK 없는 테이블, 레플리카에서 도는 무거운 조회, 레플리카의 낮은 사양이 대표적이다.

SHOW REPLICA STATUS의 Seconds_Behind_Source는 가장 쉬운 지표지만, 적용 중인 변경 기록(이벤트)의 소스 커밋 시각과 현재 시각의 차이일 뿐이다. 네트워크가 느려 받기 자체가 늦으면 받은 만큼은 다 따라잡아 0을 보일 수 있고, 적용 스레드가 멈추면 NULL이 된다. 그래서 Performance 스키마(Performance Schema)의 replication_applier_status_by_worker에서 마지막 적용 트랜잭션의 소스 커밋 시각과 적용 완료 시각을 비교하거나, 소스가 주기적으로 시각을 쓰는 하트비트 테이블을 레플리카에서 읽어 확인하기도 한다.

쓰고 바로 읽기 문제 ​

글을 저장한 직후 목록을 새로 고쳤는데 방금 쓴 글이 안 보인다. 쓰기는 소스로, 읽기는 레플리카로 보내는데 레플리카가 아직 그 변경을 적용하지 못한 것이다. 이를 쓰고 바로 읽기(read-after-write) 일관성 문제라 하며, 대응은 보통 네 가지다.

  1. 방금 쓴 사용자의 읽기는 소스로 보낸다. 쓰기 직후 몇 초 동안, 또는 그 트랜잭션과 같은 요청 안에서는 소스에서 읽는다.
  2. 내 GTID가 적용될 때까지 기다린다. session_track_gtids를 켜면 클라이언트가 커밋 응답에서 방금 만든 GTID를 받는다. 레플리카에서 SELECT WAIT_FOR_EXECUTED_GTID_SET('<GTID>', 1)을 먼저 실행하면, 그 트랜잭션이 적용될 때까지 최대 1초 기다린다. 제때 적용되지 않으면 소스에서 읽는다.
  3. 지연이 기준을 넘은 레플리카는 빼고 읽는다.
  4. 합의 기반 복제를 쓴다. 여러 서버가 합의로 트랜잭션 순서를 정하는 그룹 복제(Group Replication)는 읽기 전에 앞선 쓰기가 적용되기를 기다리는 일관성 수준을 고를 수 있다(그룹 복제와 InnoDB 클러스터 4).

반동기 복제(semisynchronous replication)는 레플리카가 받았다는 확인만 기다릴 뿐 적용을 기다리지 않으므로 이 문제를 풀지 못한다.

면접 포인트 ​

  • "레플리카는 어떻게 병렬로 적용하나요?" → 코디네이터 + 작업 스레드(8.4 기본 4). 쓰기 집합 의존 정보로 무관한 트랜잭션만 동시 적용, 커밋 순서는 보존.
  • "크래시 세이프하려면?" → 적용 위치를 데이터와 같은 트랜잭션으로 기록, GTID 자동 위치 지정, relay_log_recovery.
  • "Seconds_Behind_Source가 0이면 지연이 없나요?" → 받기가 늦으면 0으로 보일 수 있다. 커밋 시각 비교나 하트비트로 확인.
  • "방금 쓴 글이 안 보여요" → 소스에서 읽기, GTID 대기, 지연 레플리카 제외.

관련 문서