Large-Scale Data Migration

대규모 데이터 시스템 마이그레이션의 안전한 실행을 위한 전략과 패턴


핵심 개념

대규모 데이터 시스템 마이그레이션은 단순히 “새 시스템으로 전환”하는 것이 아니라, 수만 개의 작업을 데이터 품질 검증·롤백 안전성·자동화를 갖추고 점진적으로 전환하는 엔지니어링 과제다. CDC 기반 시스템에서는 잘못된 데이터가 후속 파티션에 전파되므로, 조기 탐지와 빠른 차단이 특히 중요하다.

Shadow / Reverse-Shadow 패턴

Meta의 데이터 인제스천 시스템 마이그레이션에서 검증된 3단계 수명주기:

Phase 1: Shadow

  • 새 시스템이 동일 소스를 소비하되 별도 “섀도 테이블”에 적재
  • 프로덕션 작업과 row count + checksum을 지속적으로 비교
  • 프로덕션 데이터에 영향 없이 실제 워크로드에서 새 시스템 검증

Phase 2: Reverse Shadow

  • 새 시스템이 프로덕션 테이블에 쓰고, 기존 시스템이 섀도 테이블에 쓰도록 역전
  • 지속적인 데이터 품질 비교 가능 + 불일치 발견 시 즉시 롤백
  • 롤백 시 구 시스템을 재설정할 필요 없음

Phase 3: Cleanup

  • 불일치 없음을 확인한 후 구 시스템 섀도 작업 제거
  • 새 시스템이 정식 프로덕션으로 확정

배치 계획과 자동화

  • 제한된 용량에서 작업을 배치로 선별하여 마이그레이션
  • 알려진 이슈가 있는 작업은 수정 완료까지 배치에서 제외 — 불필요한 full dump 방지
  • 자동 프로모션/디모션: 마이그레이션 기준(데이터 품질, 랜딩 레이턴시, 리소스 사용량) 충족 여부를 시스템이 지속 판단
  • 대시보드로 전체 진행률 + 개별 작업 디버깅 가시성 제공

CDC 마이그레이션의 특수성

  • CDC는 이전 적재 데이터를 기반으로 새 데이터를 생성 → 잘못된 파티션이 후속 데이터에 전파
  • 파티션 메타데이터에 bad-data 마킹 → delta 파티션이면 적재 중단, target 파티션이면 이전 정상 파티션 + delta 병합
  • 롤백 시 bad-data 마킹된 파티션만 backfill하여 빠르게 복구

Like-for-Like + Decider 패턴 (Netflix Cassandra)

Netflix는 Cassandra→Iceberg 커넥터(Casspactor)를 교체하면서 세 기둥과 Decider 패턴으로 무중단 전환을 달성했다.

마이그레이션 3대 기둥

  1. Validation: shadow 모드로 신/구 출력을 행 단위 비교. 집합 C(레거시)와 M(신규)에 대해 C=M 증명 — C-M(누락)·M-C(팬텀 데이터) 모두 0이 목표. 차이 발견 시 investigation log로 분류·전파
  2. Visibility: 마이그레이션 진행률·런타임·비용 비교 대시보드, 의존성 실패 추적, 폴백/불일치 알림
  3. Safety: 시스템은 결국 실패한다는 전제 하에 사용자 영향 제거

Like-for-Like 전략

사용자 인터페이스·출력 계약·최종 데이터 아티팩트(스키마·메타데이터·데이터)를 완전히 동일하게 유지 → 수십 개 다운스트림 팀과의 조율 불필요. 마이그레이션을 “분산된 다중 팀 과제”에서 “플랫폼 내부 구현 디테일”로 전환.

Decider 패턴

워크플로(Maestro)에 Decider step을 추가하고, Connector Controller(동적 레지스트리)가 cohort·작업 속성에 따라 레거시/신규 커넥터를 선택. 설정 변경만으로 즉시 업그레이드/롤백 가능하며, 신규 커넥터 실패 시 조건부 step이 자동으로 레거시로 폴백 → 사용자는 다소 긴 런타임만 경험할 뿐 실패·stale 데이터를 보지 않음.

Shadow/Reverse-Shadow가 “데이터 검증”에 초점이라면, Like-for-Like + Decider는 “사용자 영향 제거(추상화 + 폴백)“에 초점을 둔다. 둘 다 shadow 검증을 공유한다.

트레이드오프

