---
title: "Read replica 승격 절차: 정합성 확인, DNS·Proxy 전환과 쓰기 차단"
description: "MySQL Read replica 승격에서 GTID 정합성 확인, 기존 writer 쓰기 차단, DNS·Proxy와 연결 풀 전환, Aurora 차이를 운영 절차로 정리한다."
tags: [ MySQL, 복제, 고가용성, 운영, DBA ]
image: "mysql-report-bg.png"
published: "2026-09-15"
updated: "2026-09-15"
author: "MySQL 기술 노트"
source_url: ""
---

Read replica 승격은 읽기 전용 변수를 끄는 작업이 아니다. **어느 서버가 마지막으로 승인된 데이터를 보유하는지 확인하고, 이전 writer가 더 이상 쓰기를 승인하지 못하도록 만든 뒤, 새 writer로 애플리케이션의 쓰기 경로를 넘기는 작업**이다. 이 중 하나라도 빠지면 복제 스레드가 정상이어도 데이터 손실이나 이중 쓰기가 발생한다.

이 글은 GTID 기반 비동기 복제를 사용하는 Community MySQL 8.0.26 이상을 중심으로 설명한다. `SOURCE`·`REPLICA` 명령과 변수 이름을 사용하며, MySQL 8.4에서도 개념은 같다. 다중 source, 의도적인 복제 필터, 지연 복제, Group Replication에는 별도의 후보 선정 규칙이 필요하다. Aurora의 클러스터 내부 reader 승격은 마지막 절에서 구분한다.

## 1. 계획 전환과 장애 승격은 같은 절차가 아니다

계획 전환(switchover)에서는 기존 writer에 접근할 수 있으므로 쓰기를 정지하고 최종 커밋 경계를 확정할 수 있다. 후보가 그 경계까지 적용했다는 증거를 얻으면, 이 절차가 대상으로 삼는 정상 복제 데이터에 대해 손실 없는 전환을 목표로 할 수 있다.

장애 승격(failover)에서는 마지막 커밋 경계를 읽지 못할 수 있다. 비동기 복제에서 source가 클라이언트에 성공을 반환한 트랜잭션이 아직 어떤 replica에도 전달되지 않았다면, 남은 replica만 보고 이를 복구할 수 없다. 후보의 지연 시간이 작았다는 기록은 RPO 0의 증명이 아니다.

| 구분 | 계획 전환 | 기존 writer에 접근할 수 없는 장애 |
|---|---|---|
| 기준 경계 | 쓰기 차단 후 source의 최종 GTID 집합 | 남아 있는 후보의 수신·적용 이력과 외부 증거 |
| 손실 판단 | 기준 집합의 적용 여부를 확인 | 미수신 커밋이 있을 수 있으므로 불확실성 기록 |
| 기존 writer 격리 | 애플리케이션 차단, 세션 정리, DB 읽기 전용 | 전원·네트워크·스토리지 등 외부 fencing 필요 |
| 승인 조건 | 검증 완료 후 정해진 전환 창에서 진행 | 격리 증거와 손실 허용 범위를 확인한 뒤 진행 |

승격 소요 시간인 RTO와 손실 허용 범위인 RPO는 다른 지표다. 신속한 승격을 위해 미수신 여부를 확인하지 않았다면 RTO는 줄어도 RPO가 충족되었다고 말할 수 없다. 반동기 복제 역시 수신 확인을 적용 완료나 모든 장애 조합에서의 무손실과 동일시하면 안 된다.

## 2. 승격의 핵심은 단일 writer 보장이다

다음 상태 전이는 계획 전환의 기본 순서다. 일반 애플리케이션 쓰기는 최종 확인이 끝날 때까지 닫아 둔다.

