---
title: "Semi-sync replication의 원리와 장애 시나리오"
description: "MySQL 반동기 복제의 ACK와 커밋 대기 지점, 비동기 전환 조건, 장애 조치 시 데이터 보존 범위와 진단 절차를 정리한다."
tags: [ MySQL, 복제, 고가용성, 운영 ]
image: "mysql-report-bg.png"
published: "2026-09-10"
updated: "2026-09-10"
author: "MySQL 기술 노트"
source_url: ""
---

복제본이 있다는 사실과, 애플리케이션이 성공으로 확인한 트랜잭션이 그 복제본에 남아 있다는 사실은 다르다. 비동기 복제에서는 source가 커밋을 끝내고 응답한 뒤에도 해당 binary log가 replica에 도착하지 않았을 수 있다. 이 구간에서 source를 잃으면 승격한 replica에 이미 성공 처리된 주문이나 결제가 없을 수 있다.

반동기 복제(Semi-sync replication)는 커밋 경로에 replica의 수신 확인인 ACK를 추가한다. 그러나 ACK는 SQL 적용 완료를 뜻하지 않으며, 시간 초과 시 쓰기를 거부하는 장치도 아니다. 운영자가 이해해야 할 핵심은 **어디까지 기다리는지, 기다림이 언제 해제되는지, 어느 서버를 승격해야 그 보장을 유지하는지**다.

이 글은 MySQL Community 8.0.26 이상에서 제공하는 `source`·`replica` 명칭을 기준으로 설명하며 MySQL 8.4의 공식 동작 설명을 함께 참고한다. SQL 예제는 별도 MySQL 8.0 테스트 인스턴스에서 실행한다. 실제 두 서버 사이의 ACK, 네트워크 장애, 자동 승격은 재현 범위에 포함하지 않는다.

## 1. 반동기 복제가 보장하는 지점

복제 경로를 다음 세 단계로 구분하면 ACK의 의미를 잘못 해석할 가능성이 줄어든다.

| 단계 | 기록 또는 처리 위치 | 그 시점에 알 수 있는 것 |
| --- | --- | --- |
| source의 로그 기록 | source의 binary log 및 InnoDB 복구 경로 | source 자체의 복구 근거가 마련된다. replica 보유 여부는 별도다. |
| replica의 수신 및 영속화 | receiver가 기록한 relay log | ACK 대상으로 삼는 트랜잭션 이벤트가 replica에 기록되고 디스크로 flush되었다. |
| replica의 적용 및 커밋 | applier가 변경한 InnoDB 데이터 | 해당 트랜잭션이 replica의 실행 완료 집합에 들어간다. 읽기 가시성은 세션의 Read View도 고려해야 한다. |

반동기 ACK는 두 번째 단계와 연결된다. replica는 트랜잭션 이벤트를 relay log에 기록하고 디스크로 flush한 뒤 ACK를 보낸다. source는 설정한 수만큼의 replica ACK를 기다리지, 모든 replica가 SQL 적용을 끝낼 때까지 기다리지 않는다.

따라서 source에서 쓰기 성공을 받은 직후 임의의 replica로 읽기를 보내면 과거 값이 보일 수 있다. ACK를 보낸 replica조차 applier가 잠금이나 대형 트랜잭션 때문에 뒤처질 수 있다. 쓰기 직후 읽기 일관성은 source 읽기, 특정 GTID의 적용 대기, 읽기 라우팅 정책 등으로 별도 설계해야 한다.

또한 반동기 복제는 선거·quorum·분산 합의 프로토콜이 아니다. ACK 개수를 늘려도 누가 유일한 writer인지 결정하거나 두 서버의 동시 쓰기를 차단해 주지는 않는다.

## 2. AFTER_SYNC와 AFTER_COMMIT의 차이

`rpl_semi_sync_source_wait_point`는 replica ACK를 기다리는 위치를 지정한다. 기본값인 `AFTER_SYNC`에서는 binary log sync 단계 뒤, 스토리지 엔진 커밋 전에 기다린다. `AFTER_COMMIT`에서는 스토리지 엔진 커밋 뒤, 클라이언트 응답 전에 기다린다.

