GitLab 내 ClickHouse

이 문서는 GitLab Rails 애플리케이션에서 ClickHouse를 사용하여 기능을 개발하는 고수준 개요를 제공합니다.

참고: 대부분의 도구 및 API는 불안정한 것으로 간주됩니다.

GDK 설정

로컬에서 ClickHouse 서버를 설정하는 방법은 ClickHouse 설치 문서를 참조하세요.

Rails 애플리케이션 구성

  1. 예제 파일을 복사하고 자격 증명을 구성합니다:

    cp config/click_house.yml.example
    config/click_house.yml
    
  2. clickhouse-client CLI 도구를 사용하여 데이터베이스를 만드세요:

    clickhouse-client --password
    
    create database gitlab_clickhouse_development;
    

설정 유효성 검사

Rails 콘솔을 실행하고 간단한 쿼리를 호출합니다:

ClickHouse::Client.select('SELECT 1', :main)
# => [{"1"=>1}]

데이터베이스 스키마 및 마이그레이션

데이터베이스 마이그레이션을 실행하려면 다음을 실행하세요:

bundle exec rake gitlab:clickhouse:migrate

마지막 N개의 마이그레이션을 롤백하려면 다음을 실행하세요:

bundle exec rake gitlab:clickhouse:rollback:main STEP=N

또는 모든 마이그레이션을 롤백하려면 다음 명령을 사용하세요:

bundle exec rake gitlab:clickhouse:rollback:main VERSION=0

db/click_house/migrate 폴더에 Ruby 마이그레이션 파일을 만들어 마이그레이션을 생성할 수 있습니다. 이 파일은 YYYYMMDDHHMMSS_description_of_migration.rb 형식의 타임스탬프로 접두사를 붙여야 합니다.

# 20230811124511_create_issues.rb
# frozen_string_literal: true

class CreateIssues < ClickHouse::Migration
  def up
    execute <<~SQL
      CREATE TABLE issues
      (
        id UInt64 DEFAULT 0,
        title String DEFAULT ''
      )
      ENGINE = MergeTree
      PRIMARY KEY (id)
    SQL
  end

  def down
    execute <<~SQL
      DROP TABLE sync_cursors
    SQL
  end
end

데이터베이스 쿼리 작성

ClickHouse 데이터베이스에 대해 ORM (Object Relational Mapping)을 사용하지 않습니다. 주된 이유는 GitLab 애플리케이션이 ActiveRecord PostgreSQL 어댑터에 대해 많은 사용자 정의를 가지고 있으며, 애플리케이션은 일반적으로 모든 데이터베이스가 PostgreSQL를 사용한다고 가정합니다. ClickHouse 관련 기능이 아직 매우 초기 단계에 있기 때문에 여러 ActiveRecord 어댑터로 작업할 때 발생할 수 있는 버그 및 장기간의 디버그 시간을 피하기 위해 간단한 HTTP 클라이언트를 구현하기로 결정했습니다.

또한 ClickHouse는 ActiveRecord 어댑터와 동일한 방식으로 사용되지 않을 수 있습니다. 액세스 패턴이 전통적인 트랜잭션형 데이터베이스와 다르며 ClickHouse는 다음과 같은 특징을 갖습니다:

  • GROUP BY 절을 사용하는 중첩 집계 SELECT 쿼리를 사용합니다.
  • 단일 INSERT 문을 사용하지 않습니다. 데이터는 백그라운드 작업을 통해 일괄적으로 삽입됩니다.
  • 다른 일관성 특성, 트랜잭션이 없습니다.
  • 매우 적은 데이터베이스 수준 유효성 검사가 있습니다.

데이터베이스 쿼리는 ClickHouse::Client 젬의 도움을 받아 작성 및 실행됩니다.

events 테이블에서의 간단한 쿼리:

rows = ClickHouse::Client.select('SELECT * FROM events', :main)

플레이스홀더가 있는 쿼리를 작업할 때 ClickHouse::Query 객체를 사용할 수 있으며, 여기서 플레이스홀더 이름과 데이터 유형을 지정해야 합니다. 실제 변수 대체, 인용 및 이스케이핑은 ClickHouse 서버에서 처리됩니다.

raw_query = 'SELECT * FROM events WHERE id > {min_id:UInt64}'
placeholders = { min_id: Integer(100) }
query = ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders)

rows = ClickHouse::Client.select(query, :main)

플레이스홀더를 사용할 때 클라이언트가 로깅 시스템에 인계할 수 있는 쿼리의 가려진 플레이스홀더 값을 제공할 수 있습니다. to_redacted_sql 메서드를 호출하여 쿼리의 가려진 버전을 볼 수 있습니다:

puts query.to_redacted_sql

ClickHouse에서는 요청 당 하나의 문만 허용됩니다. 이는 문이 ; 문자로 종료되고 다른 쿼리가 “주입”되는 공통 SQL 인젝션 취약점을 악용할 수 없음을 의미합니다:

ClickHouse::Client.select('SELECT 1; SELECT 2', :main)

