---
title: "Parallel replication 설정: 논리 시계, worker, 의존성 추적"
description: "MySQL 병렬 복제에서 논리 시계와 WRITESET 의존성 추적, worker 수, 커밋 순서 보존의 관계를 설명하고 설정·진단 절차를 정리한다."
tags: [ MySQL, 복제, 성능최적화, 운영 ]
image: "mysql-report-bg.png"
published: "2026-09-13"
updated: "2026-09-13"
author: "MySQL 기술 노트"
source_url: ""
---

## 1. worker를 늘리기 전에 무엇이 병렬화되는지 구분한다

복제 지연이 커질 때 `replica_parallel_workers`를 늘리는 것은 적용 처리량을 개선하는 한 방법이다. 그러나 수신 자체가 멈추었거나, 하나의 큰 트랜잭션이 오래 실행되거나, 동일 행을 바꾸는 트랜잭션이 연속되는 상황에서는 worker 수만 늘려도 지연이 줄지 않는다. 병렬 복제는 임의의 SQL을 잘게 쪼개 여러 CPU에 배분하는 기능이 아니다. **서로 독립적으로 적용할 수 있다고 판단된 트랜잭션을 여러 worker에 배분하는 기능**이다.

설정은 세 계층으로 나누어야 한다. Source는 트랜잭션 사이의 의존성 정보를 만든다. Replica의 coordinator는 그 정보를 읽고 실행 가능한 트랜잭션을 배분한다. Worker는 실제 변경을 적용하며, 커밋 순서 보존이 켜져 있으면 앞선 트랜잭션의 커밋도 기다린다. 따라서 실행 병렬성, worker 가동률, 최종 커밋 처리량은 같은 지표가 아니다.

이 글은 InnoDB와 ROW 기반 비동기 binlog 복제를 대상으로 한다. 설정 이름은 MySQL 8.0.26 이상을 기준으로 하며, MySQL 8.4의 제거된 변수는 별도로 구분한다. SQL은 MySQL 8.0 폐기용 인스턴스에서 실행하고, 복제 관측 예제는 별도의 source·replica 쌍에서 확인한다. Aurora 클러스터 내부의 스토리지 기반 복제는 이 설정의 직접 대상이 아니다.

## 2. 논리 시계는 시간이 아니라 의존성의 경계다

### 수신, 배분, 적용, 커밋의 경로

```mermaid
flowchart TD
    A[Source의 트랜잭션 변경] --> B[커밋 이력 또는 WRITESET으로 의존성 계산]
    B --> C[Binlog에 트랜잭션과 논리 시계 기록]
    C --> D[Replica receiver와 relay log]
    D --> E[Coordinator가 의존성 확인]
    E --> F[Worker 1에서 변경 적용]
    E --> G[Worker 2에서 변경 적용]
    E --> H[Worker 3에서 변경 적용]
    F --> I[커밋 순서 보존 경계]
    G --> I
    H --> I
    I --> J[Replica에서 커밋된 변경으로 공개]
```

Receiver가 relay log를 받아 놓았다고 해서 worker가 모두 처리한 것은 아니다. Coordinator가 relay log 끝까지 읽었더라도 이미 worker에 배분한 트랜잭션이 잠금이나 커밋 순서를 기다릴 수 있다. 이 때문에 coordinator의 유휴 상태만으로 적용 완료를 판정하면 안 된다.

`LOGICAL_CLOCK`은 binlog에 기록된 `sequence_number`, `last_committed` 같은 논리 시계 정보를 사용한다. `sequence_number`는 해당 로그에서의 트랜잭션 순서를 나타내고, `last_committed`는 앞선 트랜잭션에 대한 의존성 경계를 나타낸다. 이 값은 실제 시각이나 GTID의 전역 대체 식별자가 아니다. 로그 회전과 의존성 이력의 경계를 무시한 채 서로 다른 파일의 숫자를 직접 비교하지 않는다.