다음 그림은 반동기 상태가 유지되고 필요한 ACK가 정상적으로 도착하는 `AFTER_SYNC` 경로를 개념적으로 단순화한 것이다. 실제 binlog group commit, 로그 전송, 여러 세션의 처리는 겹쳐 진행될 수 있다.

```mermaid
sequenceDiagram
    participant C as 클라이언트
    participant S as Source
    participant R as Replica receiver
    participant A as Replica applier
    C->>S: COMMIT 요청
    S->>S: binary log 기록과 sync 단계
    S->>R: 트랜잭션 이벤트 전송
    R->>R: relay log 기록과 디스크 flush
    R-->>S: 수신 ACK
    S->>S: InnoDB 커밋
    S-->>C: 성공 응답
    Note over R,A: ACK는 SQL 적용 완료를 기다리지 않음
    R->>A: relay log의 이벤트 제공
    A->>A: 변경 적용과 커밋
```

| 항목 | AFTER_SYNC | AFTER_COMMIT |
| --- | --- | --- |
| ACK 대기 위치 | 엔진 커밋 이전 | 엔진 커밋 이후 |
| ACK 대기 중 source의 다른 세션 | 아직 엔진 커밋되지 않은 변경을 일반 읽기로 볼 수 없다. | 이미 커밋된 변경을 볼 수 있다. |
| 주요 장애 위험 | 미응답 트랜잭션의 결과 불확실성, 비동기 전환 구간, 잘못된 승격 대상 | 왼쪽 위험에 더해 ACK 전 다른 세션이 관찰한 변경이 승격 후 사라질 수 있다. |
| 운영 해석 | ACK와 source의 커밋 가시성 순서를 맞추는 기본 선택 | 클라이언트 응답 전이라도 다른 세션에는 변경이 노출될 수 있음을 수용해야 한다. |

`AFTER_SYNC`에서 ACK를 기다리는 동안 엔진 커밋이 늦어지므로 트랜잭션이 보유한 잠금의 해제도 늦어질 수 있다. replica 네트워크나 relay log 저장 장치 지연이 source의 커밋 지연뿐 아니라 잠금 대기와 연결 풀 포화로 확대될 수 있는 이유다.

여기서 “sync 단계”라는 이름을 실제 디스크 내구성의 대체 증거로 사용하면 안 된다. source의 로컬 내구성은 `sync_binlog`, `innodb_flush_log_at_trx_commit`, 저장 장치의 flush 보장도 함께 점검한다. 강한 내구성을 요구하는 InnoDB 구성에서는 일반적으로 두 변수를 각각 `1`로 두는 기준에서 출발한다. 반동기 플러그인이 부적절한 로컬 내구성 설정까지 자동으로 보완하는 것은 아니다.

## 3. 타임아웃은 롤백이 아니라 보장 수준의 전환이다

운영에서 가장 위험한 오해는 “ACK를 못 받으면 COMMIT이 실패한다”는 것이다. 반동기 source는 ACK 대기가 설정된 시간 안에 충족되지 않으면 비동기 복제로 전환할 수 있다. 트랜잭션은 source에서 커밋되고 성공 응답을 받을 수 있으므로 애플리케이션 오류율만으로는 보호 수준 저하를 알기 어렵다.

| 변수 | 의미 | 운영 판단 |
| --- | --- | --- |
| `rpl_semi_sync_source_enabled` | source 쪽 반동기 기능 사용 의도 | `ON`만으로 현재 반동기 동작을 증명할 수 없다. |
| `rpl_semi_sync_replica_enabled` | replica 쪽 반동기 기능 사용 의도 | receiver 연결에서 반동기 참여가 실제 협상되었는지도 확인한다. |
| `rpl_semi_sync_source_timeout` | ACK를 기다릴 시간, 밀리초 단위 | 기본값은 `10000`이다. 짧게 하면 비동기 전환이 쉬워지고, 길게 하면 쓰기 정체가 길어질 수 있다. |
| `rpl_semi_sync_source_wait_for_replica_count` | 필요한 ACK 개수 | 기본값은 `1`이다. 모든 replica나 특정 승격 후보를 의미하지 않는다. |
| `rpl_semi_sync_source_wait_no_replica` | 대기 중 참여 replica 수가 요구 개수보다 적어져도 타임아웃까지 기다릴지 여부 | 기본값 `ON`은 기다림을 유지한다. `OFF`이면 수 부족 시 타임아웃 전에도 비동기로 전환할 수 있다. |

