GitLab 中的 ClickHouse
本文档概述了如何在 GitLab Rails 应用中使用 ClickHouse 开发功能。
大多数工具和 API 都被认为是不稳定的。
GDK 设置
设置 ClickHouse 服务器
-
按照 ClickHouse 安装文档 中的说明在本地安装 ClickHouse。如果使用 QuickInstall,它将安装在当前目录;如果使用 Homebrew,它将安装到
/opt/homebrew/bin/clickhouse -
向您的
gdk.yml添加 ClickHouse 部分。参见gdk.example.yml -
调整
gdk.yml中的 ClickHouse 配置文件,使其指向您的本地 ClickHouse 安装和本地数据存储。例如:clickhouse: bin: "/opt/homebrew/bin/clickhouse" enabled: true # 如果有多个 GDK,这些是可选的: # http_port: 8123 # interserver_http_port: 9009 # tcp_port: 9001 -
运行
gdk reconfigure -
使用
gdk start clickhouse启动 ClickHouse
配置您的 Rails 应用
-
复制示例文件并配置凭据:
cp config/click_house.yml.example config/click_house.yml -
使用捆绑的
clickhouse client创建数据库:gdk clickhousecreate database gitlab_clickhouse_development; create database gitlab_clickhouse_test;
验证您的设置
运行 Rails 控制台并执行一个简单查询:
ClickHouse::Client.select('SELECT 1', :main)
# => [{"1"=>1}]数据库模式和迁移
要生成 ClickHouse 数据库迁移,请执行:
bundle exec rails generate gitlab:click_house:migration MIGRATION_CLASS_NAME要运行数据库迁移,请执行:
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 数据库部署后迁移,请执行:
bundle exec rails generate gitlab:click_house:post_deployment_migration MIGRATION_CLASS_NAME这些迁移默认会与常规迁移一起运行,但可以跳过,
例如,在使用 SKIP_POST_DEPLOYMENT_MIGRATIONS 环境变量部署到生产环境之前跳过,例如:
export SKIP_POST_DEPLOYMENT_MIGRATIONS=true
bundle exec rake gitlab:clickhouse:migrate编写数据库查询
对于 ClickHouse 数据库,我们不使用 ORM(对象关系映射)。主要原因是 GitLab 应用程序对 ActiveRecord PostgreSQL 适配器有很多自定义,并且应用程序通常假设所有数据库都使用 PostgreSQL。由于 ClickHouse 相关功能仍处于开发初期,我们决定实现一个简单的 HTTP 客户端,以避免在处理多个 ActiveRecord 适配器时出现难以发现的错误和漫长的调试时间。
此外,ClickHouse 的使用方式可能与 ActiveRecord 的其他适配器不同。访问模式与传统事务性数据库不同,ClickHouse:
- 使用嵌套聚合
SELECT查询和GROUP BY子句。 - 不使用单个
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_sqlClickHouse 每个请求只允许一个语句。这意味着常见的 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::Client::QueryBuilder 类。该类使用 Arel gem 生成查询,并提供类似于 ActiveRecord 的查询接口。
builder = ClickHouse::Client::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 gem 来实现:
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::Client::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::Client::QueryBuilder.new('events')
# 按 target_type 和字符串化的遍历 ids/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 workers
利用 ClickHouse 数据库的 Sidekiq workers 应包含 ClickHouseWorker 模块。
这确保了在数据库迁移运行时 worker 会暂停,
并且在 worker 运行时不会执行迁移。
# events_sync_worker.rb
# frozen_string_literal: true
module ClickHouse
class EventsSyncWorker
include ApplicationWorker
include ClickHouseWorker
...
end
end最佳实践
在构建需要 ClickHouse 数据的功能时,您应该首先使用 Sidekiq workers 或其他策略从 PostgreSQL 表(如 events 或 issues)复制原始数据。然后,在该数据之上构建单独的聚合。通过避免直接从 PostgreSQL 进行聚合,您可以提高可维护性并启用数据重新处理。
测试
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 标签旁边选择计数。
测试中处理 Siphon 错误
GitLab 使用一个名为 Siphon 的工具不断将 PostgreSQL 中指定表的数据同步到 ClickHouse。 此过程要求对于每个指定的表,ClickHouse 模式必须包含 PostgreSQL 模式的副本。
在 GitLab 开发过程中,如果您向 PostgreSQL 添加新列而没有在 ClickHouse 中添加相应列,它将失败并显示错误:
This table is synchronised to ClickHouse and you've added a new column!要解决此问题,您应该添加一个迁移,也将该列添加到 ClickHouse 中。
示例
-
向同步到 ClickHouse 的表(如
milestones)添加一个类型为int4的新列new_int。 -
注意 CI 将失败并显示错误:
This table is synchronised to ClickHouse and you've added a new column! -
生成一个新的 ClickHouse 迁移来添加新列,注意 ClickHouse 表以
siphon_为前缀:bundle exec rails generate gitlab:click_house:migration add_new_int_to_siphon_milestones -
在生成的文件中,定义添加/删除新列的 up/down 方法。ClickHouse 数据类型大致映射到 PostgreSQL。 检查
Gitlab::ClickHouse::SiphonGenerator::PG_TYPE_MAP以获取新列的适当映射。使用错误的类型将触发不同的错误。 此外,在适当的地方考虑使用LowCardinaility,并谨慎使用Nullable,尽可能选择默认值。class AddNewIntToSiphonMilestones < ClickHouse::Migration def up execute <<~SQL ALTER TABLE siphon_milestones ADD COLUMN new_int Int64 DEFAULT 42; SQL end def down execute <<~SQL ALTER TABLE siphon_milestones DROP COLUMN new_int; SQL end end
如果您需要进一步帮助,请在内部联系 #f_siphon。
故障排除
如果在执行查询时遇到 MEMORY_LIMIT_EXCEEDED 错误,请在您的 gdk.yml 文件中增加 clickhouse.max_memory_usage 和 clickhouse.max_server_memory_usage 设置。
有关默认设置,请参阅 gdk.example.yml 文件。您必须重新配置 GDK 才能使更改生效。
获取帮助
有关更多信息或具体问题,请在 #f_clickhouse Slack 频道中联系 ClickHouse Datastore 工作组,或在 GitLab.com 的评论中提及 @gitlab-org/maintainers/clickhouse。