Coordinator는 필요한 선행 작업이 완료된 범위와 각 트랜잭션의 의존성 경계를 비교해 배분 가능 여부를 판단한다. `last_committed`가 같다는 것은 병렬 실행 후보를 이해하는 데 유용하지만, 운영자가 그 숫자만 보고 실제 동시 실행을 보장해서는 안 된다. Worker 부족, 앞선 작업, 큐 공간, InnoDB 잠금, DDL 등 다른 제약도 작용한다.

### WRITESET과 LOGICAL_CLOCK은 경쟁하는 두 모드가 아니다

MySQL 8.0의 `binlog_transaction_dependency_tracking`은 **source가 의존성 정보를 만드는 방법**이다. `replica_parallel_type=LOGICAL_CLOCK`은 **replica가 그 정보를 이용하는 배분 방식**이다. 그러므로 “LOGICAL_CLOCK 대신 WRITESET을 켠다”는 설명은 계층을 혼동한 것이다. 일반적인 조합은 source의 WRITESET과 replica의 LOGICAL_CLOCK이다.

| Source의 추적 방식 | 의존성을 판단하는 주된 근거 | 운영상 의미 |
|---|---|---|
| `COMMIT_ORDER` | Source에서의 커밋 시각·동시성 정보 | Source에서 함께 진행되는 트랜잭션이 많을수록 병렬 적용 기회가 늘 수 있다. |
| `WRITESET` | 변경한 행의 키에서 계산한 해시와 이전 변경 이력 | Source에서 순차 커밋했더라도 충돌하지 않는 변경의 병렬 적용 기회를 찾는다. |
| `WRITESET_SESSION` | WRITESET에 동일 세션의 순서 제약 추가 | 같은 source 세션에서 발생한 트랜잭션 사이의 추가 순서 제약을 유지한다. |

Write set은 행 변경의 키 정보를 해시로 표현하고 이전 변경과 대조하는 데 사용된다. Source가 이를 바탕으로 논리적 의존성을 계산하는 것이 핵심이다. Replica가 binlog에서 모든 write set을 받아 임의로 행 충돌 분석을 다시 한다고 이해해서는 안 된다.

해시 추적은 모든 SQL 의미를 완벽히 해석하는 도구도 아니다. 충분한 키 정보가 없는 테이블, 외래 키 관계, DDL과 추적 이력의 경계 등에서는 보수적인 의존성이 생길 수 있다. 또한 같은 계좌나 재고 행을 계속 갱신하는 실제 업무 의존성은 WRITESET으로 제거할 수 없다. 해시 충돌이 생기더라도 독립 작업을 더 보수적으로 취급하는 방향이며, 정확성을 포기해 병렬화하는 기능은 아니다.

`binlog_transaction_dependency_history_size`는 이력에 보관하는 행 해시 수의 상한이다. 트랜잭션 수나 메모리 바이트 수가 아니다. 이력 한도에 도달하면 추적 이력이 비워지므로 의존성을 판단할 수 있는 범위가 제한된다. 값을 크게 늘리기 전에는 source의 메모리·CPU 부담과 실제 적용 처리량을 함께 측정해야 한다.

## 3. 버전과 서버 역할을 나누어 설정한다

### 버전별로 복사하면 안 되는 항목

| 항목 | MySQL 8.0 | MySQL 8.4 |
|---|---|---|
| `replica_parallel_workers` | 8.0.26부터 이 이름을 사용하며, 8.0.27부터 기본값은 4 | Worker 수를 설정하는 항목으로 사용 |
| `replica_preserve_commit_order` | 8.0.27부터 기본값 ON | 커밋 순서 보존 여부를 명시적으로 관리 |
| `replica_parallel_type` | `LOGICAL_CLOCK`을 사용하며, 8.0.27부터 기본값 | 여전히 존재하지만 deprecated. 기본값 LOGICAL_CLOCK을 사용 |
| `binlog_transaction_dependency_tracking` | `WRITESET` 등을 선택. 8.0.35부터 deprecated | 제거됨. WRITESET 동작을 내부적으로 사용 |
| `transaction_write_set_extraction` | WRITESET 사용을 위해 해시 추출 설정 확인 | 제거됨. 예전 명시 설정을 이식하지 않음 |

