GitLab 내의 ClickHouse

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

note
대부분의 툴링 및 API는 안정성이 없는 것으로 간주됩니다.

GDK 설정

ClickHouse 서버 설정

  1. ClickHouse 설치 문서에 설명된 대로 ClickHouse를 로컬에 설치합니다. QuickInstall을 사용하면 현재 디렉터리에 설치되고, homebrew를 사용하면 /opt/homebrew/bin/clickhouse에 설치됩니다.
  2. gdk config set gdk.experimental.ruby_services true를 사용하여 실험적 서비스를 활성화합니다.
  3. gdk.yml에 clickhouse 섹션을 추가합니다. gdk.example.yml을 참조하세요.
  4. gdk.yml clickhouse 구성을 조정하여 로컬 clickhouse 설치와 로컬 데이터 리포지터리를 가리키도록합니다. 예:

    clickhouse:
      bin: "/opt/homebrew/bin/clickhouse"
      enabled: true
      # 다수의 GDK가 있는 경우 선택 사항:
      # http_port: 8123
      # interserver_http_port: 9009
      # tcp_port: 9001
    
  5. gdk reconfigure를 실행합니다.
  6. gdk start clickhouse로 clickhouse를 시작합니다.

Rails 애플리케이션 구성

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

    cp config/click_house.yml.example
    config/click_house.yml
    
  2. clickhouse-client CLI 도구를 사용하여 데이터베이스를 만듭니다.

    clickhouse client --port 9001
    
    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 (객체 관계 매핑)을 사용하지 않습니다. 주요 이유는 GitLab 애플리케이션에 ActiveRecord PostgreSQL 어댑터에 대한 많은 사용자 정의가 있고, 애플리케이션은 일반적으로 모든 데이터베이스가 PostgreSQL을 사용한다고 가정하기 때문입니다. ClickHouse 관련 기능이 여전히 매우 초기 단계에 있기 때문에, 여러 ActiveRecord 어댑터를 다룰 때 발생할 수 있는 심각한 버그와 긴 디버깅 시간을 피하기 위해 간단한 HTTP 클라이언트를 구현하기로 결정했습니다.

또한, ClickHouse는 SELECT 쿼리와 GROUP BY 절을 사용하는 중첩 집계 쿼리를 사용합니다. 단일 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))

서브쿼리

ClickHouse::Client::Query 클래스를 사용하여 특수 Subquery 유형의 쿼리 플레이스홀더를 지정함으로써 복잡한 쿼리를 작성할 수 있습니다. 라이브러리는 쿼리와 플레이스홀더를 정확하게 Merge합니다.

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)

이러한 방식으로 데이터를 삽입하는 것은 허용됩니다만:

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

데이터를 삽입할 때는 항상 여러 행이 한꺼번에 삽입되는 일괄 처리를 사용해야 합니다. 대규모 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
note
PostgreSQL의 데이터베이스 레코드를 효율적으로 일괄 처리하기 위해서는 이 곳에 설명된 기술을 고려해야 합니다.

테이블 반복

ClickHouse에서 대량의 데이터를 일괄 처리하기 위해 ClickHouse::Iterator 클래스를 사용할 수 있습니다. 이 반복자는 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')

# 대상 유형 및 문자열로 변환된 탐색 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 '행을 반환합니다' 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

또한 Web 상호 작용에서 ClickHouse 쿼리를 보려면, 성능 표시줄의 ch 레이블 옆에 횟수를 선택하십시오.

도움 받기

추가 정보나 구체적인 질문이 있으시면, #f_clickhouse Slack 채널의 ClickHouse 데이터 리포지터리 워킹 그룹에 문의하시거나 GitLab.com의 코멘트에서 @gitlab-org/maintainers/clickhouse를 언급해 주시기 바랍니다.