ClickHouse 아키텍처 개요(Architecture Overview)

ClickHouse 아키텍처 개요(Architecture Overview)

이 문서는 ClickHouse의 VLDB 2024 논문의 웹 버전입니다. 페타바이트 규모 데이터셋에 대해 높은 수집 속도로 고성능 분석을 제공하는 오픈소스 OLAP 데이터베이스 ClickHouse의 스토리지 계층, 쿼리 처리 계층, 통합 계층을 학술적으로 설명합니다.

출처: 문서

본문

이것은 우리 VLDB 2024 과학 논문의 웹 버전입니다. 우리는 또한 그 배경과 여정에 대해 블로그를 올렸으며, ClickHouse CTO이자 창시자인 Alexey Milovidov의 VLDB 2024 프레젠테이션 시청을 권장합니다:

ABSTRACT

지난 수십 년 동안 저장되고 분석되는 데이터의 양은 기하급수적으로 증가했습니다. 다양한 산업과 분야의 기업들은 제품 개선, 성능 평가, 비즈니스 중대한 결정을 위해 이 데이터에 의존하기 시작했습니다. 그러나 데이터 볼륨이 점점 인터넷 규모가 되면서 기업들은 과거 및 신규 데이터를 비용 효율적이고 확장 가능하게 관리하면서, 동시에 많은 수의 쿼리와 실시간 지연(예: 사용 사례에 따라 1초 미만)에 대한 기대를 가지고 분석해야 합니다. 이 논문은 높은 수집 속도로 페타바이트 규모 데이터셋에 대한 고성능 분석을 위해 설계된 인기 있는 오픈소스 OLAP 데이터베이스 ClickHouse의 개요를 제시합니다. 그 스토리지 계층은 전통적인 로그 구조 병합(LSM) 트리에 기반한 데이터 형식과 백그라운드에서 과거 데이터를 연속적으로 변환(예: 집계, 아카이브)하는 새로운 기법을 결합합니다. 쿼리는 편리한 SQL 방언으로 작성되며 선택적 코드 컴파일을 지원하는 최첨단 벡터화된 쿼리 실행 엔진으로 처리됩니다. ClickHouse는 쿼리에서 관련 없는 데이터를 평가하지 않도록 가지치기 기법을 적극적으로 사용합니다. 다른 데이터 관리 시스템은 테이블 함수, 테이블 엔진, 또는 데이터베이스 엔진 수준에서 통합될 수 있습니다. 실제 벤치마크는 ClickHouse가 시장에서 가장 빠른 분석 데이터베이스 중 하나임을 보여줍니다.

1 INTRODUCTION

이 논문은 수조 개의 행과 수백 개의 컬럼을 가진 테이블에 대한 고성능 분석 쿼리를 위해 설계된 컬럼형 OLAP 데이터베이스 ClickHouse를 설명합니다. ClickHouse는 2009년 웹 규모 로그 파일 데이터를 위한 필터 및 집계 연산자로 시작되었고 2016년에 오픈소스화되었습니다. 그림 1은 이 논문에 설명된 주요 기능이 ClickHouse에 도입된 시점을 보여줍니다. ClickHouse는 현대 분석 데이터 관리의 다섯 가지 핵심 과제를 해결하도록 설계되었습니다:

  1. 높은 수집 속도를 가진 대규모 데이터셋. 웹 분석, 금융, 전자상거래 같은 산업의 많은 데이터 중심 애플리케이션은 거대하고 지속적으로 성장하는 데이터 양으로 특징지어집니다. 대규모 데이터셋을 처리하려면 분석 데이터베이스는 효율적인 인덱싱과 압축 전략뿐 아니라, 단일 서버가 수십 테라바이트의 스토리지로 제한되므로 여러 노드에 데이터를 분산(scale-out)할 수 있어야 합니다. 또한 최근 데이터가 과거 데이터보다 실시간 통찰에 더 관련이 있는 경우가 많습니다. 결과적으로 분석 데이터베이스는 새로운 데이터를 일관되게 높은 속도로 또는 버스트로 수집하고, 병렬 보고 쿼리를 늦추지 않으면서 과거 데이터를 지속적으로 "우선순위 낮추기"(예: 집계, 아카이브)할 수 있어야 합니다.
  2. 낮은 지연을 기대하는 많은 동시 쿼리. 쿼리는 일반적으로 임시(ad-hoc)(예: 탐색적 데이터 분석) 또는 반복(recurring)(예: 주기적 대시보드 쿼리)으로 분류할 수 있습니다. 사용 사례가 더 대화형일수록 쿼리 지연이 더 낮아질 것이 기대되며, 이는 쿼리 최적화와 실행에 도전을 줍니다. 반복 쿼리는 추가로 물리적 데이터베이스 레이아웃을 워크로드에 적응시킬 기회를 제공합니다. 결과적으로 데이터베이스는 빈번한 쿼리를 최적화할 수 있는 가지치기 기법을 제공해야 합니다. 쿼리 우선순위에 따라 데이터베이스는 많은 수의 쿼리가 동시에 실행되더라도 CPU, 메모리, 디스크 및 네트워크 I/O 같은 공유 시스템 리소스에 동등하거나 우선된 접근을 부여해야 합니다.
  3. 다양한 데이터 저장소, 저장 위치, 형식의 환경. 기존 데이터 아키텍처와 통합하려면 현대 분석 데이터베이스는 어떤 시스템, 위치, 형식에서든 외부 데이터를 읽고 쓸 수 있는 높은 수준의 개방성을 나타내야 합니다.
  4. 성능 인트로스펙션을 지원하는 편리한 쿼리 언어. OLAP 데이터베이스의 실제 사용은 추가적인 "소프트" 요구 사항을 제기합니다. 예를 들어 틈새 프로그래밍 언어 대신 사용자는 중첩 데이터 타입과 광범위한 일반·집계·윈도우 함수를 가진 표현력 있는 SQL 방언으로 데이터베이스와 인터페이스하기를 선호합니다. 분석 데이터베이스는 또한 시스템이나 개별 쿼리의 성능을 인트로스펙트할 수 있는 정교한 도구를 제공해야 합니다.
  5. 업계 수준의 견고성과 다양한 배포. 상용 하드웨어는 신뢰할 수 없으므로 데이터베이스는 노드 실패에 대한 견고성을 위해 데이터 복제를 제공해야 합니다. 또한 데이터베이스는 오래된 랩톱에서 강력한 서버까지 어떤 하드웨어에서든 실행되어야 합니다. 마지막으로 JVM 기반 프로그램의 가비지 컬렉션 오버헤드를 피하고 베어메탈 성능(예: SIMD)을 가능하게 하려면 데이터베이스는 대상 플랫폼용 네이티브 바이너리로 배포되는 것이 이상적입니다.

그림 1: ClickHouse 타임라인.

2 ARCHITECTURE

그림 2: ClickHouse 데이터베이스 엔진의 고수준 아키텍처.