```mermaid
flowchart TD
    A["기존 writer와 승격 후보 확인"] --> B["신규 쓰기 중단 및 진행 중 트랜잭션 정리"]
    B --> C["기존 writer fencing 및 읽기 전용 확인"]
    C --> D["최종 GTID 경계 확정"]
    D --> E["후보 수신·적용 완료와 정합성 확인"]
    E --> F{"승격 조건 충족?"}
    F -- "아니오" --> X["쓰기 차단 유지 · 전환 중단 또는 재판단"]
    F -- "예" --> G["후보 복제 정지 · 새 writer 역할 부여"]
    G --> H["DNS·Proxy 반영 · 기존 연결 정리"]
    H --> I["일반 계정으로 쓰기·읽기 확인 후 트래픽 개방"]
```

**Fencing은 이전 writer가 더 이상 쓰기 권한을 행사할 수 없게 만드는 조치**다. DNS 레코드 변경은 신규 이름 해석 결과를 바꿀 뿐 fencing이 아니다. Proxy에서 백엔드를 제외하는 작업도 직접 접속, 다른 Proxy, 이미 열린 세션이 남으면 불완전하다.

접근 가능한 MySQL에서는 `super_read_only=ON`을 설정하여 특권 계정의 일반 테이블 쓰기까지 막고, 실제 변수 값과 애플리케이션 경로에서의 쓰기 거부를 확인한다. 그러나 이 변수는 이를 다시 끌 수 있는 관리자나 독립적인 자동화와 경쟁할 수 있다. 장애 시 서버 상태를 신뢰할 수 없다면 외부 격리가 우선이며, 격리 성공을 확인하지 못한 상태에서 새 writer의 쓰기를 여는 것은 이중 쓰기 위험을 받아들이는 결정이다.

## 3. 후보 자격은 읽기 지연보다 넓게 확인한다

읽기 부하를 잘 처리하던 replica가 반드시 writer에 적합한 것은 아니다. 다음은 승격 후보의 기본 설정을 읽는 쿼리다.

```sql
SELECT @@GLOBAL.gtid_mode AS gtid_mode,
       @@GLOBAL.log_bin AS log_bin,
       @@GLOBAL.log_replica_updates AS log_replica_updates,
       @@GLOBAL.read_only AS read_only,
       @@GLOBAL.super_read_only AS super_read_only;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT @@GLOBAL.gtid_mode AS gtid_mode,
    ->        @@GLOBAL.log_bin AS log_bin,
    ->        @@GLOBAL.log_replica_updates AS log_replica_updates,
    ->        @@GLOBAL.read_only AS read_only,
    ->        @@GLOBAL.super_read_only AS super_read_only;

+-----------+---------+---------------------+-----------+-----------------+
| gtid_mode | log_bin | log_replica_updates | read_only | super_read_only |
+-----------+---------+---------------------+-----------+-----------------+
| ON        |       1 |                   1 |         1 |               1 |
+-----------+---------+---------------------+-----------+-----------------+
1 row in set (0.00 sec)
```

이 결과는 GTID 복제를 구성한 검증용 후보에서 수집한다. 일반 단독 서버나 GTID를 사용하지 않는 환경의 기본값을 뜻하지 않는다.

`log_bin=1`은 승격 이후 자신의 변경을 기록하기 위해 필요하고, `log_replica_updates=1`은 이전 source에서 적용한 트랜잭션을 후보의 binlog에도 남겨 뒤처진 다른 replica가 이어받을 수 있게 하는 데 중요하다. 그러나 설정이 켜져 있어도 필요한 과거 binlog가 이미 제거되었다면 충분하지 않다. 후속 replica의 실행 집합과 새 writer가 실제로 보유한 로그를 함께 확인한다.

추가 자격 조건은 다음과 같다.

- 복제 필터, 지연 적용 설정, 미처리 오류, 과거의 오류 건너뛰기나 빈 GTID 주입 이력을 확인한다.
- source와 후보의 데이터 계보가 같고, 후보에 독립적인 업무 쓰기가 발생하지 않았는지 확인한다.
- 쓰기 부하를 감당할 CPU·메모리·스토리지 여유, 연결 한도, 내구성 설정을 점검한다. 읽기 전용 부하에서의 성능만으로 판단하지 않는다.
- 서비스 계정, 권한, TLS, 문자셋, 시간대, 필수 플러그인 및 스케줄 작업의 활성화 정책을 비교한다.
- Event Scheduler·배치·CDC 등 역할 변경에 영향을 받는 작업의 실행 주체를 정한다. 새 writer에서 자동으로 올바르게 실행된다고 가정하지 않는다.
- 재시작 후 역할이 뒤집히지 않도록 설정 파일, 동적 영속 설정, HA 제어기의 상태를 함께 관리한다. `SET GLOBAL`만 실행하면 재시작 후 같은 값이 유지된다는 보장은 없다.

