返回博客

为什么放弃 ClickHouse

在 coomia-dip 的存储选型中,我们最初选择了 ClickHouse 作为 OLAP 引擎,与 PostgreSQL 构成"OLTP + OLAP"双引擎架构。但在 6 个月的实际使用后,我们做出了一个艰难的决定——放弃 ClickHouse,全面迁移到 Apache Doris。这篇 ADR(Architecture Decision Record)详细记录了这个决策的背景、评估过程、迁移方案和事后反思。核心教训:在中小规模场景下,"一个好用的引擎"胜过"两个理论最优的引擎"。

Coomia发布于 2026年2月17日17 分钟阅读
分享本文Twitter / X

系列:S14 工程实录 · 第 2 篇 | 难度:中级 | 阅读时间:15 分钟

为什么放弃 ClickHouse

#TL;DR

在 coomia-dip 的存储选型中,我们最初选择了 ClickHouse 作为 OLAP 引擎,与 PostgreSQL 构成"OLTP + OLAP"双引擎架构。但在 6 个月的实际使用后,我们做出了一个艰难的决定——放弃 ClickHouse,全面迁移到 Apache Doris。这篇 ADR(Architecture Decision Record)详细记录了这个决策的背景、评估过程、迁移方案和事后反思。核心教训:在中小规模场景下,"一个好用的引擎"胜过"两个理论最优的引擎"。

#1. 背景:我们为什么需要 OLAP

#1.1 coomia-dip 的查询场景

coomia-dip 作为本体驱动的决策平台,面临两类截然不同的查询需求:

事务性查询(OLTP)

  • 按 ID 查找单个 Object
  • 创建/更新/删除 Object
  • Action 执行时的状态检查
  • 权限验证

分析性查询(OLAP)

  • 跨大量 Object 的聚合分析(如"过去 30 天各产线的良品率趋势")
  • 多维下钻(按时间、地域、产品线分组)
  • 复杂 JOIN(Object 之间通过 Link 关联后的聚合)
  • 时序分析(设备传感器数据的趋势和异常检测)

在 v1 阶段,我们用 PostgreSQL 处理所有查询。对于小数据量(10 万级),这完全够用。但当测试数据量增长到百万级时,分析性查询开始变得不可接受——一个典型的聚合查询需要 10-30 秒。

#1.2 为什么最初选择 ClickHouse

在 2024 年中的技术选型中,ClickHouse 有几个显著优势:

  1. 极致的列式存储性能:ClickHouse 在 OLAP 基准测试中几乎稳居第一
  2. 活跃的社区:GitHub 30k+ stars,中文社区活跃
  3. MergeTree 引擎:天然支持时序数据和实时聚合
  4. 成熟的生态:与 Kafka、Spark、Flink 都有成熟的集成方案
  5. SQL 兼容:支持标准 SQL,学习成本低

基于这些理由,我们在 v2 架构中引入了 ClickHouse,形成了"PostgreSQL(OLTP)+ ClickHouse(OLAP)+ MinIO(对象存储)"的三层存储架构。

#2. ClickHouse 带来的问题

#2.1 数据同步的噩梦

引入 ClickHouse 后,我们面临的第一个问题就是数据同步。当用户通过 Action 修改了一个 Object 时,这个变更需要:

  1. 写入 PostgreSQL(保证事务性)
  2. 同步到 ClickHouse(保证分析查询的数据新鲜度)

我们尝试了三种同步方案:

方案 A:双写 在 Action Engine 中同时写入 PostgreSQL 和 ClickHouse。问题:两个数据库没有分布式事务保证,一旦 ClickHouse 写入失败,数据就会不一致。

方案 B:CDC(Change Data Capture) 通过 Debezium 捕获 PostgreSQL 的 WAL 日志,实时同步到 ClickHouse。问题:引入了新的中间件(Debezium + Kafka),增加了运维复杂度;同步延迟在高负载下可达数秒。

方案 C:定时批量同步 每 5 分钟全量或增量同步。问题:数据延迟太大,用户创建 Object 后立即查分析报表看不到。

我们最终选择了方案 B(CDC),但它带来的运维负担远超预期。

#2.2 Schema 变更的痛苦

在 coomia-dip 中,用户可以动态添加 ObjectType 的属性。在 PostgreSQL 中,我们使用 JSONB 列存储动态属性,Schema 变更几乎零成本。

但在 ClickHouse 中,情况完全不同:

  • ClickHouse 的 ALTER TABLE ADD COLUMN 在大表上非常耗时
  • ClickHouse 不支持 JSONB(虽然有实验性的 JSON 类型,但性能和稳定性都不理想)
  • 每次用户添加新属性,都需要在 ClickHouse 中执行 DDL