그림 2가 보여주듯 ClickHouse 엔진은 세 가지 주요 계층으로 나뉩니다: 쿼리 처리 계층(섹션 4에서 설명), 스토리지 계층(섹션 3), 통합 계층(섹션 5). 이 외에도 접근 계층이 다양한 프로토콜을 통해 사용자 세션과 애플리케이션과의 통신을 관리합니다. 스레딩, 캐싱, 역할 기반 접근 제어, 백업, 지속적 모니터링을 위한 직교 구성 요소가 있습니다. ClickHouse는 의존성 없는 단일 정적 링크 바이너리로 C++로 구축되었습니다. 쿼리 처리는 들어오는 쿼리를 파싱하고, 논리적·물리적 쿼리 계획을 구축·최적화하고, 실행하는 전통적인 패러다임을 따릅니다. ClickHouse는 MonetDB/X100 [11]과 유사한 벡터화 실행 모델과 기회주의적 코드 컴파일 [53]을 결합합니다. 쿼리는 기능이 풍부한 SQL 방언, PRQL [76], 또는 Kusto의 KQL [50]로 작성할 수 있습니다. 스토리지 계층은 테이블 데이터의 형식과 위치를 캡슐화하는 다양한 테이블 엔진으로 구성됩니다. 테이블 엔진은 세 가지 범주로 나뉩니다: 첫 번째 범주는 ClickHouse의 기본 지속성 형식을 나타내는 MergeTree* 계열의 테이블 엔진입니다. LSM 트리 [60]의 아이디어에 기반해 테이블은 수평의 정렬된 파츠로 분할되며, 백그라운드 프로세스에 의해 지속적으로 병합됩니다. 개별 MergeTree* 테이블 엔진은 머지가 입력 파츠의 행을 결합하는 방식에서 다릅니다. 예를 들어 오래된 행은 집계되거나 교체될 수 있습니다. 두 번째 범주는 쿼리 실행을 빠르게 하거나 분산하는 데 사용되는 특수 목적 테이블 엔진입니다. 이 범주는 딕셔너리라고 불리는 인메모리 key-value 테이블 엔진을 포함합니다. 딕셔너리는 내부 또는 외부 데이터 소스에 대해 주기적으로 실행되는 쿼리의 결과를 캐시합니다. 이것은 어느 정도의 데이터 부실함이 허용될 수 있는 시나리오에서 접근 지연을 크게 줄입니다. 특수 목적 테이블 엔진의 다른 예로는 순수 인메모리 엔진과 일부 다른 것들이 있습니다.

3 STORAGE LAYER

이 섹션은 ClickHouse의 네이티브 저장 형식인 MergeTree* 테이블 엔진을 논의합니다. 디스크상 표현을 설명하고 ClickHouse의 세 가지 데이터 가지치기 기법을 논의한 다음, 동시 삽입에 영향을 주지 않고 데이터를 지속적으로 변환하는 머지 전략을 제시합니다. 마지막으로 업데이트와 삭제가 어떻게 구현되는지, 그리고 데이터 중복 제거, 데이터 복제, ACID 준수를 설명합니다.

3.1 디스크상 형식 (On-Disk Format)

MergeTree* 테이블 엔진의 각 테이블은 변경 불가능한 테이블 파츠의 모음으로 구성됩니다. 파츠는 행 집합이 테이블에 삽입될 때마다 생성됩니다. 파츠는 중앙 카탈로그에 대한 추가 조회 없이 내용을 해석하는 데 필요한 모든 메타데이터를 포함한다는 의미에서 자체 완결적입니다. 테이블당 파츠 수를 낮게 유지하기 위해 백그라운드 머지 작업이 설정 가능한 파트 크기(기본 150 GB)에 도달할 때까지 주기적으로 여러 작은 파츠를 더 큰 파츠로 결합합니다. 파츠가 테이블의 프라이머리 키 컬럼으로 정렬되어 있으므로(섹션 3.2 참고) 효율적인 k-way 병합 정렬 [40]이 머지에 사용됩니다. 소스 파츠는 비활성으로 표시되고 참조 카운트가 0으로 떨어지면 즉, 더 이상 어떤 쿼리도 읽지 않으면 결국 삭제됩니다. 행은 두 가지 모드로 삽입될 수 있습니다: 동기 삽입 모드에서 각 INSERT 문은 새 파츠를 만들고 테이블에 추가합니다. 머지 오버헤드를 최소화하기 위해 데이터베이스 클라이언트는 튜플을 대량으로(예: 한 번에 20,000행) 삽입하도록 권장됩니다. 그러나 데이터를 실시간으로 분석해야 한다면 클라이언트 측 배치로 인한 지연은 종종 용납할 수 없습니다. 예를 들어 관측성 사용 사례는 소량의 이벤트 및 메트릭 데이터를 지속적으로 보내는 수천 개의 모니터링 에이전트를 자주 포함합니다. 이러한 시나리오는 ClickHouse가 같은 테이블에 대한 여러 수신 INSERT의 행을 버퍼링하고 버퍼 크기가 설정 가능한 임계값을 초과하거나 타임아웃이 만료된 후에만 새 파츠를 만드는 비동기 삽입 모드를 활용할 수 있습니다.

그림 3: MergeTree* 엔진 테이블의 삽입과 머지.

그림 3은 MergeTree* 엔진 테이블에 대한 네 번의 동기 삽입과 두 번의 비동기 삽입을 보여줍니다. 두 번의 머지가 활성 파츠 수를 처음 다섯 개에서 두 개로 줄였습니다. 다양한 데이터베이스 [13, 26, 56]에서의 LSM 트리 [58] 구현과 비교할 때, ClickHouse는 파츠를 계층으로 배열하는 대신 모든 파츠를 동등하게 취급합니다. 결과적으로 머지는 더 이상 같은 레벨의 파츠로 제한되지 않습니다. 이것은 또한 파츠의 암묵적 시간순 정렬을 포기하므로, 툼스톤(tombstone)에 기반하지 않은 업데이트와 삭제의 대체 메커니즘이 필요합니다(섹션 3.4 참고). ClickHouse는 다른 LSM 트리 기반 저장소가 일반적으로 write-ahead 로그(섹션 3.7 참고)를 사용하는 반면, 삽입을 디스크에 직접 씁니다. 파츠는 디스크의 디렉터리에 해당하며, 각 컬럼에 대해 하나의 파일을 포함합니다. 최적화로, 작은 파츠(기본적으로 10 MB 미만)의 컬럼은 읽기와 쓰기의 공간적 지역성을 높이기 위해 단일 파일에 연속적으로 저장됩니다. 파츠의 행은 8192개의 레코드 그룹, 즉 그래뉼(granules)로 더 논리적으로 나뉩니다. 그래뉼은 ClickHouse에서 스캔과 인덱스 조회 연산자가 처리하는 가장 작은 분할 불가능 데이터 단위를 나타냅니다. 그러나 디스크상 데이터의 읽기와 쓰기는 그래뉼 수준이 아니라 블록 단위로 수행되며, 블록은 컬럼 내 여러 인접 그래뉼을 결합합니다. 새 블록은 블록당 설정 가능한 바이트 크기(기본 1 MB)에 기반해 형성됩니다. 즉 블록의 그래뉼 수는 변수이며 컬럼의 데이터 타입과 분포에 따라 달라집니다. 블록은 또한 크기와 I/O 비용을 줄이기 위해 압축됩니다. 기본적으로 ClickHouse는 범용 압축 알고리즘으로 LZ4 [75]를 사용하지만, 사용자는 부동소수점 데이터에 Gorilla [63]나 FPC [12] 같은 특수 코덱을 지정할 수도 있습니다. 압축 알고리즘은 연결될 수도 있습니다. 예를 들어 숫자 값의 논리적 중복을 델타 코딩 [23]으로 먼저 줄인 다음 중량 압축을 수행하는 것이 가능합니다.

3.2 데이터 가지치기 (Data Pruning)

대부분의 사용 사례에서 단일 쿼리에 답하기 위해 페타바이트의 데이터를 스캔하는 것은 너무 느리고 비쌉니다. ClickHouse는 검색 중 대부분의 행을 건너뛸 수 있게 해 쿼리를 크게 가속화하는 세 가지 데이터 가지치기 기법을 지원합니다. 첫째, 사용자는 테이블에 프라이머리 키 인덱스를 정의할 수 있습니다. 프라이머리 키 컬럼은 각 파츠 내 행의 정렬 순서를 결정합니다. 즉 인덱스는 지역적으로 클러스터링됩니다. ClickHouse는 또한 각 파츠에 대해 각 그래뉼의 첫 번째 행의 프라이머리 키 컬럼 값에서 그래뉼 id로의 매핑을 저장합니다. 즉 인덱스는 희소(sparse)합니다 [31]. 결과적인 데이터 구조는 보통 전체를 메모리에 유지할 만큼 충분히 작습니다. 예를 들어 810만 행을 인덱싱하는 데 1000개 항목만 필요합니다. 프라이머리 키의 주요 목적은 자주 필터링되는 컬럼에 대한 동등 및 범위 조건을 순차 스캔 대신 이진 탐색으로 평가하는 것입니다(섹션 4.4). 지역적 정렬은 또한 파츠 머지와 쿼리 최적화에 활용될 수 있습니다. 예를 들어 정렬 기반 집계 또는 프라이머리 키 컬럼이 정렬 컬럼의 접두사를 형성할 때 물리적 실행 계획에서 정렬 연산자를 제거하는 것입니다. 그림 4는 페이지 노출 통계가 있는 테이블에 대한 EventTime 컬럼의 프라이머리 키 인덱스를 보여줍니다. 쿼리의 범위 조건과 일치하는 그래뉼은 EventTime을 순차 스캔하는 대신 프라이머리 키 인덱스를 이진 탐색하여 찾을 수 있습니다.