## 4. GTID 정합성은 숫자가 아니라 집합으로 판정한다

계획 전환에서는 쓰기 정리가 끝난 기존 writer의 `@@GLOBAL.gtid_executed`를 기준 집합 S로 보관한다. 후보의 실행 집합 C에 대해 `GTID_SUBSET(S, C)=1`이어야 필요한 트랜잭션 식별자를 모두 적용한 것이다. `GTID_SUBTRACT(C, S)`가 비어 있지 않으면 후보 고유 이력이 있으므로 그 출처를 조사한다.

아래 SQL은 집합 관계를 확인하는 **실행 가능한 교육용 예제**다. UUID와 구간은 설명을 위해 정한 값이며 운영 서버의 측정값이 아니다. 마지막 번호만 비교하면 서로 다른 UUID에서 생긴 이력을 놓칠 수 있다.

```sql
SET @source_set = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-5';
SET @candidate_set = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-4';
SELECT GTID_SUBSET(@source_set, @candidate_set) AS contains_target,
       GTID_SUBTRACT(@source_set, @candidate_set) AS missing_gtids;
SET @candidate_set = CONCAT(@source_set,
  ',bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb:1');
SELECT GTID_SUBSET(@source_set, @candidate_set) AS contains_target,
       GTID_SUBTRACT(@candidate_set, @source_set) AS extra_gtids;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SET @source_set = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-5';

Query OK, 0 rows affected (0.00 sec)

mysql> SET @candidate_set = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-4';

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT GTID_SUBSET(@source_set, @candidate_set) AS contains_target,
    ->        GTID_SUBTRACT(@source_set, @candidate_set) AS missing_gtids;

+-----------------+----------------------------------------+
| contains_target | missing_gtids                          |
+-----------------+----------------------------------------+
|               0 | aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:5 |
+-----------------+----------------------------------------+
1 row in set (0.00 sec)

mysql> SET @candidate_set = CONCAT(@source_set,
    ->   ',bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb:1');

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT GTID_SUBSET(@source_set, @candidate_set) AS contains_target,
    ->        GTID_SUBTRACT(@candidate_set, @source_set) AS extra_gtids;

+-----------------+----------------------------------------+
| contains_target | extra_gtids                            |
+-----------------+----------------------------------------+
|               1 | bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb:1 |
+-----------------+----------------------------------------+
1 row in set (0.00 sec)
```

첫 비교는 source의 다섯 번째 트랜잭션이 빠졌음을 보여 준다. 두 번째 비교는 source 이력을 포함하더라도 후보 고유 트랜잭션이 남을 수 있음을 보여 준다. 후보들의 집합이 서로 포함 관계가 아니라면 단순히 “가장 앞선 서버” 하나를 고를 수 없다. 차집합의 실제 내용을 조사하고 충돌·손실 정책을 결정해야 한다.

GTID 일치는 행 내용 동일성의 충분조건이 아니다. 복제 필터, `sql_log_bin=0`으로 수행한 변경, 빈 트랜잭션 주입, 과거 수동 복구는 GTID 집합과 실제 데이터 사이에 차이를 만들 수 있다. 최종 확인에서는 중요한 주문·결제 식별자, 마지막 승인 이벤트, 변경 구간의 행 수와 업무 불변식을 함께 확인한다. 전체 테이블의 `COUNT(*)` 하나만 같다고 정합성이 증명되지는 않는다. 대용량 전체 비교는 전환 창에 즉흥적으로 넣지 말고 평상시 검증 결과와 최근 변경 범위를 결합한다.

## 5. 수신 완료와 적용 완료를 나누어 관측한다