我们不得不维护一套复杂的"Schema 同步器",它需要:

  1. 监听 Schema Registry 的变更事件
  2. 将 coomia-dip 的类型映射到 ClickHouse 的列类型
  3. 执行 ALTER TABLE
  4. 处理各种边界情况(类型不兼容、列名冲突等)

这套 Schema 同步器的代码量超过了 3000 行,Bug 频繁,成为整个系统中最不稳定的组件。

#2.3 JOIN 的局限性

ClickHouse 对 JOIN 的支持是有限的。虽然它支持各种 JOIN 语法,但在以下场景中性能急剧下降:

  • 多表 JOIN:OQL 中一个典型的查询可能涉及 3-5 个 ObjectType 的关联查询,ClickHouse 在 3 个以上的表 JOIN 时性能显著下降
  • 大表 JOIN 大表:当两个参与 JOIN 的表都超过百万行时,ClickHouse 的内存消耗可能导致 OOM
  • 频繁的小查询:ClickHouse 针对大批量查询优化,单条查询的延迟(约 50-100ms)比 PostgreSQL(约 1-5ms)高一个数量级

#2.4 Windows 开发环境的兼容性

这个问题看似小,但实际影响很大。我们的开发团队主要使用 Windows + WSL2 环境。ClickHouse 在 WSL2 中运行时存在以下问题:

  • 内存分配器(jemalloc)在 WSL2 中偶发 crash
  • 文件系统性能在 WSL2 挂载的 Windows 目录下急剧下降
  • Docker Desktop 中运行 ClickHouse 的内存消耗是 Linux 原生的 2-3 倍

这意味着开发者在本地跑集成测试时,经常遇到 ClickHouse 相关的随机失败。

#2.5 运维复杂度

引入 ClickHouse 后,我们的运维清单增加了:

  • ClickHouse 集群的部署和监控
  • Debezium + Kafka 的部署和监控
  • Schema 同步器的部署和监控
  • 数据一致性检查脚本
  • ClickHouse 的备份和恢复

对于一个 4 人团队来说,这个运维负担是不可承受的。

#3. 评估替代方案

#3.1 评估标准

我们制定了 7 个评估标准:

标准权重说明
OLAP 性能25%百万级数据的聚合查询性能
OLTP 兼容性20%能否同时处理事务性查询
JOIN 能力15%多表 JOIN 的性能和正确性
Schema 灵活性15%动态 Schema 变更的支持
运维简单性10%部署、监控、备份的复杂度
开发者体验10%本地开发环境的友好度
生态成熟度5%社区活跃度、文档质量

#3.2 候选方案

我们评估了以下方案:

方案 1:继续 PostgreSQL + ClickHouse 保持现状,投入更多精力优化同步和 Schema 管理。

方案 2:PostgreSQL + StarRocks StarRocks 是 ClickHouse 的竞品,号称对 JOIN 更友好。

方案 3:Apache Doris Doris 是一个兼容 MySQL 协议的 MPP 数据库,同时支持 OLTP 和 OLAP。

方案 4:DuckDB(嵌入式) DuckDB 作为嵌入式 OLAP 引擎,无需独立部署。

#3.3 评估结果

标准PG+CHPG+StarRocksDorisDuckDB
OLAP 性能9987
OLTP 兼容性5583
JOIN 能力5789
Schema 灵活性4578
运维简单性34810
开发者体验4579
生态成熟度9776
加权总分5.86.27.67.0

Doris 以 7.6 分胜出。它的核心优势是"够用的 OLAP 性能 + 足够的 OLTP 能力 + 简单的运维"。

#4. 为什么 Doris 胜出

#4.1 统一引擎,消灭数据同步

Doris 最大的优势是它同时支持 OLTP 和 OLAP 查询。这意味着我们不再需要维护两个数据库之间的数据同步。数据只写一次,既可以做事务性查询,也可以做分析性查询。

这一个优势就消灭了我们 30% 的运维工作量和 Schema 同步器的全部 3000 行代码。

#4.2 MySQL 协议兼容

Doris 兼容 MySQL 协议,这意味着:

  • 大量现成的客户端库可以直接使用
  • 开发者已经熟悉的 MySQL SQL 语法可以直接使用
  • 迁移成本大大降低

#4.3 优秀的 JOIN 能力

Doris 的 MPP 架构天然适合分布式 JOIN。在我们的基准测试中,3 表 JOIN 查询 Doris 比 ClickHouse 快 2-3 倍,5 表 JOIN 快 5-10 倍。

#4.4 动态 Schema 支持