그림 4: 프라이머리 키 인덱스로 필터 평가하기.

둘째, 사용자는 테이블 프로젝션(table projections) 을 만들 수 있습니다. 즉 다른 프라이머리 키로 정렬된 같은 행을 포함하는 테이블의 대체 버전입니다 [71]. 프로젝션은 기본 테이블의 프라이머리 키와 다른 컬럼으로 필터링하는 쿼리를 가속화할 수 있게 하며, 삽입, 머지, 공간 소비에 대한 오버헤드 증가를 대가로 합니다. 기본적으로 프로젝션은 기본 테이블에 새로 삽입된 파츠에서만 지연 채워지며, 사용자가 프로젝션을 전체적으로 구체화하지 않는 한 기존 파츠에서는 채워지지 않습니다. 쿼리 최적화 프로그램은 추정된 I/O 비용에 따라 기본 테이블 또는 프로젝션에서 읽을지 선택합니다. 파츠에 대해 프로젝션이 없으면 쿼리 실행은 해당 기본 테이블 파츠로 폴백합니다. 셋째, 스킵핑 인덱스(skipping indices) 는 프로젝션에 대한 가벼운 대안을 제공합니다. 스킵핑 인덱스의 아이디어는 여러 연속 그래뉼 수준에서 소량의 메타데이터를 저장하여 관련 없는 행을 스캔하지 않게 하는 것입니다. 스킵핑 인덱스는 임의의 인덱스 표현식과 설정 가능한 그래뉼러리티, 즉 스킵핑 인덱스 블록의 그래뉼 수로 만들 수 있습니다. 사용 가능한 스킵핑 인덱스 유형은: 1. 각 인덱스 블록에 대한 인덱스 표현식의 최소·최대 값을 저장하는 Min-max 인덱스 [51]. 이 인덱스 유형은 작은 절대 범위를 가진 지역적으로 클러스터링된 데이터(예: 느슨하게 정렬된 데이터)에 잘 작동합니다. 2. 설정 가능한 수의 고유 인덱스 블록 값을 저장하는 Set 인덱스. 이 인덱스는 작은 지역 카디널리티, 즉 "뭉쳐진" 값을 가진 데이터에 가장 잘 사용됩니다. 3. 행, 토큰, 또는 n-gram 값에 대해 설정 가능한 오탐(false positive) 비율로 구축되는 Bloom filter 인덱스 [9]. 이 인덱스는 텍스트 검색 [73]을 지원하지만, min-max와 set 인덱스와 달리 범위 또는 부정 조건에는 사용할 수 없습니다.

3.3 머지 시점 데이터 변환 (Merge-time Data Transformation)

비즈니스 인텔리전스와 관측성 사용 사례는 종종 지속적으로 높은 속도 또는 버스트로 생성되는 데이터를 처리해야 합니다. 또한 최근에 생성된 데이터가 의미 있는 실시간 통찰을 위해 과거 데이터보다 더 관련이 있는 경우가 많습니다. 이러한 사용 사례는 높은 데이터 수집 속도를 유지하면서 집계나 데이터 노화 같은 기법으로 과거 데이터의 볼륨을 지속적으로 줄여야 합니다. ClickHouse는 다양한 머지 전략을 사용하여 기존 데이터의 지속적 증분 변환을 허용합니다. 머지 시점 데이터 변환은 INSERT 문의 성능을 손상시키지 않지만, 테이블에 원치 않는(예: 오래되었거나 비집계된) 값이 절대 없다는 것을 보장하지는 못합니다. 필요한 경우 SELECT 문에 FINAL 키워드를 지정하여 모든 머지 시점 변환을 쿼리 시점에 적용할 수 있습니다. 교체 머지(Replacing merges) 는 파츠의 생성 타임스탬프를 기준으로 튜플의 가장 최근에 삽입된 버전만 유지하고, 더 오래된 버전은 삭제합니다. 튜플은 같은 프라이머리 키 컬럼 값을 가질 경우 동등한 것으로 간주됩니다. 어떤 튜플이 보존될지 명시적으로 제어하기 위해 비교용 특수 버전 컬럼을 지정하는 것도 가능합니다. 교체 머지는 머지 시점 업데이트 메커니즘(보통 업데이트가 빈번한 사용 사례)으로 또는 삽입 시점 데이터 중복 제거(섹션 3.5)의 대안으로 흔히 사용됩니다. 집계 머지(Aggregating merges) 는 동등한 프라이머리 키 컬럼 값을 가진 행을 집계된 행으로 축소합니다. 프라이머리 키가 아닌 컬럼은 요약 값을 담는 부분 집계 상태(partial aggregation state)여야 합니다. 두 개의 부분 집계 상태(예: avg()에 대한 sum과 count)는 새로운 부분 집계 상태로 결합됩니다. 집계 머지는 일반적으로 일반 테이블 대신 머티리얼라이즈드 뷰에서 사용됩니다. 머티리얼라이즈드 뷰는 소스 테이블에 대한 변환 쿼리를 기반으로 채워집니다. 다른 데이터베이스와 달리 ClickHouse는 소스 콘텐츠 전체로 머티리얼라이즈드 뷰를 주기적으로 리프레시하지 않습니다.

그림 5: 머티리얼라이즈드 뷰의 집계 머지.

TTL(time-to-live) 머지 는 과거 데이터에 대한 노화를 제공합니다. 삭제 및 집계 머지와 달리 TTL 머지는 한 번에 하나의 파츠만 처리합니다. TTL 머지는 트리거와 액션이 있는 규칙으로 정의됩니다. 트리거는 모든 행에 대한 타임스탬프를 계산하는 표현식이며, TTL 머지가 실행되는 시점과 비교됩니다. 이는 사용자가 행 단위로 액션을 제어할 수 있게 하지만, 우리는 모든 행이 주어진 조건을 만족하는지 확인하고 액션을 전체 파츠에 대해 실행하는 것으로 충분하다는 것을 발견했습니다. 가능한 액션은 1. 파츠를 다른 볼륨(예: 더 저렴하고 느린 스토리지)으로 이동, 2. 파츠 재압축(예: 더 무거운 코덱으로), 3. 파츠 삭제, 4. 롤업(roll-up), 즉 그룹화 키와 집계 함수를 사용해 행 집계입니다. 예를 들어 Listing 1의 로깅 테이블 정의를 고려해 보세요. ClickHouse는 타임스탬프 컬럼 값이 일주일보다 오래된 파츠를 느리지만 저렴한 S3 오브젝트 스토리지로 이동합니다.

1 CREATE TABLE tab ( ts DateTime , msg String )
2 ENGINE MergeTree PRIMARY KEY ts
3 TTL ( ts + INTERVAL 1 WEEK ) TO VOLUME 's3 '

Listing 1: 일주일 후 오브젝트 스토리지로 파츠 이동.

3.4 업데이트와 삭제 (Updates and Deletes)

