TimeSeries Tiered Storage Journey: Kafka/Flink Streams to Native Cassandra Cold Reads
Author: Netflix Technology Blog | Source: netflixtechblog.medium.com | Published: 2026-08-04
한 줄 요약
Kafka/Flink로 S3에 스트리밍하는 cold tier가 실제 절감을 만들었지만 스트리밍 인프라 자체가 비쌌고, “이미 있는 Cassandra 백업에서 직접 읽으면 어떨까” 라는 급진적 질문이 pluggable memtable을 이용한 네이티브 cold tier로 이어졌다.
핵심 주장/내용
- 문제 — 복합적으로 쌓이는 도전: 보존 요구 증가(사용자가 더 넓은 유스케이스를 위해 보존을 늘리길 원하고 그게 스토리지 비용을 올린다), 비용 급등(자주 접근되지 않는 히스토리 데이터를 위해 거대하고 밀도 높은 클러스터를 유지하는 것이 금지적으로 비싸다), 희소한 히스토리 접근(대부분의 쿼리가 최근 이벤트를 타겟하지만 오래된 데이터도 가끔의 감사·조사를 위해 즉시 가용해야 한다), 운영 부담(hot 노드의 높은 스토리지 밀도가 스케일링·배포·클러스터 안정성을 복잡하게), 레이턴시 요구(히스토리 데이터조차 내부 도구·분석을 위해 sub-second 성능이 필요할 때가 있다)
- TimeSeries Abstraction의 Time Slice: 데이터셋을 시간 윈도우를 커버하는 여러 테이블로 파티션한다. 이것이 여러 최적화를 가능하게 하는데 가장 중요한 것은 테이블이 쓰기 경계를 넘으면 immutable해질 수 있다는 것이고, 그러면 안전하게 추가 최적화를 적용할 수 있다. slice 정보는 전용 메타데이터 테이블에 유지되고 control plane이 새 slice 생성과 메타데이터 유지를 담당, data plane이 그것으로 read/write 트래픽을 올바른 테이블로 라우팅
- Solution 1 — Kafka/Flink 스트리밍: Cassandra에 영속화하면서 동시에 실시간으로 S3에 스트리밍
- 효율적으로 S3에 넣기: Time Series ID를 버킷으로 hash-partition해 스트림을 나누고 전용 Flink task가 각 버킷을 처리해 S3 쓰기 처리량을 배치·감소. Flink 워커가 이벤트 시각 기준 시간 단위로 그룹화해 Parquet 파일로 배치. 고처리량 스트리밍 쓰기에는 좋지만 저지연 읽기에는 좋지 않다
- Compaction 잡이 읽기를 최적화: 매일 돌며 작은 Parquet 파일들을 네임스페이스당 하나의 최적화된 일별 파일로 병합. 추가 최적화 3가지 —
- 작은 row group (4~8MB) 으로 조회 비용 최소화. Parquet Java 라이브러리로 S3에서 row group의 range read 수행. 의도적으로 Parquet 권장 row group 하한보다 훨씬 아래로 잡았는데, 읽기가 full scan이 아니라 point/range 조회에 지배되기 때문에 작은 그룹이 더 나았다
- row group 내 이벤트를 시간순 정렬해 시간 범위 질의 시 brute-force 스캔 제거
- Bloom filter와 파일 메타데이터를 EVCache에 캐싱해 쿼리 플래너가 S3에서 불필요한 읽기를 효율적으로 건너뛰게
- Cold Tier Data Plane이 compaction 메타데이터로 공격적 플래닝·프루닝을 해 S3 쿼리 fan-out을 극적으로 줄인다. 결과적으로 하루치 전체 데이터를 가져오는 데 (ID가 두 row group에 걸치는 드문 경우에도) 최대 3번의 순차 S3 read만 필요
- 왜 여기서 멈추지 않았나: 스트리밍 cold tier가 Cassandra의 EBS footprint와 S3의 Cassandra 백업 footprint를 모두 줄이는 실제 절감을 냈다. 하지만 스트리밍 인프라 자체가 비싸고 팀의 on-call 범위를 넓히는 움직이는 부품을 더한다. 두 질문 — 오브젝트 스토어로 데이터를 스트리밍하는 것과 그에 따른 데이터 전송 비용을 피할 수 있나? 새 컴포넌트의 컴퓨트·운영 오버헤드를 줄일 수 있나? → 이미 있는 S3의 Cassandra 백업에서 직접 읽을 수 있나?
- Solution 2 — 불변 데이터를 위한 Cassandra Cold Storage: Netflix는 DR을 위해 정기적으로 Cassandra 데이터를 S3에 백업한다. 발상은 단순하다 — 별도 S3 데이터 전송 비용을 내는 대신 그 백업에서 직접 읽자. TimeSeries가 hot Time Slice를 cold 준비 완료로 표시하면 Cassandra를 커스터마이즈해 백업에서 직접 읽게 하고 on-disk 데이터 복사본을 삭제할 수 있다. TimeSeries read path 변경도 최소
- ReadOnlyCompactionStrategy (ROCS): slice가 cold-tier 경계를 넘으면 TimeSeries가 쓰기를 멈춰 immutable해진다. 이 불변성이 최적화 기회를 연다 — 표준 전략(LCS, TWCS)은 종종 토큰 공간에서 겹치는 SSTable을 만들어 단일 파티션 키가 여러 파일에 존재할 수 있고, read가 키의 토큰 범위를 커버하는 모든 SSTable을 건드려야 하므로 단일 point-read가 여러 조회가 되어 모든 티어에서 레이턴시가 오른다. ROCS는 모든 SSTable을 한 번 재compact해 토큰 범위와 정확히 정렬시킨다
- ROCS는 모든 TimeSeries 유스케이스에 더 넓게 적용되어 read 시 SSTable fan-out을 줄임으로써 모든 스토리지 티어의 tail latency를 낮춘다
- 백업 활용: Cassandra가 클러스터의 모든 노드를 백업하며 S3에 선언적 manifest + 키스페이스 테이블을 뒷받침하는 SSTable로 조직. manifest가 모든 SSTable 경로를 나열하고, 큰
Data.db외에 index·메타데이터·무결성 파일의 compact한 집합이 포함된다. 이 구조가 전체 데이터셋을 스캔하지 않고 특정 파티션·범위를 빠르게 위치 확인·검증·복원할 수 있게 한다. manifest가 모든 S3 경로를 카탈로그하므로Data.db를 제외한 대부분의 컴포넌트를 Cassandra 노드에 직접 다운로드할 수 있고 무시할 만한 공간을 차지하며, S3에서 데이터를 읽기 전에 효율적 쿼리 플래닝을 가능하게 한다 - pluggable memtable로 백업에서 읽기: Cassandra read는 보통 두 소스(최근 미flush 쓰기를 담은 memtable, durable 스토리지의 flush된 SSTable)에서 온다. Cassandra 4.1의 pluggable memtable로 “cold” 테이블의 read를 S3의 백업 스토리지로 직접 리디렉트할 수 있다 — 테이블 정의에
memtable = 'backupmemtable:timestamp=...'. 타임스탬프는 백업이 테이블 전체를 커버함이 보장되고 테이블이 immutable함이 보장되는 시점을 표시- backup memtable을 열면 상태 기계 초기화가 시작되어 S3에서 index 데이터를 노드로 한 번 끌어온다. resilience 메커니즘: 모든 S3 연산·파일시스템 캐시 쓰기에 지수 백오프 재시도, 정상 Cassandra 연산과 격리된 전용 스레드 풀, 부분 다운로드 보호(다운로드된 컴포넌트 크기가 manifest와 안 맞으면 삭제 후 재다운로드), DDL 변경 보호(
.timestamp파일이 완료된 초기화를 표시하고 백업 타임스탬프에 묶임 — 타임스탬프가 바뀌면 파일이 제거되고 새 타임스탬프로 초기화 재시작), read 보호(초기화가 비동기이고 초기화 중인 memtable로의 read는 최대 read RPC 타임아웃의 절반 동안 블록) - 레이턴시 최소화를 위해 요청·SSTable·개별 청크에 걸쳐 S3 read를 병렬화하고, 공유 캐시가 fetch를 중복 제거하고 prefetch해 청크가 필요하기 전에 준비되게 한다
- S3 호출 수를 줄이는 설정: 더 큰 compression chunk(파티션당 S3 호출을 줄이되 작은 read에서 over-fetch 수용), read repair 비활성화(불변·사전 repair된 백업 데이터에 대한 비싸고 불필요한 repair 트래픽 회피), speculative retry 비활성화(S3의 높은 레이턴시가 중복 read를 촉발하는 것 방지)
- backup memtable을 열면 상태 기계 초기화가 시작되어 S3에서 index 데이터를 노드로 한 번 끌어온다. resilience 메커니즘: 모든 S3 연산·파일시스템 캐시 쓰기에 지수 백오프 재시도, 정상 Cassandra 연산과 격리된 전용 스레드 풀, 부분 다운로드 보호(다운로드된 컴포넌트 크기가 manifest와 안 맞으면 삭제 후 재다운로드), DDL 변경 보호(
- 언제 백업에서 읽어도 안전한지 아는 법 — Virtual Table: ring 전체에서 두 가지를 확인해야 한다. ROCS 재compaction이 성공했는지(
system_views.sstable_tasks로 활성 compaction이 없음을 확인)와 백업이 완전히 캡처됐는지(complete_backups에서uploaded=True) - 추가 부하 처리 — 격리된 cold 클러스터: S3에서 데이터를 서빙하는 것은 로컬 스토리지 read보다 훨씬 CPU 집약적이다. read 요청의 급격한 스파이크가 Cassandra 클러스터를 압도해 hot path에 영향을 줄 수 있고, 비용 절감 기능을 신뢰성 리스크로 바꿔버린다. 그래서 cold read를 hot 클러스터의 백업을 가리키는 별도 Cassandra 클러스터에서 선택적으로 돌릴 수 있게 했다. cold 클러스터는 원본과 토폴로지는 일치해야 하지만 하드웨어는 일치할 필요가 없어 훨씬 작고 싸고 스토리지가 가벼운 인스턴스로 돌릴 수 있다
- ReadOnlyCompactionStrategy (ROCS): slice가 cold-tier 경계를 넘으면 TimeSeries가 쓰기를 멈춰 immutable해진다. 이 불변성이 최적화 기회를 연다 — 표준 전략(LCS, TWCS)은 종종 토큰 공간에서 겹치는 SSTable을 만들어 단일 파티션 키가 여러 파일에 존재할 수 있고, read가 키의 토큰 범위를 커버하는 모든 SSTable을 건드려야 하므로 단일 point-read가 여러 조회가 되어 모든 티어에서 레이턴시가 오른다. ROCS는 모든 SSTable을 한 번 재compact해 토큰 범위와 정확히 정렬시킨다
주요 수치 / 사실
- Solution 1 성능: cold tier read p50 ~150ms, p99 ~500ms. 글로벌하게 cold 스토리지에서 초당 최대 1,250 read 처리. 가장 큰 데이터셋의 compaction 잡은 보통 약 12시간 소요 (실행 속도보다 낮은 컴퓨트 비용을 우선)
- Solution 2 성능: 가장 스토리지 집약적인 TimeSeries 클러스터에 롤아웃. 현재 Cassandra cold storage에 15PB 이상의 압축 데이터 저장. 디스크 사용률 80% → 7% 로 attached storage 대폭 감축. 어느 리전에서든 초당 수천 read 서빙. 이전 cold storage 구현 대비 p90 레이턴시 약 30% 개선(더 효율적인 S3 접근 패턴과 작은 read 크기 덕분)하면서 p99를 ~500ms 미만으로 유지. Cassandra가 chunk cache로 cold read 버스트를 이미 우아하게 처리하므로 이 설계가 S3 기반 cold tier에도 같은 resilience를 보존
- TimeSeries Abstraction이 Netflix 최대 데이터셋들의 운영 backbone이고 이제 여러 페타바이트에 걸쳐 있다. Netflix에서 비용 효율성은 일시적 이니셔티브가 아니라 지속적 규율
- 각 slice가 언제든 어떤 생명주기 단계에 있어도 시스템 가용성에 영향을 주지 않고, 시스템이 지연된 백업이나 뒤처진 compaction에 resilient하다
관련 위키
- Object Storage Evolution
- Partition Sizing and Management
- Data Engineering FinOps
- Real-Time Stream Processing
- Distributed Systems Reliability
- Lake Point Query Indexing
- Database Concurrency Control
Source: 원문 보기