MySQL 8.0.26보다 앞선 릴리스에서는 일부 항목에 `slave_` 이름을 사용한다. 이름만 치환해서 모든 버전의 기본값과 동작까지 같다고 가정하지 않는다. 특히 업그레이드 시에는 “지정하지 않은 값”이 새 기본값으로 바뀌는지 확인한다.

### Source의 설정

다음은 기존 복제 구성이 갖추어진 MySQL 8.0 source에 추가할 병렬성 관련 설정의 예다. 서버 ID, binlog 활성화, 계정, TLS, GTID 구성까지 포함한 완전한 복제 설정 파일은 아니다. **8.4에 아래 세 줄을 그대로 적용하지 않는다.**

```ini
[mysqld]
binlog_format=ROW
transaction_write_set_extraction=XXHASH64
binlog_transaction_dependency_tracking=WRITESET
```

실제 확인은 설정 파일이 아니라 실행 중인 변수값을 기준으로 한다. 다음 쿼리는 source에서 실행한다. 존재하는 변수만 조회하므로 8.4에서 제거된 항목은 행 자체가 나오지 않는다. 누락을 설정 성공으로 해석하지 말고 서버 버전과 대조한다.

```sql
SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.global_variables
WHERE VARIABLE_NAME IN (
    'version', 'log_bin', 'binlog_format',
    'transaction_write_set_extraction',
    'binlog_transaction_dependency_tracking',
    'binlog_transaction_dependency_history_size'
)
ORDER BY VARIABLE_NAME;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT VARIABLE_NAME, VARIABLE_VALUE
    -> FROM performance_schema.global_variables
    -> WHERE VARIABLE_NAME IN (
    ->     'version', 'log_bin', 'binlog_format',
    ->     'transaction_write_set_extraction',
    ->     'binlog_transaction_dependency_tracking',
    ->     'binlog_transaction_dependency_history_size'
    -> )
    -> ORDER BY VARIABLE_NAME;

+--------------------------------------------+----------------+
| VARIABLE_NAME                              | VARIABLE_VALUE |
+--------------------------------------------+----------------+
| binlog_format                              | ROW            |
| binlog_transaction_dependency_history_size | 25000          |
| binlog_transaction_dependency_tracking     | WRITESET       |
| log_bin                                    | ON             |
| transaction_write_set_extraction           | XXHASH64       |
| version                                    | 8.0.46         |
+--------------------------------------------+----------------+
6 rows in set (0.01 sec)
```

Source의 추적 방식을 바꾸어도 이미 생성된 binlog와 replica에 쌓인 relay log의 의존성 정보가 다시 작성되지는 않는다. 설정 직후 기존 backlog가 빠르게 줄지 않는다는 이유만으로 효과가 없다고 단정하지 않는다. 변경 시점 이후에 생성된 트랜잭션 구간을 따로 관찰해야 한다.

`binlog_group_commit_sync_delay`를 늘려 source의 그룹 커밋 폭을 키우는 방법도 있지만, source 커밋 지연을 의도적으로 추가하는 절충이다. WRITESET을 사용할 수 있는 환경에서 replica worker가 덜 바쁘다는 이유만으로 이 값을 먼저 늘리는 것은 적절하지 않다. Source 응답 시간의 서비스 목표를 우선 보호한다.

### Replica의 설정과 적용 절차

