pile·
DB / 데이터·channel-talk채널톡·

메시지 전송 트래픽 100배에도 끄떡 없는 User 테이블로 뜯어고치기 (2)

1편에서 설계한 UserBadge 분리 파이프라인을 실제로 수행한 기록으로, 16억 8천만 레코드 마이그레이션의 단계별 디테일과 비용·시간 비교를 담았다. 자체 Java 마이그레이션이었다면 $366.71에 최소 7일이 걸렸을 작업이 AWS 관리형 서비스 조합으로 6시간 만에 끝났다. 실패해도 실패한 단계만 재시도할 수 있다는 점이 시간보다 더 큰 차이였다.

핵심 포인트
  • Dual Write에는 서로 다른 버전의 변경이 순서가 뒤바뀌어 도착할 수 있어 ConditionExpression으로 LWW 전략을 적용했다.
  • Spark는 아이템에 필드가 없으면 NULL로 채우는데 DynamoDB는 NULL을 저장하지 않아, 그대로 Import하면 실패한다.
  • Glue ETL 첫 실행은 Worker type G 1X에 10대로 돌렸다가 타임아웃이 났다. 1.8TB를 처리하기엔 턱없이 부족했다.
  • Worker 수는 감이 아니라 CloudWatch의 필요 Worker 수 메트릭을 보고 조정하면 비용과 성능의 균형을 잡을 수 있다.
  • 1.8TB 테이블이 gzip 압축으로 약 280GB가 됐고, Export는 테이블 RCU를 소모하지 않아 프로덕션에 영향이 없었다.
상세 정리
  • 1차 배포: 기존 Badge 업데이트가 성공한 뒤 TmpUserBadge에 upsert하는 구조이며, Dual Write가 실패해도 원본 업데이트에 영향을 주지 않도록 try-catch로 감쌌다.
  • LWW 적용: 예컨대 User version 5의 업데이트가 version 3보다 먼저 도착할 수 있어, 저장된 것보다 새로운 버전일 때만 업데이트되고 아이템이 아직 없으면 그대로 쓰이도록 조건을 걸었다. 이 전략은 3차 마이그레이션에서도 그대로 쓰인다.
  • Export 준비: PITR을 활성화하고 콘솔에서 S3 버킷 경로를 지정해 실행하면 각 줄이 하나의 아이템인 JSON Lines 형식의 gzip 파일이 생성된다.
  • Glue 스크립트: Visual 에디터의 드래그앤드롭 대신 LLM의 도움을 받아 PySpark 커스텀 스크립트를 작성했다.
  • DynamicFrame을 쓴 이유는 스키마가 불완전하거나 필드가 누락된 데이터도 유연하게 처리할 수 있기 때문이다. 16억 8천만 레코드 중 일부 아이템에만 존재하는 필드가 있어 이 유연성이 중요했다.
  • 필드 추출: DynamoDB JSON은 타입 정보가 포함된 구조라 수십 개 필드 중 Badge에 해당하는 것만 골라 적절한 타입으로 매핑해야 한다.
  • NULL 처리: Spark가 없는 필드를 NULL로 채우는 것과 DynamoDB가 NULL을 저장하지 않는 것의 차이를 맞추지 않으면 Import가 실패한다.
  • 품질 검증: Glue의 Data Quality 기능으로 변환 데이터를 검증하되, 규칙이 맞지 않아도 Job 자체를 중단하지 않는 설정으로 두고 콘솔의 Data Quality 탭에서 실패 데이터를 확인했다.
  • Glue 튜닝 항목: Worker type을 대용량 I/O에 유리한 쪽으로 변경하고, 비용 효율을 위해 실제 필요한 만큼만 투입되는 옵션을 켜고, 타임아웃 경험 때문에 안전하게 최댓값으로 설정했다.
  • Import 결과: 16억 8천만 레코드가 모두 들어갔고 새 테이블은 S3 데이터를 기반으로 스키마가 자동 감지돼 별도 정의가 필요 없었다.
  • 나중에 알게 된 함정: Import 시 데이터가 특정 순서로 정렬돼 있으면 같은 파티션 키의 레코드가 물리 파티션에 순차적으로 몰려 쓰로틀링이 발생할 수 있다. Glue ETL 단계에서 데이터를 랜덤하게 섞으면 방지된다.
  • 이번 마이그레이션은 PK만 있고 SK가 없어 이 문제가 없었지만 PK+SK 구조 테이블을 Import할 때는 기억해 두라고 덧붙인다.
  • 2차 배포: Dual Write 대상을 TmpUserBadge에서 UserBadge로 전환하는데, DAO 클래스명만 바뀌고 LWW 전략과 조건식은 그대로다. 1편에서 동기 Dual Write를 고른 이유가 최종 구조 전환의 사전 리허설이었고 이 단계가 그 리허설이라고 밝힌다.
  • 3차 마이그레이션: Export 시점과 UserBadge Dual Write 시작 시점 사이의 변경분은 TmpUserBadge에만 있으므로, Full Scan으로 옮기되 UserBadge가 이미 Dual Write로 갱신 중이라 여기서도 LWW를 적용했다.
  • TmpUserBadge는 마이그레이션 기간의 변경분만 담아 약 1,100만 건으로 원본에 비하면 매우 적었고, 서비스에 영향을 주지 않는 테이블이라 온디맨드로 빠르게 처리했다.
  • 3차 배포: 애플리케이션에서 Badge 관련 조회와 수정 로직을 모두 제거하고 UserBadge를 바라보게 일괄 변경하면서, User 테이블이 Badge 트래픽으로부터 완전히 분리됐다.
  • 비용 비교 상세: 기존 방식의 $366.71은 User 테이블 Full Scan의 RCU 비용 $11.84와 UserBadge Full Write의 WCU 비용 $354.87을 합산한 값으로, 1.8TB를 500 RCU/초로 스캔하고 16억 8천만 레코드를 3,000 WCU/초로 쓰는 기준이다.
  • 새 방식에서 가장 큰 비용은 Export의 $183.55로 GB당 약 $0.1 수준이다. Glue ETL은 G 2X Worker 80대로 30분 돌려 $19.93이었고, Import는 압축 해제 기준 용량으로 과금돼 $41.93이었다.
  • 운영상의 차이: 기존 방식은 마이그레이션 트래픽이 운영 트래픽과 경합하지만 새 방식은 그렇지 않고, Glue ETL에서 타임아웃이 났을 때도 Export부터 다시 할 필요 없이 Glue Job 설정만 고쳐 재실행하면 됐다.
  • 최종 효과: 메시지 전송 트래픽이 100배 이상 늘어도 UserBadge 테이블의 프로비저닝 용량만 조절하면 대응할 수 있는 구조가 됐다.
  • 기존에 프로비저닝을 높여도 안 되던 이유 둘을 다시 짚는다. 트랜잭션 충돌 시 재시도로 WCU가 누적되는 문제는 용량을 높여도 막을 수 없고, GSI 파티션당 1,000 WCU 제한은 테이블 전체 용량 설정과 무관하다.
