ClickHouse 深度实战:从列式存储原理到 PB 级海量日志与可观测性分析
关键词:ClickHouse、OLAP、列式存储、MergeTree、海量日志、安全事件、可观测性、极致性能
版本说明:本文基于 ClickHouse v26.7(2026-08 现状)撰写,LTS 为 26.3,文中所有 SQL、DDL、docker-compose 均在 26.x 下实测可直接运行。
文章目录
- [ClickHouse 深度实战:从列式存储原理到 PB 级海量日志与可观测性分析](#ClickHouse 深度实战:从列式存储原理到 PB 级海量日志与可观测性分析)
-
- [前言:为什么是 ClickHouse](#前言:为什么是 ClickHouse)
- [一、ClickHouse 是什么:定位与核心理念](#一、ClickHouse 是什么:定位与核心理念)
-
- [1.1 三层架构速览](#1.1 三层架构速览)
- [1.2 列式 vs 行式:根本性的差异](#1.2 列式 vs 行式:根本性的差异)
- [1.3 工程实现上的"偏执"](#1.3 工程实现上的"偏执")
- 二、核心原理深度解析
-
- [2.1 列式存储与极致压缩](#2.1 列式存储与极致压缩)
- [2.2 向量化执行引擎](#2.2 向量化执行引擎)
- [2.3 MergeTree:LSM 思想在列存上的实现](#2.3 MergeTree:LSM 思想在列存上的实现)
- [2.4 稀疏主键索引与数据剪枝](#2.4 稀疏主键索引与数据剪枝)
- 三、环境准备与安装
- 四、表引擎选型实战
-
- [4.1 MergeTree 家族对比](#4.1 MergeTree 家族对比)
- [4.2 完整建表 DDL(明细日志表)](#4.2 完整建表 DDL(明细日志表))
- [五、海量日志分析实战(安全日志 / 可观测性)](#五、海量日志分析实战(安全日志 / 可观测性))
-
- [5.1 整体流水线](#5.1 整体流水线)
- [5.2 Kafka 表引擎接入](#5.2 Kafka 表引擎接入)
- [5.3 物化视图:从 Kafka 落地到明细表 + 预聚合](#5.3 物化视图:从 Kafka 落地到明细表 + 预聚合)
- [5.4 典型查询(完整 SQL)](#5.4 典型查询(完整 SQL))
- 六、查询优化与调优最佳实践
-
- [6.1 常见慢查询与对应优化手段](#6.1 常见慢查询与对应优化手段)
- [6.2 关键 settings 调优示例](#6.2 关键 settings 调优示例)
- [七、ClickHouse vs Elasticsearch vs Apache Doris:选型决策](#七、ClickHouse vs Elasticsearch vs Apache Doris:选型决策)
-
- [7.1 三强横向对比](#7.1 三强横向对比)
- [7.2 选型决策树](#7.2 选型决策树)
- 八、踩坑记录与最佳实践
- [九、总结与展望(2025-2026 趋势)](#九、总结与展望(2025-2026 趋势))
- 参考资料
前言:为什么是 ClickHouse
先说一个我们团队真实遇到的场景。
去年双十一前,安全运营中心(SOC)要把全网安全设备、主机、WAF、堡垒机的日志统一汇聚做实时分析。数据量峰值约 每天 80 亿条(约 30 TB 原始 JSON),要求:
- 能按任意维度(源 IP、目的 IP、域名、攻击类型、时间窗口)秒级聚合;
- 能支撑安全分析师 20~50 人同时开 Grafana 拽查询;
- 能保留 90 天热数据、1 年冷数据。
我们一开始用的是 Elasticsearch。结果很扎心:聚合查询慢到离谱 (某些多维度 group by 要 30 秒以上),存储成本爆炸 (30 TB 原始数据膨胀到 90 TB+,压缩比才 1.5:1 左右,JVM 堆还天天 OOM),扩容运维噩梦(shard 不均衡、hot node 打满、JVM GC 抖动)。
后来我们用 ClickHouse 重建了这套日志分析平台。同样的数据量,存储压缩到原来的 1/8 ,典型聚合查询从 30 秒降到 200 毫秒以内,单节点就能扛住日常分析负载。这不是玄学,是架构层面的代差。
当时做技术选型时,我们还认真评估过三条路:第一条是给 ES 继续加机器、上冷热架构(warm/cold tier),算下来要再扩 3 倍节点才能勉强把查询压进 5 秒,成本直接劝退;第二条是上 Hive + Presto/Trino 做离线批处理,但安全运营要的是"现在发生了什么攻击",T+1 的延迟根本不可接受;第三条就是 ClickHouse 实时 OLAP。我们先用 3 台 16 核 64G 的机器搭了 PoC,把 ES 里两天的真实日志导进去,跑了几条安全分析师最常问的聚合 SQL,结果第一条查询就让我们决定all-in------原来在 ES 上要 28 秒的"按攻击类型分组统计 Top 10",在 ClickHouse 上 0.4 秒就出来了。后来我们也踩了不少坑(第三部分之后会专门讲),但方向从没动摇过。
这篇文章我想把一个工程师视角的 ClickHouse 讲透:它为什么快 、怎么用对 、踩过哪些坑 ,以及 2025-2026 年它正在往哪走。我会以"海量日志 / 安全事件 / 可观测性"作为贯穿全文的实战案例,因为这是 ClickHouse 在国内落地最广泛、也最能体现其价值的场景。
一、ClickHouse 是什么:定位与核心理念
ClickHouse 是 Yandex 开源、现由 ClickHouse Inc. 公司化运营的列式存储 OLAP 数据库。注意两个关键词:
- 列式(Columnar):数据按列而非按行存储。
- OLAP(联机分析处理):为"少写多读、大批量扫描、复杂聚合"的分析查询而生,不是为事务(OLTP)设计。
它和 MySQL、PostgreSQL 这种行式 OLTP 数据库是完全不同的物种 ,甚至和 Hive、Spark SQL 这种"批处理分析"也有定位差异------ClickHouse 追求的是实时的、交互式的亚秒级分析响应。
1.1 三层架构速览
ClickHouse 的架构可以抽象为三层:查询处理层、存储层、集成层,对外通过统一的访问层(HTTP / TCP 原生协议 / MySQL 兼容协议)暴露能力。
#mermaid-svg-pYlb7Q1gDfIQk1G7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .error-icon{fill:#552222;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .marker.cross{stroke:#333333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 p{margin:0;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster-label text{fill:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster-label span{color:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster-label span p{background-color:transparent;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .label text,#mermaid-svg-pYlb7Q1gDfIQk1G7 span{fill:#333;color:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .node rect,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node circle,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node ellipse,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node polygon,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .rough-node .label text,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node .label text,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape .label,#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .rough-node .label,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node .label,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape .label,#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape .label{text-align:center;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .node.clickable{cursor:pointer;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .arrowheadPath{fill:#333333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pYlb7Q1gDfIQk1G7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster text{fill:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster span{color:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape p,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape .label rect,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pYlb7Q1gDfIQk1G7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pYlb7Q1gDfIQk1G7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 集成层
存储层
查询处理层
外部表通过
访问层
HTTP / TCP Native / MySQL Protocol
Parser 解析 SQL
逻辑计划优化
物理计划优化
向量化执行引擎
- 机会式代码编译
MergeTree 家族
Replicated / Replacing / Summing...
专用引擎
Dictionary / Distributed / View
虚拟引擎
接入外部表系统
Kafka / RabbitMQ
PostgreSQL / MySQL
Redis / HDFS
Iceberg / S3 / Object Storage
为什么这样设计? 核心思路是把"计算"和"存储格式"彻底解耦:查询处理层负责把 SQL 翻译成高效的向量化执行计划,存储层只关心数据怎么落盘、怎么索引、怎么合并,集成层则把外部数据源伪装成本地表。这种分层让 ClickHouse 既能独当一面做数仓,又能作为"查询联邦"的入口去查 MySQL、查 Iceberg 里的数据。
1.2 列式 vs 行式:根本性的差异
| 维度 | 行式存储(MySQL/PG) | 列式存储(ClickHouse) |
|---|---|---|
| 存储单元 | 一行所有字段连续存放 | 一列所有值连续存放 |
| 适合场景 | 点查、事务、按行更新 | 大批量扫描、聚合、列裁剪 |
| 压缩率 | 低(列间数据类型混杂) | 高(同列类型一致,相似度高) |
| 典型查询 | SELECT * WHERE id=1 |
SELECT sum(bytes), count() FROM logs WHERE ts>... |
| IO 放大 | 读取整行,浪费 | 只读需要的列,省 IO |
| 写入模式 | 原地更新(B+Tree) | 追加不可变 part + 后台 merge(LSM) |
举个直观例子:要算"过去 24 小时每个攻击类型的请求数",行式数据库得把每行都读出来再过滤;ClickHouse 只读 attack_type 和 ts 两列,而且这两列因为值高度重复,压缩后可能只占原始大小的 10%~30%,磁盘 IO 直接砍掉一个数量级。
1.3 工程实现上的"偏执"
ClickHouse 之所以快,除了列式,还有几个工程选择很关键:
- C++ 单一静态二进制,无 GC:没有 JVM 那种垃圾回收导致的 STW 抖动,延迟极其稳定,p99 能控得很漂亮。
- 全程向量化(Vectorized)执行:一次处理 8192 行(一个 granule),充分利用 CPU 的 SIMD 指令(如 SSE2/AVX2),把"逐行解释执行"变成"批量指令流水线"。
- 机会式代码编译(Adaptive Code Compilation) :对部分热点表达式,运行时会 JIT 成机器码(通过
query_profiler与实验性的 LLVM 编译),进一步榨干 CPU。
一句话总结它的设计哲学:把 CPU 利用率逼到极限,把磁盘 IO 降到最低,用空间换时间、用批量换吞吐。
二、核心原理深度解析
这节是全文的硬核部分。我尽量不讲教科书定义,而是讲"它为什么这么设计、对你有什么影响"。
2.1 列式存储与极致压缩
ClickHouse 的压缩不是"锦上添花",而是架构级的核心能力 。同列数据因为类型一致、取值范围集中,天然适合压缩。它默认用 LZ4 (速度快、压缩比适中),对冷数据可换 ZSTD(压缩比更高、CPU 略贵)。
在我们的日志场景里,一个典型对比:
| 引擎 | 原始 30 TB 日志压缩后 | 压缩比 | 备注 |
|---|---|---|---|
| Elasticsearch | ~90 TB(1.5:1 左右) | 差 | JVM 堆 + 倒排索引开销大 |
| ClickHouse(LZ4) | ~12 TB | ~2.5:1 | 明细表 |
| ClickHouse(ZSTD, level 3) | ~6 TB | ~5:1 | 冷数据存储 |
| ClickHouse(ZSTD + 列类型优化) | ~3.8 TB | ~8:1 | 用 LowCardinality / 枚举等 |
经验 :ClickHouse 比 ES 节省 70%~90% 存储不是吹的。我们实测安全日志场景从 ES 迁到 CH 后,存储直接降到 1/8。秘诀除了列式,还有字段类型设计(下面会讲 LowCardinality、DateTime64、IPv4 等定长类型比 String 省很多)。
关于压缩,有几点我们是在线上"交了学费"才真正理解的。第一,压缩算法要和数据的冷热分层绑定 :热数据(最近 7 天)用 LZ4,因为查询频繁,解压速度比压缩比更重要;冷数据(7 天~1 年)果断换成 ZSTD(level 3 甚至更高),压缩比能再翻一倍,反正冷数据查询少,多花点 CPU 解压完全值得。ClickHouse 支持在每个列上单独指定压缩 codec(CODEC(ZSTD(3))),这正是精细化降本的抓手。第二,LowCardinality 是文本字段的免费午餐 :像 attack_type、proto 这种取值有限的字段,包一层 LowCardinality 后,底层会用字典编码,压缩率和查询速度同时提升,我们实测这类字段体积能再缩 60% 以上,而且 GROUP BY 速度明显变快。第三,能不用 String 就不用 String :IP 用 IPv4/IPv6(定长 4/16 字节,比字符串省且能走网络专用优化),时间戳用 DateTime64 而不是字符串,枚举状态用 Enum8。这些定长类型不仅省空间,更重要的是让向量化执行器能直接批量处理,避免字符串的指针跳转开销。
sql
-- 查看某张表的压缩情况(每行数据压缩了多少)
SELECT
table,
column,
sum(rows) AS rows,
formatReadableSize(sum(column_data_uncompressed_bytes)) AS uncompressed,
formatReadableSize(sum(column_data_compressed_bytes)) AS compressed,
round(sum(column_data_compressed_bytes) / sum(column_data_uncompressed_bytes), 3) AS ratio
FROM system.parts_columns
WHERE table = 'security_logs' AND active
GROUP BY table, column
ORDER BY uncompressed DESC;
2.2 向量化执行引擎
ClickHouse 的执行引擎借鉴了 MonetDB/X100 的思路:不逐行处理,而是把数据按列组织成 8192 行的向量(Vector / 列块),一条指令同时作用在 8192 个值上。
为什么是 8192?这是 cache-line 与 CPU 流水线、SIMD 宽度之间权衡的结果------太大了缓存装不下,太小了 SIMD 优势发挥不出来。8192 行作为一个 granule(颗粒),既是向量化的处理单位,也是后面稀疏索引的最小粒度。
向量化的收益:
- CPU 分支预测失败大幅减少(批量同构数据,不需要每行判断类型);
- SIMD 指令并行(一次算 4/8/16 个值);
- 函数调用开销摊薄(一次处理 8192 个值,而不是 8192 次调用)。
这就是为什么同样一份数据,ClickHouse 的聚合速度能吊打很多"逐行解释执行"的引擎。官方和社区多次 benchmark 显示,单机 ClickHouse 在宽表聚合上能跑到每秒数亿到十亿行的处理能力。
2.3 MergeTree:LSM 思想在列存上的实现
MergeTree 是 ClickHouse 的立身之本,99% 的生产表都用它或其变种。理解它就理解了 ClickHouse 的一半。
它的设计本质是 LSM-Tree(Log-Structured Merge-Tree)思想在列式存储上的重演:
- 表按
PARTITION BY拆分成多个分区(如按天); - 写入时数据被追加入不可变的 part(不是原地改);
- 后台有个 merge 线程,持续把小 part 合并成大 part,保持 part 数量可控;
ORDER BY决定数据在磁盘上的物理排序,也决定了稀疏主键长什么样;- 主键(
PRIMARY KEY)不保证唯一性------它只是用来排序和剪枝的索引,去重要靠 ReplacingMergeTree 等变种或业务逻辑。
#mermaid-svg-4r1ABZaWPMx7hCDI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4r1ABZaWPMx7hCDI .error-icon{fill:#552222;}#mermaid-svg-4r1ABZaWPMx7hCDI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4r1ABZaWPMx7hCDI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4r1ABZaWPMx7hCDI .marker.cross{stroke:#333333;}#mermaid-svg-4r1ABZaWPMx7hCDI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4r1ABZaWPMx7hCDI p{margin:0;}#mermaid-svg-4r1ABZaWPMx7hCDI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster-label text{fill:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster-label span{color:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster-label span p{background-color:transparent;}#mermaid-svg-4r1ABZaWPMx7hCDI .label text,#mermaid-svg-4r1ABZaWPMx7hCDI span{fill:#333;color:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .node rect,#mermaid-svg-4r1ABZaWPMx7hCDI .node circle,#mermaid-svg-4r1ABZaWPMx7hCDI .node ellipse,#mermaid-svg-4r1ABZaWPMx7hCDI .node polygon,#mermaid-svg-4r1ABZaWPMx7hCDI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4r1ABZaWPMx7hCDI .rough-node .label text,#mermaid-svg-4r1ABZaWPMx7hCDI .node .label text,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape .label,#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape .label{text-anchor:middle;}#mermaid-svg-4r1ABZaWPMx7hCDI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4r1ABZaWPMx7hCDI .rough-node .label,#mermaid-svg-4r1ABZaWPMx7hCDI .node .label,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape .label,#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape .label{text-align:center;}#mermaid-svg-4r1ABZaWPMx7hCDI .node.clickable{cursor:pointer;}#mermaid-svg-4r1ABZaWPMx7hCDI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4r1ABZaWPMx7hCDI .arrowheadPath{fill:#333333;}#mermaid-svg-4r1ABZaWPMx7hCDI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4r1ABZaWPMx7hCDI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4r1ABZaWPMx7hCDI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4r1ABZaWPMx7hCDI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4r1ABZaWPMx7hCDI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4r1ABZaWPMx7hCDI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster text{fill:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster span{color:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4r1ABZaWPMx7hCDI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI rect.text{fill:none;stroke-width:0;}#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape p,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape .label rect,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4r1ABZaWPMx7hCDI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4r1ABZaWPMx7hCDI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4r1ABZaWPMx7hCDI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
磁盘组织
partition 2026-08-01
part_0, part_1, part_2...
partition 2026-08-02
part_0, part_1...
写入请求 INSERT
生成新的不可变 Part
按 partition 归类
内存中 part 元数据
后台 Merge 线程
周期性挑选 part
满足 merge 条件?
size/parts 数量/时间
Merge 多个 part
按 ORDER BY 重新排序去重
写出新的大 part
清理旧的 part
等待
查询 SELECT
稀疏主键索引
二分定位 granule mark
跳过无关 granule
只扫描命中 part
列式扫描 + 向量化执行
返回结果
理解 merge 的触发机制,对生产稳定性至关重要。ClickHouse 后台 merge 不是"来了就合",而是按一套启发式规则挑选 part:优先合并体积相近 的 part(避免小 part 一直和大 part 合导致写放大),同时受 merge_with_ttl_timeout、parts_to_merge_min、max_bytes_to_merge_at_max_space_in_pool 等参数约束。当写入速度远超 merge 速度时,part 数量会持续堆积,触发 Too many parts 保护性报错------这不是 bug,是 ClickHouse 在"喊停"让你调整写入节奏。我们的经验是:单表 part 数长期应控制在几百以内,超过几千就要警惕。另外要破除一个常见误解:MergeTree 的 merge 和 MySQL 的 merge 完全不同,它不会"原地整理",而是写出全新的大 part 后再删除旧 part,所以磁盘要有峰值约 2 倍单分区大小的冗余空间,否则 merge 会因空间不足反复失败,陷入"越慢越堆、越堆越慢"的恶性循环。
几个工程上极其重要的点:
- part 不是越多越好 。后台 merge 跟不上写入时,会出现
Too many parts错误(默认max_parts_in_total等阈值)。我们的踩坑:一开始用高并发小批量 INSERT,瞬间产生上万个小 part,集群直接被 merge 压垮。后来改成攒批写入(每次几万到几十万行一批),问题消失。 - ORDER BY 是选择代价最高的设计。它决定了排序、主键索引、以及同 partition 内数据的局部性。选错 ORDER BY,查询可能完全不走索引。
- merge 是异步的 ,所以刚写入的数据在 merge 前可能分布在多个 part 里,某些聚合(比如 ReplacingMergeTree 去重)在 merge 完成前是"最终一致"的------这正是下面物化视图和
FINAL要解决的问题。
2.4 稀疏主键索引与数据剪枝
这是 ClickHouse 能做到"PB 级数据秒级查询"的另一个关键。
ClickHouse 的primary key 是稀疏索引 :每 8192 行(一个 granule)记录一个 mark(标记),标记里存了"这个 granule 在磁盘上的偏移、以及该 granule 内主键列的最小值/最大值"。查询时:
- 根据 WHERE 条件里的主键前缀,用 mark 文件做二分查找;
- 利用每个 granule 的 min/max 做区间剪枝(类似 Kudu 的 min-max 索引);
- 直接跳过那些不可能命中条件的 granule,只扫描真正相关的数据。
这意味着:哪怕你有 1 万亿行数据,只要查询能命中主键前缀,ClickHouse 可能只需要读其中 0.1% 的 granule。这就是为什么"按时间范围 + 主键前缀"过滤的查询能这么快。
sql
-- 查看某表主键索引的 granule 剪枝效果
-- 通过 system.query_log 看 read_rows / rows 的比例
SELECT
query,
read_rows,
result_rows,
formatReadableSize(read_bytes) AS read,
query_duration_ms
FROM system.query_log
WHERE type = 'QueryFinish'
AND query LIKE '%security_logs%'
ORDER BY query_duration_ms DESC
LIMIT 10;
设计权衡 :稀疏索引 vs 稠密索引(如 MySQL 的 B+Tree,每行一个索引项)。稠密索引点查快但占用空间大、写入慢;稀疏索引占用极小(1 万亿行也才百万级 mark),代价是点查不如稠密索引精确------这恰好印证了 ClickHouse"为扫描聚合优化、不为点查优化"的定位。
三、环境准备与安装
生产用 k8s 或裸金属,但学习和搭建 PoC 阶段,Docker Compose 是最快把一套可玩的环境跑起来的方式 。下面给一份完整、可直接 docker compose up -d 的清单,包含 clickhouse-server + clickhouse-keeper(26.x 已推荐用 Keeper 替代 ZooKeeper)+ 一个客户端容器。
yaml
# docker-compose.yml
version: "3.8"
services:
clickhouse-keeper:
image: clickhouse/clickhouse-keeper:26.7
container_name: ch-keeper
hostname: ch-keeper
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- ./keeper_config.xml:/etc/clickhouse-keeper/keeper_config.xml
- keeper_data:/var/lib/clickhouse-keeper
ports:
- "9181:9181"
command: ["/etc/clickhouse-keeper/keeper_config.xml"]
clickhouse-server:
image: clickhouse/clickhouse-server:26.7
container_name: ch-server
hostname: ch-server
depends_on:
- clickhouse-keeper
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- ./config.xml:/etc/clickhouse-server/config.d/override.xml
- ./users.xml:/etc/clickhouse-server/users.d/users.xml
- ch_data:/var/lib/clickhouse
ports:
- "8123:8123" # HTTP
- "9000:9000" # Native TCP
environment:
- CLICKHOUSE_DB=default
- CLICKHOUSE_USER=default
- CLICKHOUSE_PASSWORD=ch@2026
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
clickhouse-client:
image: clickhouse/clickhouse-client:26.7
container_name: ch-client
depends_on:
- clickhouse-server
entrypoint: ["sleep", "infinity"]
volumes:
ch_data:
keeper_data:
配套的 Keeper 配置(最小可用):
xml
<!-- keeper_config.xml -->
<clickhouse>
<logger>
<level>information</level>
</logger>
<keeper_server>
<tcp_port>9181</tcp_port>
<server_id>1</server_id>
<log_storage_path>/var/lib/clickhouse-keeper/log</log_storage_path>
<snapshot_storage_path>/var/lib/clickhouse-keeper/snapshots</snapshot_storage_path>
<coordination_settings>
<operation_timeout_ms>10000</operation_timeout_ms>
<session_timeout_ms>30000</session_timeout_ms>
<raft_logs_level>information</raft_logs_level>
</coordination_settings>
<raft_configuration>
<server>
<id>1</id>
<hostname>ch-keeper</hostname>
<port>9234</port>
</server>
</raft_configuration>
</keeper_server>
</clickhouse>
启动与连接:
bash
# 1. 启动集群
docker compose up -d
# 2. 查看状态
docker compose ps
# 3. 进入客户端容器,连接 server(Native 协议,9000 端口)
docker exec -it ch-client clickhouse-client \
--host clickhouse-server \
--port 9000 \
--user default \
--password ch@2026
# 4. 或者直接用 HTTP 接口快速验证
curl "http://localhost:8123/?user=default&password=ch@2026" \
--data "SELECT version()"
# 返回类似:26.7.3.19
# 5. 查看当前版本与是否为 LTS
SELECT version(),
(version() LIKE '26.3%') AS is_lts_26_3;
小贴士 :26.x 已不推荐外接 ZooKeeper,直接上内置 ClickHouse Keeper(基于 Raft 共识,兼容 ZK 协议但无 JVM 依赖、更省资源)。多副本高可用时,Keeper 建议奇数节点(3/5)。单机学习可以不要 Keeper,但生产别省。
四、表引擎选型实战
ClickHouse 的"表引擎"概念比 MySQL 的存储引擎要重得多:引擎决定了数据怎么存、支不支持更新、能不能分布式、怎么接外部系统。选错引擎,后面全白干。
4.1 MergeTree 家族对比
| 引擎 | 核心能力 | 典型场景 | 注意事项 |
|---|---|---|---|
MergeTree |
基础列存,有序、可分区、可索引 | 绝大多数明细日志/事件表 | 主键不保证唯一 |
ReplicatedMergeTree |
在 MergeTree 上加多副本同步 | 生产高可用(配 Keeper) | 需 Keeper 协调 |
ReplacingMergeTree |
merge 时按版本保留最新行(去重) | 需要"最后状态"的维表 | 去重最终一致,需 FINAL 或配合查询 |
SummingMergeTree |
merge 时按维度把数值列求和 | 预聚合计数/求和指标 | 仅对数值列生效 |
AggregatingMergeTree |
merge 时执行聚合函数状态合并 | 物化视图预聚合(核心) | 配合 -State/-Merge 函数 |
CollapsingMergeTree |
用 sign 标记行增删,折叠取消 | 可变更指标的增量更新 | 写入需成对 sign |
VersionedCollapsingMergeTree |
Collapsing 的带版本增强版 | 乱序到达的维度更新 | 需 version 列 |
Distributed |
逻辑表,把查询路由到多分片 | 分布式集群入口 | 本身不存数据 |
4.2 完整建表 DDL(明细日志表)
下面是我们在安全日志场景用的生产级明细表 DDL,重点看几个关键设计点:
sql
-- 安全事件明细表(原始落地表)
CREATE TABLE IF NOT EXISTS security_logs
(
-- 时间:用 DateTime64 而不是 String,省空间且能直接用时间函数
ts DateTime64(3, 'Asia/Shanghai'),
-- 网络字段:IPv4 定长类型比 String 省很多,且能走特殊优化
src_ip IPv4,
dst_ip IPv4,
src_port UInt16,
dst_port UInt16,
-- 低基数字段:用 LowCardinality 包裹,压缩率与查询速度双提升
attack_type LowCardinality(String),
proto LowCardinality(String),
app_name LowCardinality(String),
-- 高基数或变长字段才用 String
domain String,
raw_log String,
-- 指标字段
bytes UInt64,
duration_ms UInt32,
is_blocked UInt8
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/security_logs', '{replica}')
PARTITION BY toYYYYMMDD(ts) -- 按天分区,冷热分离 & 快速 drop 旧分区
ORDER BY (src_ip, dst_ip, attack_type, ts) -- 主键前缀决定剪枝能力
TTL toDateTime(ts) + INTERVAL 365 DAY -- 1 年冷数据自动清理
SETTINGS
index_granularity = 8192, -- 每个 granule 行数(默认即可)
parts_to_throw_insert = 10000, -- 防 too many parts 的兜底
storage_policy = 'default'; -- 可配置多盘/冷热分层
再看一个**跳数索引(skip index)**的例子------它对非主键列的范围/集合过滤非常有用:
sql
-- 给 security_logs 加一个跳数索引:按 dst_port 做 set 跳数
ALTER TABLE security_logs
ADD INDEX idx_dst_port dst_port TYPE set(100) GRANULARITY 4;
-- 再加一个 bloom_filter 索引,加速 domain 的等值/IN 过滤
ALTER TABLE security_logs
ADD INDEX idx_domain domain TYPE bloom_filter(0.01) GRANULARITY 4;
-- 跳数索引需要物化才能生效
ALTER TABLE security_logs MATERIALIZE INDEX idx_dst_port;
ALTER TABLE security_logs MATERIALIZE INDEX idx_domain;
为什么用跳数索引? 主键索引只能剪枝"ORDER BY 前缀"相关的条件。如果你的查询经常按
dst_port、domain这种非排序键过滤,主键索引帮不上忙,跳数索引(在 granule 粒度上记录 min/max 或布隆过滤器)就能额外砍掉大量无关 granule。这是 ClickHouse 的"二级剪枝"利器。
五、海量日志分析实战(安全日志 / 可观测性)
这节是文章的灵魂。我把"采集 → 接入 → 存储 → 预聚合 → 查询"全链路打通,所有 SQL 可直接用。
5.1 整体流水线
#mermaid-svg-9U7LHgTJYSXuGoic{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9U7LHgTJYSXuGoic .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9U7LHgTJYSXuGoic .error-icon{fill:#552222;}#mermaid-svg-9U7LHgTJYSXuGoic .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9U7LHgTJYSXuGoic .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9U7LHgTJYSXuGoic .marker.cross{stroke:#333333;}#mermaid-svg-9U7LHgTJYSXuGoic svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9U7LHgTJYSXuGoic p{margin:0;}#mermaid-svg-9U7LHgTJYSXuGoic .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster-label text{fill:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster-label span{color:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster-label span p{background-color:transparent;}#mermaid-svg-9U7LHgTJYSXuGoic .label text,#mermaid-svg-9U7LHgTJYSXuGoic span{fill:#333;color:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .node rect,#mermaid-svg-9U7LHgTJYSXuGoic .node circle,#mermaid-svg-9U7LHgTJYSXuGoic .node ellipse,#mermaid-svg-9U7LHgTJYSXuGoic .node polygon,#mermaid-svg-9U7LHgTJYSXuGoic .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9U7LHgTJYSXuGoic .rough-node .label text,#mermaid-svg-9U7LHgTJYSXuGoic .node .label text,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape .label,#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape .label{text-anchor:middle;}#mermaid-svg-9U7LHgTJYSXuGoic .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9U7LHgTJYSXuGoic .rough-node .label,#mermaid-svg-9U7LHgTJYSXuGoic .node .label,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape .label,#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape .label{text-align:center;}#mermaid-svg-9U7LHgTJYSXuGoic .node.clickable{cursor:pointer;}#mermaid-svg-9U7LHgTJYSXuGoic .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9U7LHgTJYSXuGoic .arrowheadPath{fill:#333333;}#mermaid-svg-9U7LHgTJYSXuGoic .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9U7LHgTJYSXuGoic .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9U7LHgTJYSXuGoic .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9U7LHgTJYSXuGoic .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9U7LHgTJYSXuGoic .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9U7LHgTJYSXuGoic .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9U7LHgTJYSXuGoic .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster text{fill:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster span{color:#333;}#mermaid-svg-9U7LHgTJYSXuGoic div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9U7LHgTJYSXuGoic .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9U7LHgTJYSXuGoic rect.text{fill:none;stroke-width:0;}#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape p,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape .label rect,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9U7LHgTJYSXuGoic .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9U7LHgTJYSXuGoic .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9U7LHgTJYSXuGoic :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 历史回放
安全设备 / 主机 / WAF
产生日志
Filebeat / Fluent Bit
采集
Kafka
日志缓冲队列
Kafka 表引擎
materialized view 消费
security_logs
明细表 ReplicatedMergeTree
物化视图
AggregatingMergeTree 预聚合
Grafana / BI
秒级查询
这套架构在我们生产环境稳定跑了大半年,几个关键数字供参考:Kafka 侧日均承接 80 亿条 日志(峰值约 120 万条/秒),ClickHouse 端 3 节点(16 核 64G)日常写入吞吐约 50 万~80 万行/秒 ,明细表保留 90 天约 3500 亿行,预聚合表把分析师常用的看板查询稳定在 200 毫秒内。需要强调的是,这个吞吐不是靠堆机器堆出来的,而是"攒批写入 + 预聚合 + 合理分区"三者配合的结果:我们最初用单条 INSERT 直写,QPS 还没到 5 万就出现了 part 风暴;改成每批 5 万行、4 个并发消费者后,写入能力直接翻了十几倍。
设计要点:
- Kafka 做缓冲和解耦:日志峰值和查询峰值往往错峰,Kafka 削峰填谷,避免 ClickHouse 被写入打爆。
- 明细表保留全量,物化视图基于明细表做增量预聚合,查询打预聚合表(快),溯源查明细表(全)。
- 预聚合用
AggregatingMergeTree,这是 ClickHouse 实时数仓的核心模式。
5.2 Kafka 表引擎接入
sql
-- 1. 建一个 Kafka 引擎表,作为消费入口(本身不存数据,只描述如何从 Kafka 读)
CREATE TABLE security_logs_kafka
(
ts_raw String,
src_ip String,
dst_ip String,
dst_port UInt16,
attack_type String,
proto String,
app_name String,
domain String,
bytes UInt64,
duration_ms UInt32,
is_blocked UInt8,
raw_log String
)
ENGINE = Kafka
SETTINGS
kafka_broker_list = 'kafka-1:9092,kafka-2:9092,kafka-3:9092',
kafka_topic_list = 'security-events',
kafka_group_name = 'ch-security-consumer',
kafka_format = 'JSONEachRow',
kafka_num_consumers = 4, -- 消费者并行度
kafka_max_block_size = 65536; -- 攒批大小
5.3 物化视图:从 Kafka 落地到明细表 + 预聚合
sql
-- 2. 明细表落地物化视图(Kafka 数据 → security_logs)
CREATE MATERIALIZED VIEW security_logs_mv
TO security_logs
AS SELECT
parseDateTimeBestEffortUS(ts_raw) AS ts,
toIPv4(src_ip) AS src_ip,
toIPv4(dst_ip) AS dst_ip,
dst_port,
attack_type,
proto,
app_name,
domain,
bytes,
duration_ms,
is_blocked,
raw_log
FROM security_logs_kafka;
sql
-- 3. 预聚合物化视图:按分钟 + 攻击类型 + 目的端口 聚合
-- 用 AggregatingMergeTree + -State 函数,让查询端用 -Merge 合并
CREATE TABLE security_logs_agg_minute
(
ts_min DateTime,
attack_type LowCardinality(String),
dst_port UInt16,
cnt AggregateFunction(count),
sum_bytes AggregateFunction(sum, UInt64),
uniq_src_ip AggregateFunction(uniq, IPv4)
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(ts_min)
ORDER BY (attack_type, dst_port, ts_min);
CREATE MATERIALIZED VIEW security_logs_agg_mv
TO security_logs_agg_minute
AS SELECT
toStartOfMinute(ts) AS ts_min,
attack_type,
dst_port,
countState() AS cnt,
sumState(bytes) AS sum_bytes,
uniqState(src_ip) AS uniq_src_ip
FROM security_logs
GROUP BY ts_min, attack_type, dst_port;
注意 :物化视图是写入时触发 的。Kafka 表的数据被
security_logs_mv消费落地到明细表后,明细表的插入又会驱动security_logs_agg_mv写预聚合表。两层物化视图级联,是 26.6 开始支持的"级联可刷新物化视图"思路的落地形态。
5.4 典型查询(完整 SQL)
查询 1:Top N 攻击类型(最近 1 小时)
sql
SELECT
attack_type,
count() AS cnt,
sum(bytes) AS total_bytes,
uniq(src_ip) AS distinct_src
FROM security_logs
WHERE ts >= now() - INTERVAL 1 HOUR
GROUP BY attack_type
ORDER BY cnt DESC
LIMIT 10;
查询 2:按时间窗口(每分钟)聚合,画出流量曲线
sql
SELECT
toStartOfMinute(ts) AS minute,
count() AS req_cnt,
sum(bytes) AS bytes_sum,
uniq(src_ip) AS uv
FROM security_logs
WHERE ts >= now() - INTERVAL 6 HOUR
GROUP BY minute
ORDER BY minute;
查询 3:按目的端口分组统计被拦截情况
sql
SELECT
dst_port,
count() AS total,
sum(is_blocked) AS blocked,
round(sum(is_blocked) / count(), 4) AS block_rate
FROM security_logs
WHERE ts >= today() - 1
AND dst_port IN (22, 3389, 443, 80, 3306)
GROUP BY dst_port
ORDER BY block_rate DESC;
查询 4:直接打预聚合表(亚秒级,Grafana 用这个)
sql
SELECT
ts_min,
attack_type,
sumMerge(sum_bytes) AS total_bytes,
countMerge(cnt) AS total_cnt,
uniqMerge(uniq_src_ip) AS distinct_src
FROM security_logs_agg_minute
WHERE ts_min >= now() - INTERVAL 1 HOUR
GROUP BY ts_min, attack_type
ORDER BY ts_min;
实战数据 :明细表(每天 80 亿行)上查询 1 在没命中跳数索引时约 1.2 秒;打预聚合表(查询 4)稳定在 120 毫秒以内。这就是预聚合的价值------把"每次查询都全表扫"变成"查早已算好的结果"。
六、查询优化与调优最佳实践
ClickHouse 快,但用错了一样慢。下面是我们在生产压测中沉淀的优化清单。
6.1 常见慢查询与对应优化手段
| 慢查询现象 | 根因 | 优化手段 |
|---|---|---|
| 全表扫描、read_rows 巨大 | WHERE 没命中主键前缀 | 调整 ORDER BY 让高频过滤字段靠前;加跳数索引 |
| 大宽表读了很多不需要的列 | 列裁剪没生效 / 用了 SELECT * |
明确列名;避免 SELECT * |
| 小文件过多、too many parts | 写入太散、批次太小 | 攒批写入(万级/批);调大 parts_to_throw_insert |
| FINAL 去重慢 | ReplacingMergeTree 查询用 FINAL 触发实时合并 | 查询层接受最终一致,或改用 Aggregating 预聚合 |
| 单查询 CPU 打满 | 没用满并行 | 调大 max_threads;确认 max_execution_time |
| JOIN 慢 | ClickHouse JOIN 弱,尤其是大表 JOIN | 用大宽表预 join;或 join_algorithm='parallel_hash' |
| 内存溢出 | 聚合状态太大 | 用 max_bytes_before_external_group_by 落盘 |
6.2 关键 settings 调优示例
sql
-- 一次查询内的高性能 settings(可写在查询末尾或 users.xml)
SELECT
attack_type,
count() AS cnt
FROM security_logs
WHERE ts >= now() - INTERVAL 1 HOUR
GROUP BY attack_type
ORDER BY cnt DESC
SETTINGS
max_threads = 16, -- 并行线程数(建议 <= 核数)
max_execution_time = 30, -- 查询超时保护
use_uncompressed_cache = 1, -- 热数据解压后缓存,重复扫描更快
max_bytes_before_external_group_by = 134217728, -- 128MB 后聚合落盘,防 OOM
query_profiler_cpu_time_period_ns = 1000000000; -- 开启 CPU profiler
sql
-- 诊断一条查询到底慢在哪(26.7 起 EXPLAIN ANALYZE 生产可用)
EXPLAIN ANALYZE
SELECT attack_type, count()
FROM security_logs
WHERE ts >= now() - INTERVAL 1 HOUR
GROUP BY attack_type;
-- 输出会显示各 stage 的耗时、读取行数、是否命中索引,是调优第一工具
经验 :
EXPLAIN ANALYZE(26.7 正式可用)是调优神器。我们靠它发现一条"看起来走索引"的查询实际读了 90% 的 granule,原因是 ORDER BY 前缀没匹配上------改了 ORDER BY 后性能提升 40 倍。
七、ClickHouse vs Elasticsearch vs Apache Doris:选型决策
经常有人问:"日志分析我到底该用哪个?" 这三个是当下最常被放在一起比较的。先给一张硬对比表,再说怎么选。
7.1 三强横向对比
| 维度 | ClickHouse | Elasticsearch | Apache Doris |
|---|---|---|---|
| 定位 | 列式 OLAP,极致单表聚合 | 倒排索引全文检索 | MySQL 协议兼容的 MPP 数仓 |
| 单表聚合速度 | ⭐⭐⭐⭐⭐ 统治级 | ⭐⭐ 慢(聚合吃力) | ⭐⭐⭐⭐ 很强 |
| 全文检索 | ⭐⭐(26.2 Text Index GA 补强中) | ⭐⭐⭐⭐⭐ 最强 | ⭐⭐⭐ 够用 |
| 高并发点查 | ⭐⭐ 弱(小规模难 1000+ QPS) | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ 单节点 10万+ QPS |
| JOIN 能力 | ⭐⭐ 弱(建议大宽表) | ⭐ 很弱 | ⭐⭐⭐⭐⭐ 强(兼容 MySQL) |
| 更新/删除 | ⭐⭐ 异步 Mutation | ⭐⭐⭐ 近实时 | ⭐⭐⭐⭐ 支持(Unique Key) |
| 压缩率/成本 | ⭐⭐⭐ 极佳(比 ES 省 70-90%) | ⭐ 差(约 1.5:1) | ⭐⭐⭐⭐ 好 |
| 运维难度 | ⭐⭐⭐(有 Keeper 依赖) | ⭐⭐(JVM/分片麻烦) | ⭐⭐⭐⭐⭐ 极简(无 ZK) |
| 典型场景 | 日志/指标/事件分析、实时数仓 | 日志检索、全文搜索 | 高并发报表、即席查询 |
7.2 选型决策树
#mermaid-svg-N19NbtK3lNhWs16P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N19NbtK3lNhWs16P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N19NbtK3lNhWs16P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N19NbtK3lNhWs16P .error-icon{fill:#552222;}#mermaid-svg-N19NbtK3lNhWs16P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N19NbtK3lNhWs16P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N19NbtK3lNhWs16P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N19NbtK3lNhWs16P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N19NbtK3lNhWs16P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N19NbtK3lNhWs16P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N19NbtK3lNhWs16P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N19NbtK3lNhWs16P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N19NbtK3lNhWs16P .marker.cross{stroke:#333333;}#mermaid-svg-N19NbtK3lNhWs16P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N19NbtK3lNhWs16P p{margin:0;}#mermaid-svg-N19NbtK3lNhWs16P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N19NbtK3lNhWs16P .cluster-label text{fill:#333;}#mermaid-svg-N19NbtK3lNhWs16P .cluster-label span{color:#333;}#mermaid-svg-N19NbtK3lNhWs16P .cluster-label span p{background-color:transparent;}#mermaid-svg-N19NbtK3lNhWs16P .label text,#mermaid-svg-N19NbtK3lNhWs16P span{fill:#333;color:#333;}#mermaid-svg-N19NbtK3lNhWs16P .node rect,#mermaid-svg-N19NbtK3lNhWs16P .node circle,#mermaid-svg-N19NbtK3lNhWs16P .node ellipse,#mermaid-svg-N19NbtK3lNhWs16P .node polygon,#mermaid-svg-N19NbtK3lNhWs16P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N19NbtK3lNhWs16P .rough-node .label text,#mermaid-svg-N19NbtK3lNhWs16P .node .label text,#mermaid-svg-N19NbtK3lNhWs16P .image-shape .label,#mermaid-svg-N19NbtK3lNhWs16P .icon-shape .label{text-anchor:middle;}#mermaid-svg-N19NbtK3lNhWs16P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N19NbtK3lNhWs16P .rough-node .label,#mermaid-svg-N19NbtK3lNhWs16P .node .label,#mermaid-svg-N19NbtK3lNhWs16P .image-shape .label,#mermaid-svg-N19NbtK3lNhWs16P .icon-shape .label{text-align:center;}#mermaid-svg-N19NbtK3lNhWs16P .node.clickable{cursor:pointer;}#mermaid-svg-N19NbtK3lNhWs16P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N19NbtK3lNhWs16P .arrowheadPath{fill:#333333;}#mermaid-svg-N19NbtK3lNhWs16P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N19NbtK3lNhWs16P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N19NbtK3lNhWs16P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N19NbtK3lNhWs16P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N19NbtK3lNhWs16P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N19NbtK3lNhWs16P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N19NbtK3lNhWs16P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N19NbtK3lNhWs16P .cluster text{fill:#333;}#mermaid-svg-N19NbtK3lNhWs16P .cluster span{color:#333;}#mermaid-svg-N19NbtK3lNhWs16P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N19NbtK3lNhWs16P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N19NbtK3lNhWs16P rect.text{fill:none;stroke-width:0;}#mermaid-svg-N19NbtK3lNhWs16P .icon-shape,#mermaid-svg-N19NbtK3lNhWs16P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N19NbtK3lNhWs16P .icon-shape p,#mermaid-svg-N19NbtK3lNhWs16P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N19NbtK3lNhWs16P .icon-shape .label rect,#mermaid-svg-N19NbtK3lNhWs16P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N19NbtK3lNhWs16P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N19NbtK3lNhWs16P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N19NbtK3lNhWs16P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
业务需求
核心是全文检索?
日志搜索/内容搜索
选 Elasticsearch
倒排索引 + ELK 生态
高并发点查 + 多表 JOIN
报表/即席分析
选 Apache Doris
MySQL 协议 / 运维极简
海量单表聚合
低成本 / 实时分析
选 ClickHouse
极致压缩 + 向量化
混合架构
CH 做分析 + ES 做检索
注意: 聚合慢/存储贵
大日志量慎用
注意: 超大规模单表
扩展性略逊 CH
注意: 高并发 & JOIN
需大宽表规避
我的判断(观点,不是结论):
- 纯日志检索/全文搜索 → ES 仍是最优解,尤其 ELK 生态成熟。
- 海量日志的"分析"而非"检索" (比如安全事件聚合、可观测性指标分析、APM 指标)→ ClickHouse 完胜 ,成本和速度都碾压。很多团队现在是 ES 负责检索、ClickHouse 负责分析的双引擎。
- 高并发 BI 报表、需要频繁 JOIN、希望运维简单 → Doris 更顺手,尤其它兼容 MySQL 协议,BI 工具零改造接入。
- 超大规模单表、极致成本 → ClickHouse 没有对手。
我们没有"一个引擎解决所有问题"的银弹。但 ClickHouse 在"单表海量聚合 + 低成本"这个象限里,目前是天花板。
八、踩坑记录与最佳实践
这部分是"用钱和停机换来的经验",建议直接抄作业。
-
Keeper 不是可选配件。多副本高可用必须上 ClickHouse Keeper(或 ZK)。我们用过外接 ZK,结果是 Java 堆 + GC 拖慢了整个元数据协调;换内置 Keeper 后稳定很多,且 26.x 已完全去 ZK 化。
-
Too many parts 是新手第一大坑 。高并发小批量 INSERT 会瞬间生成海量小 part,merge 跟不上就报错。解法:攒批(每批 1 万~10 万行)、控制写入并发、调大
parts_to_throw_insert/max_parts_in_total。 -
大宽表优先于 JOIN 。ClickHouse 的 JOIN 实现相对弱(基于哈希、内存压力大),大表 JOIN 大表尤其容易 OOM 或超时。生产上我们尽量在写入端预 JOIN 成大宽表 ,或者把维表做成
Dictionary引擎做常数时间查找。 -
高并发点查要谨慎。ClickHouse 不是为千级 QPS 的点查设计的,小规模集群往往卡在 CPU。如果是高并发点查场景,要么上 Doris,要么在 ClickHouse 前加一层缓存(Redis)。
-
Mutation(UPDATE/DELETE)是异步的 。26.x 虽然增强了轻量事务和高效 Mutation,但
ALTER TABLE ... UPDATE/DELETE仍是后台异步执行,不是事务级即时生效。需要"即时生效"的更新请在数据模型层设计(如 ReplacingMergeTree 的版本覆盖),别指望 UPDATE 马上查到。 -
分区粒度别太细也别太粗 。
PARTITION BY toYYYYMMDD(ts)按天是日志场景的甜点;如果按小时分区,90 天就是 2160 个分区、part 数量爆炸;如果按月分区,单分区太大、drop 旧数据不灵活。 -
版本升级认准 LTS 。当前 LTS 是 26.3 (支持到 2027-03),下一 LTS 26.8 约 2026-08 发布,25.8 LTS 将于 2026-08-29 EOL ,请及时升级。生产环境别追每月的非 LTS 版本,稳定性优先。升级前务必在测试集群跑一遍
clickhouse-local的 schema 兼容性检查。
九、总结与展望(2025-2026 趋势)
写完原理和实战,再聊聊方向。ClickHouse 在 2025-2026 年的演进主线非常清晰:
- 去 ZooKeeper 化 / Raft 共识成熟:内置 ClickHouse Keeper 已完全可替代 ZK,26.x 下元数据协调不再有 JVM 包袱。
- 云原生与存算分离:ClickHouse Cloud、S3 对象存储做数据盘、Serverless 化,让"按需扩缩容"成为可能。冷热分层 + S3 让 PB 级数据的存储成本再降一截。
- 增强更新/事务:轻量事务、高效 Mutation,正在补齐它传统的"弱更新"短板。
- AI 与向量搜索 :原生
Vector类型 + 向量索引 + ANN 相似搜索,26.2 的 QBit 向量精度已生产可用,AI embedding 函数、可作 RAG 向量库------这意味着 ClickHouse 开始从"分析库"向"AI 基础设施"延伸。 - 湖仓互通:Iceberg 写入(26.2)与读取、Delta Lake 互通,让 ClickHouse 能直接吃湖里的数据,架构上更开放。
- 查询体验持续打磨 :26.7 的
EXPLAIN ANALYZE生产可用、GROUP BY/ORDER BY/LIMIT 提速、3 项 JOIN 改进、正则引擎提速约 2.5 倍;26.6 的假设性跳数索引(hypothetical skip indexes)能帮你在建索引前"试算"索引收益。
顺着这些趋势看,我认为 ClickHouse 接下来两年最有想象力的方向,是把"分析引擎"和"AI 基础设施"彻底打通。当它有原生 Vector 类型、ANN 向量索引、AI embedding 函数之后,安全团队完全可以把攻击样本的向量、用户行为的向量、日志语义的向量直接存进 ClickHouse,用一条 SQL 同时做"结构化过滤 + 向量相似检索"------比如"找出过去 1 小时里,和已知勒索软件行为向量最相似、且来自高危 IP 段的所有请求"。这种把向量检索塞进同一个分析库、避免再引入一套独立向量数据库(如 Milvus/Weaviate)的架构,对中小型团队尤其有吸引力,也正好契合当下 RAG(检索增强生成)把企业日志/文档变成可检索知识库的浪潮。当然,现阶段 ClickHouse 的向量能力还在快速演进(26.2 才把 QBit 精度做到生产可用),大规模高并发向量检索的成熟度还不如专业向量库,但"一个引擎兼顾分析和向量"的收敛趋势已经很清楚了。
我的结论:如果你在做日志、安全事件、可观测性、IoT、埋点、风控这类"海量、append-only、重聚合"的数据分析,ClickHouse 几乎是目前性价比最高的选择。它不完美(高并发点查和 JOIN 仍是软肋),但在它的主场上,几乎没有对手。
落地的三句话建议:
- 模型层先设计好 ORDER BY 和分区,这决定了你 80% 的查询性能;
- 明细表 + 物化视图预聚合,把慢查询变快查询;
- 认准 LTS(当前 26.3),别在生产追每月新版。
参考资料
- ClickHouse 官方文档(架构与 MergeTree):https://clickhouse.com/docs/engines/table-engines/mergetree-family/mergetree
- ClickHouse 官方文档(主键与索引):https://clickhouse.com/docs/guides/optimizing-performance/performance-tuning-principles
- ClickHouse 官方 releases(版本与 LTS 说明,26.7 / 26.3 LTS):https://github.com/ClickHouse/ClickHouse/releases
- VLDB 2024 --- ClickHouse: Lightweight Interactive Analytics At Scale(论文):https://www.vldb.org/pvldb/vol17/p5320-clickhouse.pdf
- ChistaDATA --- ClickHouse 26.x 生产部署与调优建议:https://chistadata.com/blog/
- CSDN --- ClickHouse vs Elasticsearch vs Doris 选型对比文章:https://blog.csdn.net/ 搜索 "ClickHouse Elasticsearch Doris 选型对比"(可在站内检索最新实战对比)
本文所有代码、DDL、docker-compose 均基于 ClickHouse v26.7 编写,已在 26.x 环境验证可运行。如版本升级导致语法差异,请以官方文档为准。欢迎在评论区交流你在 ClickHouse 实战中踩过的坑。