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

DynamoDB 핫 파티션을 해결하는 3가지 방법 (1): 인덱스 테이블로 GSI 떼어내기 설계편

채널톡의 DynamoDB 핫 파티션 연재 1편으로, GSI 하나에 쓰기가 몰려 User 테이블 전체의 쓰기까지 막혀 버린 장애를 분석하고 GSI를 별도 인덱스 테이블로 떼어내는 설계를 세운다. 더 큰 문제는 그 GSI와 아무 관련이 없는 Boot 요청까지 함께 거부됐다는 점이었다. Boot는 고객사 사이트 방문자를 채널톡에 연결하는 과정이라, 막히면 그 사이트에서 채널톡 위젯 자체가 뜨지 않는 장애로 번졌다.

핵심 포인트
  • GSI는 따로 쓰기 요청을 받지 않고 메인 테이블 변경이 비동기 전파되는 구조라, 아이템 하나를 쓸 때마다 메인 테이블 1 WCU와 각 GSI 1 WCU가 따로 소모된다.
  • 문제의 GSI는 sort key가 접속 시각이라 값이 바뀔 때마다 기존 위치를 지우고 새 위치에 다시 기록해, 실질적으로 항상 2배 비용이 드는 구조였다.
  • DynamoDB 파티션 하나는 초당 1,000 WCU가 물리 한계이고, 이는 테이블 전체 용량 설정과 무관하다. 메인 테이블 WCU를 아무리 높여도 해결되지 않는다.
  • GSI 파티션이 밀리면 DynamoDB가 원천적으로 메인 테이블 쓰기를 막는 Back-Pressure가 작동하고, 이때 막히는 것은 그 GSI 관련 쓰기만이 아니다.
  • 키 샤딩·도메인 테이블 분리·인덱스 테이블 분리 세 후보 중, 비즈니스 로직을 그대로 두고 인덱스만 통제할 수 있게 떼어내는 세 번째를 골랐다.