반동기 참여 replica가 따라잡으면 source는 반동기 동작으로 복귀할 수 있다. 그러나 복귀했다고 해서 이전 비동기 구간에 발생한 손실 위험이 소급해서 없어지는 것은 아니다. 장애 시점의 상태뿐 아니라 **언제 비동기로 전환했고, 그 구간의 트랜잭션을 어느 replica가 보유했는지**를 기록해야 한다.

타임아웃을 매우 크게 설정하는 것도 “무손실 구성 완료”와 동의어가 아니다. 네트워크 단절이 길어지면 커밋 세션과 연결 풀이 누적되고 서비스가 사실상 정지할 수 있다. 엄격한 RPO가 필요하면 비동기 전환을 허용할지, 관측된 보호 수준 저하 시 외부 제어 계층에서 쓰기를 중단할지, 장애 복구 시 어떤 증거를 요구할지까지 정책에 포함한다. 모니터링 후 쓰기를 차단하는 방식에는 탐지·제어 지연이 있다는 점도 계산해야 한다.

## 4. 플러그인 설치 여부와 기본 설정을 확인하는 실습

아래 첫 블록은 **플러그인이 설치되지 않은 일회용 MySQL Community 테스트 인스턴스 전용 준비 절차**다. 운영 서버에 설치 명령을 무조건 재실행하지 않는다. 설치에는 관리 권한과 배포 패키지의 공유 라이브러리가 필요하며, 이미 설치된 경우에는 설치 명령을 제외하고 조회만 수행한다.

실습에서는 두 플러그인을 한 인스턴스에 로드하여 변수와 관측점을 확인한다. 기능은 기본값인 비활성 상태로 두며 binary log와 복제 채널도 활성화하지 않는다. 따라서 이 결과는 반동기 복제 구성 완료나 ACK 전달 성공을 뜻하지 않는다.

```sql
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
SELECT PLUGIN_NAME, PLUGIN_STATUS
FROM information_schema.PLUGINS
WHERE PLUGIN_NAME IN ('rpl_semi_sync_source', 'rpl_semi_sync_replica')
ORDER BY PLUGIN_NAME;
SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.global_variables
WHERE VARIABLE_NAME IN (
  'rpl_semi_sync_source_enabled',
  'rpl_semi_sync_replica_enabled',
  'rpl_semi_sync_source_timeout',
  'rpl_semi_sync_source_wait_point',
  'rpl_semi_sync_source_wait_for_replica_count',
  'rpl_semi_sync_source_wait_no_replica'
)
ORDER BY VARIABLE_NAME;
```

실행 결과(MySQL 8.0.x):

아래는 조회 SQL의 반복을 줄인 실행 화면 발췌다. 두 조회의 결과 행은 모두 표시했다.

```text
mysql> INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
Query OK, 0 rows affected (0.01 sec)

mysql> INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT PLUGIN_NAME, PLUGIN_STATUS ...;
+-----------------------+---------------+
| PLUGIN_NAME           | PLUGIN_STATUS |
+-----------------------+---------------+
| rpl_semi_sync_replica  | ACTIVE        |
| rpl_semi_sync_source   | ACTIVE        |
+-----------------------+---------------+
2 rows in set (0.01 sec)

mysql> SELECT VARIABLE_NAME, VARIABLE_VALUE ...;
+-------------------------------------------+----------------+
| VARIABLE_NAME                             | VARIABLE_VALUE |
+-------------------------------------------+----------------+
| rpl_semi_sync_replica_enabled              | OFF            |
| rpl_semi_sync_source_enabled               | OFF            |
| rpl_semi_sync_source_timeout               | 10000          |
| rpl_semi_sync_source_wait_for_replica_count | 1              |
| rpl_semi_sync_source_wait_no_replica        | ON             |
| rpl_semi_sync_source_wait_point             | AFTER_SYNC     |
+-------------------------------------------+----------------+
6 rows in set (0.00 sec)
```

