Help us learn about your current experience with the documentation. Take the survey.

GitLab 中的 ClickHouse

本文档概述了如何在 GitLab Rails 应用中使用 ClickHouse 开发功能。

大多数工具和 API 都被认为是不稳定的。

GDK 设置

设置 ClickHouse 服务器

  1. 按照 ClickHouse 安装文档 中的说明在本地安装 ClickHouse。如果使用 QuickInstall,它将安装在当前目录;如果使用 Homebrew,它将安装到 /opt/homebrew/bin/clickhouse

  2. 向您的 gdk.yml 添加 ClickHouse 部分。参见 gdk.example.yml

  3. 调整 gdk.yml 中的 ClickHouse 配置文件,使其指向您的本地 ClickHouse 安装和本地数据存储。例如:

    clickhouse:
      bin: "/opt/homebrew/bin/clickhouse"
      enabled: true
      # 如果有多个 GDK,这些是可选的:
      # http_port: 8123
      # interserver_http_port: 9009
      # tcp_port: 9001
  4. 运行 gdk reconfigure

  5. 使用 gdk start clickhouse 启动 ClickHouse

配置您的 Rails 应用

  1. 复制示例文件并配置凭据:

    cp config/click_house.yml.example config/click_house.yml
  2. 使用捆绑的 clickhouse client 创建数据库:

    gdk clickhouse
    create 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_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::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 中。

示例

  1. 向同步到 ClickHouse 的表(如 milestones)添加一个类型为 int4 的新列 new_int

  2. 注意 CI 将失败并显示错误:

    This table is synchronised to ClickHouse and you've added a new column!
  3. 生成一个新的 ClickHouse 迁移来添加新列,注意 ClickHouse 表以 siphon_ 为前缀:

    bundle exec rails generate gitlab:click_house:migration add_new_int_to_siphon_milestones
  4. 在生成的文件中,定义添加/删除新列的 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_usageclickhouse.max_server_memory_usage 设置。

有关默认设置,请参阅 gdk.example.yml 文件。您必须重新配置 GDK 才能使更改生效。

获取帮助

有关更多信息或具体问题,请在 #f_clickhouse Slack 频道中联系 ClickHouse Datastore 工作组,或在 GitLab.com 的评论中提及 @gitlab-org/maintainers/clickhouse