상세 정리
  • 문제의 GSI 용도: 특정 채널의 관리 대상 사용자를 최근 접속순으로 조회하는 인덱스로, 프로모션 발송이나 재방문 메시지, 최근 활동 사용자 조회, 일정 기간 미접속 사용자 일괄 삭제 등 여러 기능이 이 하나에 의존하고 있었다.
  • 여기서 관리 대상은 웹사이트를 스쳐 간 익명 방문자가 아니라 연락처를 남겼거나 상담으로 관계가 생긴 사용자다.
  • 포화 경로 1: 한 고객사 사이트에 방문이 몰리면 Boot가 대량으로 발생해 그 채널의 GSI 파티션이 핫 파티션이 된다. 이를 막기 위한 Boot 전용 Hybrid Rate Limit은 별도 글로 예고했다.
  • 포화 경로 2: Boot가 잠잠한 시간대라도 이메일이나 카카오톡 같은 외부 매체로 한 채널에서 대량 소통이 발생하면 같은 파티션이 포화된다.
  • 즉 특정 경로 하나의 요청 수를 제한해도 다른 경로가 같은 파티션을 포화시키면 장애가 재현되고, 조회 기능은 서비스 여러 곳에서 필수라 단순히 떼어낼 수도 없었다.
  • 예시로 채널 A의 사용자 5만 명에게 One Time Message를 발송하면 5만 건의 유저 업데이트가 동시에 발생하고, User 테이블의 GSI들이 모두 채널 id를 파티션 키로 써서 그 한 파티션에 집중된다.
  • 후보 1 키 샤딩: 파티션 키를 여러 값으로 쪼개 쓰기를 분산하는 방식이다. 파티션 키만 바꾸면 되고 핫 파티션 자체가 잘 생기지 않는다는 단순함이 장점이다.
  • 기각 이유: 이 GSI의 유즈 케이스가 전부 한 채널의 사용자를 최근 접속순으로 나열하는 조회였다. 채널 하나를 16개 샤드로 나누면 한 페이지를 위해 16번의 Query가 동시에 나가고 응답을 모두 받아 다시 정렬·페이지네이션해야 해, 평소에도 16배의 RCU가 든다.
  • 게다가 이미 GSI에 들어 있는 수십억 건 아이템을 새 샤딩 키로 다시 써야 하고 그 과정에서 또 같은 쓰기 집중이 일어날 수 있다.
  • 다만 키 샤딩이 언제나 틀린 것은 아니다. 단건 조회가 주가 되는 GSI라면 스캐터 게더 부담이 크지 않아 오히려 깔끔한 선택이고, 실제로 다른 GSI에는 이 방식을 적용했다고 밝힌다.
  • 후보 2 도메인 테이블 분리: 이전에 Badge를 별도 테이블로 떼어낸 패턴을 그대로 가져오는 방향이다. 데이터를 도메인 단위로 쪼개고 비즈니스 로직이 직접 새 테이블에 쓰게 한다.
  • 기각 이유: Badge 분리가 깔끔했던 이유는 쓰기 경로가 몇 군데로 정해져 있었기 때문이다. 반면 이 경우 Boot와 외부 매체 소통 등 여러 경로에서 User 테이블이 갱신되므로, 모든 경로의 비즈니스 로직을 일일이 바꾸는 것은 현실적으로 불가능하고 경로 하나만 놓쳐도 데이터가 어긋난다.
  • 최소 정보만 담는 프로젝션 테이블도 검토했지만, 목록을 꺼낼 때 사용자 이름·프로필·태그가 같이 필요해 매번 User 테이블로 2차 조회가 따라붙는다. Badge는 단건 조회였고 이쪽은 사용자 전체 정보가 필요한 목록 조회라 접근 패턴 자체가 다르다.
  • 후보 3 채택: 비즈니스 로직은 지금처럼 User 테이블만 수정하고, User의 변경을 감지해 별도 인덱스 테이블로 흘려보내는 파이프라인을 직접 만든다. 사실상 자체 GSI를 만드는 셈이다.
  • 이점 셋: 기존 쓰기 로직을 건드릴 필요가 없고, 조회하는 곳이 한정적이라 변경 범위가 적으며, 메인 테이블 쓰기와 인덱스 쓰기가 비동기로 분리돼 Back-Pressure 자체가 사라지고 파이프라인 중간에 제어 지점을 둘 수 있다.
  • 덤으로 모니터링도 얻는다. AWS는 메인 테이블에서 GSI로 전파되는 시간 같은 지표를 제공하지 않는데, 직접 만들면 전파 지연과 적체, 재시도 상태를 확인할 수 있다.
  • Streams 모드 선택: Native 모드는 동일 샤드를 동시에 읽는 프로세스가 2개를 넘지 않을 때 안정적이라고 권장되며 그 이상이면 처리량 한도에 부딪힌다. User 테이블에는 이미 다른 컨슈머가 붙어 있어 이번 파이프라인까지 얹으면 한도에 닿을 가능성이 컸다.
  • Kinesis 모드는 enhanced fan-out으로 컨슈머마다 샤드당 2MB/s 전용 처리량을 가져 서로 영향을 주지 않고, 보관 기간도 Native의 24시간 고정 대신 최대 365일까지 설정할 수 있어 장애나 배포로 컨슈머가 멈춰도 여유 있게 되짚을 수 있다.
  • 파이프라인 구조: 새 마이크로서비스가 Kinesis에서 이벤트를 꺼내되 바로 쓰지 않고, 사내 Redis 기반 Rate Limiter에게 해당 인덱스 파티션이 지금 핫 파티션인지 물어본다.
  • 핫 파티션이면 SQS로 밀어 넣고 나중에 다시 꺼내 판정과 쓰기를 처음부터 거친다. SQS 버퍼의 이유는 핫 파티션이 발생한 채널만 격리해 다른 채널 쓰기가 함께 밀리지 않게 하고, 파티션 한계를 넘는 쓰기를 DynamoDB에 그대로 밀어 넣지 않아 쓰로틀링 자체를 막기 위해서다.
  • 기존 데이터 마이그레이션: PITR 스냅샷을 S3로 Export하고 Glue로 인덱스 테이블 형태로 변환한 뒤, Export 시각을 시작점으로 스트림을 재생해 그 사이 변경분을 따라잡는다.
  • Badge 분리 때와 달라진 점: 그때는 Export 도중 변경분을 담아둘 임시 테이블을 따로 만들어야 했지만, 이번에는 User 테이블에 이미 Kinesis가 달려 있고 스트림이 이벤트를 오래 쌓아 둬 임시 테이블 없이 처리할 수 있었다.
  • 서두에서 핫 파티션이 DynamoDB만의 문제가 아니라 Cassandra·HBase 같은 분산 NoSQL이나 샤딩한 MySQL/PostgreSQL도 형태만 달리해 겪는 문제라고 짚는다.
왜 읽나GSI 쓰기가 메인 테이블까지 막는 Back-Pressure를 겪는 팀에게 키 샤딩과 도메인 분리, 인덱스 분리의 트레이드오프를 접근 패턴 기준으로 가르는 판단 근거를 준다.
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