在生产环境运行 ClickHouse:20 亿行规模下的写入、合并与成本
我们在生产环境运行 ClickHouse 的经验:schema 设计、查询优化,以及如何在 1 亿多条设备事件上实现亚秒级分析。
在我们开始构建 tracio.ai 的分析层时,需要一个能应对特定工作负载的数据库:每秒写入 50,000 条设备识别事件、存储 20 亿以上行数据,并在一秒以内回答分析查询。我们评估了 PostgreSQL(在这种规模下做聚合太慢)、Elasticsearch(用于时序分析成本太高)以及 ClickHouse。ClickHouse 以压倒性优势胜出。
为什么选 ClickHouse
ClickHouse 是一款面向实时分析设计的列式 OLAP 数据库。对我们的工作负载而言,它的关键优势在于每次查询只读取所需的列。当反欺诈分析师提出「给我看过去 7 天各国的欺诈率」时,ClickHouse 只读取 country、timestamp 和 risk_score 列,忽略事件表中另外 40 多个列。在一张 20 亿行的表上,这将 I/O 降低了 95%。
ClickHouse 的数据压缩也极为出色。我们那张 20 亿行的事件表在磁盘上仅占用 340 GB,压缩后约每行 170 字节,而未压缩时每行约 1.2 KB。7:1 的压缩比意味着能有更多数据装进内存,这直接带来更快的查询。
Schema 设计
我们的主表为每条识别事件存储一行:
该表使用 MergeTree 引擎,按 (workspace_id, toDate(timestamp), visitor_hash) 排序。这个排序至关重要,它意味着按 workspace 和日期范围过滤的查询只需读取极少量数据。visitor_hash 列则支持按访客 ID 快速查找,无需二级索引。
我们为 country、device_type、browser_family 和 os_family 选择了 LowCardinality(String),因为这些列的不同取值少于 10,000 个。ClickHouse 将 LowCardinality 列以字典编码的整数形式存储,相比普通字符串可减少 80% 的存储,并加快 GROUP BY 操作。
分片策略
我们用 workspace_id 的哈希把事件表分片到 6 个节点上。这确保了同一客户的所有事件都落在同一个分片上,也就意味着大多数查询(按 workspace_id 过滤)只命中单个分片。只有内部分析才需要跨分片查询。
每个分片有 2 个副本以实现高可用。复制使用 ClickHouse 内置的 ReplicatedMergeTree 引擎,配合 ZooKeeper 协调。故障切换是自动的,如果某个分片宕机,查询会被路由到副本,客户端无需任何改动。
写入管道
事件从我们的 Kafka topic 通过一个自研的 Go 服务批量写入 ClickHouse。我们每 500 毫秒以 10,000 行为一批写入,这样能在写入延迟(亚秒级)和写入效率(ClickHouse 在大批量下表现最佳)之间取得平衡。
该写入服务能优雅地处理背压。如果 ClickHouse 接收写入变慢(在合并或高查询负载期间),服务会在内存中缓冲多达 100 万条事件,并对 Kafka 消费者施加背压。在 18 个月的生产运行中,我们从未丢失过一条事件。
查询优化
物化视图
对于常见的仪表盘查询,我们使用物化视图来预聚合数据。例如,我们的欺诈率仪表盘从一个物化视图读取数据,该视图按 workspace、country 和小时聚合 fraud_detected 的计数。这个视图把该查询扫描的数据量从 20 亿行降到 500 万行。
Projection 排序
ClickHouse 的 projection 让我们能为一张表定义额外的排序方式,而无需重复存储数据。我们为访客时间线查询添加了一个按 (workspace_id, visitor_hash, timestamp) 排序的 projection。没有这个 projection 时,这类查询会扫描整个日期范围;有了它,只需读取包含目标访客的数据块。
近似函数
对于那些不要求精确计数的仪表盘查询,我们使用 ClickHouse 的近似函数:用 uniqCombined 计算去重数(误差 2%,比 uniqExact 快 10 倍),用 quantileTDigest 计算百分位。反欺诈分析仪表盘完全使用近似函数,从而让所有仪表盘查询都保持在 200 毫秒以内。
性能数据
以下是我们 20 亿行生产集群上有代表性的查询基准:
按国家统计过去 7 天的欺诈率:120 毫秒。访客时间线(50 条事件):8 毫秒。过去 30 天每日独立访客数:340 毫秒。过去 24 小时风险分数分布:95 毫秒。过去 30 天按事件数排名的前 100 台设备:210 毫秒。
这些数字包含了从我们应用服务器到 ClickHouse 集群的网络往返时间。纯查询执行时间通常还要低 30%–50%。
运维经验
经验一:监控合并延迟
ClickHouse 的 MergeTree 引擎会持续把小的数据 part 合并成更大的 part。如果合并跟不上(由于写入速率过高或磁盘 I/O 争用),查询性能会下降,因为查询必须扫描更多的 part。我们监控每个分区的 part 数量,并在其超过 300 时告警。
经验二:避免大型 ALTER TABLE 操作
在 ClickHouse 中给一张 20 亿行的表添加列是瞬时完成的(只涉及元数据)。但更改列的类型需要重写所有数据 part,这个过程在我们的集群上耗时 6 小时。现在我们把 schema 当作只追加的:可以自由添加新列,但类型变更要走一张迁移表。
经验三:谨慎使用 TTL
ClickHouse 通过 TTL 支持数据自动过期。我们在事件表上设置了 90 天的 TTL。需要注意的坑是:TTL 删除发生在合并期间,这意味着被删除的数据在 TTL 过期后可能仍会保留数小时甚至数天。对于合规关键的删除,我们会按计划运行显式的 ALTER TABLE DELETE 查询。
成本
我们这套 6 节点的 ClickHouse 集群(每个节点:32 vCPU、128 GB 内存、2 TB NVMe)在裸金属托管上每月成本约 8,400 美元。它存储着保留 90 天的 20 亿行数据,并处理每秒 5 万次写入外加 200 个并发仪表盘查询。每条存储事件的成本为 0.0000042 美元,比托管型云数据库上的同类分析便宜好几个数量级。