V2 DB 무중단 마이그레이션 설계 - 100-hours-a-week/KTB4-3rd-CLOUD GitHub Wiki
V2 Prod 배포 전에 V1 Prod EC2의 MySQL 컨테이너 데이터를 RDS for MySQL(Single-AZ)로 옮겨야 한다. 이 문서는 V1 DB의 현재 상태를 정리하고, 공식 문서와 공개 벤치마크를 근거로 이관 방식을 비교해 쓰기 중단 시간을 최소화하는 이관 방식을 선택한다. V1 Prod MySQL 실측(2026-10-14) 결과 데이터가 1 MB 미만이어서, 데이터 이동보다 전환 순서와 검증이 쓰기 중단 시간을 결정한다. 데이터베이스 토폴로지(RDS Single-AZ) 선택 근거는 V2 DB 인프라 설계를 따른다.
V1 Prod는 t3.medium(2 vCPU / 4 GiB) EC2 1대에서 Nginx·Next.js·Spring Boot·MySQL을 Docker Compose로 함께 실행한다. MySQL은 같은 호스트의 다른 컨테이너와 CPU·메모리·디스크를 나눠 쓴다.
| 항목 | 현재 값 | 출처 |
|---|---|---|
| 실행 형태 |
mysql:8.4 컨테이너 1개, restart: unless-stopped
|
deploy/v1-compose.yaml |
| 컨테이너 자원 | CPU 0.50, 메모리 768 MB(swap 없음) | deploy/v1-compose.yaml |
| 호스트 | Prod t3.medium, root 30 GiB + data EBS gp3 30 GiB | infra/locals.tf |
| 데이터 위치 | data EBS에 Docker data-root와 mysql-data named volume을 둔다 |
infra/storage.tf, infra/user_data.sh.tftpl
|
| 스키마·계정 | DB moyeota, 앱 계정 moyeota(MYSQL_*는 볼륨이 빈 최초 기동 때만 적용) |
deploy/env/mysql.env.example |
| 앱 연결 | backend가 DB_HOST=mysql, DB_PORT=3306을 Compose에서 고정 주입받고, HikariCP 풀은 10 |
deploy/v1-compose.yaml, V1 Docker Compose 구조의 한계 분석
|
| 설정 | V1 값 | 이관 영향 |
|---|---|---|
| 엔진 버전 |
8.4.11 (실측. 이미지 태그는 mysql:8.4로 마이너 버전 미고정) |
RDS도 8.4.11로 만든다. RDS for MySQL은 2026-08-24부터 8.4.11을 지원한다(AWS 발표). MySQL은 새 버전에서 옛 버전으로 가는 복제를 "일반적으로 지원하지 않는다"고 명시하므로(MySQL 8.4 복제 호환성), 소스보다 낮은 마이너 버전을 쓰면 안 된다. 이미지 태그가 고정돼 있지 않아 V1 컨테이너가 재생성되면 버전이 올라갈 수 있으므로, 이관 전에 다시 확인한다. |
| 문자셋 |
utf8mb4 / utf8mb4_0900_ai_ci
|
RDS 파라미터 그룹의 character_set_server, collation_server를 같게 맞춘다. |
| 시간대 |
--default-time-zone=+09:00, 컨테이너 TZ=Asia/Seoul
|
RDS 기본값은 UTC다. 파라미터 그룹에 time_zone=Asia/Seoul을 넣지 않으면 NOW()·CURRENT_TIMESTAMP 결과가 9시간 어긋난다. |
| buffer pool | innodb_buffer_pool_size=384M |
RDS는 인스턴스 클래스 메모리 비율로 기본값을 정한다. 이관 자체에는 영향이 없다. |
max_connections |
151 | 이관 도구(병렬 스레드)와 앱 풀을 합쳐도 여유가 있다. |
| binlog | 실측: log_bin=ON, binlog_format=ROW, binlog_row_image=FULL, server_id=1, gtid_mode=OFF, enforce_gtid_consistency=OFF, binlog_expire_logs_seconds=2592000(30일) |
별도 설정 없이 8.4 기본값으로 동작한다. 재기동 없이 binlog 복제 소스로 쓸 수 있다(부록 A). GTID는 꺼져 있어서 GTID 기반 복제(R2)를 쓰려면 단계적으로 켜면서 재기동해야 한다. |
| 인증 플러그인 | 8.4 기본 caching_sha2_password
|
RDS가 외부 소스에 복제 접속할 때 TLS가 필요하다(ssl_encryption=1). AWS 복제 프로시저
|
| 지표 | 값 | 이관에 주는 의미 |
|---|---|---|
| 실측 데이터 크기 | 16개 테이블, 약 0.7 MB(반올림 값 합계, 1 MB 미만), 행 약 830건 | 초기 적재가 수 초 안에 끝나는 규모다. 이관 방식의 우열을 바꾸는 가장 큰 변수다(1.6, 5장). |
| V1 설계 피크 | 조회 1.25 RPS, 쓰기 0.27 RPS | 실제 쓰기량이 매우 적다. 복제를 걸면 지연(lag)이 짧게 유지되고, 전환 때 따라잡는 데 걸리는 시간도 짧다. |
| 실측 조회 한계 | detail 조회 274.5 RPS에서 MySQL CPU 0.105 cores(21%) | 덤프를 뜨면서 생기는 읽기 부하를 견딜 여유가 있다. 그래도 덤프는 저트래픽 시간대에 실행한다. |
| 실측 쓰기 병목 | 채팅 저장 47.4 msg/s에서 HikariCP 10/10, 커넥션 획득 p95 445 ms | 병목은 DB CPU가 아니라 앱의 커넥션 점유다. 이관 중에 앱 풀을 늘리지 않는다. |
수치 출처는 V2 DB 인프라 설계와 V1 Docker Compose 구조의 한계 분석이다.
- MySQL은
internal: true인database네트워크에만 연결돼 있고 호스트 포트를 게시하지 않는다. 따라서 현재 상태로는 EC2 밖에서 V1 MySQL에 접속할 수 없다. - V2는 V1 VPC(
10.20.0.0/16)를 공유하고, RDS용 data 서브넷을 같은 VPC 안에 만든다(v2/network.tf). V1 EC2와 RDS는 VPC 로컬 라우팅으로 통신할 수 있으므로, 퍼블릭 공개나 VPN이 필요하지 않다. - 선택한 방식(6장)에서는 V1 MySQL을 노출하지 않는다. 덤프는 EC2 안에서 실행하고, V1 EC2에서 RDS로 나가는 3306 연결만 RDS SG에서 허용한다.
- binlog 복제(부록 A)로 바꿀 때만 MySQL 3306을 EC2 사설 IP에만 게시하고, V1 Prod SG의 3306 인바운드는 RDS SG에서 오는 것만 허용한다.
0.0.0.0바인딩과 CIDR 개방은 금지한다. Dev에 적용한 루프백 게시 원칙(MySQL SSM 접근 구성 스프린트 백로그의 CLOUD-DB-05)과 같은 취지다.
- S3 backup 버킷의
moyeota-db-backups/prod/prefix와 보존기간(backup_retention_days), EC2 업로드 IAM 권한은 Terraform으로 만들어져 있다(infra/storage.tf,infra/iam.tf). - 마일스톤 문서에는
backup-mysql.sh와 "Prod 배포 전 MySQL backup이 S3에 생성된다"는 완료 조건이 있다. 하지만 스크립트는 레포에 없어서 실제 백업이 주기적으로 실행되는지 확인되지 않았다. 이관 전에 최신 백업 객체가 있는지 확인하고, 이관 직전에 수동 백업을 하나 더 남긴다.
V1 Prod MySQL에서 직접 쿼리해 확인한 값이다. 실행한 쿼리는 부록 B에 있다.
| 항목 | 실측 값 | 이관 판단에 주는 의미 |
|---|---|---|
| 버전 | 8.4.11 |
RDS도 8.4.11로 만든다. |
moyeota 데이터+인덱스 크기 |
16개 테이블, 약 0.7 MB | 덤프와 적재가 수 초 안에 끝난다. 병렬 덤프 도구나 물리 백업을 쓸 이유가 없다. |
| 행 수 | 약 830건 (information_schema.table_rows는 InnoDB 추정치) |
검증할 때는 COUNT(*)로 정확한 값을 다시 센다. |
| binlog |
ON / ROW / FULL, server_id=1, 보존 30일. 측정 시점 위치 binlog.000003:7959133
|
binlog 복제가 필요해지면 재기동 없이 바로 쓸 수 있다. |
| GTID |
gtid_mode=OFF, enforce_gtid_consistency=OFF
|
복제가 필요하면 파일·위치 기반(R1)으로 한다. |
| PK 없는 테이블 | 없음 | ROW 복제와 DMS 모두에서 제약이 없다. 체크섬 비교를 PK 순서로 할 수 있다. |
| 트리거·루틴·이벤트 | 모두 0 |
--routines=0 --triggers=0 --events=0으로 덤프해도 빠지는 객체가 없다. RDS에서 따로 만들 객체가 없다. |
테이블별 크기(mb는 소수 첫째 자리 반올림)는 다음과 같다.
| 테이블 | 행(추정) | MB | 테이블 | 행(추정) | MB |
|---|---|---|---|---|---|
| session_tokens | 309 | 0.1 | community_comments | 23 | 0.0 |
| companion_participants | 65 | 0.1 | oauth_accounts | 30 | 0.0 |
| companion_ratings | 3 | 0.1 | sessions | 49 | 0.0 |
| companions | 45 | 0.1 | chat_rooms | 45 | 0.0 |
| messages | 161 | 0.1 | community_posts | 20 | 0.0 |
| reports | 0 | 0.1 | current_locations | 0 | 0.0 |
| user_reports | 1 | 0.1 | signup_sessions | 16 | 0.0 |
| users | 30 | 0.0 | user_agreements | 30 | 0.0 |
데이터보다 binlog(binlog.000003 하나만 약 7.6 MB)가 더 크다. 이관할 데이터 양이 아니라 전환 순서와 정합성 검증이 이관의 핵심이라는 뜻이다.
V2 배포 일정은 10/16 stg 배포 → 10/17 QA → 10/19 Prod 배포이며, Prod DB 이관이 끝나야 Prod를 배포할 수 있다.
| 기준 | 요구 수준 |
|---|---|
| 쓰기 중단 시간 | 전환 순간에만 짧게 허용한다. 조회는 중단하지 않는다. 목표값은 stg 리허설 실측으로 확정한다. |
| 데이터 정합성 | 전환 시점에 소스와 대상의 테이블별 행 수와 체크섬이 일치해야 한다. |
| 롤백 | 전환 직후 문제가 생기면 V1 DB로 돌아갈 수 있어야 한다. |
| 운영 부담 | 인프라 담당 1명이 3일 안에 리허설과 실행을 끝낼 수 있어야 한다. 새로 도입하는 도구는 최소화한다. |
| 추가 비용 | 이관용 상시 리소스(복제 인스턴스 등)는 피한다. |
| 공식 지원 | RDS for MySQL 8.4를 대상으로 하는 AWS 공식 절차가 있는 방식을 우선한다. |
이관은 초기 적재(기존 데이터를 통째로 옮기기)와 변경분 동기화(적재하는 동안 생긴 쓰기를 따라잡기)의 조합이다. 초기 적재만 하면 적재하는 내내 쓰기를 멈춰야 하는 오프라인 이관이 되고, 변경분 동기화를 붙이면 전환 순간에만 쓰기를 멈추는 무중단에 가까운 이관이 된다.
| ID | 방식 | 형태 | 동작 | 공식 근거 | RDS 8.4 적용성 |
|---|---|---|---|---|---|
| L1 | mysqldump | 논리, 단일 스레드 |
--single-transaction으로 일관된 SQL 덤프를 만들고 mysql 클라이언트로 적재한다. --source-data=2로 binlog 위치를 기록한다. |
AWS: 다운타임을 줄인 RDS for MySQL 가져오기 | ✅ AWS 공식 절차의 기본 도구다. SQL 파일이 하나라 적재도 단일 스레드다. |
| L2 | mysqlpump | 논리, 테이블 단위 병렬 덤프 | 덤프는 병렬이지만 적재는 단일 스레드다. | MySQL 8.4.0 Release Notes | ❌ MySQL 8.4에서 제거됐다. 공식 문서는 mysqldump나 MySQL Shell을 대신 쓰라고 안내한다. |
| L3 | mydumper / myloader | 논리, 행 청크 단위 병렬 | 테이블을 청크 파일로 나눠 병렬로 덤프·적재한다. binlog 위치를 metadata 파일에 남긴다. | AWS Prescriptive Guidance: mydumper | ✅ AWS가 대용량 이관용으로 안내한다. 사용자 계정은 넘어가지 않는다. 별도 오픈소스 설치가 필요하다. |
| L4 | MySQL Shell util.dumpInstance / util.loadDump
|
논리, 청크 단위 병렬 + zstd | 병렬 덤프 후 병렬 적재한다. @.json 메타데이터에 binlog 위치와 gtidExecuted를 기록한다. |
MySQL Shell 8.4: Load Dump Utility | △ MySQL 공식 문서는 HeatWave와 자체 운영 MySQL만 대상으로 명시한다. RDS에서는 local_infile=ON, 권한 제약(compatibility 옵션)을 직접 검증해야 한다. |
| L5 | MySQL Shell util.copyInstance / copySchemas
|
논리, 중간 파일 없는 스트리밍 | 덤프와 적재를 한 번에 수행하고, 원본에서 대상으로 바로 스트리밍한다. 중단 후 재개(resume)는 지원하지 않는다. | MySQL Shell: Copy Instance, Schemas, and Tables | △ L4와 같은 제약이 있다. 디스크 스테이징이 필요 없지만 실패하면 처음부터 다시 해야 한다. |
| P1 | Percona XtraBackup → S3 → RDS 복원 | 물리 | 데이터 파일을 물리 백업해 S3에 올리고, RDS가 S3에서 새 인스턴스로 복원한다. | AWS: RDS for MySQL로 백업 복원 | △ 새 인스턴스로만 복원할 수 있다(Terraform으로 만든 RDS를 그대로 쓸 수 없다). 사용자·함수·프로시저·시간대 정보는 다시 만들어야 한다. 문서에 8.4는 XtraBackup 8.4를 쓰라고만 나와 있고, 복원 대상 버전은 콘솔에서 확인해야 한다. |
| ID | 방식 | 동작 | 공식 근거 | 특징과 제약 |
|---|---|---|---|---|
| R1 | RDS 외부 소스 binlog 복제 (파일·위치 기반) | 초기 적재 시점의 binlog 파일·위치부터 RDS가 V1 MySQL을 복제한다. 8.4에서는 mysql.rds_set_external_source, mysql.rds_start_replication을 쓴다. |
AWS: 외부 소스 binlog 파일 위치 복제, AWS: 복제 저장 프로시저 | 추가 비용이 0이다. 소스에 binlog만 켜져 있으면 되고 GTID는 필요 없다. 8.4 RDS는 caching_sha2_password 때문에 ssl_encryption=1이 필요하다. 인증서 검증(SOURCE_SSL_VERIFY_SERVER_CERT)은 지원하지 않는다. |
| R2 | RDS 외부 소스 binlog 복제 (GTID auto-position) |
mysql.rds_set_external_source_with_auto_position으로 GTID 기준으로 복제한다. |
AWS: 복제 저장 프로시저 | 복제가 끊긴 뒤 다시 붙이기 쉽다. 대신 소스의 gtid_mode=ON, enforce_gtid_consistency=ON이 필요하다. V1은 기본값(OFF)으로 추정되므로 단계적으로 켜면서 재기동해야 한다. 1회성 이관에는 얻는 이득보다 사전 작업이 크다. |
| R3 | AWS DMS 전체 적재 + CDC (복제 인스턴스) | DMS 복제 인스턴스가 테이블을 전체 적재한 뒤 binlog를 읽어 변경분을 적용한다. | AWS DMS: MySQL 소스, re:Post: DMS MySQL→MySQL |
AUTO_INCREMENT 속성이 넘어가지 않는다. ON DELETE/UPDATE CASCADE로 바뀐 자식 행이 복제되지 않는다. 보조 인덱스, PK 외 제약, 기본값을 만들지 않는다. 그래서 AWS도 스키마는 네이티브 도구로 미리 만들라고 안내한다. 복제 인스턴스 비용이 추가된다. |
| R4 | AWS DMS 동종 데이터 마이그레이션 (Homogeneous) | 서버리스 환경에서 mydumper/myloader로 전체 적재한 뒤, 적재 시작 시점의 binlog 위치부터 binlog 복제를 설정한다. | AWS DMS: MySQL 동종 데이터 마이그레이션 | 내부 동작은 L3 + R1과 같고, AWS가 관리해 준다. DMS 프로젝트, 데이터 공급자, 인스턴스 프로파일, 네트워크 설정을 새로 만들어야 하고, 사용량 과금이 붙는다. |
| A1 | 애플리케이션 이중 쓰기 (dual write) | 앱이 기존 DB와 새 DB에 함께 쓰고, 백필 → 읽기 전환 → 쓰기 전환 → 기존 DB 제거 순서로 진행한다. | Stripe: Online migrations at scale | 스키마 구조를 바꾸면서 옮길 때 쓰는 패턴이다. 엔진과 스키마가 같은 이번 이관에서는 BE 코드 변경, 두 DB 사이의 정합성 보정, 기간이 모두 과하다. |
| 방식 | 적용할 수 없는 이유 | 근거 |
|---|---|---|
| RDS Blue/Green Deployments | 블루(원본) 환경이 RDS여야 한다. V1은 EC2 위의 컨테이너다. 이관 후 RDS 버전 업그레이드나 파라미터 변경(V3 등)에 쓸 수 있다. | AWS: Blue/Green Deployments 개요 |
| RDS 읽기 전용 복제본 생성 후 승격 | 원본이 RDS 인스턴스여야 한다. | AWS: Read replica |
| EBS 스냅샷으로 데이터 볼륨 이동 | EC2 위의 MySQL로 옮길 때만 쓸 수 있다. RDS는 스토리지에 직접 접근할 수 없다. | — |
공개 벤치마크는 대부분 V1보다 훨씬 큰 데이터와 장비에서 측정됐다. 따라서 절대 시간은 그대로 옮기지 않고, 도구 간 상대 순위와 단일 스레드 적재의 비용을 근거로 쓴다.
| 출처 | 환경 | 데이터 | 결과 |
|---|---|---|---|
| Pythian: MySQL Shell vs mysqldump vs XtraBackup | 2 core / 4 GB, MySQL 8.0.27 | ① sysbench 8M행(약 1.56 GB) ② 32M행(약 6.4 GB) | 아래 4.2 표 참고. V1 Prod(t3.medium, 2 vCPU / 4 GiB)와 장비 규모가 가장 가깝다. |
| Percona: XtraBackup으로 RDS 이관 | EC2 소스 MySQL 5.6 → RDS db.m4.xlarge(1000 PIOPS) | 약 13.5 GB | 백업: XtraBackup 8분 / mysqldump 7.5분. 복원: S3에서 RDS 생성 10분 50초(+S3 업로드 37초) / mysqldump 적재 22.5분. 저자는 전체적으로 약 60% 빠르다고 평가했다. RDS 생성 시간에는 인스턴스 기동 오버헤드가 포함된다. |
| MySQL 공식 블로그: Shell Dump & Load 벤치마크 | OCI 베어메탈 88 vCPU / 512 GB, MySQL 8.0.21 | 64~410 GB 데이터셋 3종 | 덤프는 한 경우(바이너리 컬럼이 많은 Wikipedia, mydumper가 빠름)를 빼면 MySQL Shell이 가장 빨랐고 최대 약 3 GB/s였다. 적재는 MySQL Shell이 myloader보다 모든 경우에 빨랐다. mysqldump·mysqlpump는 SQL 파일 하나를 단일 스레드로 적재해서 훨씬 느렸다. 적재 200 MB/s 이상은 redo log를 끈 조건이라 RDS에서는 재현할 수 없다. |
| Percona: 백업·복원 성능 결론 | m5dn.8xlarge 32 vCPU / 128 GB, io1 5,000 IOPS, MySQL 8.0.26 | 약 96 GB | 백업+복원 합계는 XtraBackup이 가장 빨랐다. mydumper와 MySQL Shell도 두 단계 모두 좋은 성능을 냈다. mysqlpump는 백업은 빨랐지만 복원이 매우 느렸다(단일 스레드). mysqldump가 가장 비효율적이었다. 압축·TLS는 합계 시간에 큰 영향이 없었다. |
| Percona: 덤프 성능 비교 | m5dn.8xlarge, MySQL 8.0.26, 16~64 스레드 | sysbench TPC-C(약 92 GB) | 덤프 속도는 mydumper(zstd)가 가장 빨랐고, MySQL Shell, XtraBackup, mydumper(gzip), mysqlpump, mysqldump 순이었다. mydumper는 zstd로 gzip 대비 실행 시간이 약 50% 줄었다. |
| Oracle: copyInstance 예시 | 로컬 Mac, MySQL 9.0 | 164개 테이블, 76.7만 행(약 171 MB) |
copyInstance가 전체 6초에 끝났다. 비교 기준이 없는 단일 실행 예시라 참고용이다. |
| 도구 | 데이터 | 백업 | 백업 크기 | 복원 |
|---|---|---|---|---|
| MySQL Shell (zstd) | 8M행 / 1.56 GB | 12초 | 708 MB | 3분 8초 |
| XtraBackup (qpress) | 8M행 / 1.56 GB | 12초 | 1.2 GB | 1분 54초 |
| mysqldump (gzip) | 8M행 / 1.56 GB | 1분 23초 | 738 MB | 24분 22초 |
| MySQL Shell (zstd) | 32M행 / 약 6.4 GB | 1분 5초 | 2.83 GB | 10분 16초 |
| XtraBackup (qpress) | 32M행 / 약 6.4 GB | 29초 | 4.6 GB | 4분 44초 |
| mysqldump (gzip) | 32M행 / 약 6.4 GB | 6분 47초 | 2.9 GB | 원문 표기 "3h 59 sec"(표기 오류 가능성) |
- mysqldump가 느린 지점은 덤프가 아니라 적재다. 같은 1.56 GB에서 복원 시간이 MySQL Shell의 약 7.8배, XtraBackup의 약 12.8배였다. 원인은 SQL 파일 하나를 단일 스레드로 적재하는 구조이며, 세 벤치마크가 같은 결론을 낸다.
- 이 차이는 데이터 크기에 비례한다. Pythian의 mysqldump 복원은 1.56 GB에 1,462초, 약 1.07 MB/s였다. V1 실측 크기(약 0.7 MB)를 대입하면 약 1초다. 같은 비율로 MySQL Shell(약 8.3 MB/s)과 XtraBackup(약 13.7 MB/s)은 1초 미만이다. 그래서 V1에서는 도구 간 적재 시간 차이가 무시할 수 있는 수준이다.
- 변경분 동기화(R1)의 가치는 "적재 시간을 쓰기 중단에서 빼는 것"이다. 데이터가 클 때는 결정적이지만, 적재가 약 1초인 V1에서는 R1으로 줄일 수 있는 쓰기 중단도 약 1초뿐이다.
- 따라서 V1에서는 이관 도구의 속도나 동기화 방식이 아니라, 전환 단계의 앱 전환 시간(backend 재기동, DNS 전파)과 운영 변경의 위험이 쓰기 중단 시간과 방식 선택을 결정한다.
1.6의 실측 크기(약 0.7 MB)와 4.3의 처리율을 대입해 산정했다. 모든 방식에 공통으로 들어가는 앱 전환 시간(V1 backend를 RDS로 다시 연결하는 재기동과 헬스체크, Compose start_period 60s 기준 최대 약 1분)은 표의 "공통 전환 시간"으로 따로 둔다.
| 조합 | 데이터 이동에 따른 쓰기 중단(산정) | 공통 전환 시간 | 추가로 바뀌는 Prod 구성 | 추가 비용 | RDS 8.4 공식 절차 | 판단 |
|---|---|---|---|---|---|---|
| L1 mysqldump, 읽기 전용 전환 | 덤프 <1초 + 적재 약 1초 + 검증 수 초 → 수 초 | 최대 약 1분 | RDS SG에 V1 EC2에서 오는 3306 인바운드만 추가(V1 MySQL은 노출하지 않음) | 없음 | ✅ | ✅ 선택 |
| L1 + R1 (binlog 복제) | 복제 지연 해소 수 초 → 수 초 | 최대 약 1분 | V1 MySQL 3306을 EC2 사설 IP에 게시(Compose ports 변경 시 MySQL 컨테이너 재생성, 또는 중계 컨테이너 추가), V1 SG 인바운드, 복제 계정, TLS, RDS read_only 관리 |
없음 | ✅ | 보류. 줄어드는 쓰기 중단은 약 1초인데, Prod V1 MySQL 노출과 재생성 위험이 추가된다. 데이터가 크게 늘면 부록 A 절차로 바꾼다. |
| L4/L5 MySQL Shell | 수 초(L1과 차이 1초 미만) | 최대 약 1분 | mysqlsh 설치, RDS local_infile 등 권한 검증 |
없음 | △ | ❌ 이득이 없다. |
| L3 mydumper / R4 DMS 동종 | 수 초 | 최대 약 1분 | mydumper 또는 DMS 프로젝트·데이터 공급자·네트워크 구성 | R4는 DMS 사용료 | ✅ | ❌ 도입 비용이 이득보다 크다. |
| P1 XtraBackup → S3 → RDS | RDS 새 인스턴스 생성 시간이 포함됨(Percona 사례 10분 50초) | 최대 약 1분 | xtrabackup 8.4, S3, IAM role, Terraform 밖의 인스턴스 | S3 임시 비용 | △ | ❌ 가장 느리고 Terraform 관리와 맞지 않는다. |
| R3 DMS 전체 적재 + CDC | 수 초 | 최대 약 1분 | 복제 인스턴스, 엔드포인트, 태스크, 스키마 별도 생성 | 복제 인스턴스 비용 | ✅ | ❌ AUTO_INCREMENT·CASCADE·보조 인덱스 제약이 있고 비용만 추가된다. |
| A1 애플리케이션 이중 쓰기 | 0에 가까움 | 없음 | BE 코드 변경 | 개발 공수 | — | ❌ 10/19 일정 안에 불가능하고 규모 대비 과하다. |
산정 근거:
- 덤프·적재 시간은 4.2의 Pythian 측정(2 core / 4 GB, mysqldump 복원 약 1.07 MB/s)을 실측 크기에 비례 적용한 값이다. 네트워크 왕복과 테이블 생성(16개) 오버헤드가 더해져도 수 초 안쪽으로 본다. 8장 리허설로 실측한다.
- 공통 전환 시간은 V1
deploy/v1-compose.yaml의 backend 헬스체크start_period: 60s를 상한으로 잡았다.
- 데이터 이동으로 인한 쓰기 중단이 수 초다. 실측 데이터가 1 MB 미만이라, binlog 복제를 붙여도 줄일 수 있는 시간이 약 1초뿐이다. 쓰기 중단의 대부분은 어떤 방식을 쓰든 같은 앱 전환 시간이다.
-
조회는 중단하지 않는다. V1 MySQL을
super_read_only로 바꾸면 쓰기만 실패하고, 조회는 V1에서 계속 처리된다. - Prod V1 MySQL을 바깥에 노출하지 않는다. 덤프는 V1 EC2 안에서 실행하고, V1 EC2에서 RDS로 나가는 연결만 연다. R1처럼 V1 MySQL 포트를 게시하거나 컨테이너를 재생성할 필요가 없다.
-
AWS 공식 절차의 덤프·적재 단계를 그대로 쓴다. 다운타임을 줄인 가져오기의 mysqldump 옵션(
--single-transaction,--order-by-primary)과 적재 방식을 따르고, 데이터가 작아 복제 단계만 생략한다. - 롤백도 수 초다. 전환 후 문제가 생기면 같은 방법으로 RDS → V1 역방향 덤프를 하면 된다(7.6).
-
새 도구와 상시 리소스가 없다. mysqldump와 mysql 클라이언트는
mysql:8.4.11이미지에 들어 있다.
| 조건 | 바꿀 내용 |
|---|---|
| 리허설에서 덤프+적재가 1분을 넘음(데이터가 크게 늘었거나 네트워크 문제) | 부록 A의 L1 + R1(binlog 복제)로 바꾼다. 쓰기 중단에서 적재 시간을 뺄 수 있다. |
| V1 MySQL이 재생성돼 버전이 8.4.11보다 올라감 | RDS 버전을 같은 버전 이상으로 맞춘다. |
| 앱 전환 시간(backend 재기동)이 허용 범위를 넘음 | 7.5의 B안(V1은 읽기 전용 유지, DNS로만 전환)과 비교해 다시 정한다. |
- RDS 파라미터 그룹을 만든다:
time_zone=Asia/Seoul,character_set_server=utf8mb4,collation_server=utf8mb4_0900_ai_ci. - RDS for MySQL 8.4.11을 V2 data 서브넷에 만든다. 서울 리전에서 이 버전을 쓸 수 있는지 먼저 확인한다.
aws rds describe-db-engine-versions --engine mysql --engine-version 8.4.11 \ --region ap-northeast-2 --query 'DBEngineVersions[].EngineVersion' - RDS SG에 V1 Prod EC2 SG에서 오는 3306 인바운드를 추가한다(전환 기간 V1 backend와 덤프 적재용). V2 ECS SG에서 오는 인바운드도 함께 둔다.
- RDS에 앱 계정을 만든다. V1
backend.env의DB_USERNAME과 같은 이름을 쓰되 비밀번호는 새로 만들어 SSM에 넣는다. - 앱이 기동할 때 스키마를 바꾸는 설정(예: JPA
ddl-auto, Flyway)이 있는지 BE와 확인한다. 있다면 V2 BE의 스키마가 V1 데이터와 호환되는지 확인한다. - Route 53
moyeota.com레코드의 TTL을 전환 전날까지 60초로 낮춘다. - 전환 직전 S3에 최종 백업을 남긴다.
V1 MySQL 컨테이너는 internal: true 네트워크에만 붙어 있어 RDS에 직접 연결할 수 없다. 따라서 덤프는 컨테이너 안에서 실행하고, 적재는 호스트 네트워크를 쓰는 임시 클라이언트 컨테이너로 보낸다.
# V1 Prod EC2 (/opt/moyeota)
docker compose exec -T mysql sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \
--single-transaction --order-by-primary --set-gtid-purged=OFF \
--routines=0 --triggers=0 --events=0 --databases moyeota' > moyeota-cutover.sql
docker run -i --rm --network host mysql:8.4.11 \
mysql -h <rds-endpoint> -u <master> -p"<password>" < moyeota-cutover.sql- 트리거·루틴·이벤트가 0개이므로(1.6) 위 옵션으로 빠지는 객체가 없다.
-
mysql스키마(계정)는 덤프하지 않는다. 계정은 7.1의 4단계에서 따로 만든다.
-- V1과 RDS 양쪽에서 실행해 결과 비교
SELECT 'users', COUNT(*) FROM moyeota.users
UNION ALL SELECT 'messages', COUNT(*) FROM moyeota.messages; -- 16개 테이블 모두
CHECKSUM TABLE moyeota.users, moyeota.messages, ...; -- 16개 테이블 모두
SELECT NOW(), @@time_zone, @@character_set_server, @@collation_server;같은 8.4.11, InnoDB, 같은 행 형식이므로 CHECKSUM TABLE 값이 일치해야 한다.
| 순서 | 작업 | 쓰기 | 조회 | 예상 시간 |
|---|---|---|---|---|
| 0 | 7.2·7.3을 stg RDS로 리허설해서 실측 시간 확보(8장) | 정상 | 정상 | 사전 |
| 1 | V1 MySQL SET GLOBAL super_read_only = ON;
|
중단 시작 | 정상(V1) | 즉시 |
| 2 | 7.2 덤프·적재 | 중단 | 정상(V1) | 수 초 |
| 3 | 7.3 행 수·체크섬·시간대 검증 | 중단 | 정상(V1) | 수 초 |
| 4 | V1 backend의 DB_HOST를 RDS 엔드포인트로 바꾸고 backend만 재생성 |
중단 → 재개 | backend 재기동 동안 일시 실패 가능 | 최대 약 1분 |
| 5 | V2 Prod ECS 배포(SSM DB_HOST=RDS), 헬스체크 통과 확인 |
정상(RDS) | 정상 | ECS 배포 시간 |
| 6 | Route 53을 V2 ALB로 전환. TTL 동안 V1·V2 모두 RDS를 쓴다 | 정상 | 정상 | TTL 60초 + 클라이언트 캐시 |
| 7 | 트래픽이 V1에서 빠진 것을 확인한 뒤 V1 앱 컨테이너 중지. V1 MySQL은 읽기 전용 상태로 보존 | 정상 | 정상 | — |
산정 쓰기 중단: 수 초(덤프·적재·검증) + 최대 약 1분(backend 재기동) ≈ 1분 이내. 쓰기 중단 동안에는 로그인 토큰 저장(session_tokens)도 실패하므로 신규 로그인이 막힌다. 저트래픽 시간대에 실행한다.
| 선택지 | 방법 | 쓰기 중단 | 위험 |
|---|---|---|---|
| A. V1 backend도 RDS로 연결(7.4의 4단계) | V1 backend DB_HOST를 RDS로 바꾸고 backend만 재생성 |
backend 재기동 시간(최대 약 1분) | V1과 V2가 같은 DB를 함께 쓴다. V2 BE에 스키마 변경이 있다면 V1과 호환돼야 한다. |
| B. V1은 읽기 전용 유지 | 4단계를 건너뛰고 V2 배포 후 DNS만 전환 | 1~5단계 시간 + DNS TTL 60초 + 클라이언트 캐시 | 쓰기 실패가 더 길게 사용자에게 보인다. V1 쪽 변경은 없다. |
권장은 A다. 쓰기 중단을 backend 재기동 시간으로 묶을 수 있고, DNS 캐시가 남은 클라이언트도 정상적으로 쓸 수 있다.
-
4단계 이전: V1 MySQL
SET GLOBAL super_read_only = OFF;로 되돌린다. RDS에 쓰기가 없었으므로 데이터 손실이 없다. -
4단계 이후: RDS에 새 쓰기가 쌓인다. 롤백 기준(전환 후 N분 안에 오류율 X% 초과 등, 리허설 후 확정)을 넘으면 같은 방식을 역방향으로 실행한다. RDS 파라미터
read_only=1로 쓰기를 막고, RDS → V1 덤프·적재(데이터가 작아 수 초)를 한 뒤 V1super_read_only=OFF로 되돌린다. 그다음 backendDB_HOST와 DNS를 V1으로 되돌린다. - V1 data EBS와 7.1의 최종 백업은 이관이 안정될 때까지 삭제하지 않는다.
10/16 stg 배포 때 V1 Prod를 소스로 7.2·7.3을 stg RDS에 실행한다. --single-transaction 덤프는 V1 서비스를 멈추지 않으므로 읽기 전용 전환 없이 리허설할 수 있다.
| 지표 | 측정 방법 | 판정 기준 |
|---|---|---|
| 덤프 시간 | time |
수 초 |
| 적재 시간 | time |
수 초. 1분을 넘으면 6.2에 따라 부록 A로 바꿈 |
| 행 수·체크섬 | 7.3 | 16개 테이블 모두 일치 |
| 시간대·문자셋 |
NOW(), @@time_zone, 한글 데이터 조회 |
V1과 같음 |
| V1 backend 재기동 시간 |
DB_HOST 변경 후 헬스체크 통과까지 시간(Dev에서 측정) |
Prod 쓰기 중단 목표값을 확정하는 기준 |
| 역방향 롤백 시간 | stg RDS → 임시 MySQL 덤프·적재 | 수 초 |
리허설이 끝나면 stg RDS는 기존 운영 방침대로 삭제한다.
데이터가 커져서 6.2의 조건에 해당할 때 쓰는 절차다. 절차는 AWS: 다운타임을 줄인 가져오기와 외부 소스 binlog 파일 위치 복제를 따른다.
- V1 MySQL 3306을
<Prod EC2 사설 IP>:3306에만 게시하고, V1 Prod SG에 RDS SG에서 오는 3306 인바운드를 추가한다. Composeports를 바꾸면 MySQL 컨테이너가 재생성되므로 저트래픽 시간대에 하거나,database네트워크에 붙는 중계 컨테이너로 게시한다. - V1에 복제 계정을 만들고
REPLICATION CLIENT,REPLICATION SLAVE권한을 준다(접속 호스트는 V2 data 서브넷 대역으로 제한). - RDS 파라미터 그룹에
autocommit=1, 복제 기간 동안read_only=1을 둔다. -
mysqldump에--source-data=2를 추가해 덤프하고, 헤더의SOURCE_LOG_FILE·SOURCE_LOG_POS를 기록한 뒤 RDS에 적재한다. - RDS에서 복제를 시작한다. 8.4의 기본 인증
caching_sha2_password때문에 마지막 인자(TLS)를 1로 준다(AWS 복제 프로시저).CALL mysql.rds_set_external_source('<V1 사설 IP>', 3306, 'repl_user', '<password>', '<SOURCE_LOG_FILE>', <SOURCE_LOG_POS>, 1); CALL mysql.rds_start_replication; SHOW REPLICA STATUS\G -- Seconds_Behind_Source = 0 확인
- 전환: V1
super_read_only=ON→ 지연 0 확인 →CALL mysql.rds_stop_replication;→CALL mysql.rds_reset_external_source;→ RDSread_only=0→ 7.4의 4단계부터 같다.
SELECT VERSION();
SELECT table_name, table_rows,
ROUND((data_length + index_length) / 1024 / 1024, 1) AS mb
FROM information_schema.tables
WHERE table_schema = 'moyeota'
ORDER BY (data_length + index_length) DESC;
SHOW VARIABLES WHERE Variable_name IN
('log_bin', 'server_id', 'binlog_format', 'binlog_row_image',
'gtid_mode', 'enforce_gtid_consistency', 'binlog_expire_logs_seconds');
SHOW BINARY LOG STATUS;
SELECT t.table_name
FROM information_schema.tables t
LEFT JOIN information_schema.table_constraints c
ON c.table_schema = t.table_schema AND c.table_name = t.table_name
AND c.constraint_type = 'PRIMARY KEY'
WHERE t.table_schema = 'moyeota' AND t.table_type = 'BASE TABLE'
AND c.constraint_name IS NULL;
SELECT 'trigger', COUNT(*) FROM information_schema.triggers WHERE trigger_schema = 'moyeota'
UNION ALL SELECT 'routine', COUNT(*) FROM information_schema.routines WHERE routine_schema = 'moyeota'
UNION ALL SELECT 'event', COUNT(*) FROM information_schema.events WHERE event_schema = 'moyeota';공식 문서
- AWS: Importing data to an Amazon RDS for MySQL database with reduced downtime
- AWS: Configuring binary log file position replication with an external source instance
- AWS: Configuring, starting, and stopping binary log replication (stored procedures)
- AWS: Restoring a backup into an Amazon RDS for MySQL DB instance
- AWS Prescriptive Guidance: Migration options for large MySQL and MariaDB databases
- AWS Prescriptive Guidance: mydumper
- AWS DMS: Using a MySQL-compatible database as a source
- AWS DMS: Migrating data from MySQL databases with homogeneous data migrations
- AWS re:Post: DMS MySQL source to MySQL target
- AWS: Blue/Green Deployments overview
- AWS: Amazon RDS for MySQL now supports new minor version 8.4.11
- MySQL 8.4: Replication Compatibility Between MySQL Versions
- MySQL 8.4.0 Release Notes (mysqlpump 제거)
- MySQL 8.4: What Is New (SHOW MASTER STATUS 제거, expire_logs_days 제거)
- MySQL Shell 8.4: Load Dump Utility
- MySQL Shell: Copy Instance, Schemas, and Tables
벤치마크와 사례
- Pythian: Backup/Restore Comparison Using MySQL Shell vs mysqldump vs XtraBackup
- Percona: Migrate to Amazon RDS Using Percona XtraBackup
- MySQL Blog: MySQL Shell Dump & Load Part 2 – Benchmarks
- Percona: Backup/Restore Performance Conclusion
- Percona: Backup Performance Comparison
- Oracle MySQL Blog: Copy Data Directly to a MySQL Instance with MySQL Shell
- Stripe: Online migrations at scale