`PLUGIN_STATUS = ACTIVE`는 플러그인이 로드되었다는 뜻이다. `rpl_semi_sync_source_enabled = OFF`와 모순되지 않는다. 실제 구성에서는 source와 replica의 역할에 맞는 플러그인을 활성화하고, 이미 연결된 replica라면 receiver 재접속 절차를 통해 반동기 참여가 협상되었는지 확인한다. 초기 데이터 정합성, binary log, 복제 계정 권한, GTID 또는 로그 위치 설정은 그 이전의 선행 조건이다.

MySQL 8.0의 오래된 환경에서는 `master`·`slave` 명칭의 플러그인이 남아 있을 수 있다. 기존 플러그인과 새 명칭 플러그인을 무조건 함께 로드하지 말고 설치 상태와 해당 버전의 전환 절차를 확인한다. 이 글의 설치 SQL은 이전 명칭에서의 무중단 마이그레이션 절차가 아니다.

## 5. 의도·현재 상태·기간별 증거를 분리해서 관측한다

source에서는 다음 조회를 사용한다. 플러그인이 설치되지 않은 서버에서 관련 행이 없으면 “정상값 0”으로 처리하지 말고 관측 대상이 존재하지 않는 것으로 구분한다.

```sql
SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME IN (
  'Rpl_semi_sync_source_status',
  'Rpl_semi_sync_source_clients',
  'Rpl_semi_sync_source_yes_tx',
  'Rpl_semi_sync_source_no_tx',
  'Rpl_semi_sync_source_no_times',
  'Rpl_semi_sync_source_wait_sessions'
)
ORDER BY VARIABLE_NAME;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT VARIABLE_NAME, VARIABLE_VALUE
    -> FROM performance_schema.global_status
    -> WHERE VARIABLE_NAME IN (
    ->   'Rpl_semi_sync_source_status',
    ->   'Rpl_semi_sync_source_clients',
    ->   'Rpl_semi_sync_source_yes_tx',
    ->   'Rpl_semi_sync_source_no_tx',
    ->   'Rpl_semi_sync_source_no_times',
    ->   'Rpl_semi_sync_source_wait_sessions'
    -> )
    -> ORDER BY VARIABLE_NAME;

+------------------------------------+----------------+
| VARIABLE_NAME                      | VARIABLE_VALUE |
+------------------------------------+----------------+
| Rpl_semi_sync_source_clients       | 0              |
| Rpl_semi_sync_source_no_times      | 0              |
| Rpl_semi_sync_source_no_tx         | 0              |
| Rpl_semi_sync_source_status        | OFF            |
| Rpl_semi_sync_source_wait_sessions | 0              |
| Rpl_semi_sync_source_yes_tx        | 0              |
+------------------------------------+----------------+
6 rows in set (0.00 sec)
```

실습에서는 기능을 켜지 않았으므로 `status`는 `OFF`이고 연결된 참여 replica와 처리 카운터는 0이다. 이 결과를 장애 재현 결과로 읽으면 안 된다. 운영에서는 다음 조합이 의미가 있다.

