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

메시지 트래픽 100배에도 끄떡 없게 고객 테이블 뜯어고치기 (1)

채널톡이 16억 8천만 레코드짜리 User 테이블에서 Badge 필드를 별도 테이블로 떼어내기로 한 원인 분석과 마이그레이션 전략 설계다. Badge는 메시지를 보낼 때마다 갱신되고 OTM처럼 한 번에 여러 사용자에게 보내면 트래픽이 몰리는데, 이 스파이크가 User 테이블 전체를 느리게 만들어 부트 기능까지 마비시켰다. 아이템 분리로는 부족하고 테이블 자체를 나눠야 하는 이유, 그리고 자체 Java 마이그레이션 대신 DynamoDB Export/Import와 Glue를 고른 근거를 다룬다.

핵심 포인트
  • DynamoDB는 아이템의 일부 필드만 수정해도 아이템 전체 크기 기준으로 WCU를 소모하고, 최소 1 WCU 단위로 올림된다.
  • ChatSession과 Badge를 TransactWriteItems로 묶어 원자성을 보장하는 구조라, 낙관적 동시성에서 충돌이 나면 재시도할 때마다 WCU가 4, 8, 16으로 누적될 수 있었다.
  • 같은 테이블 안에서 아이템만 분리하는 안은 트랜잭션 충돌과 GSI 파티션 집중이라는 두 문제를 그대로 남겨 기각했다.
  • 파티션당 1,000 WCU 제한은 테이블 전체 용량 설정과 무관하므로 프로비저닝을 아무리 높여도 GSI Back-Pressure는 해결되지 않는다.
  • 자체 Java 마이그레이션은 총 $366.71에 최소 7일이 걸리고, 중간에 OOM이나 코드 결함이 나면 처음부터 다시 해야 했다.
