rocksdb

3 개의 포스트

Apache Flink + RocksDB 튜닝으로 광고 Frequency Capping 실시간 집계를 일주일까지 확장하기 (새 탭에서 열림)

토스 데이터 서비스 플랫폼 팀은 광고 노출 집계의 정확성을 높이고 서빙 효율을 개선하기 위해, 기존 Airflow 배치와 Flink 스트리밍이 혼재된 시스템을 전면 Flink 기반의 실시간 슬라이딩 집계 시스템으로 전환했습니다. 1분부터 7일까지의 광범위한 집계 구간을 단일 Redis 조회로 제공하기 위해 집계 특성별로 Flink 앱을 분리하고, RocksDB 및 런타임 설정을 최적화하여 비즈니스 오차를 최소화했습니다. 이 과정에서 대규모 상태(State) 관리와 초기 데이터 적재의 정합성 문제를 해결하며 운영 신뢰성을 확보했습니다. ### 광고 노출 제어(Frequency Capping)의 중요성 * 광고주 예산 낭비를 막고 노출 기회 손실을 방지하기 위해 사용자별 광고 노출 횟수를 정확하게 카운트하고 제어하는 메커니즘입니다. * 광고 상품에 따라 '하루 3회', '7일간 1회' 등 집계 구간이 다양하므로, 1분부터 7일까지의 모든 구간에 대해 이벤트 단위의 정밀한 슬라이딩 윈도우 집계가 필요합니다. ### 기존 시스템의 한계와 개선 동기 * 기존에는 Airflow를 이용해 당일(Head), 과거(Mid), 경계 보정(Tail)의 3단계로 나누어 처리하는 배치 구조를 사용했으나, 유지보수해야 할 DAG가 너무 많고 구조가 복잡했습니다. * 서빙 시점에 구간별로 Redis를 최대 4회 조회해야 하는 구조적 번거로움이 있었으며, 실시간으로 변하는 슬라이딩 윈도우를 정밀하게 구현하는 데 한계가 있었습니다. ### 병목 패턴에 따른 앱 분리 및 아키텍처 * 집계 구간별 병목 현상이 다르다는 점에 착안하여 시스템을 **Minutes**(1~30분), **Hours**(최대 12시간), **Days**(최대 7일)의 3개 앱으로 분리했습니다. * **Minutes**: 빈번한 만료 처리로 인한 Write Stall이 주요 병목이며, RocksDB Write 경로 튜닝이 핵심입니다. * **Hours**: 대량의 광고 ID 누적으로 인한 Filter Block Cache Miss와 CPU 포화가 발생하여 Managed Memory 증설이 필요합니다. * **Days**: Savepoint가 230GB에 달하는 대규모 상태가 병목이며, Checkpoint I/O 문제를 해결하기 위해 Changelog State Backend를 활용합니다. * Flink State를 '단일 진실 공급원(SSOT)'으로 삼아, 장애 발생 시에도 Redis를 State로부터 언제든 다시 구성할 수 있도록 설계했습니다. ### 초기 적재와 전환 정합성 확보 * 7일치의 과거 데이터를 채우는 과정에서 '백필(카운트만 수행)'과 '캐치업(카운트와 만료 타이머 함께 등록)' 파이프라인을 분리하는 2단계 구조를 설계했습니다. * 백필 도중 만료 타이머가 미리 발화하여 집계가 틀어지는 문제를 방지하기 위해, 백필 완료 후 특정 시점부터만 Redis에 쓰기가 수행되도록 제어했습니다. * `withIdleness` 설정을 통해 특정 파티션의 지연이 전체 Watermark 진행을 막지 않도록 하고, `timerState`의 TTL을 윈도우보다 길게 설정해 지연 상황에서도 감소 로직이 누락되지 않도록 보장했습니다. ### RocksDB와 런타임 최적화 * **Minutes 앱**: Write Buffer Manager(WBM) 압박을 완화하여 RocksDB가 쓰기를 멈추는 Write Stall 현상을 방지했습니다. * **Hours 앱**: Bloom Filter 및 메모리 설정을 통해 캐시 미스를 줄여 CPU 효율을 높였습니다. * **Days 앱**: 거대한 SST 파일로 인한 체크포인트 부하를 줄이기 위해 레벨 최적화와 Changelog 메커니즘을 적용했습니다. 대규모 데이터를 다루는 실시간 집계 시스템에서는 모든 구간을 하나의 설정으로 처리하기보다, 데이터의 규모와 병목 지점에 따라 앱을 분리하고 각기 다른 RocksDB 튜닝 전략을 적용하는 것이 운영 안정성 측면에서 효과적입니다. 또한, 상태(State)를 시스템의 최상위 데이터 원천으로 관리하는 원칙을 지킬 때 장애 복구와 데이터 정합성 유지가 훨씬 용이해집니다.