MergeTree* 테이블 엔진의 설계는 append-only 워크로드를 선호하지만, 일부 사용 사례는 규제 준수 같은 이유로 기존 데이터를 가끔 수정해야 합니다. 데이터를 업데이트하거나 삭제하는 두 가지 접근 방식이 있으며, 둘 다 병렬 삽입을 차단하지 않습니다. 뮤테이션(Mutations) 은 테이블의 모든 파츠를 제자리에서 다시 씁니다. 테이블(삭제) 또는 컬럼(업데이트)이 일시적으로 크기가 두 배가 되는 것을 방지하기 위해 이 연산은 비원자적입니다. 즉 병렬 SELECT 문이 뮤테이션된 파츠와 안 된 파츠를 읽을 수 있습니다. 뮤테이션은 연산이 끝날 때 데이터가 물리적으로 변경된다는 것을 보장합니다. 삭제 뮤테이션은 모든 파츠의 모든 컬럼을 다시 쓰므로 여전히 비쌉니다. 대안으로 경량 삭제(lightweight deletes) 는 행이 삭제되었는지 여부를 나타내는 내부 비트맵 컬럼만 업데이트합니다. ClickHouse는 결과에서 삭제된 행을 제외하기 위해 SELECT 쿼리에 비트맵 컬럼에 대한 추가 필터를 덧붙입니다. 삭제된 행은 미래의 불특정 시점에 일반 머지에 의해서만 물리적으로 제거됩니다. 컬럼 수에 따라 경량 삭제는 뮤테이션보다 훨씬 빠를 수 있지만, SELECT가 더 느려지는 대가가 있습니다. 같은 테이블에 대해 수행되는 업데이트와 삭제 연산은 드물 것으로 예상되며 논리적 충돌을 피하기 위해 직렬화됩니다.

3.5 멱등적 삽입 (Idempotent Inserts)

실무에서 자주 발생하는 문제는 테이블에 삽입하기 위해 서버로 데이터를 보낸 후 연결 타임아웃을 클라이언트가 어떻게 처리해야 하는지입니다. 이 상황에서 클라이언트는 데이터가 성공적으로 삽입되었는지 여부를 구분하기 어렵습니다. 이 문제는 전통적으로 클라이언트에서 서버로 데이터를 다시 보내고 중복 삽입을 거부하기 위해 프라이머리 키 또는 unique 제약에 의존하여 해결됩니다. 데이터베이스는 이진 트리 [39, 68], 기수 트리 [45], 또는 해시 테이블 [29]에 기반한 인덱스 구조를 사용해 필요한 포인트 조회를 빠르게 수행합니다. 이러한 데이터 구조는 모든 튜플을 인덱싱하므로, 대규모 데이터셋과 높은 수집 속도에서는 공간과 업데이트 오버헤드가 금지됩니다. ClickHouse는 각 삽입이 결국 파츠를 만든다는 사실에 기반한 더 가벼운 대안을 제공합니다. 더 구체적으로 서버는 마지막으로 삽입된 N개 파츠(예: N=100)의 해시를 유지하고 알려진 해시를 가진 파츠의 재삽입을 무시합니다. 비복제 및 복제 테이블의 해시는 각각 로컬로 또는 Keeper에 저장됩니다. 결과적으로 삽입은 멱등적이 됩니다. 즉 클라이언트는 타임아웃 후 같은 행 배치를 다시 보내고 서버가 중복 제거를 처리한다고 가정할 수 있습니다. 중복 제거 과정에 대한 더 많은 제어를 위해 클라이언트는 파츠 해시 역할을 하는 삽입 토큰을 선택적으로 제공할 수 있습니다. 해시 기반 중복 제거는 새 행을 해싱하는 오버헤드를 수반하지만, 해시를 저장하고 비교하는 비용은 무시할 만합니다.

3.6 데이터 복제 (Data Replication)

복제는 고가용성(노드 실패에 대한 내성)의 전제 조건이지만, 로드 밸런싱과 무중단 업그레이드[14]에도 사용됩니다. ClickHouse에서 복제는 테이블 파츠 집합(섹션 3.1)과 컬럼 이름과 타입 같은 테이블 메타데이터로 구성된 테이블 상태의 개념에 기반합니다. 노드는 세 가지 연산으로 테이블의 상태를 진행합니다: 1. 삽입은 상태에 새 파츠를 추가하고, 2. 머지는 상태에 새 파츠를 추가하고 기존 파츠를 삭제하며, 3. 뮤테이션과 DDL 문은 구체적인 연산에 따라 파츠를 추가, 그리고/또는 삭제, 그리고/또는 테이블 메타데이터를 변경합니다. 연산은 단일 노드에서 로컬로 수행되고 전역 복제 로그에 상태 전이 시퀀스로 기록됩니다. 복제 로그는 Raft 합의 알고리즘 [59]을 사용해 ClickHouse 노드 클러스터에 분산되고 내결함성 있는 조정 계층을 제공하는 보통 세 개의 ClickHouse Keeper 프로세스 앙상블에 의해 유지됩니다. 모든 클러스터 노드는 처음에 복제 로그의 같은 위치를 가리킵니다. 노드가 로컬 삽입, 머지, 뮤테이션, DDL 문을 실행하는 동안 복제 로그는 다른 모든 노드에서 비동기적으로 재생됩니다. 결과적으로 복제 테이블은 결국 일관성만 가집니다. 즉 노드는 최신 상태로 수렴하는 동안 일시적으로 오래된 테이블 상태를 읽을 수 있습니다. 대부분의 앞서 언급한 연산은 노드 쿼럼(예: 과반수 또는 모든 노드)이 새 상태를 채택할 때까지 대안적으로 동기적으로 실행될 수 있습니다. 예를 들어 그림 6은 세 개의 ClickHouse 노드 클러스터에서 처음에 빈 복제 테이블을 보여줍니다. 노드 1은 먼저 두 개의 삽입 문을 받아 Keeper 앙상블에 저장된 복제 로그에 (1 2)로 기록합니다. 다음으로 노드 2는 첫 번째 로그 항목을 가져와(3) 노드 1에서 새 파츠를 다운로드하여(4) 재생하는 반면, 노드 3은 두 로그 항목을 모두 재생합니다(3 4 5 6). 마지막으로 노드 3은 두 파츠를 새 파츠로 병합하고, 입력 파츠를 삭제하고, 다시 재생합니다.

그림 6: 세 개 노드 클러스터의 복제.

동기화를 빠르게 하기 위한 세 가지 최적화가 있습니다: 첫째, 클러스터에 추가된 새 노드는 복제 로그를 처음부터 재생하는 대신 마지막 복제 로그 항목을 쓴 노드의 상태를 복사합니다. 둘째, 머지가 로컬로 반복하거나 다른 노드에서 결과 파츠를 가져와 재생됩니다. 정확한 동작은 구성 가능하며 CPU 소비와 네트워크 I/O를 균형 있게 조정할 수 있습니다. 예를 들어 크로스 데이터센터 복제는 운영 비용을 최소화하기 위해 일반적으로 로컬 머지를 선호합니다. 셋째, 노드는 서로 독립적인 복제 로그 항목을 병렬로 재생합니다. 여기에는 같은 테이블에 연속적으로 삽입된 새 파츠의 가져오기나 다른 테이블에 대한 연산이 포함됩니다.

3.7 ACID 준수 (ACID Compliance)

동시 읽기와 쓰기 연산의 성능을 극대화하기 위해 ClickHouse는 가능한 한 래칭(latching)을 피합니다. 쿼리는 쿼리 시작 시 생성된 관련된 모든 테이블의 모든 파츠의 스냅샷에 대해 실행됩니다. 이것은 병렬 INSERT나 머지(섹션 3.1)로 삽입된 새 파츠가 실행에 참여하지 않도록 보장합니다. 파츠가 동시에 수정되거나 제거되는 것을 방지(섹션 3.4)하기 위해 처리된 파츠의 참조 카운트가 쿼리 시간 동안 증가됩니다. 공식적으로 이것은 버전이 있는 파츠에 기반한 MVCC 변형 [6]에 의해 실현된 스냅샷 격리에 해당합니다. 결과적으로 스냅샷을 찍을 때 동시 쓰기가 각각 단일 파츠에만 영향을 주는 드문 경우를 제외하고 문은 일반적으로 ACID를 준수하지 않습니다. 실제로 ClickHouse의 쓰기 중심 의사 결정 사용 사례 대부분은 정전의 경우 새 데이터를 잃는 작은 위험조차도 용인합니다. 데이터베이스는 새로 삽입된 파츠의 커밋(fsync)을 기본적으로 디스크에 강제하지 않음으로써 이를 활용하며, 커널이 쓰기를 일괄 처리하도록 하면서 원자성을 포기합니다.

4 QUERY PROCESSING LAYER

