테이블당 12 GB/s를 가능하게 하는 아키텍처와 무한한 가능성에 대한 심층 분석
작성자: 알렉산다르 토미치, Victoria Bukta, Nikola Obradović, Danilo Najkov, Branko Grbić , Milos Milovanovic
텔레메트리 데이터는 어디에나 있습니다. 공장 생산 라인의 IoT 센서부터 대기를 관측하는 인공위성 군집, 초당 수천 개의 이벤트를 기록하는 자율주행 차량까지 다양합니다. 이 모든 시스템은 동일한 근본적인 문제를 안고 있습니다. 바로 쿼리가 가능한 곳에 저장되어야 하는 지속적이고 대용량인 시계열(time-series) 관측 데이터 스트림입니다. 이 시스템은 빠르고 안정적이어야 하며, 엔지니어링 팀이 일반적인 Kafka 기반 워크로드처럼 인프라를 튜닝하고 유지 관리하는 데 몇 주씩 시간을 허비하지 않아야 합니다.
이것이 바로 Zerobus Ingest가 해결하고자 하는 문제입니다. Zerobus는 Databricks의 완전 관리형 서버리스 스트리밍 수집 서비스입니다. 이는 모든 프로듀서로부터 데이터를 수신하여 Unity Catalog의 거버넌스 하에 Delta 테이블에 직접 기록하는 푸시(push) 기반 API입니다.
대신 테이블을 생성하고 데이터를 푸시하기만 하면 됩니다. 데이터가 레이크하우스에 저장되어 몇 초 만에 쿼리할 수 있는 상태가 됩니다. 최종 목적지가 레이크하우스라면 더 이상 Kafka를 파이프로 실행할 필요가 없습니다.
저희는 Zerobus Ingest의 성능을 벤치마크하기 위해 11년간 2,000억 개의 데이터 포인트를 나타내는 NASA의 NEOWISE 데이터 세트를 사용하여, 사전 구성 없이 안정적인 지연 시간(latency)으로 24시간 이내에 1 PB를 수집했습니다.
24시간 이내에 1PB를 수집함으로써, 단일 테이블에 대해 12 GB/s의 지속적인 처리량(throughput)을 유지하는 Zerobus의 성능을 입증했습니다! 🚀
이제 페타바이트 규모 지원: 은하수 스트리밍 (테이블당 12GB/초)
벤치마크를 직접 실행하는 방법에 대한 자세한 내용은 Databricks 커뮤니티의 관련 블로그 글을 읽어보세요.
이 게시물에서는 이를 가능하게 한 세 가지 설계 결정 사항을 살펴봅니다.
저희의 목표는 페타바이트 규모를 지원하고 변동하는 수집 패턴을 처리하기 위해 자동 확장할 수 있는 스트리밍 시스템을 구축하는 것이었습니다.
기존의 스트리 밍 아키텍처에서는 특정 워크로드에 필요한 브로커와 파티션의 수를 직접 결정해야 합니다. 이를 위해서는 엔드투엔드 파이프라인에 대한 이해와 예측뿐만 아니라, 피크 부하 및 컨슈머의 수집 제약 조건에 대한 지식이 필요합니다.
저희는 기본 원칙으로 돌아가 데이터 프로듀서를 위해 페타바이트 규모의 워크로드를 처리하도록 "마법처럼" 확장되는 시스템을 설계하고 구축했습니다.

저희가 해결하고자 했던 문제는 탄력적이고 "한계가 없는" 확장을 달성하기 위해 효율적인 자동 확장을 구현하는 방법이었습니다.
저희의 가설은 정적 파티셔닝에서 벗어나 스트림/연결이라는 논리적 단위로 이동함으로써, 소비 워크로드에 중요한 순서 보장(ordering guarantees)을 유지하면서 진정한 자동 확장(autoscaling) 및 재분배(rebalancing)를 실현할 수 있다는 것이었습니다.
메시지 버스 아키텍처에서 파티션은 병렬 처리와 순서 보장의 단위입니다. 이러한 결합은 이에 의존하는 컨슈머가 있을 때 까다로운 제약 조건을 생성합니다.
순서 보장은 일반적으로 프로듀서별이 아니라 파티션별로 보장됩니다. 파티션 수와 파티션 간의 데이터 분포는 컨슈머가 수집 속도를 따라잡는 능력에 영향을 미칩니다. 즉, 다음과 같은 의미입니다.