Evolving our real-time timeseries storage again: Built in Rust for performance at scale (새 탭에서 열림)

데이터독(Datadog)은 급증하는 데이터 볼륨과 고카디널리티(high-cardinality) 워크로드를 처리하기 위해 Rust 기반의 6세대 실시간 시계열 데이터베이스 엔진을 새롭게 설계했습니다. 기존 시스템의 한계를 극복하기 위해 인제스션(Ingestion), 저장, 쿼리 실행 구조를 근본적으로 재구성함으로써 수집 성능은 60배, 쿼리 속도는 최대 5배까지 향상시키는 성과를 거두었습니다. 이 글은 지난 15년간 데이터독이 카산드라에서 시작해 Rust 기반의 전용 엔진에 이르기까지 거쳐온 기술적 진화 과정과 그 과정에서 얻은 교훈을 다룹니다. ### 데이터독 시계열 저장소의 아키텍처 데이터독의 메트릭 플랫폼은 데이터의 효율적인 처리를 위해 실시간 저장소와 인덱스 데이터베이스를 분리하여 운영합니다. * **RTDB (Real-time DB):** `<timeseries_id, timestamp, value>` 형태의 원시 메트릭 데이터를 저장하고 집계하며, 최신 데이터를 실시간으로 서빙합니다. * **인덱스 데이터베이스:** 메트릭 식별자와 태그 정보를 `<timeseries_id, tags>` 형태로 관리합니다. * **데이터 흐름:** 쿼리가 발생하면 상위 서비스가 RTDB와 인덱스 노드에 각각 접속하여 결과를 가져오고, RTDB 노드 내부는 인테이크(Intake), 스토리지 엔진, 스냅샷 모듈, gRPC 쿼리 실행 계층 등으로 구성되어 유기적으로 동작합니다. ### 1세대부터 3세대: 확장성과 운영 효율의 탐색 초기 데이터독은 기성 솔루션을 활용하며 실시간 쿼리 성능과 운영 편의성을 확보하는 데 집중했습니다. * **Gen 1 (Cassandra):** 뛰어난 쓰기 확장성을 제공했으나, 알람 및 분석에 필요한 복잡한 실시간 쿼리를 지원하기 어렵고 대규모 데이터셋 반환 시 효율이 떨어지는 한계가 있었습니다. * **Gen 2 (Redis):** 빠른 읽기 속도와 운영 가시성을 제공했지만, 싱글 스레드 특성상 라이브 트래픽 처리 중 스냅샷 작업이 어려웠고 데이터 직렬화/역직렬화에 따른 CPU 및 메모리 비용이 증가했습니다. * **Gen 3 (MDBM):** `mmap`을 통해 OS 페이지 캐시를 활용하는 메모리 맵 방식의 키-값 저장소를 도입했으나, 대규모 워크로드에서 성능과 정확성 이슈가 발생하며 명시적인 I/O 관리의 필요성을 체감했습니다. ### 4세대와 5세대: 커스텀 엔진과 기능 확장 성능 한계를 돌파하기 위해 범용 DB를 벗어나 전용 스토리지 엔진을 직접 구현하기 시작했습니다. * **Gen 4 (Go 기반 B+ Tree):** Go 언어로 구현된 커스텀 B+ 트리 엔진을 도입하여 '코어당 스레드(thread-per-core)' 모델의 기초를 닦았으며, 처리량과 지연 시간 면에서 큰 진전을 이루었습니다. * **Gen 5 (RocksDB 통합):** 분포 메트릭(distribution metrics)과 DDSketch 타입을 지원하기 위해 RocksDB를 병행 도입했습니다. 하지만 기존 Go 엔진과 RocksDB가 공존하는 구조는 관리가 복잡하고 효율성이 분산되는 결과를 낳았습니다. ### 6세대: Rust 기반의 통합 엔진으로의 전환 파편화된 엔진을 통합하고 성능을 극대화하기 위해 Rust를 선택하여 차세대 시스템을 구축했습니다. * **통합 및 최적화:** 스칼라 값과 스케치 데이터를 모두 처리할 수 있는 단일 엔진을 Rust로 구축하여 언어 차원의 안정성과 고성능 I/O 제어권을 확보했습니다. * **성능 성과:** 이 구조적 변화를 통해 데이터 수집 성능을 60배 높였으며, 피크 시간대 쿼리 속도를 5배 향상시켜 전례 없는 규모의 트래픽을 효율적으로 수용하게 되었습니다. **결론 및 추천** 시스템 규모가 커짐에 따라 범용 데이터베이스나 `mmap`과 같은 추상화 계층은 오히려 성능 병목이 될 수 있습니다. 데이터독의 사례처럼 워크로드의 특성에 맞춰 I/O와 메모리 레이아웃을 직접 제어할 수 있는 전용 엔진을 구축하는 것이 기술적 부채를 해결하고 폭발적인 성장을 뒷받침하는 핵심 전략이 될 수 있습니다. 특히 Rust와 같은 시스템 프로그래밍 언어는 고성능 실시간 시스템을 재설계할 때 강력한 도구가 됩니다.

