Ken
원문: How Netflix optimized its petabyte-scale logging system with ClickHouse
"At Netflix, scale drives everything." — Daniel Muino, Software Engineer at Netflix
190개국 3억 명 이상의 가입자에게 서비스를 제공하는 Netflix는, 4만 개가 넘는 마이크로서비스가 만들어내는 로그를 ClickHouse로 처리합니다. 이 글에서는 Netflix의 로깅 시스템이 어떻게 페타바이트 규모에서 "초 단위로 검색 가능한" 상태를 유지하는지, 그 세 가지 핵심 최적화를 살펴봅니다.
규모의 문제
Netflix 로깅 시스템의 단일 최대 네임스페이스 기준:
- 하루 5 페타바이트의 로그 수집
- 평균 초당 1,060만 이벤트 처리 (최대 1,250만, 그 이상도 가능)
- 이벤트당 평균 크기 약 5 KB
- 마이크로서비스에 따라 2주 ~ 2년 보존
- 쓰기 중심이지만, 그럼에도 초당 500~1,000 쿼리 처리
아키텍처 개요
Netflix의 로깅 구조는 "심플하지만 모든 결정에 신중함이 필요하다":
- 마이크로서비스 → 경량 sidecar가 로그를 ingestion 클러스터로 전달
- ingestion 클러스터 → Amazon S3에 기록 + Amazon Kinesis로 메시지 발행
- 중앙 hub 애플리케이션 → 데이터를 네임스페이스별로 분리해 적절한 스토리지 티어에 저장
Hot tier vs. Cold tier
- Hot tier: ClickHouse — 최근 로그, 속도가 중요한 인터랙티브 디버깅 담당
- Cold tier: Apache Iceberg — 장기 보존, 비용 효율적인 대규모 스캔용
- 그 위에 통합 Query API — 어떤 네임스페이스를 검색할지 자동 판단
사용자 경험
- 로그가 생성된 후 약 20초 이내 검색 가능 (5분 SLA를 훨씬 상회)
- 일부 케이스는 2초 지연 라이브 스트리밍 가능
- JSON payload 확장, fingerprint hash로 수백만 메시지 그룹핑, 주변 로그 드릴다운 모두 인터랙티브
최적화 1: Ingestion — Fingerprinting
수많은 유사 로그를 하나의 패턴으로 묶는 fingerprinting이 인덱싱과 디버깅의 핵심.
시도 1: ML 분류
- 이론적으로는 동작
- 현실: "매우 비싸고 느리며, 제품을 망친다"
시도 2: 정규표현식
- 패턴 매칭 후 값들을 제네릭 토큰으로 치환
- 도움은 됐지만 초당 1,000만 이벤트는 따라가지 못함
시도 3: JFlex 기반 Generated Lexer
Daniel은 이렇게 말합니다:
"Recognizing entities from raw text is something compilers have been doing for a long time. It's basically the same problem with the same solution—you need a lexer."
- 패턴을 효율적인 코드로 컴파일
- 런타임의 복잡한 regex 평가 없음
- 처리량 8~10배 증가
- 평균 fingerprinting 시간 216μs → 23μs
- P99 지연도 크게 개선
최적화 2: Hub — Serialization
Fingerprinted된 로그를 초당 수백만 건씩 ClickHouse로 "쓰는" 과정이 다음 병목.
시도 1: JDBC Batch Insert
- 간단하지만 비효율
- 모든 prepared statement마다 schema/serialization 협상 발생
- 확장성에 한계
시도 2: RowBinary 포맷 (Java client low-level)
- 컬럼 단위로 직접 직렬화
- map 길이 쓰기, DateTime64를 epoch ns로 인코딩 등 수작업 필요
- 큰 성능 향상이 있었지만 여전히 "왜 이렇게 많은 작업과 메모리를 쓰는가?"
시도 3: Native Protocol — Go client 리버스 엔지니어링
- ClickHouse 블로그의 포맷 벤치마크에서 native protocol이 RowBinary보다 일관되게 빠름을 확인
- 문제: 당시 Java 클라이언트는 native protocol을 지원하지 않음 → Go 클라이언트만 지원
- 해결: Go 클라이언트를 리버스 엔지니어링해서 자체 인코더 구축
- LZ4 압축 블록을 native protocol로 ClickHouse에 직접 전송
- 결과: CPU·메모리 효율 개선, 처리량은 RowBinary와 같거나 더 좋음
최적화 3: Queries — Custom Tags
Netflix 엔지니어들은 각 로그 이벤트의 동적 key-value tag(마이크로서비스명, 요청 ID, 커스텀 속성 등)에 크게 의존합니다.
"Custom tags are a huge problem for us. They are by far the most expensive query that is commonly used by our users." — Daniel
원래 구조의 문제
- Tag는
Map(String, String)으로 저장 - ClickHouse 내부적으로는 두 개의 병렬 배열(key, value) 로 구현
- 모든 lookup은 배열을 선형 스캔
- Netflix 규모: 시간당 unique 키 최대 2.5만 개, unique 값은 수천만 개
해결책 모색
- ClickHouse 창시자 Alexei Milovidov와 직접 논의
- 제안: LowCardinality 타입 사용
- 키에는 잘 동작
- 값은 cardinality가 너무 높아 부적합 → 반쪽짜리 해결
최종 해결책: Map을 Shard로 분할
놀라울 정도로 단순한 해법:
- Tag key를 해싱해 31개의 작은 맵으로 분산
- 쿼리 시점에 해당 shard로 바로 점프 → 전체 키 스캔 불필요
결과
- 필터링 쿼리: 3초 → 1.3초
- 필터 + projection 쿼리: ~3초 → 700ms 미만
- 스캔 데이터 양: 5~8배 감소
단순함의 미학
Daniel은 자신들의 성공을 "영리한 트릭"보다는 규율 있는 엔지니어링의 결과로 봅니다.
"The key is more about how you simplify things in order to do the least amount of work."
세 가지 최적화 — fingerprinting 재설계, serialization 재작성, 쿼리 재구성 — 의 공통점은 모두 "가능한 한 적게 일하는 방향으로 단순화"였습니다.
교훈과 시사점
Hot/Cold 티어 분리는 페타바이트 시대의 표준
ClickHouse(hot) + Iceberg(cold) 조합은 비용과 성능의 균형을 맞추는 검증된 패턴이며, Netflix는 이를 "통합 query API" 위에서 엔지니어에게 투명하게 제공합니다.
오픈소스의 진짜 가치 — 리버스 엔지니어링과 직접 기여
Java 클라이언트가 native protocol을 지원하지 않자, Netflix는 Go 클라이언트를 "리버스 엔지니어링"해서 자체 인코더를 만들었습니다. 소스 코드와 다른 언어 구현체에 접근 가능한 환경이 아니었다면 불가능했을 일입니다.
창시자와의 직접 소통
Daniel이 Alexei Milovidov와 직접 이야기하며 LowCardinality, shard map 같은 해법을 도출한 과정은 활성 오픈소스 커뮤니티가 어떤 가치를 만들어내는지를 보여줍니다.
결론
Netflix의 사례는 "가장 큰 ClickHouse 배포 중 하나"가 어떻게 만들어졌는지를 보여줍니다. 그 답은 마법이 아니라 fingerprinting → serialization → query 최적화로 이어지는 정직한 엔지니어링이었습니다.
전 세계 3억 명의 시청자가 끊김 없이 Netflix를 즐길 수 있는 배경에는, 매일 5PB의 로그를 초 단위로 검색 가능하게 만드는 ClickHouse 기반 시스템이 있습니다.