기존 시스템에서 순서 보장은 파티션 수준의 보장입니다. 반면 Zerobus Ingest에서 순서 보장은 스트림 연결 수준의 보장입니다.
프로듀서가 Zerobus와 스트림(서버에 대한 연결)을 열면 서비스에 논리적 ID를 등록하게 됩니다. 해당 연결의 수명 동안 어떤 "파티션" 포드(pod)가 데이터를 처리하든 상관없이 데이터는 순서대로 도착합니다.
"스트림의 순서가 보장됩니다"이지, "파티션의 순서가 보장됩니다"가 아닙니다. 이것이 바로 계약 조건입니다.
내부적으로 Zerobus Ingest는 포드 풀 전체에 스 트림을 분산합니다. 라우팅은 휴리스틱 기반으로 이루어집니다. 즉, 특정 포드에 부하가 많이 걸리면 새로 들어오는 스트림은 다른 포드로 라우팅됩니다. 프로듀서는 이를 인지하지 못하며, 순서 보장에도 영향을 받지 않습니다.
순서 보장이 스트림 수준에서 이루어지므로 수요가 급증할 때 포드를 추가하고 수요가 감소할 때 제거할 수 있습니다. 그러면 기존 스트림은 정상적으로 드레인(drain)되고 새로운 스트림은 해당 포드로의 라우팅이 중단됩니다. 이후 풀이 축소되어 컴퓨팅 리소스 사용 효율성을 유지합니다.
이것이 바로 진정한 자동 확장입니다. 세분성의 단위는 파티션 할당이 아니라 스트림 연결입니다.
동적 파티셔닝 설계를 통해 Zerobus는 비용 효율성을 유지하면서 테이블당 초당 12 GB 이상의 처리량으로 자동 확장할 수 있습니다.

Zerobus의 주요 목표는 모든 규모의 데이터 스트림을 행 단위(row-by-row)로 효율적으로 전송하는 것입니다. 이를 달성하기 위해 클라이언트가 Zerobus로 보내는 입력 형식부터 내구성을 보장하는 내부 형식 및 오픈 Delta 형식에 이르기까지 불필요한 복사 및 메모리 할당을 완전히 피해야 했습니다.
Zerobus는 현재 다음과 같은 메시지 형식을 지원합니다.
저희가 진행한 많은 최적화 작업 중, 자체 개발한 protobuf 디코더인 ZeroParser를 통해 제로 카피(zero-copy) 접근 방식을 설명해 드리겠습니다.
표준 protobuf 디코더는 속도와 유연성 중 하나를 선택하도록 강제합니다. 일반적으로 protobuf 디코더는 빌드 타임 코드 생성(codegen) 또는 런타임 리플렉션 중 하나에 의존합니다.
두 가지 접근 방식 모두 적합하지 않았습니다. 저희에게는 코드 생성 수준의 성능 프로필을 제공하면서도 동적 디스크립터를 지원하는 솔루션이 필요했습니다.
그 결과 저희는 zeroparser를 구축했습니다. 메모리 할당이 없는 단일 패스(single-pass) 파싱을 사용하여 이 격차를 해소함으로써, 동적 디스크립터와 복잡한 스키마에서도 CPU 코어당 약 1 GB/s의 protobuf 파싱 처리량을 유지할 수 있게 되었습니다.
Zeroparser는 메모리 복사 및 할당을 유발하는 수신 객체의 분해 없이 와이어 포맷(wire format)을 직접 파싱할 수 있도록 합니다. 이 접근 방식을 통해 Zerobus는 동적으로 protobuf 디스크립터를 제공하는 완전한 유연성을 유지하면서도, 기존의 코드 생성 기반 protobuf 파싱 솔루션보다 더 나은 성능을 달성할 수 있습니다.
Rust의 라이프타임 시스템은 Zeroparser 설계의 핵심이었습니다. 이는 프로토콜 파싱 중 컴파일 타임 안전성을 보장하는 동시에, 원시 와이어 바이트(raw wire bytes)를 독점적인 네트워크 소유권 하에 유지하여 불필요한 데이터 복사를 제거합니다.

