운영 중인 서비스에서 ALTER TABLE users ADD COLUMN ...을 그냥 실행했다가 수백만 행 테이블이 수 분간 잠기고, 그 사이 쌓인 커넥션이 풀을 고갈시켜 전체 장애로 번지는 일이 흔합니다. 데이터가 작을 땐 보이지 않던 문제가, 행이 늘면 그대로 사고가 됩니다. 무중단 스키마 변경은 이 잠금을 피해 서비스를 멈추지 않고 테이블 구조를 바꾸는 기법입니다.
왜 직접 ALTER가 위험한가
MySQL은 버전·스토리지 엔진에 따라 일부 변경을 온라인 DDL로 처리하지만, 인덱스 추가나 컬럼 타입 변경 등은 테이블을 통째로 복제(COPY)하며 메타데이터 잠금을 잡습니다. 그 사이 들어오는 쓰기는 대기하고, 대기 커넥션이 max_connections를 넘으면 읽기까지 막힙니다.
| 방식 | 동작 | 운영 영향 |
|---|---|---|
직접 ALTER (COPY) | 원본 테이블 잠그고 복제 | 변경 내내 쓰기 차단 |
온라인 DDL (INPLACE) | 일부 변경만 비차단 | 변경 종류·버전에 따라 제한적 |
| gh-ost / pt-osc | 그림자 테이블 + 점진 복사 | 거의 무중단, 제어 가능 |
그림자 테이블 방식의 원리
gh-ost와 pt-online-schema-change는 공통적으로 빈 그림자(shadow) 테이블을 새 구조로 만들고, 원본 데이터를 작은 청크로 복사하면서 그동안의 변경분을 따라잡은 뒤, 마지막에 테이블 이름을 원자적으로 바꿔치기합니다.
차이는 변경분을 잡는 방식입니다.
| 도구 | 변경 추적 | 부하 제어 |
|---|---|---|
| pt-osc (Percona) | 원본에 트리거를 걸어 동기 반영 | --max-load로 임계 시 일시정지 |
| gh-ost (GitHub) | 바이너리 로그를 읽어 비동기 반영 | 복제 지연·부하 보고 자동 throttle |
트리거 방식인 pt-osc는 쓰기마다 트리거가 같이 실행돼 쓰기 부하가 큰 테이블에서 부담이 됩니다. gh-ost는 트리거 없이 binlog를 소비하므로 원본 쓰기 경로에 영향이 적어, 부하가 큰 운영 환경에서 선호됩니다.
적용 단계 (gh-ost 예시)
- 먼저 위험 없이 동작만 보는
--noop로 검증합니다.
gh-ost \
--host=db-primary --database=shop --table=orders \
--alter="ADD COLUMN coupon_id BIGINT NULL" \
--noop
- 실제 실행은
--execute로 전환하고, 컷오버(이름 바꿔치기)는 수동 트리거로 묶어 트래픽 한가한 시점에 끝냅니다.
gh-ost ... --execute \
--max-load=Threads_running=40 \
--postpone-cut-over-flag-file=/tmp/ghost.postpone
- 진행률과 복제 지연을 보며
--max-load임계를 넘으면 자동으로 멈추는지 확인합니다. 안전하면 postpone 파일을 지워 컷오버를 마칩니다.
pt-osc도 흐름은 같고, 항상 --dry-run으로 먼저 확인한 뒤 --execute로 바꿉니다.
요점 정리
- 큰 운영 테이블에 직접
ALTER는 잠금·커넥션 고갈로 장애가 된다. - gh-ost·pt-osc는 그림자 테이블에 점진 복사 후 원자적 이름 교체로 무중단을 만든다.
- 쓰기 부하가 큰 테이블은 트리거 없는 gh-ost가 유리하고, 항상
--noop/--dry-run검증을 먼저 한다. - 컷오버는 트래픽 한산한 시점에 수동으로 묶는다.
운영 DB에서 스키마를 안전하게 바꾸는 절차를 직접 실습해 보려면 데이터베이스 트랙에서 회원가입 없이 무료로 시작할 수 있습니다.