ClickHouse 深度实战:剖析下一代分析型数据库的架构内核

在大模型推理监控、实时业务指标分析、用户行为日志处理等场景中,一个核心诉求是:海量数据上的亚秒级聚合查询。传统行式数据库(如 MySQL、PostgreSQL)在亿级数据上做 GROUP BY 往往需要数十秒甚至分钟级响应,而 ClickHouse 通过彻底重构存储与执行引擎,将这一耗时压缩到毫秒级别。本文将深入剖析 ClickHouse 的架构内核,带你理解它为何能成为 OLAP 领域的事实标准。


一、OLAP 的结构性困境与 ClickHouse 的设计哲学

1.1 行存 vs 列存的本质差异

传统 OLTP 数据库采用行式存储(Row-oriented Storage),一行数据连续落盘。这种模式适合"读取单行多列"的事务处理,但对于分析型查询"读取单列全部值"的场景,行存会加载大量无用列到内存,造成严重的 I/O 浪费。

列式存储(Column-oriented Storage)将同一列的数据连续存放,带来三个关键优势:

  • I/O 效率:查询只涉及的列被读取,10 列中查 2 列只需 20% 的 I/O 量
  • 压缩率:同一列的数据类型、分布高度相似,压缩比可达 10:1 甚至更高
  • 向量化执行友好:连续内存布局天然适配 SIMD 批量处理
行存布局: [行1: id=1, name='Alice', age=30] | [行2: id=2, name='Bob', age=25] | ...
列存布局: id列: [1, 2, ...] | name列: ['Alice', 'Bob', ...] | age列: [30, 25, ...]

1.2 ClickHouse 的设计取舍

ClickHouse 自 2016 年开源以来,做出了几个关键的设计决策:

  1. 完全舍弃 UPDATE/DELETE:Append-Only 的写入模式,避免了 LSM-Tree 的写放大和锁竞争
  2. 单机优先架构:单查询首先榨干单机所有 CPU/内存资源,再通过分布式并行扩展
  3. 不做通用数据库:专注分析场景,牺牲事务能力换取极致的查询速度
  4. SQL 方言接近标准:降低学习成本,同时扩展分析专用语法

这些取舍使得 ClickHouse 在 Benchmark 中轻松达到每秒数十亿行的扫描吞吐量。


二、MergeTree 存储引擎:LSM 家族的列存变体

2.1 数据分区与排序键

MergeTree 是 ClickHouse 的核心存储引擎,理解它的关键在于 Partition(分区)和 Order By(排序键):