복제 receiver가 relay log까지 받은 트랜잭션도 applier가 커밋하기 전에는 후보의 업무 데이터가 아니다. 병렬 적용에서는 coordinator가 읽을 이벤트를 다 배정했더라도 worker가 락 대기 중일 수 있다.

```sql
SELECT CHANNEL_NAME,
       SERVICE_STATE AS receiver_state,
       LAST_ERROR_NUMBER AS receiver_error,
       GTID_SUBSET(RECEIVED_TRANSACTION_SET,
                   @@GLOBAL.gtid_executed) AS received_applied
FROM performance_schema.replication_connection_status;

SELECT CHANNEL_NAME,
       COUNT(*) AS worker_count,
       SUM(SERVICE_STATE = 'ON') AS workers_on,
       SUM(LAST_ERROR_NUMBER <> 0) AS workers_with_error
FROM performance_schema.replication_applier_status_by_worker
GROUP BY CHANNEL_NAME;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT CHANNEL_NAME,
    ->        SERVICE_STATE AS receiver_state,
    ->        LAST_ERROR_NUMBER AS receiver_error,
    ->        GTID_SUBSET(RECEIVED_TRANSACTION_SET,
    ->                    @@GLOBAL.gtid_executed) AS received_applied
    -> FROM performance_schema.replication_connection_status;

+--------------+----------------+----------------+------------------+
| CHANNEL_NAME | receiver_state | receiver_error | received_applied |
+--------------+----------------+----------------+------------------+
|              | ON             |              0 |                0 |
+--------------+----------------+----------------+------------------+
1 row in set (0.00 sec)

mysql> SELECT CHANNEL_NAME,
    ->        COUNT(*) AS worker_count,
    ->        SUM(SERVICE_STATE = 'ON') AS workers_on,
    ->        SUM(LAST_ERROR_NUMBER <> 0) AS workers_with_error
    -> FROM performance_schema.replication_applier_status_by_worker
    -> GROUP BY CHANNEL_NAME;

+--------------+--------------+------------+--------------------+
| CHANNEL_NAME | worker_count | workers_on | workers_with_error |
+--------------+--------------+------------+--------------------+
|              |            4 |          0 |                  0 |
+--------------+--------------+------------+--------------------+
1 row in set (0.01 sec)
```

이 결과는 검증용 후보의 SQL thread를 의도적으로 정지하고, source의 마지막 쓰기가 후보에 수신된 상태에서 관측한 값이다. 빈 채널명은 기본 채널을 뜻한다.

`received_applied=0`이면 수신한 집합 중 아직 실행되지 않은 GTID가 있다. `1`이어도 receiver가 source의 모든 커밋을 받았다는 뜻은 아니다. source와 단절된 상태에서 이미 받은 것만 적용해도 `1`이 될 수 있다. worker 오류가 0이라는 결과도 작업 완료나 데이터 정합성의 증거와는 구별한다. 복제가 없는 서버에서는 두 조회 모두 `Empty set`이 나오며, 이는 건강한 replica라는 뜻이 아니다.

계획 전환의 최종 문턱은 수신 상태 문자열이 아니라 **고정한 S의 실행 완료**다. 후보에서 `WAIT_FOR_EXECUTED_GTID_SET(S, 제한시간)`을 호출하면 해당 집합이 실행될 때까지 기다릴 수 있다. 반환값 `0`은 조건 충족, `1`은 시간 초과다. 오류와 NULL도 통과로 간주하지 않는다. 이 함수 자체가 복제 스레드를 시작하거나 정지하지는 않는다.

## 6. 계획 전환 실행 순서

다음은 운영 대상과 승인 조건을 대입해야 하는 절차다. SQL 일부만 복사하여 즉시 실행하는 범용 스크립트가 아니다. 세션 변수는 연결을 바꾸면 사라지므로 source에서 읽은 집합을 후보의 별도 세션으로 명시적으로 전달한다.

### 6.1 기존 writer의 마지막 커밋 경계 확정

