Scaling StreamHub: Transitioning from Kinesis to Kafka for 145 Billion Daily Events
Author: Prakash Kava (Atlassian) | Source: atlassian.com/blog/how-we-build | Published: 2026-07-29
한 줄 요약
Managed는 unlimited를 뜻하지 않는다 — 브로커·네트워크·EBS·S3·control plane 한계가 여전히 중요하고, 특히 관리형 control plane이 불건강할 때 복구 경로가 그것에 의존하지 않도록 설계해야 한다.
핵심 주장/내용
- Kinesis를 벗어난 이유 (5배 스케일 증가에 직면):
- API 신뢰성 — StreamHub API가 99.99%를 간신히 지원했고 스파이크 중 Kinesis 스로틀링이 producer에 영향. 목표는 99.999%
- 높은 파티셔닝 비용 — Kinesis는 shard로 확장하고 각 shard가 ingress 1MB/s, egress 2MB/s 한계. 피크를 감당하려면 수천 shard가 필요한데 가격이 활성 shard에 크게 의존해 월 청구서가 트래픽에 선형 증가. 이 규모에서 on-demand 오토스케일링은 금지적으로 비쌌다
- 보존 한계와 비용 페널티 — 24시간 초과 저장에 비싼 확장 보존 애드온 필요. 다운스트림 장애나 모델 재학습 시 안전한 replay를 위해 최대 7일 이력을 원했다
- 전달 SLO와 consumer 처리량 고갈 — 여러 독립 consumer group이 같은 shard egress 용량을 공유하고 Enhanced Fan Out은 가파른 프리미엄. 로컬 전달 SLO 2초 미만, 크로스 리전 3초 미만을 요구
- → Apache Kafka가 명확한 선택. 운영 오버헤드를 줄이려 AWS MSK로 이전해 작은 플랫폼 팀이 zookeeper 프로비저닝·OS 패치·하드웨어 교체 대신 스트림 아키텍처·클라이언트 라이브러리·신뢰성에 집중
- Tiered Storage — 수십억 이벤트를 고성능 EBS에 저장하는 것은 비쌌다. Local Tier(Tier 1) 는 브로커의 로컬 EBS에 “hot” 데이터를 마지막 5분 분량 저장해 실시간 consumer가 빠른 저지연 스토리지에서 직접 읽게 한다(그 윈도우를 넘어 읽는 consumer는 정의상 레이턴시에 민감하지 않다). Remote Tier(Tier 2) 는 닫힌 로그 세그먼트를 S3로 비동기 offload해 7일 보존. S3 read는 수백 ms를 더하지만 이미 5분 이상 뒤처진 consumer에게는 허용 가능
- 최악 케이스 보존을 위한 비싼 EBS 과프로비저닝을 멈췄고, 히스토리 데이터 read가 전부 S3에서 오므로 무거운 백필이 hot 실시간 consumer의 IOPS를 굶기지 않는다
- 규모에서 깨진 것들:
- ① 브로커 네트워크 한계와 인스턴스 사이징 — 클러스터 사이징을 CPU·메모리로 시작하는 게 자연스럽지만 더 중요한 제약은 종종 브로커 레벨 처리량이었다. 클러스터가 평균적으로 건강해 보이면서 일부 브로커·파티션이 이미 포화에 근접해 있을 수 있다. Kafka 트래픽은 증폭된다 — producer ingress는 그림의 일부일 뿐이고 복제, consumer fan-out, 크로스 리전 릴레이, 재시도, tiered storage remote copy가 모두 브로커 리소스를 다툰다. 트래픽 스파이크 중 작은 브로커 인스턴스가 복제·consumer 서빙·S3 offload보다 빠르게 데이터를 받아들여 CPU·I/O 압박이 오르고 백그라운드 작업이 느려지고 consumer lag이 늘고 로컬 디스크가 차오르기 시작했다
- ② Tiered storage도 로컬 headroom이 필요하다 — Kafka는 로컬에 먼저 쓰고 백그라운드 워커가 닫힌 세그먼트를 S3에 비동기 복사한다. 무거운 유입 중에는 그 워커가 producer 쓰기·복제·읽기·housekeeping과 경쟁한다. remote copy가 뒤처지면 offload되었어야 할 데이터가 로컬 디스크에 쌓이기 시작하고, 높은 유입률에서 그 디스크 증가는 운영 관점에서 선형이 된다 — 지연 1분마다 나중에 따라잡아야 할 데이터가 더 늘어난다. 핵심 교훈: tiered storage는 브로커 headroom의 대체재가 아니다. 비용·보존 전략이지 최소 로컬 용량으로 돌려도 된다는 허가증이 아니다
- ③ 보존 변경 중 S3 request rate 한계 — S3는 사실상 용량 무제한이지만 API에는 규모에서 중요한 request rate 동작이 있다. 프로덕션 조사를 위해 tiered storage 보존을 7일 → 21일로 일시 늘렸다가 되돌리자 보존 축소가 오래된 remote 데이터에 대한 대규모 delete 물결을 촉발했다. 그 delete storm이 정규 tiered storage 쓰기와 경쟁해 remote 쓰기가 느려졌고 브로커 로컬 디스크 사용률이 빠르게 올랐다. 문제는 S3에 용량이 없어서가 아니라 큰 운영 변경이 control plane·data plane API 활동의 버스트를 만들어 안정적 remote copy 경로를 방해했다는 것
- ④ 인시던트 중 스토리지 스케일링 cooldown — 디스크가 차면 디스크를 추가하는 게 자연스럽지만 스토리지 수정은 관리형 연산이라 다음 변경까지 cooldown 윈도우가 생길 수 있다. 첫 확장이 너무 작으면 cooldown이 끝나기 전에 또 확장해야 하고 복구가 길어진다. 교훈: 위기 중에 작고 점진적인 스토리지 증가를 하지 마라. 압박 하에 확장해야 한다면 전체 cooldown 윈도우와 예상 백로그를 견딜 만큼 충분히 추가하는 것이 안전하다
- ⑤ AZ 장애와 관리형 control plane 동작 — 모든 클러스터가 멀티 AZ 배포라 한 존에 문제가 생겨도 데이터 plane이 계속 운영되기를 기대했다. 그런데 데이터 plane만이 의존성이 아니었다 — control plane이 부정확한 상태를 보고해 모든 서비스를 저하시켰다. 존 장애로 클러스터가 healing 중이었고 모든 변경이 차단되어, 브로커 재부팅·클러스터 스케일·디스크 증가·일부 자동 reassignment를 정확히 그 통제가 가장 필요한 순간에 할 수 없었다. 관리형 서비스가 일상 운영 부담을 줄이지만 핵심 복구 워크플로는 여전히 control plane 불가용성을 고려해야 한다
- ⑥ 스케일링은 느리고 스토리지는 늘기만 한다 — MSK 스케일링은 즉시가 아니고 대칭적이지도 않다. 브로커 스토리지를 늘리면 나중에 줄일 수 없다 — 작은 디스크 footprint로 돌아가는 유일한 실질적 방법은 원하는 크기의 새 클러스터를 만들고 producer·consumer를 재라우팅하는 것. 브로커 인스턴스 업그레이드는 rolling restart가 필요해 브로커당 약 15분이라 큰 클러스터에서 몇 시간이 걸려 인시던트 완화책으로 부적합. 브로커 추가로 scale out해도 파티션·리더가 새 브로커로 rebalance되기 전까지는 과부하 브로커가 같은 트래픽을 계속 진다. rebalancing 자체가 네트워크·디스크·복제 부하를 더하므로 가장 심각한 실패 모드에서 인시던트 중 scale out은 현실적 복구 옵션이 아니었다
- 하드닝 7가지:
- 브로커 레벨 병목 중심 capacity planning — 집계 클러스터 처리량이 아니라 브로커별 피크 ingress·egress·EBS 처리량·CPU·파티션 배치·remote copy 동작으로 계획. 인시던트는 보통 몇몇 브로커가 hot해지면서 시작된다. CPU 활용률이 아니라 네트워크·EBS headroom으로 인스턴스 선택, 스파이크 중에도 백그라운드 작업이 계속되도록 지속 CPU를 보수적 임계 아래로 유지, 브로커별
BytesIn/BytesOut·디스크 증가·remote copy lag·파티션 skew·consumer lag을 함께 모니터링. 가장 중요한 변화는 문화적이었다 — 사용하지 않는 브로커 용량을 낭비로 취급하기를 멈췄다. 이렇게 중요한 시스템에서 headroom은 신뢰성 기능이다 - 의도적 네트워크 한계 과프로비저닝 — 높게 추측하라는 게 아니라 문서화·관측된 브로커별 한계를 하드 경계로 취급하고 정상 피크가 그 한계의 안전한 일부만 쓰도록 클러스터를 설계하라는 것. 네트워크가 병목이면 더 큰 브로커 shape 사용(CPU가 낮아 보여도 네트워크·EBS가 포화에 가까우면 인스턴스가 여전히 작다), AZ 배수로 scale out, 파티션·리더를 지속 균형화(총 용량이 고갈되기 전에 hot 파티션이 클러스터를 무너뜨릴 수 있다. round robin 할당을 쓰지만 keyed 파티션은 잦은 rebalancing이 필요), quota로 공유 브로커 예산 보호
- 더 안전한 tiered storage 운영 — 보존·세그먼트 관리 변경을 프로덕션 리스크 연산으로 취급. 적절한 곳에서 더 적고 큰 세그먼트 파일 생성, 운영 변경 stagger, 보존 조정 중 remote copy 지표 면밀 관찰, 일시적 S3 offload 지연을 견딜 비상 버퍼를 로컬 디스크에 확보. 원칙: remote 스토리지는 steady state 비용을 줄이고 로컬 스토리지는 운영 충격 흡수를 제공해야 한다
- ingress rate limiting과 quarantine — 테넌트·토픽·producer·트래픽 클래스별 동적 제한을 Kafka에 도달하기 전에 강제하고, 잘못된 형식이거나 반복 실패하는 트래픽은 hot path로 무한 재시도하는 대신 안전한 보류 경로로 우회. 모든 스파이크를 Kafka가 흡수하게 요청하는 대신 공유 브로커 용량을 위협하기 전에 shed·slow·quarantine
- noisy neighbor용 Kafka client quota — producer·consumer 대역폭 예산 강제. quota는 비용 통제 메커니즘일 뿐 아니라 이 규모에서는 blast radius 통제다
- 샤딩과 failover 클러스터 — 큰 클러스터를 여러 shard로 나눠 blast radius 축소. 관리형 control plane이 불건강할 때 복구를 기다리면 장애가 길어지므로 “failover cluster” 런북을 만들었다: ① Shard Fail over(영향받은 shard에서 건강한 shard로 라우팅 갱신, 대상 shard 용량 안에서 처리하도록 rate limit·quarantine 조정, ETA ~15분) ② Prepare(새 클러스터 프로비저닝, 토픽·tiered storage·접근 통제·클라이언트 설정에 IaC 템플릿 적용, ETA ~2시간) ③ Evaluate(깨끗한 환경이 병렬로 준비되는 동안 primary 완화 계속) ④ Cluster Fail over(결정 임계를 넘어서도 primary가 불건강하면 라우팅 갱신). on-call에게 탈출구를 준다 — primary가 관리형 서비스 연산 뒤에 갇혀 있어도 복구가 그 control plane의 회복에 전적으로 의존하지 않는다
- compliant 리전 failover를 위한 companion region — 데이터 residency 요구를 위반하며 지역을 옮길 수 없으므로 같은 컴플라이언스 경계 안에 머무는 사전 승인된 리전 존을 확립. DR을 즉흥적 결정에서 사전 검증된 경로로 바꾼다
- 브로커 레벨 병목 중심 capacity planning — 집계 클러스터 처리량이 아니라 브로커별 피크 ingress·egress·EBS 처리량·CPU·파티션 배치·remote copy 동작으로 계획. 인시던트는 보통 몇몇 브로커가 hot해지면서 시작된다. CPU 활용률이 아니라 네트워크·EBS headroom으로 인스턴스 선택, 스파이크 중에도 백그라운드 작업이 계속되도록 지속 CPU를 보수적 임계 아래로 유지, 브로커별
주요 수치 / 사실
- 일 220억 → 1,500억 이벤트 인제스트, 2,250억 이벤트 이상 전달 (평균 초당 168만, 피크 초당 320만 초과). Kinesis는 200~300억 이벤트까지 zero-ops로 잘 서빙했다
- 브로커 인스턴스 업그레이드 rolling restart 브로커당 약 15분
- 적용 가능한 교훈: managed ≠ unlimited / 평균 브로커가 아니라 가장 hot한 브로커를 기준으로 계획하라 — 파티션 skew와 hot leader가 인시던트가 시작되는 지점이다 / tiered storage는 비용을 줄이지 운영 책임을 줄이지 않는다 / 작은 비상 스토리지 증가를 하지 마라 / rate limit과 quota는 신뢰성 도구다 / 실패 중인 control plane에 의존하지 않는 failover 경로를 가져라
- 결론: 이 규모의 신뢰성은 하나의 기능이나 서비스가 아니라 보수적 capacity planning, 명시적 blast radius 통제, 테스트된 복구 경로, 그리고 모든 실패에서 배우려는 의지의 조합이다
관련 위키
- Real-Time Stream Processing
- Distributed Systems Reliability
- Large-Scale Data Migration
- Data Engineering FinOps
- Object Storage Evolution
- Silent Failures and Data Integrity
Source: 원문 보기