그림 7: SIMD 유닛, 코어, 노드 간 병렬화.

그림 7이 보여주듯 ClickHouse는 데이터 요소, 데이터 청크, 테이블 샤드 수준에서 쿼리를 병렬화합니다. 여러 데이터 요소는 SIMD 명령을 사용하여 연산자 내에서 한 번에 처리될 수 있습니다. 단일 노드에서 쿼리 엔진은 여러 스레드에서 연산자를 동시에 실행합니다. ClickHouse는 MonetDB/X100 [11]과 같은 벡터화 모델을 사용합니다. 즉 연산자는 가상 함수 호출의 오버헤드를 최소화하기 위해 단일 행이 아닌 여러 행(데이터 청크)을 만들고, 전달하고, 소비합니다. 소스 테이블이 분리된 테이블 샤드로 분할되면 여러 노드가 샤드를 동시에 스캔할 수 있습니다. 결과적으로 모든 하드웨어 리소스가 완전히 활용되고, 쿼리 처리는 노드를 추가하여 수평으로, 코어를 추가하여 수직으로 확장될 수 있습니다. 이 섹션의 나머지 부분은 먼저 데이터 요소, 데이터 청크, 샤드 단위의 병렬 처리를 더 자세히 설명합니다. 그런 다음 쿼리 성능을 극대화하기 위해 선택된 주요 최적화를 제시합니다. 마지막으로 동시 쿼리가 있을 때 ClickHouse가 공유 시스템 리소스를 어떻게 관리하는지 논의합니다.

4.1 SIMD 병렬화

연산자 사이에 여러 행을 전달하면 벡터화의 기회가 생깁니다. 벡터화는 수동으로 작성된 내장(intrinsics) [64, 80] 또는 컴파일러 자동 벡터화 [25]에 기반합니다. 벡터화의 이점을 받는 코드는 다른 컴퓨트 커널로 컴파일됩니다. 예를 들어 쿼리 연산자의 내부 핫 루프는 비벡터화 커널, 자동 벡터화된 AVX2 커널, 수동 벡터화된 AVX-512 커널로 구현될 수 있습니다. 가장 빠른 커널은 cpuid 명령에 기반해 런타임에 선택됩니다. 이 접근 방식은 ClickHouse가 15년 된 시스템(SSE 4.2가 최소)에서도 실행되면서 최신 하드웨어에서 상당한 속도 향상을 제공할 수 있게 합니다.

4.2 멀티코어 병렬화

그림 8: 세 개 레인이 있는 물리적 연산자 계획.

ClickHouse는 SQL 쿼리를 물리적 계획 연산자의 방향 그래프로 변환하는 기존 접근 방식 [31]을 따릅니다. 연산자 계획의 입력은 네이티브 또는 지원되는 서드파티 형식(섹션 5 참고)으로 데이터를 읽는 특수 소스 연산자로 표현됩니다. 마찬가지로 특수 싱크 연산자가 결과를 원하는 출력 형식으로 변환합니다. 물리적 연산자 계획은 구성 가능한 최대 작업자 스레드 수(기본적으로 코어 수)와 소스 테이블 크기에 기반해 쿼리 컴파일 시점에 독립적인 실행 레인으로 펼쳐집니다. 레인은 병렬 연산자가 처리할 데이터를 겹치지 않는 범위로 분해합니다. 병렬 처리의 기회를 극대화하기 위해 레인은 가능한 한 늦게 병합됩니다. 예를 들어 그림 8의 노드 1 상자는 페이지 노출 통계가 있는 테이블에 대한 전형적인 OLAP 쿼리의 연산자 그래프를 보여줍니다. 첫 번째 단계에서 소스 테이블의 세 개의 분리된 범위가 동시에 필터링됩니다. Repartition 교환 연산자가 처리 스레드를 균등하게 활용하기 위해 첫 번째와 두 번째 단계 사이의 결과 청크를 동적으로 라우팅합니다. 스캔된 범위가 상당히 다른 선택도를 가지면 첫 번째 단계 후 레인이 불균형해질 수 있습니다. 두 번째 단계에서 필터를 통과한 행은 RegionID로 그룹화됩니다. Aggregate 연산자는 RegionID를 그룹화 컬럼으로 하고 그룹별 sum과 count를 avg()의 부분 집계 상태로 하는 로컬 결과 그룹을 유지합니다. 로컬 집계 결과는 결국 GroupStateMerge 연산자에 의해 전역 집계 결과로 병합됩니다. 이 연산자는 또한 파이프라인 브레이커입니다. 즉 세 번째 단계는 집계 결과가 완전히 계산된 후에만 시작할 수 있습니다. 세 번째 단계에서 결과 그룹은 먼저 Distribute 교환 연산자에 의해 세 개의 동일한 크기의 분리된 파티션으로 나뉘며, 그다음 AvgLatency로 정렬됩니다. 정렬은 세 단계로 수행됩니다: 먼저 ChunkSort 연산자가 ...

4.3 멀티노드 병렬화

쿼리의 소스 테이블이 샤딩되어 있으면 쿼리를 받은 노드(개시자 노드)의 쿼리 최적화 프로그램은 가능한 한 많은 작업을 다른 노드에서 수행하려고 합니다. 다른 노드의 결과는 쿼리 계획의 다른 지점에 통합될 수 있습니다. 쿼리에 따라 원격 노드는 1. 원시 소스 테이블 컬럼을 개시자 노드로 스트리밍하거나, 2. 소스 컬럼을 필터링하고 생존 행을 보내거나, 3. 필터와 집계 단계를 실행하고 부분 집계 상태를 가진 로컬 결과 그룹을 보내거나, 4. 필터, 집계, 정렬을 포함한 전체 쿼리를 실행할 수 있습니다. 그림 8의 노드 2~N은 hits 테이블의 샤드를 보유한 다른 노드에서 실행되는 계획 조각을 보여줍니다. 이 노드들은 로컬 데이터를 필터링하고 그룹화하여 개시자 노드로 결과를 보냅니다. 노드 1의 GroupStateMerge 연산자는 결과 그룹이 마지막으로 정렬되기 전에 로컬 및 원격 결과를 병합합니다.

4.4 포괄적 성능 최적화 (Holistic Performance Optimization)

이 섹션은 쿼리 실행의 다른 단계에 적용되는 선택된 주요 성능 최적화를 제시합니다. 쿼리 최적화. 첫 번째 최적화 집합은 쿼리의 AST에서 얻은 의미적 쿼리 표현 위에 적용됩니다. 그러한 최적화의 예는 상수 폴딩(예: concat(lower('a'),upper('b'))이 'aB'가 됨), 특정 집계 함수에서 스칼라 추출(예: sum(a * 2)이 2 * sum(a)가 됨), 공통 부분식 제거, 동등 필터의 분리를 IN-리스트로 변환(예: x=c OR x=d가 x IN (c,d)가 됨)입니다. 최적화된 의미적 쿼리 표현은 이후 논리적 연산자 계획으로 변환됩니다. 논리적 계획 위의 최적화는 어느 것이 더 비싼지 추정에 따라 필터 푸시다운, 함수 평가 및 정렬 단계 재정렬을 포함합니다. 마지막으로 논리적 쿼리 계획은 물리적 연산자 계획으로 변환됩니다. 이 변환은 관련된 테이블 엔진의 특성을 활용할 수 있습니다. 예를 들어 MergeTree_ 테이블 엔진의 경우 ORDER BY 컬럼이 프라이머리 키의 접두사를 형성하면 데이터를 디스크 순서로 읽을 수 있고 정렬 연산자를 계획에서 제거할 수 있습니다. 또한 집계의 그룹화 컬럼이 프라이머리 키의 접두사를 형성하면 ClickHouse는 정렬 집계 [33]를 사용할 수 있습니다. 즉 미리 정렬된 입력에서 같은 값의 실행(run)을 직접 집계합니다. 해시 집계와 비교해 정렬 집계는 메모리 집약도가 상당히 낮고, 실행이 처리된 후 즉시 집계 값을 다음 연산자로 전달할 수 있습니다. 쿼리 컴파일. ClickHouse는 LLVM 기반 쿼리 컴파일을 사용해 인접한 계획 연산자를 동적으로 융합합니다 [38, 53]. 예를 들어 표현식 a * b + c + 1은 세 개의 연산자 대신 단일 연산자로 결합될 수 있습니다. 표현식 외에도 ClickHouse는 여러 집계 함수를 한 번에 평가(GROUP BY용)하기 위해 컴파일을 사용합니다.