1. 애플리케이션과 배치의 신규 쓰기를 중단한다. 연결을 강제로 끊기 전에 진행 중 요청의 결과 확인 방법을 확보한다.
2. 활성 쓰기 트랜잭션을 정상 종료하거나 승인된 정책에 따라 롤백한다. 장기 트랜잭션·DDL·준비된 XA 트랜잭션은 별도 확인하며, 일괄 종료가 즉시 정리를 끝내 준다고 가정하지 않는다.
3. 기존 writer에 `SET GLOBAL super_read_only=ON`을 적용하고 성공을 확인한다. 이 설정은 실행 중 트랜잭션이나 잠금 때문에 대기할 수 있으므로 반환 전부터 차단 완료로 기록하지 않는다.
4. 변수 값, 기존 애플리케이션 연결의 쓰기 거부, 우회 경로 차단을 확인한다. 필요하면 외부 fencing을 함께 적용한다.
5. 이후 `SELECT @@GLOBAL.gtid_executed`를 읽어 기준 집합 S로 기록한다. 차단 전에 읽은 집합은 마지막 커밋 경계를 놓칠 수 있다.

`read_only=ON`만으로는 `CONNECTION_ADMIN` 또는 관련 특권을 가진 계정의 쓰기를 충분히 막지 못한다. `super_read_only`도 모든 내부 작업을 중단시키는 디스크 수준의 읽기 전용 기능은 아니며, 복제 applier는 계속 적용할 수 있다. 따라서 예전 writer에 들어오는 다른 복제 채널이나 자동화가 있다면 함께 통제해야 한다.

### 6.2 후보 적용 완료 후 역할 전환

후보의 receiver와 applier를 유지한 상태에서 S까지 적용하도록 기다린다. 시간 초과 시 원인을 해결하거나 전환을 중단한다. GTID를 건너뛰거나 기준 집합을 작게 바꾸어 검사를 통과시키지 않는다.

```text
후보 세션에서 수행할 절차 구조 — <...>는 운영값으로 대체해야 한다.

SET @target = '<기존 writer 쓰기 차단 후 확보한 GTID 집합>';
SELECT WAIT_FOR_EXECUTED_GTID_SET(@target, 30) AS caught_up;
SELECT GTID_SUBSET(@target, @@GLOBAL.gtid_executed) AS contains_target;
SELECT GTID_SUBTRACT(@@GLOBAL.gtid_executed, @target) AS extra_gtids;

검사 통과와 업무 데이터 확인 후:
STOP REPLICA;
SET GLOBAL super_read_only=OFF;
SET GLOBAL read_only=OFF;
```

`30`은 예제의 대기 제한일 뿐 운영 표준 시간이 아니다. `STOP REPLICA` 성공과 채널 상태를 확인한 뒤 쓰기를 허용한다. 다중 채널 서버에서는 모든 채널을 무조건 정지시키는 대신 전환 대상과 유지할 데이터 흐름을 먼저 정의한다.

`super_read_only=OFF`만 설정하면 `read_only`는 여전히 ON일 수 있다. 반대로 `read_only=OFF`는 `super_read_only`도 OFF로 만든다. 위 순서는 의도를 명시한 것이며, 두 값만으로 HA 제어기·Proxy의 역할 상태까지 변경되지는 않는다.

이 단계에서 `RESET REPLICA ALL`로 기존 접속 메타데이터를 지울 필요는 없다. 추적 가능한 상태를 보존한 뒤 별도 정리한다. 특히 GTID 기반 승격 절차에 `RESET MASTER` 또는 MySQL 8.4의 `RESET BINARY LOGS AND GTIDS`를 끼워 넣어 실행 이력을 초기화하지 않는다. 후속 replica의 자동 위치 지정과 복구 근거를 훼손할 수 있다.

### 6.3 폐기용 두 서버에서 확인한 전환 결과

다음 실험은 MySQL 8.0의 두 폐기용 인스턴스에서 수행했다. GTID·ROW binlog·복제 변경 로깅을 켜고, 후보는 읽기 전용으로 유지했다. source에서 테스트 행을 두 번 입력한 뒤 쓰기를 차단하고 최종 GTID까지 후보를 따라잡게 했다. 그 후 후보의 복제를 정지하고 읽기 전용을 해제하여 새 행을 입력했다.