MySQL 8.0 replica의 시작점으로 사용할 수 있는 설정은 다음과 같다. 4는 이 글의 검증 구성이지 모든 시스템의 최적값은 아니다. 8.4에서도 `replica_parallel_type`은 존재하지만 deprecated이므로 신규 설정에서는 기본값 LOGICAL_CLOCK을 확인하고 불필요한 명시 설정을 줄일 수 있다. Source의 제거된 변수와 replica의 deprecated 변수를 구별해야 한다.

```ini
[mysqld]
replica_parallel_type=LOGICAL_CLOCK
replica_parallel_workers=4
replica_preserve_commit_order=ON
```

실행 중인 replica에서는 다음 순서로 변경한다.

1. 채널 목록, 수신·적용 오류, 현재 변수값과 backlog를 기록한다. 장애로 멈춘 채널을 성능 튜닝으로 복구하려 하지 않는다.
2. 변경 창을 확보하고 적용 스레드를 정상 중지한다. `STOP REPLICA SQL_THREAD`는 실행 중인 작업을 즉시 강제 종료하는 명령이 아니므로 완료 여부를 확인한다. 다중 채널이면 변경 대상 변수의 요구 조건에 맞게 모든 관련 적용 스레드를 중지한다.
3. 권한이 있는 세션에서 지원되는 global 변수를 변경한다. `replica_parallel_workers`의 변경값은 실행 중 worker에 즉시 반영되지 않고 이후 적용 스레드 시작 시 반영된다. 병렬 유형과 커밋 순서 관련 항목도 적용 스레드 중지 조건을 지킨다.
4. 대상 채널의 적용 스레드를 시작하고, 변수값뿐 아니라 실제 worker 행 수와 오류 상태를 확인한다. 수신만 계속해 두었다면 변경 창 동안 relay log가 늘 수 있다.
5. 설정 파일 또는 환경에서 지원하는 영속화 방식에도 변경을 반영한다. 런타임 변경만으로 재시작 후 값까지 보장하지 않는다. 되돌릴 때에도 기존 값과 적용 스레드 재시작 절차를 사용한다.

```sql
SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.global_variables
WHERE VARIABLE_NAME IN (
    'replica_parallel_type', 'replica_parallel_workers',
    'replica_preserve_commit_order', 'replica_pending_jobs_size_max'
)
ORDER BY VARIABLE_NAME;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT VARIABLE_NAME, VARIABLE_VALUE
    -> FROM performance_schema.global_variables
    -> WHERE VARIABLE_NAME IN (
    ->     'replica_parallel_type', 'replica_parallel_workers',
    ->     'replica_preserve_commit_order', 'replica_pending_jobs_size_max'
    -> )
    -> ORDER BY VARIABLE_NAME;

+-------------------------------+----------------+
| VARIABLE_NAME                 | VARIABLE_VALUE |
+-------------------------------+----------------+
| replica_parallel_type         | LOGICAL_CLOCK  |
| replica_parallel_workers      | 4              |
| replica_pending_jobs_size_max | 134217728      |
| replica_preserve_commit_order | ON             |
+-------------------------------+----------------+
4 rows in set (0.01 sec)
```

`replica_pending_jobs_size_max`는 미적용 이벤트를 보관하는 applier 큐 메모리의 소프트 한도다. 이를 초과하는 큰 이벤트를 처리할 때에는 worker 큐를 비운 뒤 큰 트랜잭션을 처리하고 후속 작업을 기다리게 할 수 있다. 단순히 “worker마다 이만큼 메모리를 예약한다”거나 “어떤 이벤트도 이 크기를 넘지 못한다”는 뜻은 아니다. Worker를 늘릴 때에는 큐뿐 아니라 동시 트랜잭션의 잠금, undo, buffer pool, redo와 CPU 사용도 함께 고려한다.

## 4. 실행 병렬성과 커밋 병렬성을 실제로 구분한다

### 검증 구성과 관측 범위