해시 테이블 최적화를 위한 기법들:

  • 거대 키 집합을 지원하기 위한 256개 하위 테이블(해시의 첫 바이트 기반)을 가진 2단계 레이아웃,
  • 다른 문자열 길이에 대해 다른 해시 함수를 가진 네 개의 하위 테이블을 가진 문자열 해시 테이블 [79],
  • 키가 적을 때 키를 직접 버킷 인덱스로 사용하는(해싱 없음) 룩업 테이블,
  • 비교가 비쌀 때(예: 문자열, AST) 더 빠른 충돌 해결을 위한 임베디드 해시를 가진 값,
  • 불필요한 리사이즈를 피하기 위해 런타임 통계에서 예측된 크기 기반 해시 테이블 생성,
  • 단일 메모리 슬래브에 같은 생성/소멸 수명 주기를 가진 여러 작은 해시 테이블 할당,
  • 해시맵별·셀별 버전 카운터를 사용한 재사용을 위한 해시 테이블의 즉시 클리어링,
  • 키 해싱 후 값 검색을 빠르게 하기 위한 CPU 프리페치(__builtin_prefetch) 사용.

조인 (Joins). ClickHouse는 원래 조인을 초보적으로만 지원했기 때문에 많은 사용 사례가 역사적으로 역정규화된 테이블에 의존했습니다. 오늘날 데이터베이스는 SQL에서 사용 가능한 모든 조인 타입(inner, left-/right/full outer, cross, as-of)뿐 아니라 해시 조인(naïve, grace), sort-merge 조인, 그리고 빠른 key-value 룩업을 가진 테이블 엔진(보통 딕셔너리)을 위한 인덱스 조인 같은 다양한 조인 알고리즘을 제공합니다. 조인은 가장 비싼 데이터베이스 연산 중 하나이므로 고전 조인 알고리즘의 병렬 변형을, 이상적으로는 구성 가능한 공간/시간 트레이드오프와 함께 제공하는 것이 중요합니다. 해시 조인의 경우 ClickHouse는 [7]의 비차단 공유 파티션 알고리즘을 구현합니다. 예를 들어 그림 9의 쿼리는 페이지 히트 통계 테이블에 대한 셀프 조인을 통해 사용자가 URL 사이를 어떻게 이동하는지 계산합니다. 조인의 빌드 단계는 소스 테이블의 세 개의 분리된 범위를 다루는 세 개의 레인으로 나뉩니다. 전역 해시 테이블 대신 파티셔닝된 해시 테이블이 사용됩니다. (보통 세 개의) 작업자 스레드는 해시 함수의 모듈로를 계산해 빌드 측의 각 입력 행에 대한 대상 파티션을 결정합니다. 해시 테이블 파티션에 대한 접근은 Gather 교환 연산자로 동기화됩니다. 프로브 단계는 입력 튜플의 대상 파티션을 유사하게 찾습니다. 이 알고리즘은 튜플당 두 개의 추가 해시 계산을 도입하지만, 해시 테이블 파티션 수에 따라 빌드 단계의 래치 경합을 크게 줄입니다.

그림 9: 세 개 해시 테이블 파티션이 있는 병렬 해시 조인.

4.5 워크로드 격리 (Workload Isolation)

ClickHouse는 동시성 제어, 메모리 사용 한도, I/O 스케줄링을 제공하여 사용자가 쿼리를 워크로드 클래스로 격리할 수 있게 합니다. 특정 워크로드 클래스에 대한 공유 리소스(CPU 코어, DRAM, 디스크 및 네트워크 I/O) 한도를 설정함으로써 이러한 쿼리가 다른 중요한 비즈니스 쿼리에 영향을 주지 않도록 보장합니다. 동시성 제어는 많은 수의 동시 쿼리가 있는 시나리오에서 스레드 과다 구독(thread oversubscription)을 방지합니다. 더 구체적으로 쿼리당 작업자 스레드 수는 사용 가능한 CPU 코어 수에 대한 지정된 비율에 따라 동적으로 조정됩니다. ClickHouse는 서버, 사용자, 쿼리 수준에서 메모리 할당의 바이트 크기를 추적하여 유연한 메모리 사용 한도를 설정할 수 있게 합니다. 메모리 오버커밋은 쿼리가 보장된 메모리 너머의 추가 여유 메모리를 사용할 수 있게 하면서 다른 쿼리에 대한 메모리 한도를 보장합니다. 또한 집계, 정렬, 조인 절에 대한 메모리 사용을 제한할 수 있어 메모리 한도를 초과하면 외부 알고리즘으로 폴백합니다. 마지막으로 I/O 스케줄링은 사용자가 최대 대역폭, 인플라이트 요청, 정책(예: FIFO, SFC [32])에 기반해 워크로드 클래스에 대한 로컬 및 원격 디스크 접근을 제한할 수 있게 합니다.

5 INTEGRATION LAYER

실시간 의사 결정 애플리케이션은 종종 여러 위치의 데이터에 대한 효율적이고 낮은 지연의 접근에 의존합니다. OLAP 데이터베이스에서 외부 데이터를 사용할 수 있게 하는 두 가지 접근 방식이 있습니다. 푸시 기반 데이터 접근에서 서드파티 구성 요소가 데이터베이스와 외부 데이터 저장소를 연결합니다. 그것의 한 예는 원격 데이터를 대상 시스템으로 푸시하는 특수 extract-transform-load(ETL) 도구입니다. 풀 기반 모델에서 데이터베이스 자체가 원격 데이터 소스에 연결하고 쿼리할 데이터를 로컬 테이블로 가져오거나 데이터를 원격 시스템으로 내보냅니다. 푸시 기반 접근 방식은 더 다재다능하고 흔하지만 더 큰 아키텍처 발자국과 확장성 병목을 수반합니다. 반대로 데이터베이스 내부의 원격 연결은 로컬과 원격 데이터 사이의 조인 같은 흥미로운 기능을 제공하면서 전체 아키텍처를 단순하게 유지하고 통찰까지의 시간을 줄입니다. 섹션의 나머지는 원격 위치의 데이터에 접근하기 위한 ClickHouse의 풀 기반 데이터 통합 방법을 탐구합니다. SQL 데이터베이스의 원격 연결 개념은 새롭지 않다는 점을 주목하세요. 예를 들어 2001년에 도입되고 2011년부터 PostgreSQL [65]이 구현하는 SQL/MED 표준 [35]은 외부 데이터 관리를 위한 통일된 인터페이스로 foreign data wrappers를 제안합니다. 다른 데이터 저장소 및 저장 형식과의 최대 상호 운용성은 ClickHouse의 설계 목표 중 하나입니다. 2024년 3월 기준 ClickHouse는 모든 분석 데이터베이스 중 가장 많은 내장 데이터 통합 옵션을 제공합니다. 외부 연결 (External Connectivity). ClickHouse는 ODBC, MySQL, PostgreSQL, SQLite, Kafka, Hive, MongoDB, Redis, S3/GCP/Azure 오브젝트 스토어 및 다양한 데이터 레이크와의 연결을 위한 50개 이상의 통합 테이블 함수와 엔진을 제공합니다. 이를 아래 보너스 그림이 보여주는 카테고리로 더 나눕니다(원본 vldb 논문의 일부 아님).

보너스 그림: ClickBench의 상호 운용성 옵션.