검증 범위는 **DB 계층의 계획 전환과 쓰기 차단**이다. 네트워크 파티션, 외부 fencing 장비, 실제 DNS 전파, 특정 Proxy 제품의 세션 종료 동작은 이 실험으로 검증한 것이 아니다. 실행 버전은 MySQL 8.0.46이며, 다음 값은 한 번의 검증 실행에서 얻은 결과다.

| 확인 지점 | 실제 관측값 | 해석 |
|---|---|---|
| 기준 집합 적용 대기 | `caught_up = 0` | 제한시간 안에 기준 집합 적용 완료 |
| 기준 집합 포함 여부 | `contains_target = 1` | 후보가 source의 최종 GTID 집합 포함 |
| 후보 추가 GTID 없음 | `no_extra_gtids = 1` | 비교 시점에 후보 고유 실행 이력 없음 |
| 기존 writer 쓰기 시도 | `ERROR 1290`, `--super-read-only` | 특권 테스트 계정의 INSERT도 거부 |
| 이전 writer에 남은 행 | `row_count = 2`, `max_id = 2` | 새 writer의 세 번째 행을 받지 않은 상태 |
| 승격한 서버의 행 | `row_count = 3`, `max_id = 3` | 새 쓰기 수락 확인 |

다음은 역할 해제와 새 쓰기 부분의 실제 client 기록이다. `promotion_lab.probe`는 위 실험에서 미리 만든 테스트 테이블이며 운영 테이블이 아니다.

```text
mysql> SET GLOBAL super_read_only=OFF;

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@GLOBAL.read_only AS read_only, @@GLOBAL.super_read_only AS super_read_only;

+-----------+-----------------+
| read_only | super_read_only |
+-----------+-----------------+
|         1 |               0 |
+-----------+-----------------+
1 row in set (0.00 sec)

mysql> SET GLOBAL read_only=OFF;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO promotion_lab.probe VALUES (3,'new_writer');

Query OK, 1 row affected (0.00 sec)

mysql> SELECT COUNT(*) AS row_count, MAX(id) AS max_id FROM promotion_lab.probe;

+-----------+--------+
| row_count | max_id |
+-----------+--------+
|         3 |      3 |
+-----------+--------+
1 row in set (0.00 sec)
```

작은 실험의 행 수 비교는 동작을 보여 주기 위한 것이다. 실제 업무 데이터 전체의 정합성 검사나 서비스 계정의 권한 검증을 대신하지 않는다. 신규 쓰기 후 두 서버의 데이터가 달라졌다는 결과는, 이전 writer로 경로만 되돌릴 수 없다는 점도 보여 준다.

## 7. DNS·Proxy 변경보다 연결 풀 전환이 중요하다

DB 승격과 애플리케이션 전환은 별도의 제어면이다. 안전한 전환 창에서는 일반 쓰기를 막은 상태에서 새 writer의 역할을 확정하고 경로를 변경한 뒤, 제한된 검증 요청을 통과시키고 트래픽을 연다.

### DNS를 사용하는 경우

DNS TTL은 기존 TCP 연결의 수명이 아니다. 드라이버·운영체제·런타임의 DNS 캐시 정책 때문에 새 연결도 한동안 예전 주소를 사용할 수 있다. 미리 TTL을 낮췄더라도 이미 캐시된 이전 TTL과 열린 연결은 따로 처리해야 한다.

- writer용 이름을 새 주소로 변경한 뒤 여러 애플리케이션 실행 환경에서 해석 결과를 확인한다.
- 연결 풀의 기존 연결을 폐기하거나 수명을 제한하고, 새 연결로 서버 식별자와 역할을 확인한다.
- 관리자가 직접 새 DB에 접속한 결과가 아니라 서비스가 사용하는 이름·드라이버·계정 경로에서 확인한다.
- 캐시가 남아 예전 writer에 도달해도 쓰기는 거부되어야 한다. 이것이 DNS와 fencing을 분리하는 이유다.

### Proxy를 사용하는 경우

