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(Object Relational Mapping)을 사용하지 않습니다. 주요 이유는 GitLab 애플리케이션이 ActiveRecord PostgreSQL 어댑터를 위해 많은 사용자 정의를 포함하고 있으며, 애플리케이션이 일반적으로 모든 데이터베이스가 PostgreSQL를 사용한다고 가정하기 때문입니다. ClickHouse 관련 기능이 여전히 개발 초기 단계에 있으므로, 발견하기 어려운 버그와 여러 ActiveRecord 어댑터를 다룰 때의 긴 디버깅 시간을 피하기 위해 간단한 HTTP 클라이언트를 구현하기로 결정했습니다.

추가적으로, ClickHouse는 다른 ActiveRecord 어댑터와 동일한 방식으로 사용되지 않을 수 있습니다. ClickHouse의 접근 패턴은 전통적인 트랜잭션 데이터베이스와 다르며, ClickHouse는:

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

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

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 유형으로 쿼리 자리 표시자를 지정하여 복잡한 쿼리를 작성할 수 있습니다. 이 라이브러리는 쿼리와 자리 표시자를 올바르게 병합하는 것을 보장합니다:

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 열만 사용하여 이벤트 테이블에서 삽입
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::Iterator 클래스를 사용하여 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')

# 목표 유형 및 문자열화된 순회 IDs/경로로 필터링
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

최소-최대 전략

첫 번째 단계로 iterator는 반복 데이터베이스 쿼리에서 사용할 조건으로서 데이터 범위를 결정합니다. 데이터 범위는 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

또한, 웹 상호작용에서 실행된 ClickHouse 쿼리를 보려면 성능 바에서 ch 레이블 옆에 있는 카운트를 선택하세요.

도움 요청

추가 정보나 특정 질문이 있을 경우, #f_clickhouse Slack 채널의 ClickHouse Datastore 작업 그룹에 연락하시거나, GitLab.com의 댓글에서 @gitlab-org/maintainers/clickhouse를 언급하십시오.