통합 테이블 함수로 임시 접근. 테이블 함수는 SELECT 쿼리의 FROM 절에서 호출되어 탐색적 임시 쿼리를 위해 원격 데이터를 읽을 수 있습니다. 대안으로 INSERT INTO TABLE FUNCTION 문을 사용해 원격 저장소에 데이터를 쓰는 데 사용할 수 있습니다. 영구 접근 (Persisted access). 원격 데이터 저장소 및 처리 시스템과 영구 연결을 만드는 세 가지 방법이 있습니다. 첫째, 통합 테이블 엔진은 MySQL 테이블 같은 원격 데이터 소스를 영구 로컬 테이블로 나타냅니다. 사용자는 SELECT 쿼리와 테이블 함수를 결합한 CREATE TABLE AS 구문으로 테이블 정의를 저장합니다. 예를 들어 원격 컬럼의 하위 집합만 참조하도록 사용자 지정 스키마를 지정하거나, 스키마 추론을 사용해 컬럼 이름과 동등한 ClickHouse 타입을 자동으로 결정할 수 있습니다. 우리는 수동과 능동 런타임 동작을 더 구분합니다: 수동 테이블 엔진은 쿼리를 원격 시스템으로 전달하고 결과로 로컬 프록시 테이블을 채웁니다. 반대로 능동 테이블 엔진은 주기적으로 원격 시스템에서 데이터를 가져오거나 예를 들어 PostgreSQL의 논리적 복제 프로토콜을 통해 원격 변경을 구독합니다. 결과적으로 로컬 테이블은 원격 테이블의 전체 사본을 포함합니다. 둘째, 통합 데이터베이스 엔진은 원격 데이터 저장소의 테이블 스키마의 모든 테이블을 ClickHouse에 매핑합니다. 전자와 달리 일반적으로 원격 데이터 저장소가 관계형 데이터베이스여야 하며 DDL 문에 제한된 지원을 추가로 제공합니다. 셋째, 딕셔너리는 해당 통합 테이블 함수 또는 엔진이 있는 거의 모든 데이터 소스에 대한 임의의 쿼리로 채워질 수 있습니다. 데이터가 원격 스토리지에서 일정 간격으로 가져와지므로 런타임 동작은 능동입니다. 데이터 형식 (Data Formats). 서드파티 시스템과 상호 작용하려면 현대 분석 데이터베이스도 어떤 형식으로든 데이터를 처리할 수 있어야 합니다. 네이티브 형식 외에도 ClickHouse는 90개 이상의 형식을 지원합니다.

6 PERFORMANCE AS A FEATURE

이 섹션은 성능 분석을 위한 내장 도구를 제시하고 실제 워크로드와 벤치마크 쿼리를 사용해 성능을 평가합니다.

6.1 내장 성능 분석 도구

개별 쿼리나 백그라운드 연산에서 성능 병목을 조사하기 위한 광범위한 도구를 사용할 수 있습니다. 사용자는 시스템 테이블에 기반한 통일된 인터페이스로 모든 도구와 상호 작용합니다. 서버 및 쿼리 메트릭. 활성 파츠 수, 네트워크 처리량, 캐시 히트율 같은 서버 수준 통계는 읽힌 블록 수나 인덱스 사용 통계 같은 쿼리별 통계로 보완됩니다. 메트릭은 (요청 시) 동기적으로 또는 설정 가능한 간격으로 비동기적으로 계산됩니다. 샘플링 프로파일러. 샘플링 프로파일러로 서버 스레드의 콜스택을 수집할 수 있습니다. 결과는 선택적으로 flamegraph 시각화 도구 같은 외부 도구로 내보낼 수 있습니다. OpenTelemetry 통합. OpenTelemetry는 여러 데이터 처리 시스템에 걸쳐 데이터 행을 추적하기 위한 공개 표준입니다 [8]. ClickHouse는 모든 쿼리 처리 단계에 대해 설정 가능한 세분화로 OpenTelemetry 로그 스팬을 생성할 수 있으며, 다른 시스템의 OpenTelemetry 로그 스팬을 수집하고 분석할 수 있습니다. Explain query. 다른 데이터베이스와 마찬가지로 SELECT 쿼리 앞에 EXPLAIN을 붙여 쿼리 AST, 논리적·물리적 연산자 계획, 실행 시간 동작에 대한 상세한 통찰을 얻을 수 있습니다.

6.2 벤치마크

벤치마킹이 충분히 현실적이지 않다는 비판을 받았지만 [10, 52, 66, 74], 데이터베이스의 강점과 약점을 식별하는 데 여전히 유용합니다. 다음에서 벤치마크가 ClickHouse 성능을 평가하는 데 어떻게 사용되는지 논의합니다.

6.2.1 역정규화된 테이블

역정규화된 팩트 테이블에 대한 필터 및 집계 쿼리는 역사적으로 ClickHouse의 주요 사용 사례를 나타냅니다. 우리는 클릭스트림 및 트래픽 분석에 사용되는 임시 및 주기적 보고 쿼리를 시뮬레이션하는 전형적인 워크로드인 ClickBench의 런타임을 보고합니다. 벤치마크는 웹에서 가장 큰 분석 플랫폼 중 하나에서 가져온 1억 개의 익명화된 페이지 히트가 있는 테이블에 대한 43개의 쿼리로 구성됩니다. 온라인 대시보드 [17]는 2024년 6월 기준 45개 이상의 상업 및 연구 데이터베이스에 대한 측정(콜드/핫 런타임, 데이터 가져오기 시간, 디스크상 크기)을 보여줍니다. 결과는 공개 데이터셋과 쿼리 [16]에 기반해 독립적인 기여자들이 제출합니다. 쿼리는 순차 및 인덱스 스캔 접근 경로를 테스트하고 CPU-, IO-, 또는 메모리 바인딩된 관계형 연산자를 일상적으로 노출합니다. 그림 10은 분석에 자주 사용되는 데이터베이스에서 모든 ClickBench 쿼리를 순차 실행하기 위한 총 상대 콜드 및 핫 런타임을 보여줍니다. 측정은 16 vCPU, 32 GB RAM, 5000 IOPS / 1000 MiB/s 디스크를 가진 단일 노드 AWS EC2 c6a.4xlarge 인스턴스에서 수행되었습니다. Redshift(ra3.4xlarge, 12 vCPU, 96 GB RAM)와 Snowflake(warehouse size S: 2x8 vCPU, 2x16 GB RAM)에는 비교 가능한 시스템을 사용했습니다. 물리적 데이터베이스 설계는 예를 들어 프라이머리 키를 지정하지만 개별 컬럼의 압축을 변경하지 않고, 프로젝션을 만들지 않고, 스킵핑 인덱스를 만들지 않는 등 가볍게만 튜닝합니다. 또한 각 콜드 쿼리 실행 전에 Linux 페이지 캐시를 플러시하지만 데이터베이스나 운영체제 노브는 조정하지 않습니다. 모든 쿼리에 대해 데이터베이스 간 가장 빠른 런타임이 기준선으로 사용됩니다. 다른 데이터베이스의 상대 쿼리 런타임은 (t + 10)/(t_+ 10)으로 계산됩니다. 데이터베이스의 총 상대 런타임은 쿼리별 비율의 기하 평균입니다. 연구 데이터베이스 Umbra [54]가 전반적으로 최고의 핫 런타임을 달성하지만, ClickHouse는 다른 모든 프로덕션급 데이터베이스를 능가합니다.

그림 10: ClickBench의 상대 콜드 및 핫 런타임.

시간이 지남에 따라 더 다양한 워크로드에서 SELECT 성능을 추적하기 위해 우리는 VersionsBench [19]라는 네 개의 벤치마크 조합을 사용합니다. 이 벤치마크는 새 릴리스가 발표될 때 성능을 평가[20]하고 성능을 잠재적으로 저하시킨 코드 변경을 식별하기 위해 매달 한 번 실행됩니다: 개별 벤치마크는: 1. ClickBench(위에서 설명), 2. 15개의 MgBench [21] 쿼리, 3. 6억 행이 있는 역정규화된 Star Schema Benchmark [57] 팩트 테이블에 대한 13개 쿼리, 4. 34억 행이 있는 NYC Taxi Rides [70]에 대한 4개 쿼리입니다. 그림 11은 2018년 3월과 2024년 3월 사이의 77개 ClickHouse 버전에 대한 VersionsBench 런타임의 발전을 보여줍니다. 개별 쿼리의 상대 런타임 차이를 보정하기 위해 전체 버전 중 최소 쿼리 런타임에 대한 비율을 가중치로 하는 기하 평균으로 런타임을 정규화합니다. VersionBench 성능은 지난 6년 동안 1.72배 향상되었습니다. 장기 지원(LTS) 릴리스의 날짜는 x축에 표시됩니다. 일부 기간에는 성능이 일시적으로 저하되었지만 LTS 릴리스는 일반적으로 이전 LTS 버전과 비교 가능하거나 더 나은 성능을 가집니다. 2022년 8월의 상당한 개선은 섹션 4.4에 설명된 컬럼별 필터 평가 기법에 의해 발생했습니다.