접근법장점비용
Shadow/Reverse-Shadow데이터 품질 지속 검증, 빠른 롤백2배 리소스 사용(양 시스템 병렬 운영)
Like-for-Like + Decider다운스트림 조율 불필요, 자동 폴백출력 계약을 동일하게 유지해야 하는 제약
Big-Bang 전환짧은 전환 기간롤백 어려움, 위험 집중
점진적 테이블 단위리스크 분산긴 전환 기간, 혼합 상태 관리
경계에서 번역(translate at the boundary)사용자 설정·워크플로를 전혀 안 바꿔도 됨, 앱 단위 즉시 롤백번역 레이어 자체가 유지 대상이 됨

시작 전에 처리해야 할 문제들 (Wix 플레이북)

Wix의 마이그레이션이 7개월·2년에서 4주로 줄어든 차이는 “툴링이 아니었다 — 툴링은 거의 바뀌지 않았다. 모든 마이그레이션에서 나타나는 문제가 무엇인지 마침내 배우고 그것을 도중이 아니라 시작 전에 다루기 시작한 것” 이다.

위의 Shadow/Decider 패턴이 “어떻게 안전하게 전환하는가” 라면, 아래는 “플레이북을 올바르게 따라도 여전히 나타나는 문제” 다.

1. 소스 DB에는 비밀이 있다 — read 단계를 먼저 만들어라

레거시 서비스는 수년의 변경을 지니고 있고, 거의 항상 예상보다 더 손상되어 있다.

실제로 발견된 것들: UUID 스펙을 따르지 않는 UUID, 어떤 검증도 허용하지 말았어야 할 길이의 이름, 문서화된 한도를 넘는 리스트, 생년월일이 19,653년.

이런 레코드가 존재하는 이유는 검증이 시간에 따라 바뀌었고, 옛 코드에 버그가 있었고, 다른 규칙의 시스템에서 임포트했기 때문이다.

왜 치명적인가: V2 API는 더 엄격한 검증을 가질 것이다. V1이 조용히 받아들이던 레코드가 변환 시점에 마이그레이션 서비스를 깨뜨린다.

대응 — 거의 노력이 안 드는데 가시성 이득이 엄청나다:

  1. 다른 무엇보다 먼저 마이그레이션 서비스의 read 단계만 구현
  2. 소스 DB에 바인딩
  3. 일관성이 의심되는 모든 필드·구조에 로깅 추가
  4. 스냅샷 dry run을 돌려 대시보드 생성

뭔가를 발견하게 된다. 50억 레코드를 이미 마이그레이션한 뒤에 그중 일부가 변환될 수 없음을 발견하는 게 아니라 지금 발견하라.

2. parity gap을 먼저 찾아라

parity gap = V1과 V2가 같은 입력에 다른 결과를 반환하는 것. parity gap이 있으면 롤아웃할 수 없고, 늦게 찾으면 몇 주가 든다.

  • E2E 테스트를 V2에 대해 먼저 돌려라 — API 프록시를 통해 기존 V1 E2E 스위트를. 중요한 것은 재사용만이 아니라 애초에 의미 있는 E2E 커버리지를 갖는 것이다: 유닛 테스트는 개별 로직을 검증하지만 마이그레이션은 컴포넌트 사이의 경계에서 실패한다 — 데이터·검증·부작용에 대한 가정이 만나는 곳
  • LLM으로 실제 프로덕션 사용을 분석 — V1·V2 코드베이스, 테스트 스위트, 실제 트래픽 샘플을 주고 버전 간 다르게 동작할 수 있는 유스케이스를 식별하게 한다. 기존 테스트가 커버 못 하는 격차를 드러내는데, 그 테스트들이 V1 가정에 대해 쓰였기 때문
  • compare mode를 데이터 마이그레이션 전에 돌려라 — 모든 V1 read가 V2로 비동기 shadow call을 트리거하고 응답을 비교. 핵심 단어는 before다. 데이터를 다 옮긴 뒤 돌리는 것은 버그를 발견하기 전에 이미 마이그레이션 비용을 지불한 것

3. “레코드 하나가 실패하면?” 은 설계 문서의 한 줄이어야 한다

CDC는 파티션별 메시지 순서를 보존한다. 그것이 신뢰성의 근거이고 동시에 취약성의 근거다 — 메시지 하나가 실패하면 그 파티션에서 뒤의 모든 것이 멈춘다.

커넥터를 시작하고 퇴근하고 아침에 compare 대시보드를 보러 온다. 그런데 레코드 하나가 실패해서 마이그레이션이 자정부터 멈춰 있었다.