Doris 2.x 引入了 Variant 类型,类似于 PostgreSQL 的 JSONB,支持动态 Schema。这完美匹配了 coomia-dip 的需求——用户可以动态添加属性,而无需执行 DDL。

#4.5 轻量级部署

在开发环境中,Doris 可以以单节点模式运行,资源消耗远低于 ClickHouse。在生产环境中,Doris 的 FE+BE 架构也比 ClickHouse 集群 + Debezium + Kafka 简单得多。

#5. 迁移过程

#5.1 迁移策略

我们采用了"渐进式迁移"策略:

Phase 1(2 周):搭建 Doris 环境,完成数据迁移脚本 Phase 2(2 周):将读查询逐步切换到 Doris(写仍走 PostgreSQL) Phase 3(1 周):将写操作切换到 Doris Phase 4(1 周):下线 PostgreSQL 和 ClickHouse

#5.2 数据迁移

数据迁移的核心挑战是类型映射。coomia-dip 的 ObjectType 属性有 12 种类型,需要映射到 Doris 的列类型:

coomia-dip 类型PostgreSQL 类型Doris 类型
STRINGTEXTVARCHAR(65533)
INTEGERINTEGERINT
LONGBIGINTBIGINT
DOUBLEDOUBLE PRECISIONDOUBLE
BOOLEANBOOLEANBOOLEAN
TIMESTAMPTIMESTAMPDATETIME(6)
DATEDATEDATE
ARRAYJSONBARRAY
MAPJSONBMAP
GEO_POINTPOINTVARCHAR (GeoJSON)
GEO_SHAPEGEOMETRYVARCHAR (GeoJSON)
DYNAMICJSONBVARIANT

迁移脚本运行了约 4 小时(总数据量约 500GB),期间未发生数据丢失。

#5.3 查询层适配

OQL 查询引擎需要从"PostgreSQL + ClickHouse 双路由"改为"Doris 单路由"。这实际上是一个简化——删除了约 2000 行路由和适配代码。

#5.4 测试覆盖

迁移期间,我们编写了 200+ 个迁移专用测试用例,覆盖:

  • 所有 12 种数据类型的读写
  • 边界值(NULL、空字符串、最大值、最小值)
  • 并发读写
  • 大批量导入
  • OQL 查询兼容性

所有 200+ 测试通过后,我们才执行了正式迁移。

#6. 迁移后的效果

#6.1 性能对比

场景PG+CHDoris变化
单 Object 查询2ms5ms-60%
百万级聚合800ms1.2s-33%
3 表 JOIN 聚合3.5s1.1s+218%
5 表 JOIN 聚合12s2.3s+422%
批量导入(10 万行)8s6s+33%

Doris 在单行查询和简单聚合上略逊于 PG+CH 组合,但在 JOIN 场景下有压倒性优势。考虑到 coomia-dip 的查询以 JOIN 为主(Object 之间总是有关联关系),Doris 是更好的选择。

#6.2 运维简化

指标迁移前迁移后
需要运维的组件5(PG+CH+Kafka+Debezium+Schema同步器)1(Doris)
配置文件数123
数据同步延迟1-5s0(无需同步)
随机测试失败率5%0.2%

#6.3 代码量减少

模块删除行数
Schema 同步器-3,200
查询路由器-2,100
CDC 配置-800
ClickHouse 适配器-1,500
合计-7,600

7,600 行代码的删除,意味着 7,600 行代码的潜在 Bug 被消灭了。

#7. 事后反思

#7.1 我们错在哪里

回顾这个决策,我们犯了几个错误:

错误 1:被基准测试迷惑 ClickHouse 在标准 OLAP 基准测试(如 TPC-H、ClickBench)中的表现确实惊人。但基准测试的场景(单表大规模聚合)与我们的实际场景(多表 JOIN + 动态 Schema)差距很大。

错误 2:低估了"额外组件"的成本 引入 ClickHouse 不只是引入一个数据库——它还带来了 Debezium、Kafka、Schema 同步器等一系列"附属组件"。这些组件的运维成本加在一起,远超 ClickHouse 本身。

错误 3:没有做足够的 PoC 我们在选型时做了性能基准测试,但没有做足够的集成测试。如果我们花两周时间做一个完整的 PoC(包括数据同步、Schema 变更、JOIN 查询),可能在第二个月就能发现问题。

#7.2 ClickHouse 适合什么场景

ClickHouse 并不是一个坏产品——它在以下场景中依然是最佳选择:

  • 日志分析(固定 Schema,写入量大,单表查询)
  • 时序数据分析(IoT 传感器、监控数据)
  • 大宽表报表(预聚合的数据仓库)

但它不适合我们的场景——动态 Schema、频繁 JOIN、事务性和分析性查询共存。