- `enabled = ON`, `status = OFF`: 기능 사용 의도와 실제 보호 수준이 다르다. 오류 로그와 전환 카운터, replica 연결 수를 조사한다.
- `clients`가 요구 ACK 수보다 적음: 현재 연결된 반동기 replica가 부족하다. 네트워크 단절, receiver 중단, 플러그인 설정을 확인한다.
- `no_times` 증가: 반동기 동작이 해제된 횟수의 변화다. 비동기 상태로 머문 시간이나 손실 트랜잭션 수와 같지 않다.
- `yes_tx`와 `no_tx`의 구간 증가량: ACK로 확인된 커밋과 그렇지 않은 커밋의 처리 양상을 보여준다. 누적값보다 같은 관측 구간의 변화량이 유용하다.
- `wait_sessions` 증가: 지금 ACK를 기다리는 세션이 쌓이고 있다. source의 커밋 지연, 연결 수, 네트워크 RTT, replica의 relay log 쓰기 지연을 함께 본다.

서버 재시작이나 카운터 초기화 전후의 누적값을 그대로 빼면 잘못된 증가량이 나온다. 수집기에서는 재시작과 값 감소를 구간 경계로 처리한다. `no_tx`를 곧바로 실제 손실 건수로 보고하지도 않는다. 비동기로 처리되었어도 나중에 replica가 해당 이벤트를 받을 수 있기 때문이다.

replica의 수신과 적용은 별도로 관측한다. 다음 조회는 채널·worker 존재 여부를 간단히 확인하는 시작점이다.

```sql
SELECT
  (SELECT COUNT(*)
   FROM performance_schema.replication_connection_status) AS receiver_channels,
  (SELECT COUNT(*)
   FROM performance_schema.replication_connection_status
   WHERE SERVICE_STATE = 'ON') AS running_receivers,
  (SELECT COUNT(*)
   FROM performance_schema.replication_applier_status_by_worker
   WHERE LAST_ERROR_NUMBER <> 0) AS workers_with_errors;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT
    ->   (SELECT COUNT(*)
    ->    FROM performance_schema.replication_connection_status) AS receiver_channels,
    ->   (SELECT COUNT(*)
    ->    FROM performance_schema.replication_connection_status
    ->    WHERE SERVICE_STATE = 'ON') AS running_receivers,
    ->   (SELECT COUNT(*)
    ->    FROM performance_schema.replication_applier_status_by_worker
    ->    WHERE LAST_ERROR_NUMBER <> 0) AS workers_with_errors;

+-------------------+-------------------+---------------------+
| receiver_channels | running_receivers | workers_with_errors |
+-------------------+-------------------+---------------------+
|                 0 |                 0 |                   0 |
+-------------------+-------------------+---------------------+
1 row in set (0.00 sec)
```

실습 결과의 0은 복제 채널이 없는 인스턴스를 조회했기 때문이다. 운영에서 `workers_with_errors = 0`이어도 applier가 정상 속도로 실행 중임을 증명하지 못한다. 채널별 `SERVICE_STATE`, receiver의 `LAST_ERROR_NUMBER`·`LAST_ERROR_MESSAGE`, applier worker의 서비스 상태와 오류, 수신 GTID와 실행 GTID 차이를 추가 조사한다. 여러 채널의 합계는 어떤 채널이 실패했는지 숨길 수 있으므로 최종 판단은 채널별로 내린다.

## 6. 장애 시나리오별 데이터 보존 범위

### 6.1 ACK가 가능한 replica가 사라진 경우

source의 binary log 기록이 빨라도 ACK가 오지 않으면 커밋이 기다린다. 원인은 replica receiver 중단, 네트워크 단절, relay log 저장 장치 지연 등일 수 있다. 설정된 대기 조건을 충족하지 못하면 비동기로 전환하고 쓰기가 다시 진행될 수 있다.

이때 응답 시간이 갑자기 정상화되는 현상을 복구로 오인하기 쉽다. `status`가 `OFF`로 바뀌면서 대기가 사라진 것일 수 있다. 지연 알람과 별개로 보호 수준 저하 알람을 둬야 한다.

### 6.2 replica의 applier만 중단된 경우

receiver가 relay log를 받아 영속화할 수 있다면 ACK는 계속 전달될 수 있다. source의 반동기 상태가 정상이고 커밋 지연이 낮아도 replica 읽기와 승격 준비 상태는 나쁠 수 있다.