아래 관측은 MySQL 8.0 source·replica 두 인스턴스에서 ROW·GTID 복제를 구성하고, source는 WRITESET, replica는 LOGICAL_CLOCK·worker 4개·커밋 순서 보존 ON으로 실행한 상태를 대상으로 한다. 본문의 source·replica 변수 출력도 이 구성에서 수집한다. 운영 데이터나 실제 서비스의 처리량 측정은 아니다.

재현에서는 `parallel_demo.items`에 기본 키 `id`와 값 `val`을 두고, `id=1,2,3`에 `val=0`인 행을 준비한다. 초기 복제 완료 후 테스트용 replica의 별도 세션이 `id=1`을 `FOR UPDATE`로 잠근다. Source의 한 세션은 `id=1`, `id=2`, `id=3`의 `val`을 각각 1로 바꾸는 독립 트랜잭션을 순차 커밋한다. WRITESET_SESSION이 아니라 WRITESET이므로 같은 세션에서 발생했다는 사실만으로 세 트랜잭션이 직렬화되도록 요구하지 않는다.

테스트용 replica의 잠금은 운영 replica에 따라 해 볼 조치가 아니라 병목을 명확히 만들기 위한 실험 장치다. 관측 후 해당 잠금 세션을 종료하고 세 행 모두 `val=1`로 복제되는지, source에서 기록한 GTID 지점까지 따라잡는지를 확인한다. 실제 검증에서 잠금 해제 전 일반 조회는 세 행 모두 `val=0`이었고, 해제 후에는 모두 `val=1`이 되었으며 GTID 추격도 완료되었다. 이 실험은 병렬 배분과 커밋 대기 관계를 보여 주며 worker 수별 처리량 향상률을 입증하지 않는다.

### Worker의 ON은 실행 중이라는 뜻이 아니다

다음 쿼리는 replica에서 실행한다. `has_current_trx`는 현재 적용 대상 트랜잭션 식별자가 비어 있지 않은지를 나타낸다. GTID 전체를 출력하지 않고 상태 해석에 필요한 신호만 남긴다.

```sql
SELECT w.WORKER_ID,
       w.SERVICE_STATE,
       (w.APPLYING_TRANSACTION <> '') AS has_current_trx,
       t.PROCESSLIST_STATE AS thread_state,
       w.LAST_ERROR_NUMBER
FROM performance_schema.replication_applier_status_by_worker AS w
LEFT JOIN performance_schema.threads AS t
  ON t.THREAD_ID = w.THREAD_ID
WHERE w.CHANNEL_NAME = ''
ORDER BY w.WORKER_ID;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT w.WORKER_ID,
    ->        w.SERVICE_STATE,
    ->        (w.APPLYING_TRANSACTION <> '') AS has_current_trx,
    ->        t.PROCESSLIST_STATE AS thread_state,
    ->        w.LAST_ERROR_NUMBER
    -> FROM performance_schema.replication_applier_status_by_worker AS w
    -> LEFT JOIN performance_schema.threads AS t
    ->   ON t.THREAD_ID = w.THREAD_ID
    -> WHERE w.CHANNEL_NAME = ''
    -> ORDER BY w.WORKER_ID;

+-----------+---------------+-----------------+---------------------------------------------+-------------------+
| WORKER_ID | SERVICE_STATE | has_current_trx | thread_state                                | LAST_ERROR_NUMBER |
+-----------+---------------+-----------------+---------------------------------------------+-------------------+
|         1 | ON            |               1 | Applying batch of row changes (update)      |                 0 |
|         2 | ON            |               1 | Waiting for preceding transaction to commit |                 0 |
|         3 | ON            |               1 | Waiting for preceding transaction to commit |                 0 |
|         4 | ON            |               0 | Waiting for an event from Coordinator       |                 0 |
+-----------+---------------+-----------------+---------------------------------------------+-------------------+
4 rows in set (0.00 sec)
```