#7.3 ADR 的价值

这次经历让我们深刻认识到 ADR(Architecture Decision Record)的价值。我们在做 ClickHouse 决策时没有写 ADR——如果当时写了,就需要显式地列出假设、约束和风险,可能会更早发现问题。

从这次之后,我们规定所有重大技术决策都必须写 ADR,包括:

  • 决策背景和问题陈述
  • 评估的方案和标准
  • 选择的方案和理由
  • 已知的风险和缓解措施
  • 审查者和批准日期

#8. 给读者的建议

#8.1 存储选型清单

如果你也在做存储选型,以下清单可能有用:

  1. 明确你的查询模式:OLTP 为主、OLAP 为主,还是混合?
  2. 评估 JOIN 需求:你的数据模型有多少关联关系?
  3. 考虑 Schema 变更频率:Schema 是固定的还是动态的?
  4. 计算总运维成本:不只是数据库本身,还有周边的同步、监控、备份组件
  5. 在你的真实场景下做 PoC:不要只看基准测试
  6. 考虑开发者体验:团队在本地环境能否方便地跑起来
  7. 预留迁移路径:如果选错了,迁移的成本是多少

#8.2 "足够好"胜过"理论最优"

这是这次经历最深刻的教训。在中小规模场景下(数据量 < 10TB),一个"足够好"的统一引擎,几乎总是胜过两个"理论最优"的专用引擎。因为:

  • 减少了数据同步的复杂度
  • 减少了运维的工作量
  • 减少了代码的复杂度
  • 减少了团队需要掌握的技术栈

当然,在超大规模场景下(数据量 > 100TB),专用引擎的性能优势可能会压过统一引擎的简单性。但在做出这个判断之前,请先确认你真的有那么大的数据量。

#9. 迁移的技术细节

#9.1 零停机迁移方案

为了实现零停机迁移,我们设计了一个"影子写入"机制:

Code
Phase 1: 影子写入
├── 所有写入 → PostgreSQL(主)
├── 所有写入 → Doris(影子)
└── 所有读取 ← PostgreSQL

Phase 2: 读切换
├── 所有写入 → PostgreSQL(主)
├── 所有写入 → Doris(影子)
└── 所有读取 ← Doris(逐步切换)

Phase 3: 写切换
├── 所有写入 → Doris(主)
└── 所有读取 ← Doris

Phase 4: 下线旧系统
├── 停止 PostgreSQL
├── 停止 ClickHouse
└── 停止 Debezium + Kafka

#9.2 数据一致性验证

在每个 Phase 结束时,我们运行数据一致性验证脚本:

  • 比较两个数据库的行数
  • 随机抽取 1000 行数据逐字段对比
  • 验证所有 ObjectType 的属性是否完整迁移
  • 运行全量 OQL 回归测试

#9.3 回滚预案

每个 Phase 都有对应的回滚方案:

  • Phase 1 回滚:停止 Doris 影子写入
  • Phase 2 回滚:将读切回 PostgreSQL
  • Phase 3 回滚:将写切回 PostgreSQL
  • Phase 4 回滚:从备份恢复 PostgreSQL

实际迁移中,我们没有使用任何回滚方案——一切顺利。

#10. 总结

放弃 ClickHouse 是 coomia-dip 项目中最痛苦的技术决策之一——不是因为 ClickHouse 不好,而是因为我们为这个错误选型付出了 6 个月的时间成本。但这个经历也教会了我们:技术选型没有银弹,只有"在你的场景下最合适的选择"。

如果可以重来,我们会在 Day 1 就选择 Doris。但没有那 6 个月的 ClickHouse 经验,我们可能也不会真正理解为什么 Doris 是更好的选择。

#Key Takeaways

  1. 基准测试不等于真实场景:ClickHouse 在标准测试中很快,但不适合我们的多表 JOIN + 动态 Schema 场景
  2. 总运维成本 > 单组件成本:ClickHouse 本身不贵,但加上 Debezium + Kafka + Schema 同步器,成本翻了 3 倍
  3. "足够好"的统一引擎 > "理论最优"的专用引擎组合:对于中小规模场景(< 10TB)
  4. 做 PoC 时要覆盖真实场景:特别是数据同步、Schema 变更、复杂 JOIN
  5. 重大技术决策要写 ADR:显式记录假设、约束和风险

#Next Article

下一篇:S14-03 8 Layer 合并为 3 进程 — 我们将讲述为什么 8 个 Layer 在开发期太重了,以及如何在保持架构清晰的同时合并为 3 个进程。

Tags: #coomia-dip #ClickHouse #Doris #存储选型 #ADR #架构决策 #数据库迁移