수신 데이터가 디스크에 있다는 것은 유용하지만 즉시 서비스 가능한 데이터와 같지 않다. 승격 전에 relay log의 미적용 트랜잭션을 처리할 시간이 필요하고, 적용 오류가 있다면 원인을 해결해야 한다. relay log가 계속 쌓이면 디스크 부족으로 receiver까지 멈추어 ACK 장애로 번질 수 있다.

### 6.3 source가 ACK와 클라이언트 응답 사이에서 종료된 경우

replica에는 트랜잭션이 보존되어 있어도 클라이언트는 성공 응답을 받지 못할 수 있다. 연결 단절이나 클라이언트 타임아웃은 서버 롤백의 증거가 아니다. 자동 재시도로 동일 업무가 중복 처리되지 않도록 요청 식별자, 유일 제약, 처리 결과 조회 등 멱등성 설계가 필요하다.

`AFTER_SYNC`의 ACK 대기 이전에 장애가 나도 source의 binary log에는 미응답 트랜잭션이 남을 수 있다. 원래 source를 재기동했을 때 복구 과정에서 드러나는 트랜잭션 집합이 새 source와 다를 수 있으므로, 옛 source를 즉시 writer로 복귀시키면 안 된다.

### 6.4 ACK하지 않은 replica를 승격한 경우

ACK 요구 개수가 1이고 replica A와 B가 있을 때, A가 수신을 확인했다고 해서 B에도 같은 트랜잭션이 있다는 보장은 없다. A를 잃고 B만 승격하면 반동기 상태에서 성공한 트랜잭션이라도 B에 없을 수 있다.

장애 도메인을 분리한 replica 배치는 필수 검토 사항이지만, ACK 개수 자체가 “다른 가용 영역에서 반드시 ACK”라는 배치 정책은 아니다. 참여 서버의 위치와 승격 후보 정책까지 함께 설계해야 한다. 반동기 정상 구간의 보장도 실제 이벤트를 가진 replica가 보존되어 있고 그 이벤트를 복구·적용할 수 있다는 조건 아래 해석한다.

### 6.5 비동기로 전환된 뒤 source를 잃은 경우

비동기 구간에서 성공한 쓰기는 어느 replica에도 도착하지 않았을 수 있다. 이후 상태가 다시 반동기로 바뀌었는지, 마지막 관측값이 무엇이었는지만으로는 특정 트랜잭션의 보존 여부를 판정할 수 없다.

장애 조치에서는 원래 source를 fencing하여 새 쓰기를 막고, 후보들의 수신·실행 이력과 보존된 로그를 비교한다. 자동화가 부족한 증거를 추측으로 메워 승격하는 대신, 데이터 손실 가능성을 명시하고 정책에 따라 가용성과 보존 목표 중 우선순위를 결정해야 한다.

## 7. GTID 차집합으로 수신과 적용을 구분하는 축소 예제

GTID 집합 비교는 복제 적용 위치를 확인하는 도구이지 반동기 ACK 장부가 아니다. 아래 UUID와 구간은 **원리를 설명하기 위해 정한 가상의 GTID 집합**이며 실제 서버의 장애 측정값이 아니다. SQL은 집합 연산을 실행해 결과를 확인한다.

동일한 source UUID의 트랜잭션을 replica가 1부터 12까지 수신했다고 가정하되, 10까지만 실행했다고 가정한다. 승격 판단에서 확인해야 하는 목표 집합은 1부터 12까지로 둔다.

```sql
SET @required_set = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:1-12';
SET @received_set = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:1-12';
SET @executed_set = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:1-10';
SELECT
  GTID_SUBTRACT(@received_set, @executed_set) AS received_not_executed,
  GTID_SUBSET(@required_set, @received_set) AS required_received,
  GTID_SUBSET(@required_set, @executed_set) AS required_executed;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SET @required_set = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:1-12';

Query OK, 0 rows affected (0.00 sec)

mysql> SET @received_set = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:1-12';

Query OK, 0 rows affected (0.00 sec)

mysql> SET @executed_set = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:1-10';

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT
    ->   GTID_SUBTRACT(@received_set, @executed_set) AS received_not_executed,
    ->   GTID_SUBSET(@required_set, @received_set) AS required_received,
    ->   GTID_SUBSET(@required_set, @executed_set) AS required_executed;

+--------------------------------------------+-------------------+-------------------+
| received_not_executed                      | required_received | required_executed |
+--------------------------------------------+-------------------+-------------------+
| aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:11-12 |                 1 |                 0 |
+--------------------------------------------+-------------------+-------------------+
1 row in set (0.00 sec)
```

