DynamoDB에서 GSI가 베이스 테이블 쓰기를 스로틀하는 이유
테이블에 쓰기를 합니다. 처리량 예외와 함께 쓰기가 실패하는데 — 예외가 가리키는 것은 테이블이 아니라 글로벌 보조 인덱스(GSI) 입니다. 테이블에는 여유 용량이 있습니다.
SQL에서 넘어왔다면 말이 안 됩니다. 보조 인덱스가 INSERT를 막을 수는 없으니까요.
DynamoDB에서는 막을 수 있고, 그 메커니즘을 GSI 백프레셔라고 부릅니다.
DynamoDB GSI가 베이스 테이블 쓰기를 스로틀하는 이유는?
DynamoDB가 베이스 테이블 쓰기를 스로틀하는 것은 모든 쓰기가 각 GSI에도 복제되기 때문이며, GSI 파티션이 자기 몫을 흡수하지 못하면 DynamoDB는 인덱스가 영구히 뒤처지지 않도록 백프레셔를 적용합니다. 그래서 프로비저닝이 부족하거나 카디널리티가 낮은 GSI 키는 베이스 테이블 쓰기 속도에 대한 하드 상한이 됩니다.
- 베이스 테이블에 대한 쓰기는 모든 GSI에도 쓰기를 합니다. GSI가 자기 몫을 흡수할 수 없으면, DynamoDB는 인덱스가 영구히 뒤처지지 않도록 베이스 테이블 쓰기를 스로틀합니다. (AWS 문서)
- 베이스 테이블이 고르다고 안전하지 않습니다. GSI는 자체 키로 분할됩니다.
카디널리티가 낮은 GSI 키(
status같은)는 베이스 테이블 쓰기가 완벽하게 분산되어 있어도 핫 인덱스 파티션을 만듭니다. - 예외는 피해자에 대해 거짓말을 합니다.
ResourceArn은 GSI를 가리키지만, 실제로 스로틀되는 작업은 테이블에 대한 당신의 쓰기입니다. - 해법은 용량 또는 키 설계이지 재시도 루프가 아닙니다 — GSI 처리량을 올리거나, 분산이 잘 되는 GSI 파티션 키를 고르세요.
단 한 번의 쓰기가 인덱스를 건드리는 방식
베이스 테이블에 대한 PutItem은 한 번의 쓰기가 아닙니다. DynamoDB는 아이템의 투영된
속성을 각 GSI로 비동기적으로, 최종적 일관성 모델에 따라 복제합니다. 하나의 논리적 쓰기가
N개의 물리적 쓰기로 부채처럼 펼쳐집니다 — 테이블 더하기 모든 인덱스.
그 복제는 공짜가 아니며 선택 사항도 아닙니다. GSI는 따라잡아야 하고, 그렇지 않으면 매 작업마다 인덱스가 테이블에서 점점 더 멀어집니다.
그 표류를 막기 위해 DynamoDB는 백프레셔를 적용합니다. 인덱스가 무한정 낡지 않도록 원본 쓰기를 스로틀하는 것이죠.
그래서 GSI의 쓰기 용량은 당신의 베이스 테이블 쓰기 속도에 대한 하드 상한이 됩니다 — GSI에 직접 쓰기를 한 적이 한 번도 없는데도 말입니다.
실전 예제: 주문 테이블
주문 테이블을 운영한다고 합시다. 베이스 아이템:
| field | value | note |
|---|---|---|
| PK | "CUST#8841" | partition key |
| SK | "ORD#2026-06-23#A7" | sort key |
| order_state | "PROCESSING" | |
| warehouse | "EU-MAD-2" | |
| total_cents | 4990 |
베이스 테이블 쓰기는 건강합니다. CUST#...는 카디널리티가 높아서 주문 쓰기가 베이스
파티션 전반에 고르게 분산됩니다. 핫 키도 없고, 용량도 충분합니다.
이제 "특정 상태의 모든 주문을 보여줘"에 답하기 위해 GSI를 추가합니다:
| field | value | note |
|---|---|---|
| GSI-PK | order_state | "PENDING" | "PROCESSING" | "SHIPPED" | "CANCELED" |
| GSI-SK | SK |
가능한 파티션 키 값은 네 개. 플래시 세일 동안에는 거의 모든 새 주문이
order_state = "PENDING"에 떨어집니다. 그 쓰기 하나하나가 같은 GSI 파티션을
때립니다.
그 파티션에는 파티션당 처리량 한계가 있는데, 당신은 방금 쓰기 폭풍 전체를 그쪽으로 조준한 것입니다.
베이스 테이블은 멀쩡합니다. PENDING GSI 파티션이 불타고 있습니다. DynamoDB는 인덱스를
보호하기 위해 베이스 테이블 PutItem을 스로틀합니다.
당신을 무는 흐름
여기 백프레셔 경로가 있습니다 — 베이스 쓰기는 균형 잡혀 있고, 인덱스 쓰기는 집중되어 있습니다:
스로틀은 거꾸로 거슬러 올라갑니다. 핫 GSI 파티션이 자신을 채운 베이스 테이블 쓰기를 거부하는 것입니다.
직감 말고 예외를 읽어라
예외 유형이 당신이 어느 상한에 부딪혔는지 정확히 알려줍니다. ResourceArn은 GSI를
가리키지만, 스로틀된 작업은 여전히 테이블 쓰기입니다.
| 모드 | 사유 코드 | 무엇이 고갈되었는가 |
|---|---|---|
| 프로비저닝 | IndexWriteProvisionedThroughputExceeded | GSI의 프로비저닝된 쓰기 용량 |
| 프로비저닝 | IndexWriteKeyRangeThroughputExceeded | 단일 핫 GSI 파티션 |
| 온디맨드 | IndexWriteMaxOnDemandThroughputExceeded | GSI에 설정된 온디맨드 최대 상한 |
| 온디맨드 | IndexWriteAccountLimitExceeded | 계정/리전 처리량 경계 |
위의 핫 파티션 케이스에서는 KeyRange 사유가 결정적 단서입니다. 전체 GSI 용량은
멀쩡해 보여도 하나의 키 범위가 포화될 수 있습니다.
해결 방법
GSI에 여유를 주세요. 가장 단순한 원인은 프로비저닝 부족입니다. GSI는 테이블과 완전히 분리된 자체 읽기·쓰기 용량을 가집니다 — GSI vs LSI를 참고하세요.
테이블은 넉넉히 프로비저닝하면서 GSI는 얇게 남겨뒀다면, GSI의 쓰기 용량(또는 온디맨드 최대치)을 올리세요.
파티션 키를 고치세요. 카디널리티가 낮은 키는 용량으로 구할 수 없습니다 — 단일 핫 파티션은 프로비저닝으로 이길 수 없습니다. 분산이 되는 GSI 파티션 키를 고르세요.
조합하세요. order_state#shard처럼 shard를 작은 무작위 접미사로 두거나, 날짜를 접어
넣으세요(PENDING#2026-06-23). 쓰기가 파티션 전반에 분산되고, 여전히 샤드를 조회해
상태별로 Query할 수 있습니다.
더 적은 속성을 투영하세요. 각 GSI 쓰기는 투영된 속성을 복사합니다. KEYS_ONLY나
타이트한 INCLUDE 투영은 더 작은 인덱스 쓰기를 의미하며 ALL보다 압박이 적습니다.
인덱스에서 절대 읽지 않을 것을 투영하지 마세요.
보고용일 뿐이라면 GSI를 버리세요. "상태별 주문"이 핫 경로가 아니라 가끔 하는 관리자 질문이라면, 필터를 건 주기적 스캔이 영구히 뜨거운 인덱스를 이길 수도 있습니다 — Query vs Scan과 견주어 보세요.
그 인덱스를 조회할 때는
Expression Builder가
KeyConditionExpression을 — 예: #s = :state AND begins_with(SK, :prefix) —
이름과 값을 올바르게 이스케이프하여 대신 작성해 줍니다:
KeyConditionExpression "#s = :state"
ExpressionAttributeNames { "#s": "order_state" }
ExpressionAttributeValues { ":state": { "S": "PENDING" } }
기억해야 할 함정
관계형 본능 — "인덱스는 쓰기를 조금 느리게 할 뿐" — 은 그대로 옮겨오지 않습니다. DynamoDB GSI는 수동적 구조가 아니라 처리량 의존성입니다. 너무 작게 잡거나 뭉치는 키를 고르면, 자신이 봉사하는 테이블에 백프레셔를 가합니다.
테이블의 것만이 아니라 GSI의 파티션별 ConsumedWriteCapacityUnits와
ThrottledRequests를 지켜보세요.
다음 단계
- GSI vs LSI — GSI가 자체 용량과 다른 파티션 키를 가지는 이유.
- 단일 테이블 설계 — 핫 인덱스를 늘리지 않고 하나의 GSI를 오버로딩하여 여러 패턴을 처리하기.
- Query vs Scan — 인덱스가 쓰기 비용을 들일 가치가 없는 때.
DynoTable을 사용해 세일이 테이블을 빨갛게 물들이기 전에, 자신의 테이블에서 GSI의 소비된 용량과 스로틀 이벤트를 지켜보세요.