왜 읽나DynamoDB 대용량 테이블을 무중단으로 쪼개야 하는 팀에게 Export·Glue·Import 각 단계의 실제 설정값과 함정, 그리고 자체 스크립트 대비 비용·시간 비교를 숫자로 준다.
channel-talk
채널톡 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. DB / 데이터·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    분석가가 직접 만든 마케팅 자동화 시스템 — 오디언스 빌더 제작기

    여기어때 데이터 분석팀(UB팀)이 개발 경험 없이 AI 에이전트 바이브 코딩으로 마케팅 자동화 시스템 '오디언스 빌더'를 구축한 과정이다. SQL 없이 UI에서 타겟 조건을 설정하면 BigQuery에서 수천만 행 대상자를 추출해 Kafka·S3를 통해 마케팅 도구로 전달하는 파이프라인을 직접 만들어, 캠페인 준비 시간을 2~3일에서 10분으로 단축했다.

    요약 이어보기
    #data-pipeline#kafka#postgres+2
  2. DB / 데이터·vercel-blogVercel Blog·

    모든 Vercel 빌드를 지탱하는 데이터베이스 마이그레이션 이야기

    Vercel이 빌드 웜 풀(warm pool) 상태 데이터를 Redis에서 DynamoDB로 마이그레이션한 과정을 다룬다. 빌드 풀의 청구 매핑 데이터가 Redis 장애 시 복구 불가능하다는 구조적 위험이 이주를 결정한 핵심 이유였고, 24/7 프로덕션 트래픽 속에서 다운타임 없이 5단계 점진적 전환을 완료했다.

    요약 이어보기
    #database-migration#zero-downtime#redis+2
  3. DB / 데이터·channel-talk채널톡·

    DynamoDB 핫 파티션을 해결하는 3가지 방법 (3): 조회를 인덱스 테이블로 옮기기

    채널톡이 DynamoDB 핫 파티션을 해결하려 GSI를 별도 인덱스 테이블로 떼어낸 연재의 마지막 편으로, 조회 경로를 GSI에서 인덱스 테이블로 옮긴 과정을 다룬다. 2편에서 쓰기 파이프라인이 완성돼 WriteThrottleEvents가 월 110만 회에서 0회가 됐지만, 조회 코드가 여전히 GSI를 읽고 있어 GSI를 지울 수 없었다. GSI가 읽기에서 대신해주던 키 해석·정확 일치·삭제 필터·페이지 채우기를 사내 공통 라이브러리의 조회 규칙으로 흡수해, 호출부는 GSI든 인덱스 테이블이든 같은 코드를 쓰게 만들었다.

    요약 이어보기
    #migration#dynamodb#hot-partition+5