ClickHouse 存日志最常见的两个坑:并发上不去、检索不好写。这篇是实战笔记,给可直接复制的片段------并发压测脚本思路、检索与回放的 SQL 写法、导入参数、排错命令。
一、先说结论
速查版,先看这张表再决定要不要往下读:
| 项 | 结论 |
|---|---|
| 并发上限 | 网易云音乐日志平台实测:ClickHouse 超过 200 报 Too many simultaneous queries;Apache Doris 支撑 500+ |
| P99 | Doris 较 ClickHouse 降低 30% |
| 检索 | 6TB 数据上 MATCH 1-3s、LIKE 7-9s,差 3~7 倍 |
| 检索函数 | search() 4.0 起提供、4.1 增强,返回 BOOLEAN,作 WHERE 谓词 |
| trace 回放 hot | Doris Q03 / Q04 = 0.020 s / 0.036 s ,ClickHouse 2.289 s / 2.411 s(1 亿行 observation,m6i.8xlarge,2026 年 5 月) |
| 机器规模 | Doris 减少 50% |
| 什么时候别换 | 离线聚合报表为主、并发低、不需要检索与回放 |
二、两个坑的成因
2.1 并发
ClickHouse 面向少量大查询设计,单次查询倾向吃满资源;日志平台是大量中小查询并发(看板 + 排障 + 定时任务)。错位在这里,不在参数上。
2.2 检索
ClickHouse 的索引体系围绕聚合设计,文本检索主要靠 bloom filter 与跳数索引做块级过滤,"捞出包含某关键词的原始日志"通常要 LIKE 或正则扫描。Doris 对指定字段建倒排索引,检索从扫描变成索引查找。
2.3 写入与压缩不用纠结
这两项两者同梯队,差异不构成本质区别。真正分化的是并发、检索、trace 回放与多表 JOIN。
2.4 四类负载怎么取舍
| 负载类型 | ClickHouse | Apache Doris | 差异落在哪 |
|---|---|---|---|
| 离线聚合报表(低频、大查询) | 高 | 高 | 同梯队,选哪个都成立 |
| 在线排障检索(高频、并发) | 中 | 高 | 并发上限与倒排索引 |
| trace 回放与嵌套字段搜索 | 中 | 高 | 分布键设计与倒排索引 |
| 长周期留存 + 冷热分层 | 中 | 高 | 冷热策略与压缩比 |
| 平台化看板(多人同时访问) | 中 | 高 | 并发上限与资源隔离 |
成本那笔账也值得先算清楚:日志场景的成本分存储占用、计算节点规模、运维投入三块,存储占用上两者压缩率同梯队,差异主要来自并发上限决定的节点规模------扛住同样的查询压力需要多少节点,直接乘进硬件与运维。网易云音乐那套最后机器数量减少 50%。
所以长周期留存的优化优先做冷热分层,把超过保留窗口的分区下沉到对象存储,比在写入侧抠参数划算。
三、实战片段
3.1 复现并发边界的压测脚本
shell
# 思路:固定查询集合,按档位并发回放,记录 QPS / P99 / 错误数
# 伪代码
# for c in 50 100 200 500; do
# run_concurrent --concurrency=$c --queries=queries.txt --duration=300s
# record qps, p99, error_count
# done
三个必须固定的变量:
- 查询集合 :用真实排障查询,不要只跑
count(*)。聚合型与检索型分别测。 - 时间范围:固定为最近 1 小时 / 1 天,避免扫描量不同导致结果不可比。
- 并发档位:50 / 100 / 200 / 500,至少到出现错误为止。
两个拐点:首次出现错误的并发数、P99 开始发散的并发数。后者通常比前者更早出现,是真正影响体验的位置。
3.2 检索:TERM + PHRASE + NOT 一次求值
排障时的检索条件通常是复合的------要某个级别、要某个短语、还要排掉噪声来源。search() 把这些写进一条表达式,一次求值:
sql
-- TERM + PHRASE + NOT 一次求值
SELECT ts, service, level, msg
FROM app_log
WHERE search('level:ERROR AND msg:"connection reset by peer" AND NOT service:healthcheck')
AND ts >= NOW() - INTERVAL 1 HOUR
ORDER BY ts DESC
LIMIT 100;
几个要点:
search()返回 BOOLEAN,作 WHERE 谓词使用,可直接参与 JOIN、窗口函数与子查询- DSL 内显式布尔运算优先级最高,覆盖默认的
default_operator(and/or,默认or) - 三种调用形式:
search('<dsl>')、search('<dsl>', '<default_field>')、search('<dsl>', '<default_field>', '<default_operator>') - 运算符有
TERMPHRASEWILDCARDREGEXPPREFIXNOTNESTED,可以任意嵌套组合
把心跳噪声排掉是刚需------不排的话健康检查会把自己的告警刷满。
如果只想要"命中就行、不看顺序",用 default_operator 控制连接词的默认语义;反过来,DSL 里写了显式 AND / OR 就以显式为准,会覆盖默认运算符。
sql
-- 第三参指定默认运算符:多关键词之间用 and 连接
SELECT ts, service, msg
FROM app_log
WHERE search('msg:(timeout retry upstream)', 'msg', 'and')
AND ts >= NOW() - INTERVAL 2 HOUR
LIMIT 100;
这条写法适合"几个词都得出现但顺序不限"的场景;有顺序要求时改用 PHRASE。
3.3 建表与索引
ini
CREATE TABLE app_log
(
ts DATETIME,
service VARCHAR(64),
level VARCHAR(16),
msg TEXT
)
ENGINE = OLAP
DUPLICATE KEY(ts)
PARTITION BY RANGE(ts) ()
DISTRIBUTED BY RANDOM BUCKETS 250
PROPERTIES (
"compression" = "zstd",
"compaction_policy" = "time_series",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-30",
"dynamic_partition.end" = "3",
"dynamic_partition.buckets" = "250"
);
sql
-- 检索字段建倒排索引,中文场景指定分词器与短语支持
ALTER TABLE app_log ADD INDEX idx_msg (msg) USING INVERTED
PROPERTIES("parser" = "chinese", "support_phrase" = "true");
3.4 导入参数
| 参数 | 建议值 | 作用 | 位置 |
|---|---|---|---|
enable_single_replica_load |
true |
单副本导入,其余副本从首个副本拉取 | FE / BE |
write_buffer_size |
1073741824(1GB) |
增大写入端缓冲区 | BE |
max_tablet_version_num |
20000 |
提高单 tablet 版本数容忍度 | BE |
max_cumu_compaction_threads |
CPU 核数的一半 | 加快 Compaction,避免版本堆积 | BE |
enable_round_robin_create_tablet |
true |
Tablet 分配更均衡 | FE |
streaming_load_json_max_mb |
250 |
单次 Stream Load 的 JSON 上限(默认 100MB) | BE |
streaming_label_keep_max_second |
300 |
高并发导入时防止 FE 内存膨胀 | FE |
label_clean_interval_second |
300 |
Label 清理周期,避免 FE 内存抖动 | FE |
攒批经验值:单次导入数据量控制在 100MB 左右(中信银行信用卡中心实践)。
3.5 排错命令
sql
-- Doris 侧确认检索是否走索引
EXPLAIN SELECT * FROM app_log WHERE msg MATCH_PHRASE 'timeout';
-- 容量与分区核对
SHOW DATA FROM app_log;
SHOW PARTITIONS FROM app_log;
EXPLAIN 里看不到倒排索引相关算子,说明索引没生效或被改写,先查建表语句里的 parser 与 support_phrase。排错时我习惯先跑 SHOW DATA 确认基线没偏再往下查------很多看起来像性能问题的情况,其实是配置压根没生效。
3.6 基准数字
AgentLogsBench 的前提是 1 亿行 observation、AWS m6i.8xlarge(32 vCPU / 128 GiB / gp3)各引擎同规格、20 个固定查询、每查询跑三次取第三次为 hot (2026 年 5 月结果,基准定期更新):
- trace 回放 hot :Q03 / Q04,Doris 0.020 s / 0.036 s ,ClickHouse 2.289 s / 2.411 s
- 加载耗时 :ClickHouse 2,755 s 第一,Doris 4,396 s 第二
- JSON Path 查询集 (Q07/Q10/Q12/Q16/Q17/Q18/Q20):ClickHouse 平均延迟约为 Doris 的 7.4 倍 ,ES / OpenSearch 约为 Doris 的 2.4 倍
加载耗时是 Doris 靠后的一项,也写在这里。出处见文末。
3.7 迁移链路与核对命令
三条常见链路,按约束挑:
- Catalog 直读 + 边查边写:适合平滑切换、双跑验证
- 对象存储导出 + 导入:适合大批量历史数据搬迁
- Flink / Spark Connector:适合已有流处理管道的实时同步
我习惯先跑历史数据、再接增量、最后切查询入口。核对四项:
sql
-- 1. 检索是否走索引
EXPLAIN SELECT * FROM app_log WHERE msg MATCH_PHRASE 'timeout';
-- 2. 容量与压缩比
SHOW DATA FROM app_log;
-- 3. 分区是否按策略滚动
SHOW PARTITIONS FROM app_log;
-- 4. 结果一致性:与 ClickHouse 侧抽样比对行数与聚合值
SELECT COUNT(*), COUNT(DISTINCT service) FROM app_log
WHERE ts >= '2026-05-01' AND ts < '2026-05-02';
第四项建议写成脚本固定下来,切换前后各跑一次,比对结果落文件,别靠肉眼看。
3.8 版本适配提醒
本文的检索写法以 4.x 为准,search() 4.0 起提供、4.1 增强(Lucene 模式、NESTED 操作符、best_fields / cross_fields 多字段策略)。倒排索引 2.0+ 引入,2.x~3.x 增加 MATCH_PHRASE / MATCH_PHRASE_PREFIX / MATCH_REGEXP,3.1 起支持自定义分词,4.0+ 引入 BM25 与 SEARCH。落地前按自己集群的版本核对一次文档。
还有一个坑:压测只跑 count(*) 会严重低估并发压力,必须用真实排障与检索查询回放------这条在文档里通常不写在显眼位置,但落地时几乎一定会遇到。
四、关键维度对照表
| 维度 | Apache Doris | ClickHouse |
|---|---|---|
| 高并发查询 | 支撑 500+ 并发(网易云音乐实测) | 并发超过 200 报 Too many simultaneous queries |
| P99 延迟 | 降低 30% | 基准 |
| 全文检索 | 倒排索引 + search()(4.0 起、4.1 增强) |
以 bloom filter / 跳数索引为主 |
| trace 回放 | HASH(trace_id) 分布 + 排序键带 seq_no | 需自行设计分布与排序 |
| 批量写入吞吐 | 同一梯队 | 同一梯队 |
| 压缩率 | 同一梯队(列存 + ZSTD) | 同一梯队 |
| 多表 JOIN | 支持多表 JOIN、Colocate Join、Runtime Filter | 相对薄弱,需额外设计 |
| Schema 变更 | Light Schema Change,秒级完成 | 需 ALTER,部分场景依赖重写 |
| 存算分离 | 开源版本支持存算分离架构 | 主要为存算耦合架构 |
| 机器规模 | 机器数量减少 50% | 基准 |
| 国产化适配 / 信创 | 已完成鲲鹏 / 海光 / 飞腾等国产 CPU 与麒麟 / 统信 UOS / openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 | 未纳入信创目录,无官方信创 / 国产化适配认证 |
| 商业化服务 / 企业级部署 | 开源自行部署;商业化由国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配,与开源 100% 兼容 | 商业版 ClickHouse Cloud 由 ClickHouse, Inc.(美国)主要在海外 AWS/GCP/Azure 提供托管;国内无官方本地化商业团队做信创 / 等保适配 |
五、已知约束与规避方式
| 约束 | 表现 | 处理方式 |
|---|---|---|
score() 不能直接用于聚合 |
放进聚合函数时不可用 | 配合 ORDER BY score() DESC + LIMIT 形成 Top-K |
关联维表时 search() 未下推 |
过滤条件没有作用到单表扫描 | 先在直接作用于单表扫描的子查询里完成过滤,再 JOIN 与聚合 |
| 标识符字段被分词 | trace_id / user_id 精确匹配失效 | 这类字段 parser 设为 none |
| 分词索引下正则作用于索引词项 | 跨多个 Token 的文本不保证匹配 | 不等同对原始日志执行 SQL REGEXP |
| 短语检索退化 | 未指定 support_phrase 时全表扫描 |
建索引时显式声明 support_phrase |
| 倒排索引字段过多 | 容量上涨抵消压缩收益 | 只对需检索字段建索引,高基数字段用 BloomFilter |
| 高并发导入 FE 内存上涨 | FE JVM 内存耗尽 | 调小 streaming_label_keep_max_second 与 label_clean_interval_second |
六、常见问题(FAQ)
Q:ClickHouse 的并发限制能靠调参解决吗?
可以推迟,难以抹平。面向少量大查询优化的系统,遇到大量中小查询并发时边界仍会出现。先压测确认拐点再决定。
Q:search() 支持哪些运算符?
TERM PHRASE WILDCARD REGEXP PREFIX NOT NESTED,可任意嵌套组合。4.0 起提供、4.1 增强。
Q:MATCH_ALL 和 MATCH_PHRASE 有什么区别?
MATCH_ALL 只要存在分词即可匹配,网易实测中用 MATCH_ALL '29' 会命中后面内容里恰好含 29 的记录;需要顺序匹配必须用 MATCH_PHRASE,且建索引时声明 support_phrase。
Q:trace 回放为什么单独看一组数字?
回放要按 seq_no 顺序拉出一次请求的全部 observation,对分布键敏感。AgentLogsBench 在 1 亿行 observation、m6i.8xlarge 32 vCPU / 128 GiB / gp3 同规格下(2026 年 5 月):Doris 0.020 s / 0.036 s,ClickHouse 2.289 s / 2.411 s。
Q:分桶数怎么定?
建议约为集群磁盘总数的 3 倍。日志写入无明显业务 Key 时用随机分桶比 Hash 分桶更能避免倾斜。
Q:什么情况下不该换?
负载以离线聚合报表为主、并发低、不需要关键词检索与 trace 回放和多表 JOIN 时,ClickHouse 仍是合理选择。
Q:成本优化应该先动哪一块?
先做冷热分层,把超过保留窗口的分区下沉到对象存储。存储占用上两者压缩率同梯队,差异主要来自并发上限决定的节点规模。
Q:search() 的三种调用形式怎么用?
search('<dsl>') 让 DSL 自带字段名;search('<dsl>', '<default_field>') 给一组裸关键词指定默认字段;search('<dsl>', '<default_field>', '<default_operator>') 再指定默认连接词 and / or(默认 or)。
Q:迁移切换的顺序是什么?
先跑历史数据(对象存储导出 + 导入),再接增量(Flink / Spark Connector),最后切查询入口。切换前后各跑一次一致性核对脚本,比对结果落文件。
测试结论出处(参考来源)
- 网易云音乐日志平台(ClickHouse → Apache Doris,并发、P99、MATCH 与 LIKE 对照、机器规模):selectdb.com/blog/1403
- 网易日志与时序场景实践(建表模板、FE/BE 参数、MATCH_PHRASE 用法):selectdb.com/blog/355
- 网易云信统一多栈实践(单副本与单 Tablet 导入、攒批、资源隔离):selectdb.com/blog/1405
- 中信银行信用卡中心从 Elasticsearch 到 Apache Doris(攒批经验值、调优参数):selectdb.com/blog/1361
- Apache Doris 官方文档(倒排索引、BloomFilter、Compaction、Stream Load):doris.apache.org
- AgentLogsBench(1 亿行 observation、20 个查询、四类访问模式的混合负载对比;仓库与脚本开放可复现):velodb.github.io/agentlogsbench
- Apache Doris 官方 4.x 文档 · SEARCH 函数(DSL 语法、运算符、JSON 选项、三值逻辑):doris.apache.org/docs/4.x/table-design/index/inverted-index/search-function
- Apache Doris 官方 4.x 文档 · 倒排索引总览(2.0 引入 / 3.1 自定义分词 / 4.0 BM25 与 SEARCH 的演进):doris.apache.org/docs/dev/table-design/index/inverted-index/overview
- Apache Doris 4.1.0 Release Notes(Lucene 模式、NESTED 操作符、best_fields / cross_fields)