# ClickHouse::Client::DatabaseError: Code: 62. DB::Exception: Syntax error (Multi-statements are not allowed): failed at position 9 (end of query): ; SELECT 2. . (SYNTAX_ERROR) (version 23.4.2.11 (official build))

하위 쿼리

특별한 Subquery 형식의 쿼리 플레이스홀더로 쿼리를 병합하는 것을 제대로 보장하기 위해 ClickHouse::Client::Query 클래스로 복잡한 쿼리를 구성할 수 있습니다:

subquery = ClickHouse::Client::Query.new(raw_query: 'SELECT id FROM events WHERE id = {id:UInt64}', placeholders: { id: Integer(10) })

raw_query = 'SELECT * FROM events WHERE id > {id:UInt64} AND id IN ({q:Subquery})'
placeholders = { id: Integer(10), q: subquery }

query = ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders)
rows = ClickHouse::Client.select(query, :main)

# ClickHouse는 플레이스홀더를 대체합니다
puts query.to_sql # SELECT * FROM events WHERE id > {id:UInt64} AND id IN (SELECT id FROM events WHERE id = {id:UInt64})

puts query.to_redacted_sql # SELECT * FROM events WHERE id > $1 AND id IN (SELECT id FROM events WHERE id = $2)

puts query.placeholders # { id: 10 }

동일한 이름을 갖는 플레이스홀더가 다른 값으로 있을 경우 쿼리에서 오류가 발생합니다.

질의 조건 작성

여러 필터 조건이 있는 복잡한 양식을 처리할 때는 문자열로 쿼리 조각을 연결하여 쿼리를 작성하는 것이 매우 빠르게 복잡해질 수 있습니다. 여러 조건이 있는 쿼리에는 ClickHouse::QueryBuilder 클래스를 사용할 수 있습니다. 이 클래스는 Arel 젬을 사용하여 쿼리를 생성하고 ActiveRecord와 유사한 쿼리 인터페이스를 제공합니다.

builder = ClickHouse::QueryBuilder.new('events')

query = builder
  .where(builder.table[:created_at].lteq(Date.today))
  .where(id: [1,2,3])

rows = ClickHouse::Client.select(query, :main)

데이터 삽입

ClickHouse 클라이언트는 표준 쿼리 인터페이스를 통해 데이터를 삽입하는 것을 지원합니다.

raw_query = 'INSERT INTO events (id, target_type) VALUES ({id:UInt64}, {target_type:String})'
placeholders = { id: 1, target_type: 'Issue' }

query = ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders)
rows = ClickHouse::Client.execute(query, :main)

이러한 방식으로 데이터를 삽입하는 것은 다음과 같은 경우가 가능합니다:

  • 테이블에 설정 또는 구성 데이터가 있는 경우 한 행을 추가해야 하는 경우
  • 테스트를 위해 데이터를 데이터베이스에서 준비해야 하는 경우

데이터를 삽입할 때는 항상 한 번에 여러 행을 삽입하는 배치 처리를 사용하도록 해야 합니다. 대량의 INSERT 쿼리를 메모리에 빌드하는 것은 증가된 메모리 사용량 때문에 권장되지 않습니다. 또한 이러한 쿼리 내에서 지정된 값들은 클라이언트에 의해 자동으로 삭제되지 않습니다.

데이터를 압축하고 메모리 사용량을 줄이기 위해 CSV 데이터를 삽입할 수 있습니다. 내용은 내부 CsvBuilder 젬을 사용하여 수행할 수 있습니다:

iterator = Event.find_each

# id 및 target_type 열만 사용하여 events 테이블에서 삽입
column_mapping = {
  id: :id,
  target_type: :target_type
}

CsvBuilder::Gzip.new(iterator, column_mapping).render do |tempfile|
  query = 'INSERT INTO events (id, target_type) FORMAT CSV'
  ClickHouse::Client.insert_csv(query, File.open(tempfile.path), :main)
end

참고: PostgreSQL로부터 데이터베이스 레코드의 효율적인 배치를 테스트하고 확인하는 것이 중요합니다. 일괄 처리로 테이블 반복하기에 설명된 기술을 고려해야 합니다.

테이블 반복