그림 11: VersionsBench 2018-2024의 상대 핫 런타임.

6.2.2 정규화된 테이블

고전적 웨어하우징에서 데이터는 종종 스타 또는 스노우플레이크 스키마로 모델링됩니다. 우리는 TPC-H 쿼리(스케일 팩터 100)의 런타임을 제시하지만, 정규화된 테이블은 ClickHouse의 신흥 사용 사례임을 언급합니다. 그림 12는 섹션 4.4에 설명된 병렬 해시 조인 알고리즘에 기반한 TPC-H 쿼리의 핫 런타임을 보여줍니다. 측정은 64 vCPU, 128 GB RAM, 5000 IOPS / 1000 MiB/s 디스크를 가진 단일 노드 AWS EC2 c6i.16xlarge 인스턴스에서 수행되었습니다. 다섯 번의 실행 중 가장 빠른 것이 기록되었습니다. 참고로 우리는 비슷한 크기의 Snowflake 시스템(warehouse size L, 8x8 vCPU, 8x16 GB RAM)에서 같은 측정을 수행했습니다. 11개 쿼리의 결과는 표에서 제외됩니다: Q2, Q4, Q13, Q17, Q20-22 쿼리는 ClickHouse v24.6에서 지원되지 않는 상관 서브쿼리를 포함합니다. Q7-Q9와 Q19 쿼리는 실현 가능한 런타임을 위해 조인 재정렬과 조인 조건 푸시다운(둘 다 ClickHouse v24.6에서 누락) 같은 확장된 계획 수준 조인 최적화에 의존합니다. 자동 서브쿼리 디코릴레이션과 조인에 대한 더 나은 옵티마이저 지원은 2024년에 구현될 예정입니다 [18]. 남은 11개 쿼리 중 5개(6개)는 ClickHouse(Snowflake)에서 더 빠르게 실행되었습니다. 앞서 언급한 최적화가 성능에 중요하다고 알려져 있으므로 [27], 구현되면 이 쿼리들의 런타임을 더 개선할 것으로 기대합니다.

그림 12: TPC-H 쿼리의 핫 런타임(초).

분석 데이터베이스는 최근 수십 년 동안 학술 및 상업적으로 큰 관심을 받아 왔습니다 [1]. Sybase IQ [48], Teradata [72], Vertica [42], Greenplum [47] 같은 초기 시스템은 비싼 배치 ETL 작업과 온프레미스 특성으로 인한 제한된 탄력성으로 특징지어졌습니다. 2010년대 초반에 Snowflake [22], BigQuery [49], Redshift [4] 같은 클라우드 네이티브 데이터 웨어하우스와 database-as-a-service(DBaaS) 제공물의 등장은 고가용성과 자동 리소스 스케일링의 이점을 누리면서 조직의 분석 비용과 복잡성을 극적으로 줄였습니다. 더 최근에는 분석 실행 커널(예: Photon [5]과 Velox [62])이 다양한 분석, 스트리밍, 머신 러닝 애플리케이션에서 사용할 co-modified 데이터 처리를 제공합니다. 목표와 설계 원칙 측면에서 ClickHouse와 가장 유사한 데이터베이스는 Druid [78]와 Pinot [34]입니다. 두 시스템 모두 높은 데이터 수집 속도로 실시간 분석을 목표로 합니다. ClickHouse처럼 테이블은 세그먼트라고 하는 수평 파츠로 분할됩니다. ClickHouse가 작은 파츠를 지속적으로 병합하고 선택적으로 섹션 3.3의 기법으로 데이터 볼륨을 줄이는 반면, Druid와 Pinot에서는 파츠가 영원히 변경 불가능하게 유지됩니다. 또한 Druid와 Pinot는 테이블을 만들고, 변경하고, 검색하기 위해 전문화된 노드가 필요한 반면, ClickHouse는 이러한 작업에 모놀리식 바이너리를 사용합니다. Snowflake [22]는 shared-disk 아키텍처에 기반한 인기 있는 상용 클라우드 데이터 웨어하우스입니다. 테이블을 마이크로 파티션으로 나누는 접근 방식은 ClickHouse의 파츠 개념과 유사합니다. Snowflake는 지속성에 하이브리드 PAX 페이지 [3]를 사용하는 반면 ClickHouse의 저장 형식은 엄격히 컬럼형입니다. Snowflake는 또한 자동으로 생성된 경량 인덱스 [31, 51]를 사용한 로컬 캐싱과 데이터 가지치기를 좋은 성능의 원천으로 강조합니다. ClickHouse의 프라이머리 키와 유사하게 사용자는 같은 값의 데이터를 함께 위치시키기 위해 선택적으로 클러스터 인덱스를 만들 수 있습니다. Photon [5]과 Velox [62]는 쿼리 실행 엔진 ...

8 CONCLUSION AND OUTLOOK

우리는 오픈소스 고성능 OLAP 데이터베이스 ClickHouse의 아키텍처를 제시했습니다. 쓰기 최적화된 스토리지 계층과 최첨단 벡터화된 쿼리 엔진을 기반으로 ClickHouse는 높은 수집 속도로 페타바이트 규모 데이터셋에 대한 실시간 분석을 가능하게 합니다. 데이터를 비동기적으로 백그라운드에서 병합하고 변환함으로써 ClickHouse는 데이터 유지보수와 병렬 삽입을 효율적으로 분리합니다. 그 스토리지 계층은 희소 프라이머리 인덱스, 스킵핑 인덱스, 프로젝션 테이블을 사용한 공격적인 데이터 가지치기를 가능하게 합니다. 우리는 업데이트와 삭제, 멱등적 삽입, 고가용성을 위한 노드 간 데이터 복제의 ClickHouse 구현을 설명했습니다. 쿼리 처리 계층은 풍부한 기법으로 쿼리를 최적화하고 모든 서버 및 클러스터 리소스에 걸쳐 실행을 병렬화합니다. 통합 테이블 엔진과 함수는 다른 데이터 관리 시스템 및 데이터 형식과 원활하게 상호 작용하는 편리한 방법을 제공합니다. 벤치마크를 통해 ClickHouse가 시장에서 가장 빠른 분석 데이터베이스 중 하나임을 보여주었고, 수년에 걸쳐 ClickHouse의 실제 배포에서 전형적인 쿼리 성능의 상당한 개선을 보여주었습니다. 2024년에 계획된 모든 기능과 개선은 공개 로드맵 [18]에서 찾을 수 있습니다. 계획된 개선에는 사용자 트랜잭션 지원, 대체 쿼리 언어로서의 PromQL [69], 반정형 데이터용 새 데이터 타입(예: JSON), 조인의 더 나은 계획 수준 최적화, 그리고 경량 삭제를 보완하는 경량 업데이트 구현이 포함됩니다.

ACKNOWLEDGMENTS

버전 24.6 기준 SELECT * FROM system.contributors는 ClickHouse에 기여한 1994명의 개인을 반환합니다. 우리는 ClickHouse Inc.의 전체 엔지니어링 팀과 ClickHouse의 놀라운 오픈소스 커뮤니티에 이 데이터베이스를 함께 구축한 그들의 노고와 헌신에 감사드립니다.

REFERENCES

이 논문에서 인용된 81개의 참고 문헌 목록은 원본 VLDB 2024 논문에서 확인할 수 있습니다. 주요 관련 작업으로는 MonetDB/X100 [11], C-Store [71], Snowflake [22], DuckDB [67], Druid [78], Pinot [34], Photon [5], Velox [62] 등이 포함됩니다.

더 알아보기 (Learn more)