`required_received = 1`이면서 `required_executed = 0`이면 필요한 이벤트가 수신 집합에는 있지만 적용이 끝나지 않았다는 뜻이다. `received_not_executed`는 미적용 구간을 보여준다. GTID의 최댓값이나 문자열 길이만 비교하는 대신 집합 포함 관계를 사용해야 하는 이유다.

실제 환경에서는 `performance_schema.replication_connection_status.RECEIVED_TRANSACTION_SET` 같은 수신 관측점과 `@@GLOBAL.gtid_executed`를 구분한다. 수신 집합은 채널·상태 초기화의 영향을 받고 실행 집합은 서버 전체 이력을 포함하므로, 복수 source나 채널에서는 단순 차집합을 무조건 지연량으로 취급하지 않는다. relay log의 실제 보존 여부, 필터, 임의 트랜잭션 주입 여부도 함께 확인한다.

특히 source를 완전히 잃었다면 “장애 직전 성공 응답된 모든 GTID의 집합”을 바로 얻지 못할 수 있다. 후보끼리의 집합 비교로 가장 앞선 후보를 찾는 것과, 그 후보에 성공 응답된 쓰기가 하나도 빠지지 않았음을 증명하는 것은 다른 문제다. GTID 포함 관계만으로 업무 데이터의 내용까지 동일하다고 증명할 수도 없다.

## 8. 도입과 장애 조치의 운영 순서

### 도입 전

1. 허용 RPO와 쓰기 지연 예산을 먼저 정한다. ACK 지연에는 네트워크 왕복과 replica의 relay log 영속화 시간이 포함된다.
2. replica 개수뿐 아니라 장애 도메인, ACK 참여 범위, 승격 후보 제한을 정한다.
3. 비동기 복제 자체의 정상 수신·적용과 초기 정합성을 확인한다.
4. 역할별 플러그인 설치·활성화 및 receiver 재접속 절차를 준비한다. 설정 파일 또는 관리 체계에 지속 설정도 반영한다.
5. `enabled`, `status`, 연결 수, ACK 관련 카운터, 커밋 지연을 함께 검증한다. 변수값 조회만으로 도입 완료로 처리하지 않는다.

### 장애 조치 중

1. 원래 source를 fencing하고 쓰기 라우팅을 통제한다. 연결 장애만으로 원래 source가 죽었다고 단정하지 않는다.
2. 모든 접근 가능한 후보의 수신·실행 GTID, applier 오류, 보존 로그를 확보한다. 복구에 필요한 relay log나 복제 메타데이터를 성급히 초기화하지 않는다.
3. 필요한 이벤트를 가진 후보를 선택하고 미적용 이벤트를 처리한다. 최신성 판단과 업무 정합성 검증을 분리한다.
4. 읽기·쓰기 가능 상태와 승격 조건을 확인한 뒤 라우팅을 전환한다. 미응답 요청의 중복 처리 여부를 조사한다.
5. 옛 source는 새 source 기준으로 재구성하는 것을 기본 원칙으로 삼는다. 장애 당시의 로그를 조사용으로 보존하되 독립 writer로 재사용하지 않는다.

장애 훈련에서는 receiver 중단과 applier 중단을 별도로 시험하고, 네트워크 차단 후 비동기 전환·복귀, ACK 뒤 응답 유실, 뒤처진 후보의 승격 거부까지 확인한다. 이 글의 단일 인스턴스 SQL 검증은 이러한 다중 서버 장애 훈련을 대체하지 않는다.

