GitLab 내 ClickHouse
본 문서는 GitLab Rails 애플리케이션에서 ClickHouse를 사용하여 기능을 개발하는 고수준 개요를 제공합니다.
GDK 설정
로컬에서 ClickHouse 서버를 설정하는 방법은 ClickHouse 설치 문서를 참조하십시오.
Rails 애플리케이션 구성
-
예제 파일을 복사하고 자격 증명을 구성하십시오:
cp config/click_house.yml.example config/click_house.yml
-
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 (객체 관계 매핑)을 사용하지 않습니다. 주된 이유는 GitLab 애플리케이션이 ActiveRecord
PostgreSQL 어댑터에 대해 많은 사용자 정의를 가지고 있으며 애플리케이션 전반에서 모든 데이터베이스가 PostgreSQL
를 사용한다고 가정합니다. ClickHouse 관련 기능이 아직 매우 초기 개발 단계에 있기 때문에 여러 ActiveRecord
어댑터를 다룰 때 발견하기 어려운 버그와 긴 디버깅 시간을 피하려고 간단한 HTTP 클라이언트를 구현하기로 결정했습니다.
또한, ClickHouse는 일반 ActiveRecord
어댑터와는 다르게 사용될 수 있습니다. 액세스 패턴이 전통적인 트랜잭션 데이터베이스와 다르며 ClickHouse는 다음과 같은 특성을 가집니다:
- 중첩된 집계
SELECT
쿼리 및GROUP BY
절을 사용합니다. - 단일
INSERT
문을 사용하지 않습니다. 데이터는 배경 작업을 통해 일괄적으로 삽입됩니다. - 다른 일관성 특성, 트랜잭션이 없습니다.
- 매우 적은 데이터베이스 수준의 유효성 검사가 있습니다.
데이터베이스 쿼리는 ClickHouse::Client
젬의 도움으로 작성되고 실행됩니다.
events
테이블에서 간단한 쿼리:
rows = ClickHouse::Client.select('SELECT * FROM events', :main)
플레이스홀더를 사용하여 쿼리를 사용할 때, 특정 플레이스홀더 이름과 데이터 유형을 지정해야 하는 ClickHouse::Query
객체를 사용할 수 있습니다. 실제 변수를 바꾸고 따옴표를 묶고 이스케이핑은 클라이언트에서 처리할 겁니다.
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)
이러한 방식으로 데이터를 삽입하는 것은 다음 경우에 허용됩니다:
- 테이블에 설정이나 구성 데이터가 있는 경우 한 행을 추가해야 하는 경우
- 테스트를 위해 데이터를 데이터베이스에 준비해야 하는 경우
데이터를 삽입할 때는 항상 여러 행이 한꺼번에 삽입되는 일괄 처리를 사용하도록 해야 합니다. 대용량 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
테이블 반복
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에서 활성화되어 있지만, 파이프라인 실행 시간에 큰 영향을 미치는 것을 피하기 위해 테스트 용으로 ClickHouse 서버를 실행하기로 결정했습니다. 이를 위해 :click_house
로 태그가 지정된 테스트 케이스만 실행합니다.
: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
슬랙 채널의 ClickHouse 데이터 리포지터리 워킹 그룹에 연락하거나 GitLab.com의 코멘트에서 @gitlab-org/maintainers/clickhouse
를 언급해주세요.