Scaling Apache Pinot Beyond Millions of Segments (or Parquet Files)
Author: Krishan Goyal (StarTree) | Source: startree.ai | Published: 2026-08-28
한 줄 요약
병목은 Pinot이 담을 수 있는 데이터 양이 아니라 기억해야 하는 것의 개수였고, Segment Group은 물리 세그먼트를 병합하지 않고 컨트롤 플레인의 표현만 크게 만들어 — 즉 저장 단위가 아니라 조율 단위를 바꿔 — 카디널리티를 약 2자릿수 줄인다.
핵심 주장/내용
- 문제 — 데이터 플레인은 여유가 있는데 컨트롤 플레인이 병목: Pinot 테이블은 segment(불변, 자기 완결적, 데이터와 인덱스를 담은 컬럼형 단위)로 구성된다. 세그먼트는 서버 간 배치·복제, 쿼리 라우팅·pruning, 병렬 쿼리 실행, 보존·라이프사이클 관리, 인제스션·백그라운드 처리에 쓰인다
- 각 세그먼트를 독립적으로 관리할 수 있어서 그 모델이 아주 잘 작동한다. 하지만 독립성에는 비용이 있다 — 모든 세그먼트가 Pinot 컨트롤 플레인이 알아야 하는 객체다
- 컨트롤러는 어디 살아야 하는지 추적하고, Helix는 desired state와 actual state를 조정하고, ZooKeeper는 그것을 기술하는 메타데이터를 저장하고, 브로커는 쿼리 라우팅 결정에 세그먼트 정보를 쓴다
- 평범한 규모에서는 이 오버헤드가 클러스터가 하는 일에 비해 작다. 아주 높은 세그먼트 수에서는 그 관계가 역전될 수 있다
- 메타데이터 실링을 인식하는 법 — 증상이 스토리지 문제처럼 보이지 않는다: 서버 용량은 충분할 수 있고 쿼리 실행 자체는 계속 확장될 수 있다. 하지만 데이터 주변의 운영 작업이 점진적으로 느려진다
- 인제스션이 뒤처진다 — 컨트롤러가 새로 생성된 세그먼트를 필요한 인제스션 속도를 유지할 만큼 빠르게 조율하지 못한다
- 컨트롤러 CPU와 메모리가 오른다
- 서버 복구가 느려진다 — 서버를 다시 온라인으로 가져오려면 Helix가 아주 많은 세그먼트에 대한 state transition을 조율해야 한다
- ZooKeeper가 훨씬 많은 데이터를 처리한다 — 큰 클러스터 상태 구조를 반복적으로 읽고 직렬화하고 써서 스냅샷 크기와 write 볼륨이 증가
- 모든 증상의 공통 원인 하나: 매 연산마다 너무 많은 개별 컨트롤 플레인 객체가 처리되고 있다. 데이터 서버를 더 추가해도 해결되지 않는다 — 병목이 데이터를 저장하는 서버의 용량이 아니기 때문
- 왜 이 문제가 흔해지고 있나 — 두 워크로드 패턴:
- 극단적 규모의 실시간 인제스션 — 초당 1억 이벤트를 받으며 분 미만 freshness와 30일 보존을 유지하는 애플리케이션. 피크 인제스션에서 하루 약 30만 개 새 세그먼트를 만들고 약 1천만 개 라이브 세그먼트를 축적한다. Pinot 서버는 그 인제스션과 쿼리 워크로드를 감당하도록 확장할 수 있다. 더 어려운 문제는 그 모든 세그먼트에 연관된 메타데이터를 조율하는 것
- 대형 open table format 데이터셋 — Iceberg 같은 포맷에서 완전히 다른 방향으로 같은 문제가 나타난다. 큰 Iceberg 테이블은 수백만 개 Parquet 파일을 담을 수 있고, 저지연 쿼리를 위해 노출되면 파일 메타데이터가 자연스럽게 Pinot 세그먼트로 매핑된다. 500만 파일 Iceberg 테이블은 500만 세그먼트 Pinot 테이블로 삶을 시작한다
- 실시간 케이스와 달리 높은 세그먼트 수에 점진적으로 다가가는 게 아니다. 첫날부터 거기서 시작할 수 있다
- 두 패턴 모두 같은 아키텍처 질문을 가리킨다: 어떻게 세밀한 물리 데이터를 보존하면서 세밀한 컨트롤 플레인 조율을 피하는가?
- 아키텍처를 바꾸기 전에 할 수 있는 것 — 그리고 그 한계:
- 더 적고 큰 세그먼트 쓰기 — 많은 배포에서 첫 번째로 고려할 레버. Minion merge task가 작은 세그먼트들을 큰 것으로 compact해 컨트롤 플레인이 관리할 객체 수를 직접 줄인다
- 트레이드오프: 물리적 병합은 데이터 재작성을 요구하므로 비용이 데이터 볼륨과 함께 커진다. 그리고 큰 세그먼트는 pruning 정밀도를 낮출 수 있다 — 더 넓은 시간 범위를 커버하는 세그먼트는 시간 필터로 완전히 제거될 가능성이 낮다
- compaction은 물리 세그먼트 레이아웃 자체를 단순화할 수 있을 때 잘 작동한다. 기존의 세밀한 레이아웃이 유용하고 메타데이터 카디널리티만 문제일 때는 덜 매력적이다
- 아주 큰 테이블 쪼개기 — 큰 물리 테이블을 여러 테이블로 나눠 하나의 논리 테이블 뒤에 노출. 일부 컨트롤러 압력을 줄인다 (테스트에서 같은 세그먼트 수에서 컨트롤러 GC 시간이 약 30% → 약 15%)
- 하지만 클러스터가 궁극적으로 처리해야 하는 총 메타데이터 양을 줄이지 않는다. Helix는 여전히 클러스터 전체 ZooKeeper 상태를 본다. 게다가 하나의 큰 IdealState를 여러 작은 것으로 쪼개면 commit batching의 효과가 떨어진다 — 테스트에서 워크로드를 10개 물리 테이블에 쪼개니 같은 세그먼트 생성률에서 초당 IdealState 갱신이 약 5배가 되었다
- 테이블 샤딩은 추가 헤드룸을 살 수 있지만 세그먼트 수와 컨트롤 플레인 작업 사이의 관계를 근본적으로 바꾸지 않는다. 그리고 의미 있는 아키텍처 경계를 나타내서가 아니라 주로 메타데이터 임계 아래로 유지하려고 테이블을 만든다면 그 자체로 운영 복잡도를 도입한다
- 더 적고 큰 세그먼트 쓰기 — 많은 배포에서 첫 번째로 고려할 레버. Minion merge task가 작은 세그먼트들을 큰 것으로 compact해 컨트롤 플레인이 관리할 객체 수를 직접 줄인다
- Segment Groups — 저장 단위가 아니라 조율 단위를 바꾼다: 물리 세그먼트를 크게 만드는 대신 컨트롤 플레인의 표현을 크게 만든다. Segment Group은 설정 가능한 개수(전형적으로 약 100개)의 물리 세그먼트 메타데이터를 컨트롤 플레인 관점에서 단일 객체로 결합한다
- 기저 세그먼트는 병합되지 않는다. 이 구분이 결정적이다 — 컨트롤 플레인은 그룹 위에서 동작하고, 데이터 플레인은 개별 세그먼트 위에서 계속 동작한다
- grouping factor 약 100이면 1천만 물리 세그먼트를 담은 테이블이 1천만 개 개별 조율 객체 대신 약 10만 개 컨트롤 플레인 그룹으로 표현된다
- 메타데이터 그룹핑이 세그먼트 병합과 다른 점: 기저 세그먼트 파일이 제자리에 남고 / 대규모 물리 데이터 재작성이 필요 없고 / 결합된 물리 크기와 무관하게 수백 세그먼트를 그룹 하나로 표현할 수 있고 / 서버는 여전히 개별 세그먼트에 대해 정밀한 연산 가능 / 컨트롤 플레인 컴포넌트는 훨씬 적은 객체를 열거
- 실시간 테이블에서는 개별 세그먼트 생성 후 백그라운드 연산으로 그룹핑. Iceberg 테이블에서는 파일 메타데이터가 인제스트될 때 그룹을 형성해 대량 파일이 처음부터 더 적은 수의 컨트롤 플레인 객체로 표현된 채 진입
- 물리적 granularity와 조율 granularity의 분리가 Segment Groups의 핵심 아키텍처 아이디어
- 무엇이 바뀌고 무엇이 안 바뀌나 — 효과를 컨트롤 플레인에 격리하기:
- 물리 데이터는 바뀌지 않는다 — 서버가 결국 그룹을 구성 물리 세그먼트로 다시 펼쳐 개별적으로 로드하고 쿼리한다. 약 100개 per-segment znode가 그룹 znode + members 자식 하나로 붕괴하되, 테이블 리스팅은 여전히 같은 디렉토리 shape를 반환한다 — members 자식이 iteration에 보이지 않아 기존 소비자가 전부 계속 작동한다
- 그룹 생성은 원자적 — 개별 세그먼트가 그룹으로 대체될 때 쿼리는 두 표현을 동시에 보아서도, 둘 다 못 보아서도 안 된다. Pinot의 기존 segment lineage 기계를 쓴다 — 그룹이 준비될 때까지 기존 세그먼트 표현이 활성 상태로 남고, 그룹의 모든 replica가 온라인이 되면 라우팅이 그룹으로 전환되고, 그다음 standalone 세그먼트 메타데이터를 퇴역시킨다. 쿼리 관점에서 변화는 원자적
- 서버는 보통 데이터를 다시 다운로드할 필요가 없다 — 그룹을 온라인으로 가져올 때 각 멤버에 대해 가장 싼 유효한 소스를 쓴다: 기대 CRC로 이미 로드되어 있으면 재등록만, 파일이 로컬에 있으면 디스크에서 로드, 진짜 없는 파일만 deep storage에서 가져온다. 전환 중에는 같은 물리 세그먼트가 원래 standalone 표현과 새 그룹 표현 양쪽에서 일시적으로 참조될 수 있다. 흔한 경우 Segment Group 생성은 데이터 이동이 아니라 주로 메타데이터 작업
- 쿼리 pruning은 정밀함을 유지한다 — 그룹핑이 pruning의 한 단계를 거칠게 만든다. 브로커는 개별 세그먼트가 아니라 그룹 메타데이터를 보므로 그룹이 세그먼트 하나보다 넓은 시간 범위를 커버할 수 있다. 하지만 브로커 pruning은 첫 단계일 뿐이다. 서버가 요청을 받아 그룹을 펼친 뒤 시간 범위와 파티션 정보로 개별 세그먼트에 대해 정밀 pruning을 수행한다. 브로커는 그룹 단위로 fan-out을 줄이고, 서버는 세그먼트 단위 세밀 pruning을 유지한다. 물리적 쿼리 실행 모델은 여전히 세그먼트 지향
- Segment Groups와 Iceberg — 스냅샷 일관성 요구 추가: Iceberg 스냅샷에 고정된 쿼리는 파일이 추가·제거되는 중에도 그 스냅샷에 속한 파일 집합을 정확히 봐야 한다
- 따라서 Iceberg 기반 테이블에서 Segment Group은 불변이고 스냅샷에 스코프된다. 기저 파일 집합의 변화가 옛 그룹 옆에 새 그룹을 만들고, 스냅샷 manifest가 특정 쿼리가 어떤 그룹을 써야 하는지 결정한다
- 그룹 이름은 멤버 집합 기반 content-addressed다. 같은 집합을 재생성하는 것이 idempotent하고, 바뀌지 않은 파티션은 스냅샷 간에 기존 그룹을 재사용할 수 있다. 물리 파일도 참조로 공유되고 살아 있는 그룹이 참조하지 않을 때만 회수된다
- 이것이 수백만 Iceberg 파일을 수만 개 라우팅 가능한 컨트롤 플레인 그룹으로 표현하면서 스냅샷별 쿼리 시맨틱을 포기하지 않게 한다
주요 수치 / 사실
- 스케일 테스트 결과: 약 100만 세그먼트에서 IdealState 갱신이 약 25초. 약 150만 세그먼트에서 전체 Helix reconciliation 패스가 2분 초과
- 컨트롤러 리소스: ZooKeeper 역직렬화가 CPU 약 40%와 메모리 할당 약 65%를 소비, GC가 CPU 약 30%를 추가로 차지
- 워크로드 예: 초당 1억 이벤트 + 분 미만 freshness + 30일 보존 → 하루 약 30만 세그먼트, 약 1천만 라이브 세그먼트
- 500만 파일 Iceberg 테이블 = 500만 세그먼트 Pinot 테이블 (첫날부터)
- 테이블 쪼개기: 컨트롤러 GC 약 30% → 약 15%, 하지만 10개 테이블로 쪼개면 IdealState 갱신이 초당 약 5배
- grouping factor ~100 → 1천만 세그먼트가 약 10만 그룹으로. 컨트롤 플레인 카디널리티를 약 2자릿수 감소
- 언제 고려할까: 세그먼트 수가 수백만에 이를 것으로 예상 / 높은 인제스션률과 짧은 freshness가 계속 많은 세그먼트를 생성 / 인제스션은 계산상 관리 가능한데 컨트롤러 조율이 뒤처지기 시작 / 테이블이 커지며 서버 재시작·복구 시간이 크게 증가 / 컨트롤러 CPU·GC·메모리가 점점 클러스터 메타데이터 처리에 감 / 수백만 파일의 Iceberg 데이터셋 온보딩 / 큰 세그먼트가 다른 이유로 바람직해서가 아니라 주로 메타데이터를 통제하려고 물리 세그먼트 크기를 키우고 있음 / 컨트롤 플레인 스케일링 임계 아래로 유지하려는 목적만으로 인위적 테이블 경계를 만들고 있음
- 핵심 진단 질문: 물리적 데이터 조각이 너무 많은 것인가, 아니면 컨트롤 플레인이 추적해야 할 것이 단순히 너무 많은 것인가? 물리적 granularity가 여전히 유용하다면 데이터 레이아웃 자체를 바꾸는 것보다 조율 단위를 바꾸는 것이 더 나은 답일 수 있다
- 결론: 병목은 Pinot이 얼마나 많은 데이터를 담을 수 있느냐가 아니었다. 얼마나 많은 것을 기억해야 하느냐였다. Segment Group은 무엇이 “하나”로 카운트되는지를 바꾼다
관련 위키
- Distributed SQL Engine Operations
- Partition Sizing and Management
- Catalog-Managed Tables
- Compaction Conflict Resolution
- Real-Time Stream Processing
- Query Optimization
- Lake Point Query Indexing
- Distributed Systems Reliability
Source: 원문 보기