## 9. Aurora MySQL에 그대로 대입하면 안 되는 이유

Aurora의 같은 클러스터 안에서 writer와 Aurora Replica가 사용하는 복제·스토리지 구조는 Community MySQL의 source 플러그인과 relay log ACK 구조와 다르다. Aurora 내부의 내구성 보장, replica 적용 지연, 승격 우선순위를 `rpl_semi_sync_source_status`로 설명하지 않는다.

Aurora와 외부 MySQL 또는 다른 클러스터 사이에서 사용하는 binlog 복제도 같은 클러스터 내부 복제와 구별한다. AWS의 해당 토폴로지와 버전 문서에서 지원 범위와 제약을 확인하고, Community용 `INSTALL PLUGIN`을 관리형 서비스에 그대로 실행하지 않는다. Amazon RDS for MySQL의 Multi-AZ DB cluster가 반동기 복제를 사용한다는 사실도 Aurora가 동일한 구성이라는 뜻은 아니다.

공통으로 적용되는 원칙은 수신·영속화·적용·읽기 가시성·승격 준비를 나누어 판단하는 것이다. 다만 그 증거를 수집할 지표와 장애 조치 절차는 사용하는 서비스의 실제 구조에 맞춰 선택해야 한다.

## 10. 운영 점검표

- [ ] 반동기 ACK를 SQL 적용 완료나 모든 replica의 동기화 완료로 설명하지 않는다.
- [ ] `AFTER_SYNC`와 `AFTER_COMMIT`의 커밋 가시성 차이를 이해한다.
- [ ] source의 binary log와 InnoDB 내구성 설정을 별도로 검토했다.
- [ ] `enabled`와 실제 `status`를 함께 수집하고 비동기 전환 이력을 보존한다.
- [ ] 타임아웃과 `wait_no_replica`를 가용성·RPO 정책에 맞춰 결정했다.
- [ ] receiver 장애와 applier 장애를 구분하며 relay log 용량도 감시한다.
- [ ] ACK 참여 서버의 장애 도메인과 승격 후보 선정 정책이 일치한다.
- [ ] 미응답 COMMIT 재시도에 대비한 멱등성 키와 결과 확인 경로가 있다.
- [ ] 승격 전 fencing, GTID 집합 비교, 미적용 로그 처리 절차가 마련되어 있다.
- [ ] 옛 source의 자동 writer 복귀를 막고 재구성·재검증 절차를 적용한다.

## 결론

반동기 복제는 커밋과 원격 로그 보존 사이의 간격을 줄이는 방법이다. 기본 `AFTER_SYNC`에서는 ACK를 엔진 커밋 앞에 배치하지만, 시간 초과에 따른 비동기 전환과 replica 적용 지연, 잘못된 승격 후보 선택까지 없애지는 않는다.

운영 품질은 플러그인을 켰는지가 아니라 정상 구간의 보장을 유지하고, 보호 수준이 낮아진 구간을 식별하며, 실제 이벤트를 보유한 서버를 안전하게 승격할 수 있는지로 결정된다. 이후의 복제 장애 조치와 고가용성 설계에서도 이 수신·적용·선정의 구분이 기본 기준이 된다.

## 참고 자료

- [MySQL 8.4: Semisynchronous Replication](https://dev.mysql.com/doc/refman/8.4/en/replication-semisync.html)
- [MySQL 8.4: Configuring Semisynchronous Replication](https://dev.mysql.com/doc/refman/8.4/en/replication-semisync-interface.html)
- [MySQL 8.4: Semisynchronous Replication Monitoring](https://dev.mysql.com/doc/refman/8.4/en/replication-semisync-monitoring.html)
- [MySQL 8.4: Replication Source Options and Variables](https://dev.mysql.com/doc/refman/8.4/en/replication-options-source.html)
- [AWS: Aurora MySQL과 MySQL 사이의 binlog 복제](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Replication.MySQL.html)
- [AWS: RDS for MySQL의 기능 지원 범위](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/MySQL.Concepts.FeatureSupport.html)