Proxy의 설정 저장과 실제 런타임 반영을 구분한다. 여러 Proxy 노드가 있다면 일부만 바뀌지 않았는지 확인한다. 백엔드를 신규 연결 대상에서 제외하는 기능, 기존 연결을 정상 소진하는 기능, 즉시 끊는 기능은 제품마다 다르다. 진행 중 트랜잭션이 있는 세션을 다른 DB로 투명하게 옮길 수 있다고 가정하지 않는다.

단순 TCP 연결 성공이나 `SELECT 1`은 writer 자격을 검증하지 못한다. 서버 식별자, 역할 상태, 허용된 writer 목록을 함께 점검하고 일반 서비스 계정으로 제한된 쓰기 후 읽기 검사를 수행한다. 검사에는 고유 요청 식별자를 사용하여 재시도 중복을 방지하고, 테스트 데이터의 정리 정책도 정한다.

`COMMIT` 전후에 연결이 끊기면 애플리케이션은 성공 여부를 모를 수 있다. 이를 무조건 미커밋으로 해석하여 다시 실행하면 중복 결제나 중복 작업이 생긴다. 업무 키·멱등성 키로 기존 결과를 조회한 뒤 재시도하고, 새 연결에서는 격리 수준·세션 시간대 등 필요한 초기 설정도 재적용한다.

## 8. 장애 승격과 되돌리기의 중단 기준

기존 writer가 응답하지 않으면 쓰기가 멈췄다고 추정하지 않는다. 네트워크가 분리되어 운영자에게만 안 보이고 일부 클라이언트는 여전히 쓰고 있을 수 있다. 외부 fencing 성공을 확인한 후, 후보들의 적용 집합과 수신 집합을 비교한다. 후보별로 남아 있는 relay log를 적용할 수 있는지와 적용 오류도 확인한다.

최종 source 집합을 얻을 수 없다면 그 한계를 승격 기록에 남긴다. 후보가 가진 이력이 가장 풍부하더라도 장애 직전 승인된 모든 커밋을 포함한다는 보장은 없다. 업무 이벤트·클라이언트 승인 기록과 대조할 손실 확인 절차를 준비한다.

다음 경우는 자동 승격을 멈추거나 명시적인 위험 승인으로 전환할 사유다.

- 이전 writer의 쓰기 격리 여부를 확인하지 못했다.
- 후보 간 GTID 집합이 갈라져 있고 차집합의 업무 의미를 모른다.
- 복제 필터나 수동 변경 때문에 후보의 데이터 범위가 불명확하다.
- 필요한 트랜잭션이 미적용 상태이고 오류 원인을 해결하지 못했다.
- 승격 이후 쓰기 용량·계정·접속 경로의 최소 검증이 통과하지 않았다.

**새 writer에서 업무 쓰기를 한 번이라도 받은 이후에는 DNS를 원래 주소로 돌리는 것이 롤백이 아니다.** 예전 writer는 새 이력을 모른다. 새 writer를 다시 차단하고 이력을 맞추는 별도의 계획 전환이 필요하다. 복귀한 예전 writer는 격리·읽기 전용을 유지한 채 재동기화하고, 분기 이력이 있으면 새 writer 기준으로 다시 구축한다. 양쪽의 변경을 무조건 합치는 자동 복구는 중복과 충돌을 숨길 수 있다.

후속 replica를 새 writer에 연결할 때도 GTID auto-position만 믿지 않는다. 필요한 GTID의 binlog가 남아 있는지, 후보 고유 이력과 필터가 없는지 확인한다. 로그가 부족하면 검증된 백업·스냅샷으로 다시 구성하는 경로가 필요하다.

## 9. Aurora MySQL에서는 승격의 대상부터 구분한다

같은 Aurora 클러스터 안의 reader와 writer는 클러스터 볼륨을 공유한다. 이 reader를 writer로 바꾸는 클러스터 failover는 일반 MySQL의 독립 binlog replica를 승격하는 절차와 다르다. 관리형 failover 동작과 promotion tier를 사용하며, 위의 `STOP REPLICA`·읽기 전용 변수 변경으로 역할을 강제로 만드는 방식은 적용하지 않는다.

