pt-osc와 gh-ost 비교: 운영 환경별 선택 기준
pt-osc의 트리거와 gh-ost의 binlog 적용 구조를 비교하고, 외래 키·쓰기 부하·전환 잠금·Aurora 제약에 따른 선택 기준을 정리한다.
대형 테이블의 스키마 변경에서 도구 선택은 복사 속도만의 문제가 아니다. 변경 중 업무 쓰기가 어디에서 추가 비용을 부담하는지, 복사를 멈추었을 때 어떤 작업이 계속되는지, 마지막 이름 교체를 언제 승인할 수 있는지가 더 중요하다. 같은 크기의 테이블이라도 외래 키가 많은 주문 테이블과 단순한 로그 테이블은 다른 판단이 필요하다.
pt-osc는 pt-online-schema-change의 통상적인 약칭이다. 이 글에서는 두 도구의 변경 반영 경로를 비교한 뒤, 서버 내장 DDL 가능 여부 → 구조적 제약 → 부하 제어 → 전환과 복구 순서로 선택한다. MySQL 8.0 이상을 기준으로 설명하되, gh-ost의 옵션은 1.1.11 태그의 공식 문서와 소스를 확인했다. SQL 실험은 Community MySQL의 격리된 인스턴스에서 실행하며, 두 도구의 성능 우열이나 Aurora에서의 성공을 측정한 시험은 아니다.
1. 먼저 외부 도구가 필요한 변경인지 확인한다
단순한 컬럼 추가가 해당 서버와 테이블에서 ALGORITHM=INSTANT로 가능하다면, 전체 테이블을 복사하는 외부 도구는 불필요한 작업을 만들 수 있다. 보조 인덱스 추가도 내장 INPLACE와 LOCK=NONE으로 처리할 수 있는 경우가 있다. 다만 INPLACE는 모든 변경에서 재구축이 없다는 의미가 아니며, LOCK=NONE도 메타데이터 잠금(MDL)이 없다는 뜻은 아니다.
외부 도구를 검토할 이유는 다음과 같다.
- 내장 알고리즘으로는 허용한 쓰기 중단 시간을 만족하지 못한다.
- 복사 속도와 중단 조건을 운영자가 세밀하게 조정해야 한다.
- 데이터 준비와 최종 전환 시점을 분리해야 한다.
- 변경 대상의 실제 크기와 부하에서 내장 DDL의 로그·공간·복제 지연 위험이 더 크다.
반대로 외부 도구는 새 테이블과 인덱스를 만들고 기존 데이터를 다시 기록한다. binlog와 redo, 백업 보관량, 복제본의 적용 부하도 증가할 수 있다. 원본 크기만큼의 빈 공간이 있다는 한 가지 이유로 승인하지 않는다. 새 스키마의 크기, 기존 테이블 보관, 임시 작업 공간과 로그 증가분을 함께 계산해야 한다.
2. 핵심 차이: 동기 트리거와 비동기 binlog 적용
pt-osc: 업무 트랜잭션 안에서 변경을 반영한다
pt-osc는 변경된 구조의 새 테이블을 만들고, 원본에 변경 추적용 트리거를 설치한 뒤 기존 행을 청크 단위로 복사한다. 원본에서 발생한 INSERT·UPDATE·DELETE는 트리거를 통해 새 테이블에도 반영된다. InnoDB 테이블끼리의 트리거 작업은 원래 문장과 같은 트랜잭션에 속하므로 업무 트랜잭션이 롤백되면 해당 변경도 함께 롤백된다.
이 구조의 장점은 변경을 별도의 binlog 소비자가 나중에 따라잡도록 설계하지 않아도 된다는 점이다. 그러나 새 테이블의 인덱스 갱신과 잠금 비용이 업무 쓰기 경로에 들어간다. 인기 있는 키에 갱신이 몰리거나 새 인덱스가 많으면 지연 시간이 증가할 수 있다. 청크 복사를 일시 중지해도 트리거가 설치된 동안 업무 쓰기에 따른 동기화 비용은 남는다.
gh-ost: 커밋된 변경을 binlog에서 읽어 적용한다
gh-ost는 기존 행을 복사하는 경로와 binlog의 행 변경을 새 테이블에 적용하는 경로를 조정한다. 변경 추적용 트리거를 원본에 설치하는 대신, 행 기반 이벤트를 읽고 새 테이블을 따라잡게 한다. 따라서 업무 문장이 마이그레이션 트리거를 실행하는 비용은 없다. 그렇다고 쓰기 비용이 사라지는 것은 아니다. 새 테이블 쓰기, 인덱스 갱신, 로그 생성, binlog 읽기는 같은 서버 또는 관련 서버의 자원을 사용한다.
복사나 변경 적용을 늦추면 원본과 새 테이블 사이에 차이가 쌓일 수 있다. 적용 처리량이 유입 변경량을 지속적으로 따라가지 못하면 복사 진척률이 높아도 전환할 수 없다. 필요한 binlog가 보존 기간을 넘어 삭제되는 것도 위험하다. 프로세스 메모리가 모든 지연 이벤트를 무제한 보관하는 것은 아니므로, 메모리 사용량만 보고 추적 여유를 판단하지 않는다.
flowchart TB
A[업무 쓰기] --> B{변경 추적 방식}
B --> C[pt-osc 원본 DML과 트리거]
C --> D[같은 트랜잭션에서 새 테이블 반영]
B --> E[gh-ost 원본 커밋과 binlog]
E --> F[별도 소비와 새 테이블 적용]
G[기존 행 청크 복사] --> H[새 테이블 준비]
D --> H
F --> H
H --> I[정합성·지연·MDL·업무 상태 확인]
I --> J[승인된 시점에 이름 교체]
J --> K[새 스키마에서 업무 확인]
두 도구 모두 복사와 변경 반영이 경쟁하는 상황을 처리하는 로직을 갖는다. 단순히 애플리케이션에서 INSERT SELECT를 반복하고 나중에 이름만 바꾸는 방식으로 대체해서는 안 된다. 또한 구조 설명만으로 특정 작업의 처리량이나 잠금 위험을 확정할 수 없다. 동일한 데이터 분포와 쓰기 패턴으로 리허설해야 한다.
3. 운영 환경별 선택표
| 환경 또는 요구 | 우선 검토 | 판단 근거와 남는 조건 |
|---|---|---|
| 메타데이터 변경만으로 가능한 컬럼 추가 | 내장 INSTANT | 전체 복사를 피한다. 실제 버전·행 형식·컬럼 수·MDL 제한은 확인한다. |
| FK가 없고 ROW/FULL binlog를 안정적으로 읽을 수 있음 | gh-ost 후보 | 업무 경로에 동기화 트리거를 추가하지 않고 복사·전환을 제어하기 쉽다. 지연 추적 능력은 별도 확인한다. |
| binlog 사용 또는 복제 스트림 접근이 불가능함 | pt-osc 후보 | 변경 추적 자체는 binlog 스트리밍에 의존하지 않는다. 트리거 권한·키·복제 안전성 점검은 필요하다. |
| 다른 테이블이 대상 테이블을 FK로 참조함 | 내장 DDL 또는 pt-osc의 FK 처리 검토 | 자식 테이블 제약을 새 객체로 연결해야 한다. gh-ost의 일반적인 지원 경로로 취급하지 않는다. |
| 쓰기 지연 예산이 작고 긴 일시 정지가 필요함 | gh-ost 검토 가치가 큼 | pt-osc의 복사 정지는 트리거 비용 제거가 아니다. gh-ost도 binlog 보존과 재개 후 따라잡기 여유가 필요하다. |
| 기존 업무 트리거가 있음 | 도구 버전별 보존 기능 검증 후 결정 | pt-osc의 --preserve-triggers, gh-ost의 버전별 트리거 처리 옵션을 확인한다. 자동 보존을 가정하지 않는다. |
| 공통으로 사용할 안전한 PK/UNIQUE 키가 없음 | 설계 보완 또는 별도 변경 창 | 행 식별·복사 순회·삭제 반영이 불안정하다. 안전성 검사를 끄는 방식으로 해결하지 않는다. |
| 복제 토폴로지가 복잡하거나 failover 가능성이 큼 | 실행 보류 후 연결·중단 정책 확정 | 변경 추적 위치, writer 변경, 복제 필터와 감시 대상 누락을 먼저 해결한다. |
외래 키와 기존 트리거를 구별한다
대상 테이블이 부모를 참조하는 FK와, 다른 자식 테이블이 대상을 참조하는 FK를 모두 조사한다. 특히 후자는 단순한 원자적 이름 교체만으로 업무상 기대한 참조 관계가 완성되지 않는다. pt-osc의 --alter-foreign-keys-method에는 자식 테이블 제약을 재구성하는 방법 등이 있지만, 자식의 크기와 ALTER 잠금까지 포함한 별도 작업이다. drop_swap은 기존 테이블을 먼저 제거하는 비원자적 구간과 실패 위험이 있으므로 일반적인 무중단 교체와 동일하게 평가하지 않는다.
기존 트리거도 단순히 개수만 보아서는 안 된다. 참조하는 컬럼을 삭제·변경하는지, 감사 행이나 외부 효과를 중복 생성할 가능성이 있는지, 전환 후 이름과 실행 순서를 유지해야 하는지 확인한다.
버전 주의: gh-ost 1.1.11의 요구사항 문서에는 트리거 미지원이라는 오래된 설명이 남아 있지만, 같은 태그의 옵션 문서와 소스에는 --include-triggers가 존재하고 --trigger-suffix를 함께 요구한다. 따라서 “gh-ost는 모든 버전에서 기존 트리거를 지원하지 않는다”는 비교표도 정확하지 않다. 이 글에서는 해당 옵션의 존재와 기본 검사 분기를 확인했으며, 트리거 보존 기능의 실제 마이그레이션은 시험하지 않았다. 운영 승인은 설치된 바이너리의 도움말과 원본 트리거를 사용한 리허설을 기준으로 한다. 변경 추적이 트리거를 사용하지 않는다는 사실과 업무 트리거를 보존할 수 있는지는 서로 다른 문제다.
4. 선택 전에 실행하는 SQL 점검
4.1 서버의 binlog 조건을 확인한다
다음 조회는 설정을 변경하지 않는다. gh-ost 1.1.11은 소비할 서버의 ROW 형식과 FULL 행 이미지를 요구한다. 복제본을 경유한다면 실제 이벤트를 제공하는 서버의 설정과 원본·복제본 스키마 일치 여부까지 확인해야 한다.
SELECT VERSION() AS mysql_version,
@@global.log_bin AS log_bin_enabled,
@@global.binlog_format AS binlog_format,
@@global.binlog_row_image AS binlog_row_image;
실행 결과(MySQL 8.0.x):
mysql> SELECT VERSION() AS mysql_version,
-> @@global.log_bin AS log_bin_enabled,
-> @@global.binlog_format AS binlog_format,
-> @@global.binlog_row_image AS binlog_row_image;
+---------------+-----------------+---------------+------------------+
| mysql_version | log_bin_enabled | binlog_format | binlog_row_image |
+---------------+-----------------+---------------+------------------+
| 8.0.46 | 0 | ROW | FULL |
+---------------+-----------------+---------------+------------------+
검증 인스턴스는 binlog를 끄고 실행했다. 따라서 결과에서 ROW와 FULL이 보여도 log_bin_enabled=0이면 gh-ost 실행 준비가 되었다는 뜻이 아니다. 이 조회만으로 계정 권한, TLS 연결, 이벤트 수신, 필요한 로그의 보존까지 증명하지도 않는다.
4.2 키·양방향 FK·기존 트리거를 실제 메타데이터로 찾는다
다음은 빈 실습 스키마 전용 예제다. 부모·대상·자식 테이블과 대상의 업무 트리거를 만든 다음, 선택에 필요한 메타데이터를 조회하고 정리한다. 운영에서는 준비·삭제 문장을 실행하지 말고 조회문의 스키마와 테이블 조건만 검토해 적용한다. 다른 스키마에서 들어오는 FK도 놓치지 않도록 참조 대상 스키마 조건을 따로 사용한다.
CREATE TABLE osc_parent (
id BIGINT NOT NULL PRIMARY KEY
) ENGINE=InnoDB;
CREATE TABLE osc_target (
id BIGINT NOT NULL PRIMARY KEY,
parent_id BIGINT NOT NULL,
amount INT NOT NULL,
CONSTRAINT fk_osc_parent FOREIGN KEY (parent_id) REFERENCES osc_parent(id)
) ENGINE=InnoDB;
CREATE TABLE osc_child (
id BIGINT NOT NULL PRIMARY KEY,
target_id BIGINT NOT NULL,
CONSTRAINT fk_osc_target FOREIGN KEY (target_id) REFERENCES osc_target(id)
) ENGINE=InnoDB;
CREATE TRIGGER osc_target_bu BEFORE UPDATE ON osc_target
FOR EACH ROW SET NEW.amount = GREATEST(NEW.amount, 0);
SELECT s.INDEX_NAME, s.COLUMN_NAME, s.NON_UNIQUE, c.IS_NULLABLE
FROM information_schema.STATISTICS s
JOIN information_schema.COLUMNS c
ON c.TABLE_SCHEMA = s.TABLE_SCHEMA AND c.TABLE_NAME = s.TABLE_NAME
AND c.COLUMN_NAME = s.COLUMN_NAME
WHERE s.TABLE_SCHEMA = DATABASE() AND s.TABLE_NAME = 'osc_target'
ORDER BY s.INDEX_NAME, s.SEQ_IN_INDEX;
SELECT 'outgoing' AS direction, TABLE_NAME, REFERENCED_TABLE_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'osc_target'
AND REFERENCED_TABLE_NAME IS NOT NULL
UNION ALL
SELECT 'incoming', TABLE_NAME, REFERENCED_TABLE_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE REFERENCED_TABLE_SCHEMA = DATABASE()
AND REFERENCED_TABLE_NAME = 'osc_target';
SELECT TRIGGER_NAME, ACTION_TIMING, EVENT_MANIPULATION
FROM information_schema.TRIGGERS
WHERE EVENT_OBJECT_SCHEMA = DATABASE() AND EVENT_OBJECT_TABLE = 'osc_target';
DROP TABLE osc_child;
DROP TABLE osc_target;
DROP TABLE osc_parent;
실행 결과(MySQL 8.0.x):
준비·정리 DDL과 조회문은 짧게 표시하고, 조회 결과의 행과 열은 그대로 발췌했다.
mysql> CREATE TABLE osc_parent (...);
Query OK, 0 rows affected (0.01 sec)
mysql> CREATE TABLE osc_target (...);
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE osc_child (...);
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TRIGGER osc_target_bu ...;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT s.INDEX_NAME, s.COLUMN_NAME, s.NON_UNIQUE, c.IS_NULLABLE ...;
+---------------+-------------+------------+-------------+
| INDEX_NAME | COLUMN_NAME | NON_UNIQUE | IS_NULLABLE |
+---------------+-------------+------------+-------------+
| fk_osc_parent | parent_id | 1 | NO |
| PRIMARY | id | 0 | NO |
+---------------+-------------+------------+-------------+
2 rows in set (0.01 sec)
mysql> SELECT 'outgoing' AS direction, ... UNION ALL SELECT 'incoming', ...;
+-----------+------------+-----------------------+
| direction | TABLE_NAME | REFERENCED_TABLE_NAME |
+-----------+------------+-----------------------+
| outgoing | osc_target | osc_parent |
| incoming | osc_child | osc_target |
+-----------+------------+-----------------------+
2 rows in set (0.00 sec)
mysql> SELECT TRIGGER_NAME, ACTION_TIMING, EVENT_MANIPULATION ...;
+---------------+---------------+--------------------+
| TRIGGER_NAME | ACTION_TIMING | EVENT_MANIPULATION |
+---------------+---------------+--------------------+
| osc_target_bu | BEFORE | UPDATE |
+---------------+---------------+--------------------+
1 row in set (0.00 sec)
mysql> DROP TABLE osc_child;
Query OK, 0 rows affected (0.00 sec)
mysql> DROP TABLE osc_target;
Query OK, 0 rows affected (0.00 sec)
mysql> DROP TABLE osc_parent;
Query OK, 0 rows affected (0.00 sec)
결과에는 NOT NULL인 PK가 있지만, 대상이 부모를 참조하는 FK와 자식이 대상을 참조하는 FK, 기존 BEFORE UPDATE 트리거도 함께 나타난다. 따라서 키 조건만 통과했다고 gh-ost의 일반 실행을 승인해서는 안 되며, pt-osc를 선택해도 FK 연결 변경과 업무 트리거 보존을 따로 검토해야 한다. 복합 키는 열 순서 전체를 확인하고, 표현식·접두사 인덱스 등 특수한 키는 이 단순 조회만으로 적격 판정을 내리지 않는다.
4.3 내장 DDL로 충분한지 축소 검증한다
다음 예제는 컬럼 추가를 INSTANT로, 보조 인덱스 추가를 INPLACE로 명시한다. 지원하지 않는 환경에서 알고리즘을 생략해 더 무거운 방식으로 자동 전환시키는 대신, 실패를 작업 계획 재검토의 신호로 삼는다. 실습에서 성공했다는 사실은 운영 테이블의 실행 시간이나 MDL 대기 상한을 보장하지 않는다.
CREATE TABLE osc_native (
id BIGINT NOT NULL PRIMARY KEY,
amount INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO osc_native VALUES (1, 100), (2, 200);
ALTER TABLE osc_native
ADD COLUMN migration_tag INT NOT NULL DEFAULT 7,
ALGORITHM=INSTANT;
ALTER TABLE osc_native
ADD INDEX ix_amount (amount), ALGORITHM=INPLACE, LOCK=NONE;
SELECT id, amount, migration_tag FROM osc_native ORDER BY id;
SELECT INDEX_NAME, COLUMN_NAME
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'osc_native'
ORDER BY INDEX_NAME, SEQ_IN_INDEX;
DROP TABLE osc_native;
실행 결과(MySQL 8.0.x):
mysql> CREATE TABLE osc_native (
-> id BIGINT NOT NULL PRIMARY KEY,
-> amount INT NOT NULL
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO osc_native VALUES (1, 100), (2, 200);
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE osc_native
-> ADD COLUMN migration_tag INT NOT NULL DEFAULT 7,
-> ALGORITHM=INSTANT;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE osc_native
-> ADD INDEX ix_amount (amount), ALGORITHM=INPLACE, LOCK=NONE;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SELECT id, amount, migration_tag FROM osc_native ORDER BY id;
+----+--------+---------------+
| id | amount | migration_tag |
+----+--------+---------------+
| 1 | 100 | 7 |
| 2 | 200 | 7 |
+----+--------+---------------+
2 rows in set (0.00 sec)
mysql> SELECT INDEX_NAME, COLUMN_NAME
-> FROM information_schema.STATISTICS
-> WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'osc_native'
-> ORDER BY INDEX_NAME, SEQ_IN_INDEX;
+------------+-------------+
| INDEX_NAME | COLUMN_NAME |
+------------+-------------+
| ix_amount | amount |
| PRIMARY | id |
+------------+-------------+
2 rows in set (0.00 sec)
mysql> DROP TABLE osc_native;
Query OK, 0 rows affected (0.01 sec)
동일한 목표를 내장 DDL로 달성할 수 있다면, 외부 도구의 신규 테이블·추적 경로·이름 교체를 추가할 이유가 있는지 다시 묻는다. 단, 소규모 예제의 인덱스 생성 성공을 대형 테이블의 무부하 작업으로 일반화하지 않는다.
5. 복사율보다 먼저 정할 부하·중단 기준
두 도구의 청크 크기는 같은 숫자를 넣는다고 같은 부하를 뜻하지 않는다. 행 너비, 새 인덱스 수, 버퍼 풀 적중률, 스토리지 지연과 동시 쓰기 분포가 다르기 때문이다. 평상시 기준선을 확보하고 다음 관측값을 함께 사용한다.
- 업무 지표: 쓰기 응답 시간의 상위 백분위, 오류율, 잠금 대기, 커넥션 대기열.
- 서버 지표: CPU와 스토리지 지연,
Threads_running, redo·binlog 생성 추세, 가용 공간. - 복제 지표: 실제 서비스하는 각 복제본의 적용 지연과 오류. 감시 대상에 없는 복제본은 보호되지 않는다.
- 도구 지표: 행 복사 진척, 변경 적용 지연, throttle 원인, 재시도와 cut-over 대기 상태.
pt-osc의 --max-load와 --max-lag에 따른 복사 대기, --critical-load에 따른 중단을 구별한다. gh-ost도 --max-load, --max-lag-millis, --throttle-flag-file 등의 제어와 비상 중단 정책을 분리한다. 임계값은 인터넷 예제의 숫자를 복사하지 말고 정상 피크와 장애 징후 사이에 정의한다. Threads_running 하나가 낮다는 이유만으로 디스크 포화나 업무 지연을 무시하지 않는다.
gh-ost에서는 행 복사가 끝났어도 변경 적용이 밀려 있으면 전환 준비가 아니다. 장시간 throttle을 허용하려면 binlog 보존 기간과 스토리지 여유가 그 대기 시간 및 추적 재개 시간을 감당해야 한다. pt-osc에서는 복사를 멈춘 후에도 쓰기 지연이 유지될 수 있으므로, “일시 정지했으니 원래 부하로 돌아갈 것”이라는 운영 가정을 버려야 한다.
6. cut-over와 되돌리기를 별도 변경 작업으로 취급한다
복사 완료는 준비 단계의 종료이지 작업 전체의 성공이 아니다. 최종 교체에는 MDL이 필요하다. 오래 열린 트랜잭션이 대상 테이블을 사용했다면 교체가 기다릴 수 있고, 기다리는 배타적 잠금 요청 뒤에 새 업무 쿼리가 대기하면서 영향이 커질 수 있다.
전환 승인 전에는 최소한 다음 조건을 만족해야 한다.
- 변경 적용이 전환 가능한 상태이고, 도구 로그에서 지속적인 오류가 없다.
- 대상 테이블을 붙잡은 장기 트랜잭션과 MDL 대기 체인이 확인되었다.
- 원본과 새 테이블의 핵심 데이터 및 제약 의미가 확인되었다. 행 수만 같다는 검사는 값 변경을 놓친다.
- 새 스키마와 이전 애플리케이션이 함께 동작하는 배포 순서가 확정되었다.
- 잠금 획득 실패 시 대기 상한, 재시도 횟수, 취소 주체가 정해졌다.
gh-ost의 --postpone-cut-over-flag-file은 데이터 준비와 전환 승인을 분리하는 명시적 제어다. 복사를 멈추는 throttle 파일과 혼동하지 않는다. pt-osc에서도 도구의 훅과 실행 절차를 활용할 수 있지만, 사용 중인 버전에서 교체 직전 보류·취소가 어떻게 동작하는지 검증해야 한다. 긴급 상황에서 무작정 차단 세션을 종료하는 절차를 기본값으로 두지 않는다.
이전 테이블 보관은 곧바로 롤백을 뜻하지 않는다
전환 후 새 테이블에 들어온 업무 변경은 보관한 이전 테이블에 저절로 전달되지 않는다. 이름을 되돌리기만 하면 전환 이후의 데이터를 잃을 수 있다. 이전 테이블은 조사·비교·복구의 재료이지, 항상 최신인 대기 복제본이 아니다.
gh-ost 1.1.11에는 checkpoint 기반 --resume와 --revert가 문서화되어 있다. 그러나 처음부터 기록한 checkpoint와 필요한 binlog가 남아 있어야 하며, 역방향 스키마 변환이 데이터를 보존할 수 있어야 한다. 늘어난 문자열 길이를 다시 줄이는 변경처럼 새 데이터가 이전 스키마에 들어가지 않는 경우도 있다. 해당 기능을 검증하지 않은 작업에서 “프로세스를 다시 켜면 재개된다”거나 “이전 테이블이 있으므로 즉시 원복 가능하다”고 약속하지 않는다. pt-osc의 재개·역방향 추적 관련 기능도 설치 버전과 사전 구성에 따라 따로 시험한다.
7. Aurora MySQL에서는 연결 모델부터 다시 확인한다
Aurora의 같은 클러스터 reader는 Community MySQL의 일반적인 binlog 복제본과 동일한 실험 대상이 아니다. 클러스터 내부 복제와 외부 binlog 복제를 구별해야 하며, gh-ost가 이벤트를 읽는 위치와 실제 DDL·쓰기를 수행하는 writer를 명확하게 지정해야 한다. reader endpoint에 연결했다는 사실만으로 안전한 복제본 시험 모드가 성립하지 않는다.
또한 관리형 계정에는 일반 서버의 SUPER 권한을 그대로 부여할 수 없다. parameter group 변경과 적용 시점, binlog 활성화 및 보존, TLS 접속, 필요한 객체·복제 권한을 사전에 검증한다. pt-osc도 Aurora의 공유 스토리지 모델 때문에 트리거 비용이 없어지는 것은 아니다. writer 부하와 reader 지연을 함께 관측해야 하며, 일반 복제본 자동 탐색이 클러스터 내 reader를 모두 감시한다고 가정하지 않는다.
오래된 gh-ost RDS 문서의 aurora_enable_repl_bin_log_filtering=0 설정을 최신 Aurora에 그대로 옮기지 않는다. AWS 문서는 이 파라미터가 Aurora MySQL 3에서 제거되었고, 버전 3 및 8.4에서 binlog filtering이 항상 활성화된다고 설명한다. 오래된 우회책을 적용하는 대신 정확한 Aurora 버전·gh-ost 버전·writer 연결 방식으로 cut-over까지 검증해야 한다. 그 검증이 없다면 호환 여부를 미확정으로 두고 내장 DDL이나 다른 절차를 검토한다.
이 글의 SQL 결과는 로컬 Community MySQL에서 얻었다. Aurora의 failover, reader 지연, 권한 제한 또는 두 도구의 Aurora 실행 성공을 입증한 결과가 아니다.
8. 실행 승인 체크리스트
결론
pt-osc는 변경 반영을 업무 트랜잭션의 트리거에 결합하고, gh-ost는 binlog를 소비하는 별도 적용 경로로 분리한다. 이 차이가 쓰기 지연, 정지 효과, 추적 지연과 운영 제어 방식의 차이를 만든다. 어느 도구가 항상 더 빠르거나 안전하다는 결론보다는, 먼저 내장 DDL을 평가하고 구조적 제약으로 후보를 줄인 뒤 실제 업무 부하와 전환 실패 경로를 시험하는 것이 타당하다.
다음 단계에서는 도구 선택표를 넘어, 스키마 변경과 애플리케이션 배포의 호환 기간을 어떻게 설계할지 연결해서 살펴볼 필요가 있다. 데이터베이스의 이름 교체가 성공해도 애플리케이션이 새 구조를 받아들이지 못하면 변경 작업은 끝난 것이 아니다.