ClickHouse::Iterator` 클래스를 사용하여 ClickHouse에서 대용량 데이터를 배치로 처리할 수 있습니다. 이 반복자는 기존 PostgreSQL 데이터베이스용 도구와 약간 다르게 작동합니다. (자세한 내용은 일괄 처리로 테이블 반복하기 문서를 참조하세요.) 이 도구는 데이터베이스 인덱스에 의존하지 않고 고정 크기의 숫자 범위를 사용합니다.

요구 사항:

  • 단일 정수 열
  • 열 값 사이에 큰 간격이 없으며, 이상적인 열은 자동 증가하는 PostgreSQL 기본 키일 것입니다.
  • 데이터 복제가 최소한이라면 중복된 값을 사용해도 괜찮습니다.

사용법:

connection = ClickHouse::Connection.new(:main)
builder = ClickHouse::QueryBuilder.new('events')

iterator = ClickHouse::Iterator.new(query_builder: builder, connection: connection)
iterator.each_batch(column: :id, of: 100_000) do |scope|
  records = connection.select(scope.to_sql)
end

특정 행을 반복하고 싶다면, 쿼리 빌더 개체에 필터를 추가할 수 있습니다. 효율적인 필터링과 반복을 위해서는 사용 사례에 최적화된 다른 데이터베이스 테이블 스키마가 필요할 수 있습니다. 이러한 반복을 도입할 때는 항상 데이터베이스 쿼리가 전체 데이터베이스 테이블을 스캔하지 않도록 해야 합니다.

connection = ClickHouse::Connection.new(:main)
builder = ClickHouse::QueryBuilder.new('events')

# target type 및 문자열화된 순회 id/path로 필터링
builder = builder.where(target_type: 'Issue')
builder = builder.where(path: '96/97/') # 특정 프로젝트를 가리킴

iterator = ClickHouse::Iterator.new(query_builder: builder, connection: connection)
iterator.each_batch(column: :id, of: 10) do |scope, min, max|
  puts "범위 처리 중: #{min} - #{max}"
  puts scope.to_sql
  records = connection.select(scope.to_sql)
end

최소-최대 전략

첫 번째 단계로, 반복자는 반복 데이터베이스 쿼리에서 사용할 데이터 범위를 결정합니다. 이 데이터 범위는 MIN(column)MAX(column) 집계 함수를 사용하여 결정됩니다. 일부 데이터베이스 테이블에 대해 이 전략은 비효율적인 데이터베이스 쿼리를 발생시킬 수 있습니다 (전체 테이블 스캔). 예를 들어, 파티션화된 데이터베이스 테이블의 경우가 그러합니다.

예시 쿼리:

SELECT MIN(id) AS min, MAX(id) AS max FROM events;

대신 ORDER BY + LIMIT를 이용한 다른 최소-최대 전략을 사용할 수 있습니다.

iterator = ClickHouse::Iterator.new(query_builder: builder, connection: connection, min_max_strategy: :order_limit)

예시 쿼리:

SELECT (SELECT id FROM events ORDER BY id ASC LIMIT 1) AS min, (SELECT id FROM events ORDER BY id DESC LIMIT 1) AS max;

Sidekiq 워커 구현

ClickHouse 데이터베이스를 활용하는 Sidekiq 워커는 ClickHouseWorker 모듈을 포함해아 합니다. 이렇게 함으로써 해당 워커가 데이터베이스 마이그레이션을 실행할 때 일시 중지되고, 워커가 활성화되어 있는 동안에는 마이그레이션이 실행되지 않도록 합니다.

# events_sync_worker.rb
# frozen_string_literal: true

module ClickHouse
  class EventsSyncWorker
    include ApplicationWorker
    include ClickHouseWorker

    ...
  end
end

테스트

ClickHouse는 CI/CD에서 활성화되어 있지만, 파이프라인 실행 시간에 큰 영향을 미치지 않도록, 테스트 케이스에 :click_house 태그가 지정된 경우에만 ClickHouse 서버를 실행하기로 결정했습니다.

:click_house 태그를 사용하여 데이터베이스 스키마가 각 테스트 케이스 실행 전에 올바르게 설정되도록 합니다.

RSpec.describe MyClickHouseFeature, :click_house do
  it 'returns rows' do
    rows = ClickHouse::Client.select('SELECT 1', :main)
    expect(rows.size).to eq(1)
  end
end

다중 데이터베이스

ClickHouse::Client 라이브러리는 여러 데이터베이스를 구성할 수 있도록 설계되었습니다. 개발 초기 단계이기 때문에 현재는 main이라는 하나의 데이터베이스만 사용하고 있습니다.

다중 데이터베이스 구성 예시:

development:
  main:
    database: gitlab_clickhouse_main_development
    url: 'http://localhost:8123'
    username: clickhouse
    password: clickhouse

  user_analytics: # 가상의 데이터베이스
    database: gitlab_clickhouse_user_analytics_development
    url: 'http://localhost:8123'
    username: clickhouse
    password: clickhouse

가시성

ClickHouse::Client 라이브러리를 통해 실행된 모든 쿼리는 ActiveSupport::Notifications를 통해 수행 시간, 읽기 바이트 등 성능 메트릭과 함께 노출됩니다.

ActiveSupport::Notifications.subscribe('sql.click_house') do |_, _, _, _, data|
  puts data.inspect
end

또한 웹 상호 작용에서 실행된 ClickHouse 쿼리를 볼 수 있도록, 성능 표시줄 옆 ch 레이블에서 실행 횟수를 선택할 수 있습니다.

도움 받기

추가 정보나 구체적인 질문이 있는 경우, #f_clickhouse 슬랙 채널의 ClickHouse 데이터 저장소 워킹 그룹에 문의하거나, GitLab.com의 댓글에서 @gitlab-org/maintainers/clickhouse를 언급해 주세요.