writer로 가는 cluster endpoint는 failover에 따라 대상이 바뀌지만 기존 연결이 새 writer로 그대로 이동하는 것은 아니다. 애플리케이션의 재접속, DNS 캐시, 트랜잭션 재시도 안전성은 여전히 검증해야 한다. 특정 instance endpoint에 쓰기 대상을 고정하면 역할 변경을 따라가지 못할 수 있다.

다음 세 가지를 같은 “read replica 승격”으로 취급하지 않는다.

1. **동일 클러스터의 Aurora reader 승격:** 공유 스토리지 기반의 관리형 역할 전환이다.
2. **binlog로 연결한 별도 Aurora 클러스터 또는 외부 MySQL replica:** 독립적인 수신·적용 상태, GTID와 손실 범위를 검토해야 한다. 관리형 권한과 지원 프로시저도 확인한다.
3. **Aurora Global Database의 리전 전환:** 계획된 switchover와 장애 failover의 보장 조건이 다르다. 해당 엔진 버전과 전역 복제 기능의 RPO·엔드포인트 동작을 기준으로 별도 절차를 사용한다.

AWS 제어면의 작업 성공만으로 애플리케이션 복구까지 완료되었다고 보고하지 않는다. 실제 서비스 접속 경로에서 새 writer의 식별·쓰기·재시도 동작까지 확인한다.

## 10. 승격 승인 체크리스트

- [ ] 계획 전환인지 장애 승격인지 구분하고, RPO·RTO 및 위험 승인 주체를 정했다.
- [ ] 이전 writer의 모든 쓰기 경로와 자동화를 차단했으며 fencing 증거를 확보했다.
- [ ] 진행 중 트랜잭션·DDL·XA 및 불확실한 커밋 결과의 처리 방침이 있다.
- [ ] 계획 전환은 차단 후 GTID 경계를 확정했고, 후보가 그 집합을 모두 적용했다.
- [ ] 장애 승격은 최종 source 집합을 알 수 있는지와 잠재적 손실 범위를 기록했다.
- [ ] 후보의 고유 GTID, 복제 필터, 수동 변경 이력과 주요 업무 데이터의 일관성을 확인했다.
- [ ] 후보의 복제 정지와 writer 설정, 계정·TLS·내구성·처리 용량을 확인했다.
- [ ] 모든 DNS·Proxy 경로와 연결 풀을 전환하고 일반 계정으로 쓰기 후 읽기를 확인했다.
- [ ] 실패 요청의 멱등 재시도, 새 연결의 세션 초기화, 모니터링 전환을 검증했다.
- [ ] 예전 writer의 자동 재등장을 막고, 후속 replica 재연결과 백업 재개 계획을 확보했다.

## 결론

안전한 replica 승격은 **이전 writer의 쓰기 배제, 필요한 이력의 적용 증명, 애플리케이션 경로의 전환**을 순서대로 확인하는 일이다. GTID는 중요한 판단 도구지만 데이터 내용의 검증이나 fencing을 대신하지 않는다. DNS 변경 역시 열린 연결과 불확실한 커밋 결과를 해결하지 않는다.

이 절차를 바탕으로 장애 감지·승격 자동화를 설계할 때는 명령 목록보다 통과 조건과 중단 조건을 먼저 구현해야 한다. 이후 고가용성 구성에서는 단일 writer 권한을 어떤 제어기가 결정하고, 그 결정이 네트워크 분리 상황에서도 어떻게 강제되는지를 함께 살펴보아야 한다.

## 참고 문서

- [MySQL 8.0: GTID를 이용한 failover와 확장](https://dev.mysql.com/doc/refman/8.0/en/replication-gtids-failover.html)
- [MySQL 8.0: GTID 함수](https://dev.mysql.com/doc/refman/8.0/en/gtid-functions.html)
- [MySQL 8.0: read_only·super_read_only 시스템 변수](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_super_read_only)
- [Amazon Aurora의 복제 구조](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Replication.html)
- [Amazon Aurora의 엔드포인트와 연결](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.Endpoints.html)