전략동작선택 기준
Blocking retries사람이 고칠 때까지 멈춤속도를 잃고 on-call 필요, 아무것도 조용히 건너뛰이지 않음. 데이터 일관성이 최우선일 때
Try, catch, skip로깅·메트릭 후 진행일정대로 완료, 나중에 triage(테넌트 제외 → 버그 수정 → 슬라이스 재마이그레이션). 알려진 낙오자 집합을 감내할 수 있을 때
Dead letter queue실패를 별도 토픽으로파이프라인이 멈추지 않고 아무것도 안 잃음. 가장 견고하고 가장 만들기 힘들다

요점은 무엇이 최선이냐가 아니라 이것이 발견이 아니라 사전 결정이어야 한다는 것이다.

4. 속도의 산수 — 두 가지 운영 속도밖에 없다

가장 자주 틀리는 계산이다.

50억 레코드 / DBA 허용 5,000 RPM  →  694일

그런데 이 계산은 불완전하다. eager 마이그레이션이 히스토리컬 레코드를 처리하는 동안 DB는 평균 15,000 RPM(하루 2,200만 건)으로 변경을 축적하고 lazy 마이그레이션이 그것도 처리해야 한다.

5,000 RPM으로 처리하는데 15,000 RPM으로 도착하면 결코 끝나지 않는다.

실용적 운영 속도는 사실상 둘뿐이다 — 들어오는 변경률을 앞지를 만큼 빠르거나, 몇 년을 돌려도 괜찮을 만큼 느리거나.

두 레버:

  • 파티션 — ⚠️ 시작 전에 정해야 한다. 돌아가는 마이그레이션에 파티션을 추가하려 하면 “too late, 처음부터 다시” 다. 필요 처리량 ÷ 파티션당 처리율로 미리 계산 (Wix: 100,000 RPM에 350 파티션)
  • DB 용량 — 16TB 중 14TB를 쓰던 단일 master MySQL을 8개 샤드로 분할해 write 처리량을 샤드 수만큼 곱했다. 타임라인이 DB 용량에 막혀 있다면 이 대화를 일찍 해야 한다

blast radius도 줄여라: 마이그레이션 서비스가 다른 시스템에 부작용을 만들면 그 시스템들에도 한계가 있고 많은 것이 문서화되어 있지 않다. 문서화되지 않은 것은 사람들이 와서 “상류를 다운시키고 있으니 멈추라”고 할 때 발견한다. → 직접 호출 대신 BI 이벤트/Kafka 메시지를 생산하고, 그 호출이 필요한 것은 별도 후속 마이그레이션으로

5. 정리도 마이그레이션의 일부다

마이그레이션은 데이터가 V2에 들어갔을 때 끝나지 않는다. V1이 사라졌을 때 끝난다.

V1 트래픽 0 확인 → 파드 제거·서비스 종료 → 코드 삭제 (⚠️ stale proto 참조가 인시던트를 일으키므로 proto 패키지에 특히 주의) → population 데이터 삭제(실제 스토리지 비용) → 옛 DB drop

마지막 단계가 무섭게 느껴진다. 그럴 필요 없다. 데이터는 V2에 있다. V1 DB를 남겨두는 것은 당신을 더 안전하게 만들지 않는다 — 돈만 들고 어느 것이 source of truth인지에 대한 모호함만 만든다.

6. 다음 단계 — supervised agentic 마이그레이션

위 전부가 체크리스트다: dry-run population 정의 → 실행 → 로그 연구 → 변환 버그 수정·PR → 파티션 사이징 → DB 리소스 협상 → compare mode → diff 추적 → 실제 실행 → 대시보드 감시 → 깨진 부분집합 재마이그레이션 → 정리.

길고 반복적이고 거의 모든 단계가 이제 정확히 기술할 수 있는 패턴을 따른다. 그것이 대체로 에이전트가 해야 할 일의 정의다.

세 조각: 상태를 지속 인지(대시보드·로그) → 추론하고 다음 수를 결정 → 실행(PR 열기, 리소스 조정, compare 점수가 떨어지면 재마이그레이션 트리거).

⚠️ 무거운 일을 하는 단어는 supervised다. 에이전트는 플레이북을 따를 수 있다 — 그게 플레이북을 갖는 요점이다. 어려운 부분은 플레이북 밖으로 벗어나는 순간(위의 문제들)을 인식하고 멈춰서 사람에게 묻도록 가르치는 것이다. → Loop Engineering, Multiagent Coordination

plan for the mess, build for safety, clean up after yourself, and celebrate when V1 is finally, truly dead.

연관 개념


Source: Migrating Data Ingestion Systems at Meta Scale, The Evolution of Cassandra Data Movement at Netflix, 5 Billion Records 10 Terabytes Four Weeks - A Real Data Migration Playbook, Rerouting the Stream - How Lyft Moved to the Apache Flink Operator