pt-online-schema-change 원리: 트리거(trigger), 섀도 테이블, 청크 복사
pt-online-schema-change의 변경 동기화와 청크 복사 원리, 테이블 교체 시 잠금, 외래 키·복제·Aurora 운영 주의점을 검증 예제로 설명한다.
대형 테이블의 스키마 변경에서 중요한 질문은 단순히 “쓰기 요청을 받을 수 있는가”가 아니다. 변경 중 발생하는 쓰기를 어디에 반영하고, 기존 데이터를 어떤 단위로 옮기며, 마지막에 어떤 잠금을 확보해야 하는지가 서비스 영향을 결정한다. pt-online-schema-change는 이 문제를 변경된 구조의 새 테이블, 원본의 트리거, 작은 범위의 데이터 복사, 마지막 이름 교체로 해결하는 Percona Toolkit 도구다.
온라인이라는 이름은 무잠금·무부하·무위험을 뜻하지 않는다. 특히 청크 복사를 멈추더라도 이미 설치한 트리거는 애플리케이션 쓰기 경로에 남는다. 앞선 Online DDL 및 대형 ALTER 사전 점검 내용을 바탕으로, 이 글에서는 도구의 작동 원리와 운영 판단 기준을 연결한다.
적용 범위와 검증 범위: MySQL 8.0 이상 InnoDB를 중심으로 설명한다. SQL 예제는 MySQL 8.0.46에서 실행했고, 별도 폐기용 인스턴스에서 pt-online-schema-change 3.7.1-4의 실제 복사·동시 변경·교체도 검증했다. MySQL 8.4 및 Aurora에서의 실행, 외래 키 재구성, 장애 시 재개, 부하 임계치에 의한 중단은 이번 실험 범위가 아니다. 해당 항목은 공식 문서의 동작과 운영 주의사항으로 구분한다.
1. 네이티브 DDL과 외부 복사 도구의 경계
먼저 실제 ALTER가 ALGORITHM=INSTANT로 처리되는지 확인한다. 메타데이터 변경만으로 끝낼 수 있는 작업에 전체 행을 복사하는 도구를 투입할 이유는 적다. 네이티브 INPLACE가 허용하는 동시성, 인덱스 생성 비용, 재구축 여부도 함께 비교한다. 일반적인 보조 인덱스 추가는 네이티브 온라인 DDL로 가능한 경우가 많으며, 이 글의 인덱스 추가 실험은 도구의 작동을 관찰하기 위한 것이다.
| 구분 | 네이티브 온라인 DDL | pt-online-schema-change |
|---|---|---|
| 변경 수행 주체 | MySQL 서버의 DDL 구현 | 외부 클라이언트와 서버의 일반 DDL·DML |
| 변경 중 쓰기 처리 | 작업별 InnoDB 온라인 DDL 메커니즘 | 원본 테이블의 동기식 트리거 |
| 데이터 이동 | 알고리즘에 따라 복사·재구축 여부가 다름 | 변경된 새 테이블로 행 복사 |
| 속도 조절 | 서버·작업별 제약에 따름 | 청크 크기·시간 목표·부하·복제 지연 점검 |
| 완료 경계 | 서버 내부 DDL 완료 | 일반 경로에서 원본·새 테이블의 이름 교체 |
외부 도구의 장점은 부하를 관측하면서 복사 속도를 조절할 수 있다는 점이다. 대가는 전체 데이터 재기록, 트리거에 의한 추가 쓰기, 더 복잡한 객체 수명 관리다. “네이티브보다 항상 빠르다”는 선택 기준은 성립하지 않는다.
2. 섀도 테이블을 만들고 변경을 따라잡는 과정
섀도 테이블은 임시로 사용할 일반 영속 테이블이다. 세션 종료와 함께 사라지는 TEMPORARY TABLE이 아니다. 실제 이름은 충돌 회피 등에 따라 달라질 수 있지만, 원본이 orders라면 _orders_new와 같은 이름을 사용한다.
- 원본의 정의를 바탕으로 새 테이블을 생성한다.
- 행이 아직 없는 새 테이블에 요청된 ALTER를 적용한다.
- 원본에 INSERT·UPDATE·DELETE용 트리거를 설치한다.
- 원본 행을 인덱스 범위별로 새 테이블에 복사한다.
- 복사와 필요한 준비가 끝나면 원본과 새 테이블의 이름을 교체한다.
- 정책에 따라 기존 테이블과 도구가 만든 트리거를 정리한다.
flowchart TD
A[원본 정의와 변경 가능성 점검] --> B[섀도 테이블 생성 및 ALTER]
B --> C[원본에 변경 동기화 트리거 설치]
C --> D[인덱스 범위별 청크 복사]
W[애플리케이션 쓰기] --> O[원본 테이블]
O --> T[동일 트랜잭션의 트리거 실행]
T --> N[섀도 테이블 반영]
D --> N
D --> L{부하와 복제 지연 점검}
L -->|계속| D
L -->|복사 완료| M[교체에 필요한 MDL 확보]
M --> R[원자적 이름 교체]
R --> V[데이터 및 업무 검증 후 정리]
그림의 이름 교체는 외래 키로 인한 특수 처리가 없는 일반 경로다. 외래 키의 drop_swap 방식에는 같은 원자적 교체 모델을 적용하면 안 된다.
트리거는 비동기 변경 큐가 아니다
도구가 원본에 설치한 AFTER 트리거는 애플리케이션 문장의 일부로 실행된다. InnoDB 원본과 섀도 테이블의 변경은 같은 트랜잭션에 포함되므로, 업무 UPDATE가 롤백되면 트리거가 수행한 변경도 롤백된다. 별도 프로세스가 binlog를 읽고 나중에 따라잡는 구조와 다르다.
실제 검증 버전의 INSERT 트리거는 새 행을 섀도 테이블에 REPLACE했고, DELETE 트리거는 원본의 이전 키로 섀도 행을 삭제했다. UPDATE 트리거는 키 변경 시 이전 키의 행을 제거한 뒤 새 행을 반영했다. 따라서 행을 확실하게 식별할 수 있는 PRIMARY KEY 또는 적합한 UNIQUE 인덱스가 중요하다. 키가 없거나 변경으로 키 의미가 달라지는 테이블은 별도 검토 대상이다.
이 동기성 때문에 섀도 테이블의 인덱스 갱신과 행 잠금 비용이 애플리케이션 응답 시간에 포함된다. 새 제약 조건이 기존 쓰기와 충돌하거나 트리거 경로에서 처리되지 않은 오류가 발생하면 원본의 업무 문장도 실패할 수 있다. 도구 전용 접속만 관측해서는 이러한 영향을 놓친다.
3. 청크 복사와 동시 변경은 어떻게 공존하는가
청크는 “일정 개수의 행을 무조건 같은 시간에 처리하는 단위”가 아니라, 인덱스 키로 경계를 정한 작은 복사 작업이다. 도구는 범위 경계를 찾고 해당 행들을 INSERT ... SELECT 계열 문장으로 복사한다. 실제 검증 로그에서는 INSERT LOW_PRIORITY IGNORE ... SELECT ... LOCK IN SHARE MODE가 관찰됐다.
동시에 트리거가 먼저 섀도 행을 만들 수 있으므로, 복사는 이미 존재하는 키를 고려해야 한다. INSERT IGNORE는 복사 과정의 키 충돌에서 기존 섀도 행을 덮어쓰지 않게 한다. 다만 IGNORE 하나만으로 모든 동시성 정합성이 보장되는 것은 아니다. 원본 읽기의 잠금, 트리거의 쓰기, 키 식별, 트랜잭션 순서가 함께 작용한다. 이를 단순한 비잠금 SELECT와 임의의 upsert로 다시 구현해서는 안 된다.
--chunk-time은 다음 청크의 실행 시간 목표다. 복사 속도를 관측해 청크 크기를 조정하며, 문장을 해당 시간에 강제로 취소하는 제한 시간이 아니다.--chunk-size와--chunk-time=0을 함께 지정하면 자동 크기 조절을 끈 고정 행 수 목표로 관찰하기 쉽다. 실제 행 수는 키 분포와 경계에 영향을 받는다.--max-load초과는 복사를 일시 정지시키는 조건이고,--critical-load초과는 작업을 중단시키는 조건이다. 두 옵션을 같은 의미로 해석하지 않는다.- 복사가 정지한 동안에도 업무 쓰기에 따른 트리거 동기화는 계속된다. 트리거 자체가 부하의 주원인이라면 일시 정지만으로 회복되지 않을 수 있다.
PRIMARY KEY가 희소하거나 행 크기가 불균일하면 같은 행 수라도 읽기·쓰기 비용이 달라진다. 복사 진행률과 함께 업무 지연, 처리량, 행 잠금 대기, redo 발생, 저장 공간, 복제 지연을 같은 시간축에서 보아야 한다.
4. SQL로 확인하는 축소 모델
아래 예제는 빈 실습 스키마에서 순서대로 실행한다. 실제 도구가 생성하는 SQL을 복제한 구현이 아니라, 동기식 트리거·부분 복사·롤백·이름 교체의 관계를 확인하는 교육용 모델이다. 단순화를 위해 PK 변경은 하지 않으며, 외래 키·기존 업무 트리거·동시 다중 세션 경합 처리는 포함하지 않는다. 실제 도구의 동시 변경 검증은 다음 절에서 별도로 다룬다.
4.1 새 구조와 트리거 준비
섀도 테이블에는 인덱스를 먼저 추가한다. 원본의 첫 두 행만 복사하여, 일부 행은 복사됐고 나머지는 아직 옮겨지지 않은 상태를 만든다.
CREATE TABLE osc_source (
id INT PRIMARY KEY,
value_num INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO osc_source VALUES (1,100),(2,100),(3,100),(4,100);
CREATE TABLE osc_shadow LIKE osc_source;
ALTER TABLE osc_shadow ADD INDEX idx_value (value_num);
CREATE TRIGGER osc_ins AFTER INSERT ON osc_source FOR EACH ROW
REPLACE INTO osc_shadow VALUES (NEW.id, NEW.value_num);
CREATE TRIGGER osc_upd AFTER UPDATE ON osc_source FOR EACH ROW
REPLACE INTO osc_shadow VALUES (NEW.id, NEW.value_num);
CREATE TRIGGER osc_del AFTER DELETE ON osc_source FOR EACH ROW
DELETE FROM osc_shadow WHERE id = OLD.id;
INSERT IGNORE INTO osc_shadow
SELECT * FROM osc_source WHERE id BETWEEN 1 AND 2 LOCK IN SHARE MODE;
SELECT id, value_num FROM osc_shadow ORDER BY id;
실행 결과(MySQL 8.0.x):
mysql> CREATE TABLE osc_source (
-> id INT PRIMARY KEY,
-> value_num INT NOT NULL
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO osc_source VALUES (1,100),(2,100),(3,100),(4,100);
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> CREATE TABLE osc_shadow LIKE osc_source;
Query OK, 0 rows affected (0.00 sec)
mysql> ALTER TABLE osc_shadow ADD INDEX idx_value (value_num);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> CREATE TRIGGER osc_ins AFTER INSERT ON osc_source FOR EACH ROW
-> REPLACE INTO osc_shadow VALUES (NEW.id, NEW.value_num);
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TRIGGER osc_upd AFTER UPDATE ON osc_source FOR EACH ROW
-> REPLACE INTO osc_shadow VALUES (NEW.id, NEW.value_num);
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TRIGGER osc_del AFTER DELETE ON osc_source FOR EACH ROW
-> DELETE FROM osc_shadow WHERE id = OLD.id;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT IGNORE INTO osc_shadow
-> SELECT * FROM osc_source WHERE id BETWEEN 1 AND 2 LOCK IN SHARE MODE;
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> SELECT id, value_num FROM osc_shadow ORDER BY id;
+----+-----------+
| id | value_num |
+----+-----------+
| 1 | 100 |
| 2 | 100 |
+----+-----------+
2 rows in set (0.00 sec)
4.2 복사 전후 변경과 롤백 확인
이미 복사된 행을 UPDATE·DELETE하고, 아직 복사하지 않은 행도 UPDATE한다. 새 행 INSERT와 롤백되는 UPDATE를 섞은 뒤 남은 범위를 복사한다. 두 방향의 누락과 값 불일치를 함께 확인해야 단순 행 수 비교보다 강한 검증이 된다.
UPDATE osc_source SET value_num=777 WHERE id=1;
DELETE FROM osc_source WHERE id=2;
UPDATE osc_source SET value_num=333 WHERE id=3;
INSERT INTO osc_source VALUES (5,888);
START TRANSACTION;
UPDATE osc_source SET value_num=999 WHERE id=4;
ROLLBACK;
INSERT IGNORE INTO osc_shadow
SELECT * FROM osc_source WHERE id BETWEEN 3 AND 5 LOCK IN SHARE MODE;
SELECT id, value_num FROM osc_shadow ORDER BY id;
SELECT
(SELECT COUNT(*) FROM osc_source) AS source_rows,
(SELECT COUNT(*) FROM osc_shadow) AS shadow_rows,
(SELECT COUNT(*) FROM osc_source s LEFT JOIN osc_shadow n USING(id)
WHERE n.id IS NULL OR NOT (s.value_num <=> n.value_num)) AS missing_or_changed,
(SELECT COUNT(*) FROM osc_shadow n LEFT JOIN osc_source s USING(id)
WHERE s.id IS NULL) AS extra_rows;
실행 결과(MySQL 8.0.x):
반복되는 DML 성공 출력과 긴 SELECT 원문은 줄이고, 롤백·복사·대조 결과를 발췌했다.
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT IGNORE INTO osc_shadow
-> SELECT * FROM osc_source WHERE id BETWEEN 3 AND 5 LOCK IN SHARE MODE;
Query OK, 1 row affected, 2 warnings (0.00 sec)
Records: 3 Duplicates: 2 Warnings: 2
mysql> SELECT id, value_num FROM osc_shadow ORDER BY id;
+----+-----------+
| id | value_num |
+----+-----------+
| 1 | 777 |
| 3 | 333 |
| 4 | 100 |
| 5 | 888 |
+----+-----------+
4 rows in set (0.00 sec)
mysql> SELECT ...;
+-------------+-------------+--------------------+------------+
| source_rows | shadow_rows | missing_or_changed | extra_rows |
+-------------+-------------+--------------------+------------+
| 4 | 4 | 0 | 0 |
+-------------+-------------+--------------------+------------+
1 row in set (0.00 sec)
id=4는 999가 아니라 100으로 남아야 한다. id=3과 id=5는 트리거가 먼저 반영한 상태이며, 마지막 복사에서 중복 키를 무시해 유지된다. 이때 중복 경고가 발생하는 것은 이 예제에서 의도한 동작이다. 데이터 변환이나 새 UNIQUE 제약으로 인한 예기치 않은 누락까지 정상으로 간주하라는 뜻은 아니다.
4.3 이름 교체와 원본 보관의 한계
실습에서는 동시 접속이 없는 상태에서 이름을 교체하고 도구 역할의 트리거를 제거한다. 새 테이블에 쓰기가 시작된 뒤에는 보관한 이전 테이블이 자동으로 최신 상태를 유지하지 않는다는 점도 확인한다.
RENAME TABLE osc_source TO osc_old, osc_shadow TO osc_source;
DROP TRIGGER osc_ins;
DROP TRIGGER osc_upd;
DROP TRIGGER osc_del;
UPDATE osc_source SET value_num=123 WHERE id=1;
SELECT n.id, n.value_num AS current_value, o.value_num AS old_value
FROM osc_source n JOIN osc_old o USING(id) WHERE n.id=1;
SELECT INDEX_NAME, COLUMN_NAME
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='osc_source'
ORDER BY INDEX_NAME, SEQ_IN_INDEX;
DROP TABLE osc_source, osc_old;
실행 결과(MySQL 8.0.x):
mysql> RENAME TABLE osc_source TO osc_old, osc_shadow TO osc_source;
Query OK, 0 rows affected (0.01 sec)
mysql> DROP TRIGGER osc_ins;
Query OK, 0 rows affected (0.00 sec)
mysql> DROP TRIGGER osc_upd;
Query OK, 0 rows affected (0.00 sec)
mysql> DROP TRIGGER osc_del;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE osc_source SET value_num=123 WHERE id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT n.id, n.value_num AS current_value, o.value_num AS old_value
-> FROM osc_source n JOIN osc_old o USING(id) WHERE n.id=1;
+----+---------------+-----------+
| id | current_value | old_value |
+----+---------------+-----------+
| 1 | 123 | 777 |
+----+---------------+-----------+
1 row in set (0.00 sec)
mysql> SELECT INDEX_NAME, COLUMN_NAME
-> FROM information_schema.STATISTICS
-> WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='osc_source'
-> ORDER BY INDEX_NAME, SEQ_IN_INDEX;
+------------+-------------+
| INDEX_NAME | COLUMN_NAME |
+------------+-------------+
| idx_value | value_num |
| PRIMARY | id |
+------------+-------------+
2 rows in set (0.01 sec)
mysql> DROP TABLE osc_source, osc_old;
Query OK, 0 rows affected (0.00 sec)
이전 테이블의 777과 현재 테이블의 123은 서로 다른 업무 상태다. 테이블 이름만 되돌리면 새 쓰기를 잃을 수 있다. 이전 테이블 보관은 조사·복구 재료를 남기는 선택이지, 그 자체로 롤백 계획이 아니다.
5. 실제 도구의 청크 복사 중 변경 검증
축소 SQL 모델만으로 실제 도구의 동시성 처리를 입증할 수는 없다. 별도 실험에서는 PK 1~20,000과 value_num=100인 행을 준비하고 ANALYZE TABLE로 통계를 갱신한 뒤, 실제 도구로 보조 인덱스를 추가했다. 통계가 갱신되지 않은 최초 시험에서는 도구가 테이블을 매우 작게 추정하여 한 번에 복사했으므로, 청크 사이에 변경을 넣는 검증으로 인정하지 않았다.
실행 옵션의 구조는 다음과 같다. 아래는 실험 재구성을 위한 명령 템플릿이며, 접속 대상과 권한은 환경에 맞게 준비해야 한다. 원문 실험은 폐기용 인스턴스에서 실행했고 공개 템플릿에는 인증 정보를 싣지 않았다. 옵션의 수치는 운영 권장 임계치가 아니다.
pt-online-schema-change \
--host=127.0.0.1 --port=3306 --user=osc_lab --ask-pass \
--mysql_ssl=1 \
--alter='ADD INDEX idx_value (value_num)' \
--chunk-size=200 --chunk-time=0 --sleep=0.1 \
--recursion-method=none --no-version-check \
--no-drop-old-table --print --statistics \
--dry-run D=osc_lab,t=osc_live
실제 변경 시험에서는 위와 같은 옵션 조합에서 --dry-run을 --execute로 바꿔 실행했다. --dry-run은 새 테이블 생성과 ALTER까지 수행하지만, 트리거 설치·행 복사·원본 교체는 하지 않는다. 따라서 읽기 전용 점검도 아니며 전체 변경 리허설을 대신하지도 못한다.
--recursion-method=none은 복제본 없는 단일 서버 실험에 한해 사용했다. 운영에서 이를 그대로 복사하면 자동 복제본 발견에 기반한 지연 점검을 기대할 수 없다. 또한 --mysql_ssl=1로 TLS를 사용한 이번 연결 시험은 CA와 서버 이름의 신원 검증까지 입증한 것이 아니다. 운영 인증에서는 드라이버의 TLS 검증 설정도 별도로 확인한다.
실제 트리거 세 개가 설치되고 섀도 테이블에 600행이 들어간 시점에 별도 접속으로 다음 변경을 수행했다. 600은 한 번의 관측값이며 고정된 실행 경계가 아니다.
| 변경 | 실행 후 확인한 상태 |
|---|---|
id=1의 값을 777로 변경 |
교체 후 777 유지 |
id=2 삭제 |
교체 후에도 행 없음 |
PK id=3을 id=30003으로 변경 |
이전 키 없음, 새 키의 값 100 |
id=20001, value_num=888 삽입 |
교체 후 신규 행 존재 |
id=4를 999로 변경 후 ROLLBACK |
교체 후 100 유지 |
쓰기 접속의 변경을 끝낸 뒤 얻은 원본 전체 (id, value_num) 정렬 결과와, 도구 종료 후 새 테이블의 전체 결과를 비교하여 20,000행의 키와 값이 모두 일치함을 확인했다. 단순 COUNT나 일부 표본만 비교한 결과가 아니다. 인덱스 메타데이터에도 idx_value(value_num)가 존재했고 dry-run·실제 실행의 종료 코드는 모두 0이었다.
교체 후 핵심 행 조회의 실제 결과를 행 단위로 발췌하면 다음과 같다. 삭제한 2와 이전 PK 3은 결과에 없다.
id value_num
1 777
4 100
20001 888
30003 100
--no-drop-old-table로 남긴 이전 테이블도 교체 직후에는 같은 전체 데이터를 보유했다. 이후 새 테이블의 id=1만 123으로 변경하자 이전 테이블은 777로 남았다. 이 실험은 일반적인 단방향 동기화 경로의 한계를 보여준다. 별도 역방향 트리거나 별도 복구 설계를 검증한 결과가 아니며, 고부하·장시간 트랜잭션·네트워크 단절에서도 동일하게 안전하다는 보장도 아니다.
6. 마지막 이름 교체가 장애 지점이 되는 이유
일반 경로의 RENAME TABLE은 두 이름을 원자적으로 교체하지만, 원자성은 즉시 잠금 획득을 뜻하지 않는다. 기존 트랜잭션이 원본의 metadata lock(MDL)을 보유하면 교체가 대기할 수 있다. 트리거 생성·정리 같은 DDL 단계도 잠금 영향을 받는다.
특히 오래 열린 트랜잭션이 원본을 읽고 COMMIT하지 않은 상태를 확인해야 한다. 배타적 MDL 요청이 대기하면 뒤따르는 요청까지 대기열에 영향을 받을 수 있다. 복사 진행률이 100%에 가까워도 서비스 지연이 커질 수 있는 이유다.
운영 진단은 performance_schema.metadata_locks의 해당 테이블, performance_schema.threads의 접속 정보, information_schema.INNODB_TRX의 트랜잭션 시작 시각을 연결하는 방향으로 수행한다. 잠금 instrumentation과 조회 권한을 사전에 확인하며, 테이블에 대기 행이 없다는 사실을 “앞으로도 잠금이 없다”로 해석하지 않는다.
교체 단계의 대기 시간, 업무 지연, 취소 기준과 담당자를 사전에 정한다. 장기 트랜잭션을 일괄 종료하거나 재시도 횟수를 무작정 늘리는 것은 해결책이 아니다. 취소 시점이 교체 전인지 후인지 확인하지 않고 섀도·이전 테이블을 삭제해서도 안 된다.
7. 안전 검사를 우회하면 안 되는 경우
UNIQUE 추가와 데이터 손실 위험
기존 데이터에 중복이 있는 상태에서 UNIQUE 인덱스를 가진 섀도 테이블로 복사하면 INSERT IGNORE 때문에 충돌한 행이 누락될 수 있다. 도구의 UNIQUE 변경 검사는 이를 방지하기 위한 장치다. 사전 중복 조회에서 문제가 없었더라도 복사 중 새 중복이 생길 수 있으므로, 한 번의 조회만으로 안전 검사를 끄지 않는다. 애플리케이션의 쓰기 제약과 변경 순서를 함께 설계한다.
외래 키는 부모 이름 변경만으로 해결되지 않는다
자식 테이블이 참조하는 부모를 새 객체로 교체하면 외래 키의 참조 대상도 고려해야 한다. --alter-foreign-keys-method=rebuild_constraints는 자식의 외래 키를 재정의하므로 자식 테이블의 ALTER 비용과 잠금까지 검토해야 한다. auto는 비용 추정에 따른 선택이지 무위험 옵션이 아니다.
drop_swap은 원본을 먼저 삭제하고 새 테이블을 이름 변경하는 경로다. 잠깐 테이블이 존재하지 않는 구간과, 원본 삭제 후 이름 변경 실패 위험이 있다. 이를 일반적인 원자적 교체와 같은 수준의 중단 가능성으로 승인하면 안 된다.
기존 트리거와 쓰기 증폭
현재 도구에는 --preserve-triggers가 있지만 기존 트리거가 있는 모든 테이블이 자동으로 안전한 것은 아니다. 삭제할 열을 업무 트리거가 참조하는지, 트리거 복사·재생성 권한이 있는지, 감사 테이블에 대한 동작이 보존되는지 검증한다. 옵션 간 조합 제약도 있다. “기존 트리거가 있으면 언제나 불가능”과 “옵션 하나면 아무 문제 없다”는 설명 모두 부정확하다.
섀도 테이블 전체와 그 인덱스, 변경에 따른 redo·undo, binlog를 사용하는 환경의 로그·복제 부하를 고려한다. 이전 테이블을 보관하면 교체 후에도 공간을 점유한다. 단순히 데이터 파일 크기의 두 배라는 고정 수치로 여유 공간을 승인하지 않는다.
8. 복제 환경과 Aurora MySQL에서의 적용
Community MySQL 복제에서는 원본의 변경 외에도 복사와 섀도 테이블 작업이 복제 부담을 늘릴 수 있다. 복제본을 실제로 발견했는지, 필요한 채널과 지연 지표를 관측하는지 로그로 확인해야 한다. “복제본을 찾지 못했다”는 출력은 복제 지연이 0이라는 증거가 아니다. 복제 필터로 일부 테이블이나 스키마가 제외되는 환경도 별도 검토 대상이다.
Aurora MySQL 3 및 8.4에서는 지원되는 작업의 ALGORITHM=INSTANT 가능 여부부터 검토한다. Aurora MySQL 2의 Fast DDL과 이후 계열의 Instant DDL을 동일 기능·동일 제약으로 혼용하지 않는다.
Aurora에 외부 도구를 적용할 때도 원본과 섀도 객체, 트리거 실행, MDL이라는 SQL 계층의 비용은 사라지지 않는다. writer 접속, 필요한 DDL·TRIGGER 권한, TLS, 장애 조치 중 접속 단절을 확인하고 대상 엔진 버전에서 별도 리허설한다. Aurora reader의 저장·복제 구조를 Community binlog replica와 같다고 가정해 자동 발견이나 --max-lag만 신뢰하지 않는다. AuroraReplicaLag 등 해당 환경의 지표와 업무 읽기 지연을 별도로 관측한다. 이번 로컬 검증은 Aurora 성능이나 failover 안전성을 입증하지 않는다.
9. 실행 승인과 종료 체크리스트
결론
pt-online-schema-change는 긴 변경 작업을 짧은 복사와 지속적인 변경 동기화로 나누는 도구다. 핵심은 복사 속도만이 아니라 트리거가 업무 트랜잭션에 들어온다는 점, 청크와 동시 변경의 순서, 마지막 MDL과 교체 경계에 있다. 종료 코드 0과 복사율 100%에 더해 데이터·객체·업무 경로를 확인해야 변경을 완료했다고 판단할 수 있다.
이 원리를 이해하면 이후 binlog 기반 온라인 스키마 변경 도구를 검토할 때도 변경 캡처 위치, 동기화 지연, 교체 조건, 장애 복구 상태를 같은 기준으로 비교할 수 있다.
참고 문서
- Percona Toolkit: pt-online-schema-change — 트리거·청크·dry-run·안전 검사·외래 키 옵션
- AWS: Aurora의 Fast DDL 및 Instant DDL — 엔진 계열별 지원 범위