빈 채널명은 기본 채널을 뜻한다. 명명된 채널을 사용하는 환경에서는 해당 조건을 실제 채널명으로 바꾼다. 복제가 구성되지 않았거나 대상 채널이 다르면 `Empty set`일 수 있다. 이를 “worker가 모두 유휴 상태다”로 해석하지 않는다. `SERVICE_STATE=ON`인 worker라도 relay log 작업을 기다릴 수 있으며, `LAST_ERROR_NUMBER=0`이어도 잠금 또는 선행 커밋 대기로 진행하지 못할 수 있다.

`Waiting for preceding transaction to commit`은 작업이 처음부터 배분되지 않았다는 뜻이 아니다. 후속 트랜잭션이 실행 단계에서 진전했지만, relay log상의 앞선 트랜잭션보다 먼저 커밋할 수 없어 기다리는 상태일 수 있다. 따라서 여러 worker의 이 상태를 각각 독립적인 잠금 장애로 세지 말고, 먼저 진행하지 못하는 선행 트랜잭션을 찾는다.

### 행 잠금으로 후속 트랜잭션의 실행을 확인한다

다음 쿼리는 실험 테이블의 worker별 레코드 잠금만 조회한다. 운영에서는 실제 의심 테이블로 범위를 바꾸되, 전체 잠금 덤프를 무제한으로 수집하지 않는다.

```sql
SELECT w.WORKER_ID, l.INDEX_NAME,
       l.LOCK_MODE, l.LOCK_STATUS, l.LOCK_DATA
FROM performance_schema.replication_applier_status_by_worker AS w
JOIN performance_schema.data_locks AS l
  ON l.THREAD_ID = w.THREAD_ID
WHERE w.CHANNEL_NAME = ''
  AND l.OBJECT_SCHEMA = 'parallel_demo'
  AND l.OBJECT_NAME = 'items'
  AND l.LOCK_TYPE = 'RECORD'
ORDER BY w.WORKER_ID, l.LOCK_STATUS, l.LOCK_DATA;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT w.WORKER_ID, l.INDEX_NAME,
    ->        l.LOCK_MODE, l.LOCK_STATUS, l.LOCK_DATA
    -> FROM performance_schema.replication_applier_status_by_worker AS w
    -> JOIN performance_schema.data_locks AS l
    ->   ON l.THREAD_ID = w.THREAD_ID
    -> WHERE w.CHANNEL_NAME = ''
    ->   AND l.OBJECT_SCHEMA = 'parallel_demo'
    ->   AND l.OBJECT_NAME = 'items'
    ->   AND l.LOCK_TYPE = 'RECORD'
    -> ORDER BY w.WORKER_ID, l.LOCK_STATUS, l.LOCK_DATA;

+-----------+------------+---------------+-------------+-----------+
| WORKER_ID | INDEX_NAME | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
+-----------+------------+---------------+-------------+-----------+
|         1 | PRIMARY    | X,REC_NOT_GAP | WAITING     | 1         |
|         2 | PRIMARY    | X,REC_NOT_GAP | GRANTED     | 2         |
|         3 | PRIMARY    | X,REC_NOT_GAP | GRANTED     | 3         |
+-----------+------------+---------------+-------------+-----------+
3 rows in set (0.01 sec)
```

선행 worker가 `id=1`에 대해 `WAITING` 상태이고 후속 worker들이 `id=2,3`에 대해 `GRANTED` 잠금을 보유한다면, 뒤의 작업도 이미 실행 단계에 진입했다는 증거다. 하지만 해당 행이 일반 조회에서 새 값으로 보이는지는 별개다. 순서 보존 때문에 아직 커밋되지 않았을 수 있다. Worker 번호는 관측 당시의 배분 결과이며 특정 ID와 고정적으로 연결되지 않는다.