결과에 따르면 Zeroparser는 동적 그룹에 속해 있음에도 불구하고 업계 표준인 두 가지 코드 생성 기반 구현보다 뛰어난 성능을 보였습니다.
Zeroparser는 여기에서 제공되는 Zerobus SDK의 일부로 오픈 소스화되어 있습니다.
스트리밍은 단순히 고처리량 워크로드를 처리할 수 있는 것만을 의미하지 않습니다. 진정한 스트리밍 서비스가 되려면 메시지 핸드오프(handoff)를 최대한 빠르게 지원해야 합니다. 이러한 데이터 핸드오프의 낮은 지연 시간(low latency)이야말로 스트리밍 워크로드와 배치를 진정으로 구분 짓는 요소입니다.
내구성 보장과 함께 이러한 저지연 핸드오프를 지원하기 위해, Zerobus는 지연 시간에 최적화된 Write-Ahead Log(WAL)를 구현합니다. 메시지가 영구 저장되면 Zerobus는 클라이언트에 승인(acknowledgement)을 다시 보냅니다. 모든 레코드를 개별적으로 승인하는 대신, 서버는 스트림에서 가장 높은 커밋된 오프셋을 반환합니다. 그 결과가 바로 이 비동기 승인(async ack) 루프입니다. 그런 다음 Delta에 쓰기 위한 핵심 로직으로 Delta Kernel Rust가 사용됩니다.
이 비동기 설계는 전송 중인 데이터를 버퍼링하는 클라이언트에 매우 중요합니다. Zerobus는 gRPC 양방향 스트리밍을 사용하며, 각 Zerobus 스트림에는 두 개의 통신 라인이 있습니다:
클라이언트가 해당 오프셋을 받으면 로컬 전송 중(in-flight) 버퍼에서 해당 시점까지의 모든 데이터를 안전하게 삭제할 수 있습니다. 이 모든 과정은 Zerobus SDK가 대신 처리해 줍니다.
WAL은 클라이언트를 가볍게 유지해 주는 핵심 요소입니다. 데이터를 전송하고, 승인(ack)을 받고, 버퍼를 비우면 됩니다. 이러한 저지연, 고내구성 핸드오프는 항상 많은 팀이 Kafka를 선택해 온 이유였습니다. Zerobus도 동일한 보장을 제공합니다.

시스템 벤치마킹의 핵심은 프로덕션 환경에서 시스템이 어떻게 사용될지 이해하고, 그 동작과 사용 패턴을 모방하는 데 있습니다. 이것이 바로 Zerobus Ingest에 부하를 주기 위해 NASA의 NEOWISE 데이터 세트를 선택하고, 실제 환경의 팬인(fan-in) 패턴을 모방하기 위해 Locust를 사용한 이유입니다.
Zerobus Ingest는 독립된 여러 프로듀서(producer)의 스트림을 하나의 대상 테이블로 집계하도록 구축되었습니다. 처리량은 동시에 열려 있는 스트림 수에 따라 확장됩니다. 즉, 단일 머신이나 소규모 클러스터에서는 부하를 공정하게 테스트할 수 없습니다. 단일 고성능 호스트는 서비스에 유의미한 압박을 가하기도 전에 자체 대역폭이나 CPU를 먼저 포화 상태로 만들어, 결국 Zerobus가 아닌 프로듀서를 벤치마킹하는 결과를 초래하기 때문입니다.
실제 환경의 팬인 패턴을 시뮬레이션하기 위해, 저희는 Locust를 사용하여 포드(pod)별로 개별 스트림을 열도록 조정함으로써 대규모 수집 성능을 압박 테스트합니다.
그러면 Zerobus의 오토스케일링이 스트림 수와 처리량에 반응하여 수집 속도를 처리합니다.
저희 벤치마크는 하나의 Locust 마스터와 각각 별도의 포드로 실행되는 여러 Locust 워커(worker)와 함께 Kubernetes에 배포되었습니다. 주요 매개변수는 다음과 같습니다.
각 워커는 수집할 Parquet 파일의 고유한 목록을 받습니다. 워커는 할당된 슬라이스를 스트리밍하며 행을 중복하여 처리하지 않습니다.
테스트 결과, Zerobus Ingest는 2,048개의 동시 워커로부터 단일 테이블로 24시간 동안 12 GB/s의 속도를 유지할 수 있는 성능을 보여주었습니다. 이 기간 동안 Zerobus는 1조 개 이상의 레코드를 수집했습니다.
client_ts_ms 열을 기준으로 5초 단위 버킷으로 집계하면 커밋된 행과 수신된 바이트에 대해 서버에서 확인된 정확한 뷰를 얻을 수 있습니다:
이 쿼리는 라이브 Unity Catalog 테이블을 대상으로 실행됩니다. 이 수치는 Delta 스토리지에 완전히 커밋된 행을 반영합니다.

직접 실행해 보고 싶으신가요?
데이터 세트 준비, 프로듀서 코드 및 자체 Zerobus 엔드포인트에서 실행하기 위한 지침이 포함된 전체 벤치마크 하네스입니다. 여기에서 확인해 보세요.
Zerobus Ingest를 이제 Databricks에서 정식 버전(GA)으로 사용할 수 있으며, 모든 프로덕션 워크로드를 처리할 준비가 되었습니다.
테이블당 12GB/s의 성능 메트릭은 Zerobus Ingest를 통해 기본적으로 제공되는 성능입니다. 할당량은 계정 담당 팀에 문의하여 늘릴 수 있습니다.
로드맵 예정 사항:
Zerobus가 다음에 어떤 방향으로 나아가길 원하시는지 알려주세요! 스트리밍의 다음 개척지는 어디라고 생각하시나요? Databricks 커뮤니티 블로그에 의견을 남겨주세요.