대규모 시계열 인덱싱 (새 탭에서 열림)

Datadog은 5년 사이 데이터 규모가 30배 이상 급증함에 따라, 기존의 시계열 인덱싱 시스템에서 발생하는 성능 병목과 유지보수 문제를 해결하기 위해 아키텍처를 재설계했습니다. 수조 건의 이벤트를 효율적으로 처리하기 위해 인덱스 서비스를 시계열 데이터 저장소와 분리하였으며, 쿼리 로그를 분석해 인덱스를 자동으로 생성하는 전략을 취했습니다. 이 글은 RocksDB와 SQLite를 기반으로 한 초기 인덱싱 서비스의 구조와 대규모 시계열 데이터를 관리하기 위한 Datadog의 기술적 접근 방식을 다룹니다. ### 메트릭 플랫폼의 계층별 구조 * **수집(Intake) 계층:** 데이터 포인트는 메트릭 이름, 태그(env, host, service 등), 타임스탬프, 수치 값으로 구성됩니다. 수집된 데이터는 메시지 브로커인 Kafka로 전달되어 분석, 인덱싱, 아카이빙 등 다양한 용도로 독립적으로 소비됩니다. * **저장(Storage) 계층:** 데이터 저장소는 두 개의 서비스로 나뉩니다. '시계열 데이터베이스'는 `<시계열_ID, 타임스탬프, 값>` 튜플을 저장하고, '시계열 인덱스' 서비스는 RocksDB를 기반으로 `<시계열_ID, 태그>`를 매핑하여 쿼리 시 필터링과 그룹화를 담당합니다. * **쿼리(Query) 계층:** 분산 쿼리 계층은 인덱스 노드에서 검색된 식별자를 바탕으로 시계열 데이터베이스에서 실제 값을 가져와 병합하며, 필터와 집계 함수(avg 등)를 적용해 최종 결과를 도출합니다. ### 쿼리 로그 분석을 통한 자동 인덱싱 전략 * **풀 스캔 방지:** 특정 메트릭의 전체 데이터를 조회하는 비효율적인 스캔을 피하고자, 태그 기반의 인덱스를 생성하여 쿼리 실행 속도를 최적화했습니다. * **동적 인덱스 생성:** 시스템은 백그라운드 프로세스를 통해 실시간 쿼리 로그를 분석합니다. 쿼리 횟수, 실행 시간, 입력 대비 출력 식별자 비율을 따져 리소스 소모가 큰 '고선택성' 쿼리에 대해 자동으로 인덱스를 생성합니다. * **구체화된 뷰(Materialized Views):** 자주 사용되는 복잡한 쿼리를 미리 계산된 인덱스 형태로 저장함으로써, 반복되는 쿼리 요청을 단순한 키-값 조회로 변환해 CPU와 메모리 리소스를 획기적으로 절감합니다. ### 임베디드 데이터베이스를 활용한 시스템 설계 * **SQLite 기반의 메타데이터 관리:** 인덱스 정의와 쿼리 로그 등 읽기 중심의 데이터는 Go 애플리케이션 내에 임베디드된 SQLite에 저장됩니다. SQL의 유연성 덕분에 CLI를 통한 디버깅과 테이블 관리가 용이합니다. * **RocksDB를 통한 고성능 쓰기 처리:** 매일 발생하는 수조 건의 인덱싱 데이터는 고성능 키-값 저장소인 RocksDB가 처리합니다. 별도의 서버 프로세스 없이 애플리케이션에 직접 통합되어 성능 극대화를 꾀했습니다. * **인덱스 수명 주기 관리:** 일정 기간 쿼리가 발생하지 않아 쓸모없어진 인덱스는 시스템이 자동으로 삭제하여 저장 공간을 효율적으로 관리합니다. 대규모 분산 환경에서 모든 데이터에 대해 미리 인덱스를 생성하는 것은 불가능에 가깝습니다. Datadog의 사례처럼 실제 사용자의 쿼리 패턴을 모니터링하고, 리소스 집약적인 쿼리에 대해 인덱스를 동적으로 생성하는 '쿼리 기반 최적화' 방식은 폭발적인 데이터 성장세 속에서 시스템 가용성을 유지하는 매우 실용적인 전략입니다.