이 예제에서 대기를 풀 방법은 worker 추가가 아니라 선행 잠금의 해소다. 운영 환경에서는 `data_lock_waits`와 세션·트랜잭션 정보를 연결하여 실제 차단자를 확인하고, 소유 서비스와 롤백 비용을 검토한 뒤 종료 여부를 결정한다. 실험에서 사용한 잠금 세션 종료를 일반적인 자동 복구 정책으로 확대하지 않는다.

## 5. 처리량이 늘지 않을 때의 판단 기준

| 관측 | 우선 검토할 원인 | 우선 조치 |
|---|---|---|
| Backlog가 증가하지만 worker 대부분 유휴 | 좁은 의존성 폭, 큰 트랜잭션, coordinator 배분 제약 | Source 추적 방식, 트랜잭션 경계와 의존성 확인 |
| 한 worker가 오래 실행되고 후속 worker는 커밋 대기 | 선행 트랜잭션의 잠금, I/O 또는 대량 변경 | 가장 앞선 지연 원인을 조사하고 대량 작업 분할 검토 |
| 많은 worker가 CPU를 사용하며 읽기 지연도 증가 | Replica 자원 포화와 서비스 읽기 경쟁 | Worker 증가 중단, CPU·스토리지·쿼리 경쟁 측정 |
| 동일 키 변경이 계속 이어짐 | 실제 행 의존성과 hot row | 업무별 경합 구조 개선. Worker 증설만으로 해결하지 않음 |
| Variable은 늘었으나 실제 worker 수가 그대로 | 적용 스레드 재시작 전, 다른 채널 관측 | 반영 시점과 채널 확인 |
| Coordinator는 유휴인데 worker에 현재 작업이 남음 | 배분 완료와 적용 완료의 차이 | Worker·GTID 적용 상태를 함께 확인 |

`replica_preserve_commit_order=OFF`로 후속 트랜잭션부터 커밋시키면 지연의 일부가 줄어 보일 수 있다. 대신 replica의 실행 이력에 앞선 트랜잭션이 아직 없는 틈이 생길 수 있고, 읽기에서 관찰되는 커밋 순서와 복구 판단도 달라진다. “마지막 트랜잭션을 봤으니 그 이전 변경도 모두 보인다”는 가정을 깨뜨릴 수 있으므로 단순 성능 스위치로 취급하지 않는다.

반대로 ON이라고 source의 최신 상태를 즉시 읽을 수 있는 것은 아니다. 이 옵션은 replica 내 적용·커밋 순서의 문제를 다루며, 비동기 수신 지연이나 읽기 직전의 추격 완료를 보장하지 않는다. 읽기 직후 일관성이 필요하면 writer 라우팅이나 필요한 GTID 지점 대기 등 별도의 읽기 정책이 필요하다.

Worker 수는 정상 업무 구간과 backlog 해소 구간을 나누어 단계적으로 비교한다. 동일한 입력률에서 커밋 처리량, 미적용량 추세, 지연 분포, CPU·I/O, 서비스 읽기 응답 시간, worker 대기와 오류를 함께 기록한다. Worker가 더 많이 ON이라는 사실보다 **미적용량이 실제로 줄고 서비스 목표를 유지하는지**가 판단 기준이다. 단일 트랜잭션 내부의 대량 row event는 일반적인 applier worker 증설만으로 여러 독립 트랜잭션처럼 분할되지 않는다.

## 6. Aurora MySQL에서는 복제 경로부터 식별한다

Aurora의 동일 클러스터 writer와 reader 사이 복제는 일반적인 source binlog → relay log → SQL worker 경로와 다르다. 따라서 reader의 지연을 줄이려는 목적으로 이 글의 `replica_parallel_workers`를 조정하는 것은 경로를 잘못 짚을 수 있다. Aurora Global Database의 스토리지 기반 복제도 동일한 worker 튜닝 문제로 취급하지 않는다.

