Control Plane Cardinality
“병목은 얼마나 많은 데이터를 담을 수 있느냐가 아니었다. 얼마나 많은 것을 기억해야 하느냐였다.”
핵심 개념
분산 데이터 시스템의 스케일링을 이야기할 때 대개 데이터 플레인 — 얼마나 저장하고, 얼마나 인제스트하고, 얼마나 빨리 쿼리하는가 — 을 본다. 그런데 극단적 규모에서 먼저 무너지는 것은 종종 컨트롤 플레인이다.
증상이 스토리지 문제처럼 보이지 않는다는 것이 진단을 어렵게 만든다:
- 서버 용량은 충분하다
- 쿼리 실행 자체는 계속 확장된다
- 그런데 데이터 주변의 운영 작업이 점진적으로 느려진다
공통 원인은 하나다: 매 연산마다 너무 많은 개별 컨트롤 플레인 객체가 처리되고 있다.
데이터 서버를 더 추가해도 해결되지 않는다. 병목이 데이터를 저장하는 서버의 용량이 아니기 때문이다.
왜 세밀함이 비용이 되는가
독립적으로 관리 가능한 작은 단위는 좋은 설계다. Pinot의 segment(불변, 자기 완결적 컬럼형 단위)가 그렇고, Iceberg의 data file이 그렇다. 그 단위 덕분에 배치·복제, 쿼리 라우팅·pruning, 병렬 실행, 보존 관리가 깔끔해진다.
하지만 독립성에는 비용이 있다 — 모든 단위가 컨트롤 플레인이 알아야 하는 객체다.
Pinot 기준으로 하나의 segment마다:
- 컨트롤러가 어디 살아야 하는지 추적
- Helix가 desired state와 actual state를 조정
- ZooKeeper가 메타데이터 저장
- 브로커가 라우팅 결정에 사용
평범한 규모에서는 이 오버헤드가 클러스터가 하는 일에 비해 작다. 아주 높은 카디널리티에서는 그 관계가 역전된다.
실측 — 메타데이터 실링
| 지표 | 값 |
|---|---|
| 약 100만 세그먼트 | IdealState 갱신 약 25초 |
| 약 150만 세그먼트 | 전체 Helix reconciliation 2분 초과 |
| 컨트롤러 CPU | ZooKeeper 역직렬화 ~40% + GC ~30% |
| 컨트롤러 메모리 할당 | ZooKeeper 역직렬화 ~65% |
나타나는 방식: 인제스션이 뒤처짐 / 컨트롤러 CPU·메모리 상승 / 서버 복구가 느려짐(재시작 시 Helix가 방대한 state transition 조율) / ZooKeeper 스냅샷 크기와 write 볼륨 증가.
왜 지금 흔해지는가 — 두 워크로드
극단적 실시간 인제스션
초당 1억 이벤트 + 분 미만 freshness + 30일 보존 → 하루 약 30만 개 새 세그먼트, 약 1천만 개 라이브 세그먼트. 서버는 감당하는데 메타데이터 조율이 더 어려운 문제가 된다.
대형 open table format 데이터셋
완전히 다른 방향에서 같은 문제. Iceberg 테이블의 파일 메타데이터가 자연스럽게 Pinot 세그먼트로 매핑되므로 500만 파일 Iceberg 테이블 = 500만 세그먼트 Pinot 테이블.
⚠️ 결정적 차이: 높은 카디널리티에 점진적으로 다가가는 게 아니라 첫날부터 거기서 시작한다.
흔한 대응과 그 한계
더 적고 큰 단위 (compaction/merge)
가장 먼저 고려할 레버이고 종종 옳다. 하지만:
- 물리적 병합은 데이터 재작성을 요구하므로 비용이 데이터 볼륨과 함께 커진다
- 큰 단위는 pruning 정밀도를 낮춘다 — 더 넓은 시간 범위를 커버하면 시간 필터로 제거될 가능성이 낮아진다
compaction은 물리 레이아웃 자체를 단순화할 수 있을 때 잘 작동한다. 기존의 세밀한 레이아웃이 유용하고 메타데이터 카디널리티만 문제일 때는 덜 매력적이다.
테이블/클러스터 쪼개기
일부 압력을 줄인다 (컨트롤러 GC ~30% → ~15%). 하지만 총 메타데이터 양을 줄이지 않는다. 게다가 역효과가 있다:
하나의 큰 IdealState를 여러 작은 것으로 쪼개면 commit batching의 효과가 떨어진다 — 10개 테이블로 쪼개니 같은 생성률에서 초당 IdealState 갱신이 약 5배가 되었다.
⚠️ 그리고 의미 있는 아키텍처 경계를 나타내서가 아니라 주로 메타데이터 임계 아래로 유지하려고 테이블을 만든다면 그 자체로 운영 복잡도다.
해법의 형태 — 저장 단위와 조율 단위를 분리하라
물리 단위를 크게 만드는 대신 컨트롤 플레인의 표현을 크게 만든다.
Pinot의 Segment Group이 이 패턴의 구현이다. 약 100개 물리 세그먼트의 메타데이터를 컨트롤 플레인 관점에서 단일 객체로 결합하되 기저 세그먼트는 병합하지 않는다.
컨트롤 플레인: [group] [group] [group] ← 10만 개
│ │ │
데이터 플레인: [seg×100][seg×100][seg×100] ← 1천만 개
grouping factor ~100 → 카디널리티 약 2자릿수 감소, 데이터 재작성 없이.
물리적 granularity와 조율 granularity의 분리가 핵심 아이디어다.
이 패턴이 성립하려면 — 네 가지 불변식
추상화 레이어를 하나 더 넣는 것이므로, 효과가 컨트롤 플레인에 격리되는지를 하나씩 확인해야 한다.
| 질문 | 요구되는 성질 |
|---|---|
| 데이터가 움직이나? | ❌ 아니어야 한다. 서버가 그룹을 구성 단위로 다시 펼쳐 개별적으로 로드·쿼리 |
| 전환이 원자적인가? | 쿼리가 두 표현을 동시에 보아서도, 둘 다 못 보아서도 안 된다. Pinot은 기존 segment lineage 기계를 재사용 — 그룹의 모든 replica가 온라인이 되면 라우팅 전환, 그다음 옛 메타데이터 퇴역 |
| 복구가 비싸지나? | ❌ 가장 싼 유효한 소스를 쓴다 — 기대 CRC로 이미 로드되어 있으면 재등록, 로컬에 있으면 디스크에서, 진짜 없는 것만 deep storage에서. 흔한 경우 데이터 이동이 아니라 메타데이터 작업 |
| pruning 정밀도를 잃나? | 부분적으로만. 브로커 pruning은 첫 단계일 뿐이다. 브로커는 그룹 단위로 fan-out을 줄이고, 서버는 그룹을 펼친 뒤 시간 범위·파티션으로 세그먼트 단위 정밀 pruning을 유지 |
하위 호환성 디테일도 중요하다: Pinot에서 약 100개 per-segment znode가 그룹 znode + members 자식 하나로 붕괴하되, 테이블 리스팅은 여전히 같은 디렉토리 shape를 반환한다 — members 자식이 iteration에 보이지 않아 기존 소비자가 전부 계속 작동한다.
스냅샷 시맨틱과의 상호작용
Iceberg 같은 포맷을 얹으면 요구가 하나 더 붙는다 — 스냅샷에 고정된 쿼리는 파일이 추가·제거되는 중에도 그 스냅샷의 파일 집합을 정확히 봐야 한다.
Pinot의 해법:
- 그룹이 불변이고 스냅샷에 스코프된다. 파일 집합이 바뀌면 옛 그룹 옆에 새 그룹을 만들고, 스냅샷 manifest가 어떤 그룹을 쓸지 결정
- 그룹 이름이 멤버 집합 기반 content-addressed → 같은 집합 재생성이 idempotent하고 바뀌지 않은 파티션은 스냅샷 간에 그룹을 재사용
- 물리 파일은 참조로 공유되고 살아 있는 그룹이 없을 때만 회수
이 세 성질(불변 + content-addressed + refcount)은 Compaction Conflict Resolution의 compaction map이 같은 계열의 문제를 푸는 방식과 닮았다 — 둘 다 “물리 레이아웃을 건드리지 않고 메타데이터 레이어에서 문제를 해결” 한다.
진단 질문
물리적 데이터 조각이 너무 많은 것인가, 아니면 컨트롤 플레인이 추적해야 할 것이 단순히 너무 많은 것인가?
물리적 granularity가 여전히 유용하다면 데이터 레이아웃 자체를 바꾸는 것보다 조율 단위를 바꾸는 것이 더 나은 답일 수 있다.
이 패턴을 고려할 신호:
- 단위 수가 수백만에 이를 것으로 예상
- 인제스션은 계산상 관리 가능한데 컨트롤러 조율이 뒤처지기 시작
- 서버 재시작·복구 시간이 크게 증가
- 컨트롤러 CPU·GC·메모리가 점점 클러스터 메타데이터 처리에 감
- 수백만 파일의 open table format 데이터셋 온보딩
- ⚠️ 큰 단위가 다른 이유로 바람직해서가 아니라 주로 메타데이터를 통제하려고 물리 크기를 키우고 있음
- ⚠️ 스케일링 임계 아래로 유지하려는 목적만으로 인위적 테이블·클러스터 경계를 만들고 있음
마지막 두 개가 특히 좋은 신호다 — 물리 레이아웃을 메타데이터 제약에 맞춰 왜곡하고 있다면, 그것이 바로 조율 단위를 분리할 때다.
연관 개념
- Distributed SQL Engine Operations — Pinot·Druid 계열의 운영
- Partition Sizing and Management — small-file 문제와 물리 단위 크기의 트레이드오프
- Catalog-Managed Tables — Iceberg 메타데이터가 카디널리티를 수입하는 경로
- Compaction Conflict Resolution — 메타데이터 레이어에서 해결하는 같은 계열의 문제
- Real-Time Stream Processing — 고빈도 인제스션이 카디널리티를 만든다
- Lake Point Query Indexing
- Distributed Systems Reliability
- Query Optimization