어제 배운 배치 수집은 "모았다가 한꺼번에"였다. 오늘부터는 정반대 세계, 스트리밍이다. 클릭스트림, IoT 센서, 결제 트랜잭션, 게임 텔레메트리처럼 데이터가 끊임없이 흘러들어오고, 그것을 초 단위로 처리해야 하는 상황. 그 한가운데 있는 서비스가 Amazon Kinesis Data Streams(KDS) 다.
KDS는 한마디로 "확장 가능한 실시간 데이터 파이프"다. 수많은 프로듀서가 데이터를 밀어넣고, 수많은 컨슈머가 거의 동시에 읽어간다. 오늘은 이 파이프의 내부 구조(샤드), 데이터를 어느 칸에 넣을지 정하는 파티션 키, 그리고 처리량 계산을 본다.
KDS 스트림은 하나의 거대한 통이 아니라 샤드(shard) 라는 여러 레인으로 나뉜다. 샤드는 처리량의 기본 단위이자 병렬성의 단위다. 각 샤드는 고정된 용량을 가진다.
샤드 1개의 용량:
- 쓰기(입력): 1 MB/s 또는 1,000 records/s
- 읽기(출력): 2 MB/s (공유 처리량 기준)
즉 스트림의 총 처리량 = 샤드 수 × 샤드당 용량이다. 초당 5MB를 써야 한다면 최소 5개의 샤드가 필요하다. 샤드를 늘리면(resharding) 처리량이 비례해서 늘어난다.
import boto3kinesis = boto3.client("kinesis")kinesis.create_stream(StreamName="clickstream", ShardCount=5)# 트래픽 증가 시 샤드 분할로 처리량 확장kinesis.update_shard_count( StreamName="clickstream", TargetShardCount=10, ScalingType="UNIFORM_SCALING")
💡 관련 이론: KDS에는 두 가지 용량 모드가 있다. Provisioned 모드는 샤드 수를 직접 지정하고 그만큼 과금되며 처리량 계산을 스스로 해야 한다. On-demand 모드는 트래픽에 따라 샤드를 자동 조절(다수 리전 기준 쓰기 200MB/s, 200,000 records/s까지 자동 확장하며 일부 리전은 상한이 더 높다)하고 사용량만큼 과금된다. 트래픽이 예측 가능하고 안정적이면 Provisioned가 저렴하고, 변동이 크거나 예측이 어려우면 On-demand가 운영 부담을 없앤다. 이 트레이드오프는 시나리오 문제의 판단 기준이 된다.
프로듀서가 레코드를 보낼 때 반드시 파티션 키(partition key) 를 함께 지정한다. KDS는 이 키를 MD5 해싱해서 어느 샤드로 보낼지 결정한다. 같은 파티션 키를 가진 레코드는 항상 같은 샤드로 가고, 샤드 내에서는 순서가 보장된다.
# 사용자 ID를 파티션 키로 → 같은 사용자의 이벤트는 같은 샤드, 순서 보장kinesis.put_record( StreamName="clickstream", Data=b'{"event":"page_view","page":"/checkout"}', PartitionKey="user-8821" # 이 키가 샤드를 결정)
여기서 핵심 함정은 핫 샤드(hot shard / hot partition) 다. 파티션 키 분포가 고르지 않으면 특정 샤드에만 트래픽이 몰려, 그 샤드는 ProvisionedThroughputExceededException으로 throttle되는데 다른 샤드는 한가하다. 예를 들어 파티션 키를 "country"로 잡으면 트래픽 대부분이 한 나라에 쏠릴 때 한 샤드만 터진다.
핫 샤드를 피하려면 카디널리티가 높고 분포가 고른 파티션 키를 골라야 한다. user_id, device_id, session_id처럼 값이 많고 균등한 키가 좋고, country·region·status처럼 값이 적고 편향된 키는 나쁘다. 단, "같은 키 = 순서 보장"이라는 성질 때문에 순서가 중요한 단위(예: 사용자별 이벤트 순서)를 파티션 키로 삼아야 하는 트레이드오프가 있다. 분산과 순서 보장 사이의 균형이 설계 포인트다.
- PutRecord / PutRecords API (SDK 직접 호출)
- KPL (Kinesis Producer Library): 자동 배칭·집계·재시도로 고처리량
- Kinesis Agent: 로그 파일을 자동으로 스트림에 전송
컨슈머는 스트림에서 데이터를 읽는 쪽이다. 두 가지 읽기 방식이 있다.
방식
처리량
지연
특징
공유 처리량(Shared / classic)
샤드당 2MB/s를 모든 컨슈머가 나눠 씀
폴링(GetRecords)
컨슈머가 늘면 경쟁
향상된 팬아웃(Enhanced Fan-Out)
컨슈머마다 샤드당 2MB/s 전용
push, ~70ms
컨슈머별 독립 대역폭
컨슈머가 여러 개이고 각자 풀 대역폭이 필요하면 향상된 팬아웃을 쓴다. 컨슈머 측 처리는 보통 KCL(Kinesis Client Library) 로 샤드 분배·체크포인트(어디까지 읽었는지)·장애 복구를 자동 관리하거나, Lambda 이벤트 소스 매핑으로 서버리스하게 처리한다.
# Lambda를 KDS 컨슈머로: 샤드마다 배치로 레코드가 들어옴import base64, jsondef handler(event, context): for record in event["Records"]: payload = base64.b64decode(record["kinesis"]["data"]) data = json.loads(payload) # 처리 로직... return {"processed": len(event["Records"])}
KDS는 데이터를 즉시 버리지 않고 보존 기간(retention) 동안 보관한다. 기본 24시간, 최대 365일까지 늘릴 수 있다. 이 덕분에 컨슈머가 잠시 죽었다 살아나도 놓친 데이터를 다시 읽을 수 있고, 보존 기간 안에서는 같은 데이터를 여러 컨슈머가 독립적으로 재처리할 수 있다.
보존 기간은 KDS가 단순 메시지 큐(소비하면 사라지는 SQS)와 다른 결정적 차이다. KDS는 로그(append-only log)에 가까워서, 여러 컨슈머가 같은 스트림을 각자의 위치(체크포인트)에서 독립적으로 읽는 "팬아웃" 소비가 가능하다. 처리 로직 버그를 고친 뒤 보존 기간 내 데이터를 처음부터 다시 흘려 재처리(replay)하는 패턴도 여기서 나온다. SQS는 이런 재처리/멀티 컨슈머 팬아웃이 기본 동작이 아니다.
스트림은 최근 30일 동안 관측된 쓰기 피크의 2배까지 받아낼 수 있도록 용량을 유지한다. 피크가 40MB/s 였다면 80MB/s 까지는 바로 받는다.
트래픽이 15분 안에 이전 피크의 2배를 넘게 뛰면 쓰기 스로틀이 날 수 있다. 예고된 대형 이벤트 직전의 급증이 여기에 걸린다. 프로듀서는 재시도해야 한다.
상한은 리전마다 다르다. 다수 리전은 쓰기 200MB/s·읽기 400MB/s 까지이고, 일부 대형 리전은 그보다 훨씬 높으며 상향 요청도 가능하다.
온디맨드는 트래픽이 늘면 샤드를 균등하게 쪼갠다. 특정 해시 키에 몰린 트래픽을 골라 격리하지는 않는다. 그래서 파티션 키 하나가 샤드 한도(1MB/s, 1,000건/s)를 넘으면 온디맨드에서도 스로틀된다. "온디맨드로 바꾸면 핫 샤드가 해결된다"는 보기는 키 쏠림이 원인일 때 오답이다.
프로비저닝 ↔ 온디맨드 전환은 스트림당 24시간에 2회까지 가능하고, 전환 중에도 읽기·쓰기는 계속된다.
리샤딩은 항상 둘씩 일어난다. 분할(split)은 샤드 하나를 둘로, 병합(merge)은 인접한 둘을 하나로 만든다. UpdateShardCount 는 이 과정을 목표 샤드 수에 맞춰 대신 수행한다(프로비저닝 모드 전용).
대상이 된 원래 샤드를 부모, 새로 생긴 샤드를 자식이라 부른다. 부모 샤드는 닫혀서 더는 쓰기를 받지 않지만, 안에 있던 데이터는 보존 기간이 끝날 때까지 읽을 수 있다.
순서를 지키려면 부모 샤드를 끝까지 읽은 뒤 자식 샤드를 읽어야 한다. KCL 은 리스 테이블에 부모·자식 관계를 기록해 이 순서를 자동으로 지킨다. 직접 API 로 컨슈머를 짜면 이 처리를 스스로 해야 한다.
프로비저닝 모드는 샤드 단위 과금이므로 분할은 비용을 올리고 병합은 내린다. 피크가 지난 뒤 병합하지 않으면 빈 샤드에 돈을 계속 낸다.
# 특정 핫 샤드만 골라 반으로 분할 (프로비저닝 모드)aws kinesis split-shard \ --stream-name clickstream \ --shard-to-split shardId-000000000003 \ --new-starting-hash-key 170141183460469231731687303715884105728# 인접한 한가한 샤드 두 개를 병합aws kinesis merge-shards \ --stream-name clickstream \ --shard-to-merge shardId-000000000005 \ --adjacent-shard-to-merge shardId-000000000006
⚠️ 함정: 핫 샤드의 원인이 "키 하나에 트래픽이 몰림"이면 샤드를 아무리 쪼개도 그 키는 여전히 한 샤드로 간다. 분할이 통하는 경우는 "여러 키가 우연히 같은 해시 범위에 몰린" 때뿐이다. 키 하나가 문제면 키 설계를 바꿔야 한다(예: user_id 대신 user_id#난수 접미사. 단, 그 사용자의 순서 보장은 포기하게 된다).