상세 정리
  • Badge의 성격: 읽지 않은 메시지나 새 알림이 있다는 시각적 표시로, 앱 아이콘 우측 상단의 빨간 숫자 같은 것이다. 작지만 사용자 경험에서 중요한 역할을 한다.
  • 문제 상황: 하루에도 여러 번 스파이크성으로 대량의 Badge 업데이트 요청이 몰리며 User 테이블 전체가 느려졌고, 부트가 동작하지 않으면 고객사 홈페이지에서 채널톡이 뜨지 않는다.
  • 다른 필드들은 비교적 고르게 업데이트되는 반면 Badge만 특정 이벤트에 집중된다. 한 고객사가 전체 고객 1만 명에게 OTM을 보내면 1만 건의 Badge 업데이트가 동시에 발생한다.
  • Auto Scaling을 켜도 급격한 스파이크에는 대응이 늦어져 쓰로틀링이 여전히 발생한다.
  • WCU 소모 구조: 1KB 아이템을 수정하면 1 WCU, 2KB면 2 WCU가 들고 0.3KB여도 1 WCU가 든다. 여기에 트랜잭션이 2배를 소모해 Badge 업데이트 한 번에 실제로는 4 WCU가 들었다.
  • 결과적으로 스파이크 때 User 테이블의 한정된 WCU가 빠르게 고갈되고 프로필 수정이나 태그 수정 같은 정상적인 업데이트까지 쓰로틀링에 걸렸다.
  • 프로비저닝 용량을 높이면 해결되지만 들쑥날쑥한 Badge 트래픽만을 위해 User 테이블에 추가 요금을 내는 것은 바람직하지 않다고 판단했다.
  • GSI Back-Pressure: 채널 A의 5만 명에게 OTM을 보내면 User 테이블의 GSI들이 모두 채널 id를 파티션 키로 써서 한 파티션에 집중된다. GSI 파티션의 쓰기가 밀리면 DynamoDB가 원천적으로 메인 테이블 쓰기를 막고, 이때 그 쓰기가 GSI에 영향을 주는지와 무관하게 모든 쓰기가 거부된다.
  • 후보 1 아이템 분리: 같은 테이블 안에서 Profile 아이템과 Badge 아이템을 나누는 안이다. 아이템 크기가 작아져 WCU 소모를 줄일 수 있다.
  • 기각 이유: 분리해도 Badge 업데이트는 여전히 트랜잭션으로 처리돼 충돌과 재시도로 인한 WCU 낭비가 그대로이고, Badge 아이템도 같은 GSI에 포함돼 파티션 집중이 해결되지 않는다.
  • 후보 2 테이블 분리 채택: 물리적으로 분리되므로 WCU와 RCU를 독립적으로 관리할 수 있다.
  • 채택 근거 1 트래픽 패턴: User 테이블의 일반 쓰기는 비교적 고르게 분포하는 반면 Badge는 메시지 발송이나 OTM 시점에 집중되는 스파이크성이다. 성격이 전혀 다른 두 패턴을 한 테이블에서 처리하는 것은 구조적으로 무리였다.
  • 채택 근거 2 조회 패턴: User는 여러 기준 조회가 빈번해 3개의 GSI를 쓰지만 Badge는 다른 조회 패턴이 없어, GSI 없는 테이블을 만들 수 있어 파티션 병목과 Back-Pressure를 원천 차단한다.
  • 채택 근거 3 용량 전략: 스파이크성 트래픽에는 온디맨드를 적용하거나 OTM 발송 전 프로비저닝을 미리 높이는 전략을 쓸 수 있고, User 테이블은 안정적인 설정을 유지해 비용과 안정성 양쪽에서 이점이 있다.
  • 채택 근거 4 일관성: AccountBadge와 ManagerBadge는 이미 각각 분리한 상태라 UserBadge도 같은 설계 패턴을 따르는 것이 구조적 일관성에 맞았다.
  • 설계 원칙 인용: S3도 하나의 서비스처럼 보이지만 내부적으로는 300개 이상의 마이크로서비스로 구성돼 역할과 트래픽 특성 기준으로 분리돼 있다는 점을 들어, 데이터 특성과 트래픽 패턴이 다르면 나누는 것이 산업적 설계 원칙과 맞는다고 본다.
  • 기존 방식의 비용 계산: 1.8TB를 4KB 단위 Eventually Consistent Read로 읽는 데 필요한 RCU와, 500 RCU/초로 스캔하면 168시간이 걸린다는 계산이 나왔다. 스캔을 빠르게 하면 운영 중인 테이블에 부하를 준다.
  • 시간도 문제였다. 7일 동안 스크립트가 도는 중 EC2 OOM이나 코드 결함, 네트워크 불안정이 생기면 처음부터 다시 시작해야 한다.
  • 대안 발견: DynamoDB Export는 PITR 스냅샷을 S3로 내보내는 기능이고 백그라운드 프로세스로 동작해 프로덕션 테이블의 RCU를 소모하지 않는다는 점이 핵심이었다.
  • 다만 Export는 테이블 전체를 내보내므로 Badge 필드만 따로 뽑을 수 없어, 모든 필드를 내보낸 뒤 골라내는 ETL 단계가 필요했다.
  • Glue를 고른 이유: EMR은 Spark 클러스터를 직접 띄우고 코드와 설정을 관리해야 하고 Kinesis Data Firehose는 실시간 스트리밍에 특화돼 일회성 배치 변환에 안 맞았다. Glue는 클러스터 프로비저닝과 스케일링을 알아서 처리하고 작업이 끝나면 리소스도 정리한다.
  • 최종 파이프라인 6단계: TmpUserBadge로 Dual Write 시작 → User 테이블을 S3로 Export하고 Glue ETL로 변환 → Import로 UserBadge 테이블 생성 → Dual Write 대상을 UserBadge로 전환 → TmpUserBadge를 Full Scan해 UserBadge에 변경분 반영 → 애플리케이션 로직을 UserBadge로 전환.
  • 마이그레이션과 Dual Write가 충돌하면 최신 변경만 받아들이도록 ConditionExpression을 작성해야 한다는 점을 설계 단계에서 못박았다.
왜 읽나DynamoDB에서 특정 필드의 쓰기 패턴만 유독 튀어 테이블 전체가 흔들리는 상황을 겪는 팀에게, 아이템 분리와 테이블 분리의 차이 그리고 대용량 온라인 마이그레이션 도구 선택의 근거를 준다.
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