반면 외부 MySQL의 binlog를 Aurora가 받아 적용하거나, Aurora에서 외부 MySQL로 binlog 복제를 구성하는 경우에는 source 의존성 정보와 replica applier 설정의 구분이 유효하다. 어느 쪽이 binlog를 생성하고 어느 쪽이 적용하는지 먼저 확인한 후, Aurora 엔진 버전에 맞는 DB·클러스터 파라미터 그룹과 관리형 복제 절차를 사용한다. 자체 운영 MySQL의 설정 파일과 관리자 명령을 그대로 이식하지 않는다.

Aurora에는 binlog replica의 보조 인덱스 변경을 병렬화하는 별도 기능과 in-memory relay log 관련 기능도 있다. 이들은 일반적인 트랜잭션 worker 수와 같은 설정이 아니다. 지원 버전, GTID·커밋 순서 조건, 활성화 절차를 공식 문서에서 확인해야 하며, Community MySQL에서 수행한 본문의 실험을 Aurora 성능 검증으로 간주하지 않는다.

## 7. 변경 전후 점검표

- [ ] 병목이 receiver가 아니라 applier 측에 있는지 수신·적용 상태를 나누어 확인했다.
- [ ] Source와 replica의 버전을 기록하고, 8.4에서 제거된 변수를 설정 파일에서 제외했다.
- [ ] Source 의존성 생성 방식과 replica 배분 방식을 별개의 계층으로 확인했다.
- [ ] 기존 relay log와 설정 변경 후 생성된 binlog 구간을 구분해 효과를 측정한다.
- [ ] 적용 스레드 중지·시작, 다중 채널 영향, 런타임·영속 설정과 되돌림 절차를 준비했다.
- [ ] 실제 worker 수와 현재 트랜잭션, 오류, 행 잠금, 선행 커밋 대기를 확인했다.
- [ ] 큰 트랜잭션, 큐 한도, hot row와 외래 키·DDL의 제약을 검토했다.
- [ ] Worker 증설 전후 처리량뿐 아니라 서비스 읽기 지연과 자원 포화도 비교한다.
- [ ] 커밋 순서 보존을 비동기 지연 해소나 읽기 직후 일관성 보장과 혼동하지 않는다.
- [ ] Aurora에서는 클러스터 내부 복제와 외부 binlog 복제를 먼저 구별했다.

## 8. 정리

병렬 복제의 효과는 source가 제공하는 독립성 정보, replica가 사용할 수 있는 worker와 자원, 실제 데이터 의존성, 커밋 순서 보존 경계가 함께 결정한다. 설정의 출발점은 “몇 개의 스레드가 필요한가”가 아니라 “어떤 트랜잭션을 동시에 실행할 수 있고 무엇 때문에 커밋이 막히는가”다.

WRITESET과 LOGICAL_CLOCK의 역할을 분리하고, worker의 실행 상태와 잠금을 함께 관측하면 지연을 worker 부족으로 오판하는 일을 줄일 수 있다. 이 구분은 이후 GTID 기반 추격 완료 확인, 복제 장애 복구, 승격 후보의 적용 상태 검증에서도 그대로 사용된다.

### 참고 문서

- [MySQL 8.0: Replica Server Options and Variables](https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html)
- [MySQL 8.4: Replica Server Options and Variables](https://dev.mysql.com/doc/refman/8.4/en/replication-options-replica.html)
- [MySQL 8.0: Binary Logging Options and Variables](https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html)
- [MySQL Shell 8.4: Configuring the Parallel Replication Applier](https://dev.mysql.com/doc/mysql-shell/8.4/en/configuring-parallel-applier.html)
- [MySQL 8.4: What Is New in MySQL 8.4 since MySQL 8.0](https://dev.mysql.com/doc/refman/8.4/en/mysql-nutshell.html)
- [AWS: 병렬 복제 개요와 설정 권장 사항](https://aws.amazon.com/blogs/database/overview-and-best-practices-of-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-amazon-aurora-mysql/)