-- 创建一张 AI 推理请求日志表
CREATE TABLE inference_logs (
    ts DateTime,
    model_id UInt32,
    user_id UInt64,
    latency_ms Float32,
    tokens_input UInt32,
    tokens_output UInt32,
    status_code UInt16,
    region LowCardinality(String)
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(ts)      -- 按天分区,查询时可做分区裁剪
ORDER BY (model_id, ts)           -- 按模型+时间排序,数据在磁盘上有序存放
SETTINGS index_granularity = 8192;  -- 每 8192 行一个索引标记

主键 ORDER BY (model_id, ts) 的设计决定了数据的物理排布方式:同一模型的数据聚集在一起,且按时间递增。查询 WHERE model_id = 42 AND ts > now() - INTERVAL 1 HOUR 时,ClickHouse 可以快速定位数据所在的 granule 区间,跳过无关数据块。

2.2 Part 合并机制

写入时,数据以 Part(数据部分)为单位落盘。每个 Part 是一个独立的有序数据集。后台进程会异步将多个小 Part 合并为大 Part,减少文件碎片:

写入流程:
  INSERT data → MemTable (内存排序) → flush → Part_000001

合并流程:
  Part_000001 + Part_000002 + Part_000003 → Part_000001_2_3

  合并过程中按 ORDER BY 重排序,并生成最终的跳数索引

工程启示:应避免小批量高频写入,建议批量插入(每次 10万-100万行),否则产生大量小 Part 导致合并压力过大。监控 system.parts 中 Active Part 数量是运维的基本功。

2.3 跳数索引(Data Skipping Index)

除了主键的 min-max 索引,ClickHouse 支持多种跳数索引进一步减少扫描量:

-- 添加布隆过滤器索引,加速等值查询
ALTER TABLE inference_logs 
ADD INDEX bf_status status_code TYPE bloom_filter(0.01) GRANULARITY 1;

-- 添加 set 索引,适合低基数列的 IN 查询
ALTER TABLE inference_logs 
ADD INDEX set_region region TYPE set(100) GRANULARITY 1;

-- 添加 TTL 表达式,自动过期旧数据(省存储)
ALTER TABLE inference_logs 
MODIFY TTL ts + INTERVAL 30 DAY;

跳数索引的原理是:每个 granule(默认 8192 行)记录一个索引摘要,查询时先检查索引是否可能包含目标值,若不可能则直接跳过整个 granule。这种"粗筛"机制可以将随机查询的扫描量减少 10-100 倍。


三、向量化执行引擎:榨干每一颗 CPU

3.1 批量生产(Block-based Processing)

传统火山模型(Volcano Model)逐行处理数据,函数调用开销大。ClickHouse 采用 Block 作为最小处理单元,每个 Block 包含若干列的多行数据(通常几万到几十万行):

// 简化的向量化执行概念
struct Block {
    ColumnPtr columns[];   // 列数据
    size_t rows;           // 行数
};

// 聚合操作直接在列数组上做批量计算
void executeSum(Block& block, size_t col_idx) {
    auto& col = block.columns[col_idx];
    float sum = 0;
    // 连续内存遍历,CPU 预取高效
    for (size_t i = 0; i < block.rows; i++) {
        sum += col->getFloat(i);
    }
}

这种批量处理模式有三个加速器:

  1. CPU 缓存友好:连续内存访问,L1/L2 缓存命中率接近 100%
  2. 编译器自动向量化:GCC/Clang 可将简单循环编译为 AVX2/AVX-512 指令
  3. 减少函数调用开销:一次函数调用处理数万行 vs 逐行调用

3.2 聚合操作的算法优化

ClickHouse 为聚合函数实现了大量算法特化:

-- uniqCombined: HyperLogLog 近似去重,误差 ~1.625%,速度比精确 COUNT DISTINCT 快 100x
SELECT model_id, uniqCombined(user_id) AS unique_users
FROM inference_logs
GROUP BY model_id;

-- quantileTDigest: 精确分位数近似,支持任意分位点
SELECT model_id, 
       quantileTDigest(0.99)(latency_ms) AS p99_latency
FROM inference_logs
GROUP BY model_id;

-- 某些聚合支持增量合并,天然适配分布式查询
-- 例:CountMinSketch、uniqCombined、quantileTDigest 都是可合并的

3.3 内存限制与外部排序

当 GROUP BY 的基数超过内存限制时,ClickHouse 自动触发外部排序(External Sorting),将中间结果溢写到磁盘:

-- 设置单查询最大内存使用(默认 10GB)
SET max_memory_usage = 20000000000;  -- 20GB

-- 开启溢出到磁盘,避免 OOM
SET max_bytes_before_external_group_by = 10000000000; -- 10GB 时触发

四、物化视图:实时预计算的利器

4.1 流式聚合管道

ClickHouse 的物化视图(Materialized View)不只是一个视图,它本质上是一个流式聚合管道。数据写入源表时,触发器自动将聚合结果写入目标表:

-- 创建分钟级汇总的物化视图
CREATE MATERIALIZED VIEW inference_minute_stats
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMMDD(minute)
ORDER BY (model_id, minute)
AS SELECT
    toStartOfMinute(ts) AS minute,
    model_id,
    count() AS request_count,
    sum(tokens_input) AS total_input_tokens,
    sum(tokens_output) AS total_output_tokens,
    max(latency_ms) AS max_latency
FROM inference_logs
GROUP BY model_id, minute;

注意这里用的是 SummingMergeTree 而非普通 MergeTree——ClickHouse 在 Part 合并时会对相同排序键的数值列自动求和,这意味着你写入的是明细,存储的是汇总。查询时因为数据已预聚合,响应时间是毫秒级。

4.2 漏斗分析案例

利用 WindowFunnel 函数做用户行为漏斗分析,是 ClickHouse 的经典场景:

-- 分析用户从浏览→加入购物车→下单的转化率
SELECT
    level,
    count() AS users
FROM (
    SELECT
        user_id,
        windowFunnel(3600)(ts, 
            event = 'page_view',
            event = 'add_to_cart', 
            event = 'purchase'
        ) AS level
    FROM user_events
    WHERE ts >= today() - 7
    GROUP BY user_id
)
GROUP BY level
ORDER BY level;

五、高性能基础设施:从单机到分布式

5.1 分布式查询引擎

ClickHouse 的分布式表(Distributed Engine)提供透明的分片查询能力:

-- 在每个节点上创建本地表
CREATE TABLE inference_logs_local ON CLUSTER my_cluster (...) 
ENGINE = MergeTree() ...;

-- 创建分布式表,作为查询入口
CREATE TABLE inference_logs_dist ON CLUSTER my_cluster AS inference_logs_local
ENGINE = Distributed(my_cluster, default, inference_logs_local, rand());

查询分布式表时,ClickHouse 会自动将查询下发到各分片,在协调节点做二次聚合。关键是 SELECT 中的聚合操作会被拆分为两阶段:

第一阶段(各分片并行):
  本地聚合 → 产生中间结果(可合并的聚合状态)

第二阶段(协调节点):
  合并所有分片的聚合状态 → 返回最终结果

5.2 副本与高可用

通过 ReplicatedMergeTree 实现多副本:

CREATE TABLE inference_logs ON CLUSTER my_cluster (...)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/inference_logs', '{replica}')
...;

ClickHouse 使用 ZooKeeper(或 ClickHouse Keeper)协调副本间的: - 写入同步:数据写入一个副本后,通过 ZK 通知其他副本下载 - 主副本选举:自动检测故障并选举新主 - 分布式 DDL:一条建表语句跨所有节点执行


六、生产级部署与性能调优

6.1 关键配置参数

# config.xml 核心调优项
<yandex>
    <max_memory_usage>100000000000</max_memory_usage>  <!-- 100GB -->
    <max_concurrent_queries>100</max_concurrent_queries>
    <max_thread_pool_size>128</max_thread_pool_size>

    <!-- 压缩编码器选择 -->
    <compression>
        <case>
            <min_part_size>1000000000</min_part_size>
            <min_part_size_ratio>0.01</min_part_size_ratio>
            <method>zstd</method>  <!-- LZ4 速度快,zstd 压缩率高 -->
        </case>
    </compression>
</yandex>

6.2 数据生命周期管理

-- Tiered Storage:热数据 SSD + 冷数据 S3/OSS
ALTER TABLE inference_logs 
MODIFY SETTING storage_policy = 'tiered';

-- 存储策略定义
<storage_configuration>
    <disks>
        <s3_disk>
            <type>s3</type>
            <endpoint>https://oss-cn-beijing.aliyuncs.com/bucket/</endpoint>
        </s3_disk>
    </disks>
    <policies>
        <tiered>
            <volumes>
                <hot><disk>default</disk></hot>
                <cold><disk>s3_disk</disk></cold>
            </volumes>
        </tiered>
    </policies>
</storage_configuration>

TTL(Time To Live)结合多级存储,使得 ClickHouse 在成本可控的前提下管理 PB 级数据:

-- 最近 7 天在 SSD,7-30 天迁到对象存储,90 天后自动删
ALTER TABLE inference_logs 
MODIFY TTL 
    ts + INTERVAL 7 DAY TO VOLUME 'cold',
    ts + INTERVAL 90 DAY DELETE;

6.3 写入优化最佳实践

-- 1. 批量写入:每次至少 1 万行,推荐 10-100 万行
INSERT INTO inference_logs FORMAT CSV ...;

-- 2. 异步写入(重要!):降低客户端等待,ClickHouse 端批量处理
SET async_insert = 1;
SET wait_for_async_insert = 0;   -- 不等待落盘,写入即刻返回

-- 3. 使用 Buffer 引擎解决小写入问题
CREATE TABLE inference_logs_buffer AS inference_logs
ENGINE = Buffer(default, inference_logs, 16, 10, 10, 1000000, 10000000, 100000000);

七、AI 推理监控:ClickHouse 的杀手级场景

7.1 为什么 AI 推理系统需要 OLAP?

大模型生产系统中的监控需求具有典型的 OLAP 特征:

  • 写入量大:每秒数十万甚至百万推理请求
  • 查询延迟要求高:运维 Dashboard 需要在 1-2 秒内渲染
  • 聚合维度多元:按模型、版本、用户、地域、时间切片分析
  • 保留周期长:30-90 天的明细数据用于问题回溯

ClickHouse 的列存+向量化+预聚合架构完美匹配这些需求。

7.2 监控面板的核心查询

-- 实时 QPS 与 P99 延迟(Dashboard 刷新用,需亚秒级响应)
SELECT
    toStartOfInterval(ts, INTERVAL 1 MINUTE) AS minute,
    model_id,
    count() / 60 AS qps,
    quantileTDigest(0.99)(latency_ms) AS p99,
    avg(tokens_output / latency_ms * 1000) AS tokens_per_sec
FROM inference_logs
WHERE ts >= now() - INTERVAL 1 HOUR
GROUP BY minute, model_id
ORDER BY minute DESC;

-- Token 消耗趋势(用于计费分析)
SELECT
    toStartOfDay(ts) AS day,
    model_id,
    sum(tokens_input) AS total_input,
    sum(tokens_output) AS total_output,
    round(sum(tokens_input) * 0.0015 + sum(tokens_output) * 0.002, 2) AS cost_usd
FROM inference_logs
WHERE ts >= today() - 30
GROUP BY day, model_id;

八、从数据湖走向湖仓一体

ClickHouse 正从纯数仓向"湖仓一体"演进,原生支持查询外部数据源:

-- 直接查询 S3 上的 Parquet 文件
SELECT count() 
FROM s3('https://s3.amazonaws.com/bucket/logs/*.parquet', 'access_key', 'secret')
WHERE ts >= '2026-01-01';

-- 查询 Iceberg 表
SELECT * FROM icebergS3('https://host/warehouse/db/inference_logs') LIMIT 10;

-- 与 Spark/Presto 联邦查询
SELECT a.*, b.user_name 
FROM clickhouse_table a 
JOIN postgresql('pg-host:5432', 'db', 'users', 'user', 'pass') b 
ON a.user_id = b.id;

结合 EXPLAIN PIPELINE 分析查询执行计划,可以精确诊断性能瓶颈:

EXPLAIN PIPELINE
SELECT model_id, count() 
FROM inference_logs 
GROUP BY model_id 
ORDER BY count() DESC 
LIMIT 10;

输出展示了从存储读取、聚合、排序到返回结果的完整管道,以及各阶段的并行度和内存分配。


总结

ClickHouse 之所以能在 OLAP 领域独占鳌头,核心在于三个工程取舍:列式存储消除 I/O 浪费、向量化执行榨干 CPU、预聚合视图将计算前移。对于 AI 推理监控、日志分析、实时业务分析等需要海量数据快速聚合的场景,ClickHouse 是当前技术栈中的最优解。

但需要注意它的适用边界:单行 UPDATE、高 QPS 的点查、强事务一致性——这些场景应选择其他方案。ClickHouse 不是万能的,但在它擅长的领域,几乎无可替代。

关键参考:ClickHouse 官方 Benchmark 显示,在 SSB(Star Schema Benchmark)1000 倍数据集上,ClickHouse 的查询响应中位数比 Redshift 快 5-10 倍,成本仅为后者的 1/5。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部