尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Doris与ClickHouse深度对比:实时数仓与OLAP选型实战指南

Doris与ClickHouse深度对比:实时数仓与OLAP选型实战指南 最近被问得最多的一个问题不是某张报表怎么写而是Doris 还是 ClickHouse做实时数仓、写 BI 报表、搭用户行为分析大家一上手就会撞上这两个名字。它们都是当前 OLAP 领域最热门的列式分析引擎都能扛几亿行甚至几十亿行数据的聚合查询但真到了落地阶段从建表、导数据、写查询到日常运维体感差别非常大。这篇是我的实战对比笔记不是官方文档翻译重点放在六个最影响决策的差异上架构模型、数据组织、查询能力、写入与更新、一致性、运维与生态。我分别用社区版 Docker 和二进制方式部署了两套环境造了模拟业务数据跑了典型的报表查询和写入压测也踩了不少坑。适合正在做技术选型或者已经用了其中某一个、正犹豫要不要迁移的团队参考。1. 先上结论一张表看清六个关键差异六个维度的差异先给一张表对比维度DorisClickHouse架构模型MPP 架构FE 负责元数据与 SQL 规划BE 负责存储与计算Shared-Nothing 列存MergeTree 家族引擎单节点能力强数据组织表按 tablet 分片多副本强一致管理分区 part 稀疏索引每次写入生成新 part后台异步 merge查询能力复杂多表 Join 更稳高并发点查能力更强单表聚合/扫描极快复杂 Join 易吃内存高并发是短板写入与更新支持高频小批量导入唯一键模型可直接更新删除适合大吞吐批量写入更新删除属于重操作mutation 成本高数据一致性多副本强一致写成功即可读到一致数据默认异步复制有同步窗口要求强一致需要额外配置部署与生态组件多部署稍重兼容 MySQL 协议与工具生态部署轻有原生 HTTP 接口周边工具自成一套选这六个维度不是因为官方文档只写了这些而是因为这些点几乎是所有真实选型中绕不开的决策项。团队做 OLAP 选型第一步看架构能不能匹配业务规模第二步看数据明细怎么组织、怎么保证能查得快第三步看写入链路能不能跟上实时性要求第四步看多表和并发查询是不是常态第五步看团队能不能维护得住这套系统最后再看它跟现有技术栈好不好对接。这六项全过一遍选型基本就落地了。下面我逐个展开重点讲清楚“为什么会这样”和“实际用起来是什么感受”。2. 差异的根子在基因架构设计与数据分片的底层逻辑2.1 Doris 为什么能扛复杂查询FE/BE 分工与 tablet 分片Doris 整体是 MPP 架构核心角色分两类FE 和 BE。FE 是前端节点负责元数据管理、SQL 解析、生成执行计划、查询调度BE 是后端节点负责数据存储和实际计算。客户端连的是 FEFE 把一条 SQL 拆成可以在多个 BE 上并行执行的子计划最后汇总结果。这个架构最值钱的地方有两个。第一执行计划是全局调度的多表 Join 可以在不同 BE 上并行做不依赖某一条 SQL 自己碰巧写得适合分布式。第二数据按 tablet 分片每个 tablet 有多个副本副本之间通过一致性协议同步某个 BE 挂了查询自动切换到其他副本对业务透明。tablet 的概念可以简单理解成表数据的横向切片。创建表时可以指定分桶列和分桶数Doris 2.x 之后支持 auto bucket它会根据数据量和分区大小自动调整分桶。分桶粒度直接影响查询并发度实践中见过很多人贪图省事用默认配置结果一张大表只有几个 tablet查询根本起不来并行。反过来tablet 数量太多FE 的元数据管理压力也会变大。我的建议是如果表会按时间分区每个分区的数据量控制在几百万到几千万行级别分桶数按单分区数据量设成 8 到 32 个别拍脑袋设一个固定值不管了。数据涨上去之后Doris 做分桶调整比 ClickHouse 重分布数据要友好得多但最好还是一开始就把模型定好。2.2 ClickHouse 的能力来源MergeTree、分区和分布式表ClickHouse 的核心不是分布式能力而是本地单机的 MergeTree 家族引擎。它用列式存储、稀疏主键索引、向量化执行把单节点的扫描和聚合速度做到了极致。数据写入时ClickHouse 会把数据按分区拆成一个或几个不可变的 data part落盘后由后台线程异步合并成更大的 part。查询时它借助分区裁剪跳过无关分区再借助主键的稀疏索引granule 级别的 min/max 信息跳过无关数据块。“分布式表”在 ClickHouse 里是一个逻辑层真正存储数据的是各个节点上的本地表。分布式表只是把查询分发到不同分片、汇总结果的壳。这一点和 Doris 的全局调度有本质区别Doris 的 FE 知道每个 tablet 在哪、数据长什么样可以做出全局最优的执行计划ClickHouse 的分布式查询更像把任务撒下去、各自算完再拼起来。这个设计带来的一个经典现象是ClickHouse 集群性能非常依赖分片键和分区键选得好不好。分片键选得均匀每个节点负载均衡选得不好数据倾斜和热点几乎是必然的。2.3 架构差异带来的真实体感架构差异不会直接写在性能报告里但运维时感受特别明显。扩容这件事Doris 加一个 BE 节点后tablet 会自动在集群里做均衡旧数据不用动新数据写入时 FE 也会尽量往新节点分配。ClickHouse 加新节点后旧数据不会自动搬过去常见的做法是把数据重新导出再导入或者用分布式表 后台任务慢慢搬过程比 Doris 重得多。故障恢复也是。Doris 副本切换是内部的查询层无感知ClickHouse 如果某个分片只剩一份数据且节点挂了查询要么报错要么查到不完整数据除非你在每个分片都配置了多副本并且把 internal_replication 打开。理解了这一层才能理解为什么很多人用 ClickHouse 是“单机很强集群要精心伺候”。3. ClickHouse 的 part 命名机制初看是细节细看是存储哲学这个话题是我翻了无数 ClickHouse 排错帖之后觉得最值得单独讲的一块。大多数人第一次在数据目录里看到202405_1_3_0这种文件夹都不知道它是干嘛的但理解了 part 命名你就理解了 ClickHouse 的写入、合并、变更机制。3.1 part 命名里到底藏了什么信息在默认数据目录/var/lib/clickhouse/data/default/表名/下每一张表会对应一个目录里面每个人可见的文件夹就是一个 data part。典型的命名像这样202405_1_3_0 202405_4_9_1 202406_1_1_0拆开来看part 命名通常由这几段组成第一段是分区 ID。比如202405表示这个 part 属于 2024 年 5 月这个分区。分区 ID 不是必须和原始分区字段值长得一样它是由分区表达式计算出来的。第二段是min_block_num最小块号。第三段是max_block_num最大块号。第四段是merge_level合并层级。某些新版本如果发生过 mutation命名里还会带额外的哈希后缀表示这个 part 被重写过。块号是 ClickHouse 全局递增分配的每写入一批数据就会拿到一个新的 block number 范围。所以202405_1_3_0表示202405 分区下包含块号 1 到 3 的一个 part合并层级是 0即它刚写入没多久还没被合并过。通过这个命名你只需要一条命令就能快速判断表的数据健康度ls /var/lib/clickhouse/data/default/your_table/ | head -20如果看到大量过程是_0结尾的小 part说明这张表正在被高频小批量写入或者 merge 速度跟不上写入速度。3.2 part 太多为什么会拖垮查询每个 part 都自带独立的索引和标记文件。查询时 ClickHouse 需要在内存里把所有相关 part 的索引信息合并起来扫描时也要逐个 part 读数据块最后再把结果合并。part 数量越多文件句柄占用越多索引合并开销越大查询延迟肉眼可见地变差。更麻烦的是后台 merge 线程会一直尝试把小 part 合并成大 partpart 太多意味着 merge 压力一直很大磁盘 IO 和 CPU 被 merge 吃掉了真正给查询的资源就少了。这就是为什么很多人明明配了很多 CPU压测时却发现查询和 merge 在抢资源。3.3 预防和治理的实操首选方案是控制写入批次。ClickHouse 适合大 batch 写入如果业务侧只能产生小批次数据中间加一层 Buffer 表或 Kafka 缓冲把写入聚合成几十 MB 一个 batch 再落盘。分区粒度要合理。某些场景下分区太细每个分区都只有一点点数据也会产生大量小 part。日志表按天分区通常够了没必要按小时。监控 system.parts 表。重点看 active 字段等于 1 的行数、bytes 大小、level 层级SELECT table, partition, count() AS total_parts, sum(rows) AS total_rows, sum(bytes) AS total_bytes, max(level) AS max_level FROM system.parts WHERE active AND table your_table GROUP BY table, partition ORDER BY total_parts DESC;如果某个分区的 active part 数持续超过几百甚至上千就需要考虑强制合并或调整写入模型。偶尔手工OPTIMIZE TABLE your_table FINAL可以强制合并但不建议频繁使用。FINAL 会做全量合并扫描和重写的数据量极大生产环境跑一次可能会把磁盘 IO 打满。把它当成治理手段而不是日常运维动作。我用这套方法排查过几次 ClickHouse 查询越来越慢的问题最后根因基本都是小 part 堆积。把写入聚合之后查询速度自己就回来了。4. 查询体验的分水岭多表关联和高并发谁更扛得住如果只看单表聚合ClickHouse 的扫描速度确实惊艳一条 count group by 跑几亿行也就一两秒。但真实业务很少只有单表查询一旦把多表 Join 和高并发这两个条件加进来局面就不一样了。4.1 Doris 的多表 Join 为什么更省心Doris 2.x 引入 Nereids 优化器之后Join 重写、Join Reorder、谓词下推、Colocate Join、Bucket Shuffle Join 这些能力都逐步成熟了。对常见的星型模型几张百万到亿级的表做关联只要内存足够优化器基本能选出一个合理的执行形态。尤其值得说的是 Colocate Join。如果两张大表的分桶列和分桶数一致Doris 能让 Join 在本地完成数据不用跨节点传输这在常规 MPP 引擎里属于杀手级能力。建表时把 Join 键设成相同的分桶列后面的查询收益非常大。Doris 对高并发点查也做了不少优化比如 short circuit point query、缓存、倒排索引等。几十并发以内的精确查询体验很接近在线服务。我们压测过用主键查单行几百 QPS 都很稳。4.2 ClickHouse 的 Join 为什么让人又爱又恨ClickHouse 跑单表聚合是强项多表 Join 能跑但心智负担比较重。默认的 hash join 会把右表构建到内存里大表 Join 大表很容易内存爆掉。很多团队不得不用 Global Join 把所有分片的数据广播到查询节点内存和网络压力都上去了。社区惯用的做法是用字典表、子查询、预聚合、bitmap 去重等方案绕开 Join。比如把维度表做成 Dictionary查询时直接取字典值而不是真去 Join。这确实能解决问题但意味着 SQL 写起来要顾虑很多不像 Doris 那样你可以按标准 SQL 的直觉去写剩下交给优化器。新版 ClickHouse 的 Join 能力一直在进步比如支持更丰富的 join 算法、并行 hash join但根本上它还是把“单表扫描极快”这个优势放在第一位的引擎。如果你的业务一半以上的查询要跨多张表ClickHouse 的学习和调优成本要明显高于 Doris。4.3 高并发一个容易翻车的盲区ClickHouse 的单次查询调度链路很重。每次查询都要解析 SQL、构建 pipeline、把任务分发到线程池查询越短单次开销占比越高。简单查询压测到几百 QPS 就可能把单核 CPU 打满再往上走延迟会快速劣化。这不是 ClickHouse 的缺陷而是设计取舍。它默认使用者不是拿它当在线服务而是拿它做分析查询。但如果你真的要用它支撑报表 API常见解法是加一层查询缓存、或者用 Materialized View 把结果预先算好。还有不少人会同时用多个只读副本分摊查询压力。Doris 因为 FE 层做了很多查询规划和缓存加上 BE 的并行调度能力强在高并发点查场景下的表现要好很多。这也是为什么我看到越来越多人把 Doris 用在用户标签查询、订单实时查询这类“分析库干在线活”的场景。用一句话总结我实测下来的体感如果你要做的是多表关联复杂分析Doris 会省心很多如果核心场景就是超大单表按条件做聚合ClickHouse 的原始性能优势不容忽视。5. 写入链路与更新能力实时数仓最容易翻车的环节选型时大家关注查询多但真正上线后翻车的往往在写入和更新。这一节值得仔细看。5.1 Doris 实时写入与主键更新Doris 从设计上就考虑了实时写入。Stream Load 可以把 CSV、JSON 格式的数据通过 HTTP 方式批量导入Routine Load 可以持续消费 Kafka 数据配合 Flink CDC 做数据库实时同步也已经是很成熟的方案。高频小批量导入对 Doris 来说没有太大压力因为它的存储层是支持随机写的不是靠文件合并来摊平写入代价。更重要的一点是 Doris 的唯一键模型支持真正的更新和删除。应用层发一条 DELETE 或者 UPDATE底层走 MVCC数据可以被实时修正。这对做实时数仓太重要了。比如订单表的状态字段从“待支付”变成“已支付”业务希望报表立刻反映出来Doris 可以直接更新那一行查询看到的就是最新值。当然这不意味着你可以把 Doris 当成 MySQL 用。它本质还是 OLAP 引擎超高并发、每秒几万次的单条随机更新依旧不适合。5.2 ClickHouse 的写入哲学和 UPDATE/DELETE 的代价ClickHouse 的写入哲学是“尽量一次性写入大批量数据”。一次 INSERT 产生一个或多个新 part写入的数据量越大、批次越少整体效率越高。反过来如果每秒都来几十个小 INSERTpart 会迅速膨胀后续查询变慢merge 也会把磁盘 IO 吃光。更新删除在 ClickHouse 里不是不能做但属于重操作。ALTER TABLE ... UPDATE/DELETE本质是一次 mutationClickHouse 会把涉及分区的数据整个重写一遍。几亿行的表做一次更新可能要跑几分钟甚至更久期间 IO 压力很大。所以社区更推荐的模式是“不改数据用语义去重”。ReplacingMergeTree 通过版本号或时间戳在查询时保留最新记录CollapsingMergeTree 用正负折叠的方式处理事实表修正AggregatingMergeTree 在合并时预聚合。这些模型都需要业务在写入 Schema 上提前设计而不是事后像传统数据库那样直接改。5.3 一致性窗口强一致和最终一致的取舍Doris 的多副本是强一致设计。数据写入成功后任意副本上都能读到最新数据不需要考虑复制延迟。ClickHouse 的多副本默认是异步复制。数据先写到某一个副本再由后台线程复制到其他副本中间存在一个同步窗口。窗口大小取决于数据量和网络正常情况下是秒级压力大时可能更长。如果是单副本那就不存在同步问题但节点挂了数据就没了。如果你的下游业务能接受最终一致ClickHouse 没问题。但如果要做订单、余额、库存这类对一致性敏感的分析必须考虑清楚这个窗口带来的影响或者干脆选强一致引擎。6. 运维体感与周边生态部署、连接超时、半结构化与慢查询6.1 部署难度一个轻一个重部署体感上ClickHouse 明显更轻。在 Ubuntu 上安装就是几条命令的事装好之后改一下配置文件启动服务就能查数据。单机跑起来很快开发环境十分钟搞定。Doris 相对重一些。至少要规划 FE 和 BE 两类进程生产环境一般建议 3 个 FE 组成高可用BE 节点数量和存储盘位也要事先想好。FE 的元数据目录要放到独立盘BE 的存储盘要规划多盘配置。装好之后需要用 MySQL 客户端连上 FE执行ALTER SYSTEM ADD BACKEND把 BE 节点注册进来。我的感受是如果只是想快速验证ClickHouse 先跑起来容易如果要搭一个能长期稳定服务的生产集群两者都需要认真规划只是 ClickHouse 的配置文件更集中Doris 需要照顾的进程和状态更多。6.2 连接与超时Java/Spring Boot 常见坑Doris 兼容 MySQL 协议所以 Java 项目可以直接用 MySQL JDBC 驱动连接。但这个兼容性也会带来一个经典坑长查询超时。很多团队用 Spring Boot 连 Doris连接串还是按连 MySQL 的习惯配置结果发现一个跑几十秒的 SQL 经常被中途断开。原因通常是 socketTimeout 配置不规范或者连接池里的连接被数据库空闲超时策略杀掉后没有及时重建。我建议按这个思路配置jdbc:mysql://fe_host:9030/db?connectTimeout10000socketTimeout600000rewriteBatchedStatementstrueuseSSLfalse同时把 HikariCP 的connection-timeout设成 5 秒左右max-lifetime不要超过数据库端的空闲踢出时间。Doris 的 FE 有一个 session 变量max_execution_time单位毫秒默认一般是 3000005 分钟如果业务里确实有跑很久的查询这个值也要相应调大。这几个参数配合好连接层问题基本能避免。ClickHouse 这边反而简单一些。官方提供 HTTP 接口默认 8123和原生 TCP 接口Java 里有 clickhouse-jdbc 和 clickhouse-client 两种选择。HTTP 接口对开发特别友好直接发 POST 请求带上 SQL 就能查询也很容易封装成各种服务的上报接口。6.3 半结构化数据处理variant 与 JSON 函数的对比日志、埋点这类数据通常都是半结构化 JSON字段经常变化。这个场景下两边的处理逻辑差异很大。Doris 2.1 开始支持 variant 类型可以像存 JSON 一样把一个不确定 Schema 的字段直接塞进去。它会在内部自动识别字段并建立列存结构还能对 variant 里的字段建倒排索引查询时用variantCol.fieldName的方式访问。Java 客户端拿到 variant 字段时走 MySQL 协议返回的是 JSON 字符串用 Jackson 或 Fastjson 再解析成对象就行整体体验很顺。ClickHouse 的半结构化方案相对传统一些。最常用的方式是用 String 类型把整个 JSON 存下来查询时用JSONExtractString、JSONExtractInt这些函数动态解析。新版支持了 JSON 类型可以自动识别部分字段但成熟度和索引能力还在演进中。如果你的数据是日志、埋点、爬虫结果这种字段经常变化的Doris 的 variant 会明显省事。你要做的只是建表时多声明一个 variant 列后续新字段不用改表结构。6.4 慢查询排查的基本手法对比Doris 慢查询排查我的习惯是先开审计日志和查询 Profile。设置is_report_success true之后FE 会记录每个查询的详细 Profile能看到每台 BE 上 scan 了多少行、join 是否有数据倾斜、内存和耗时都花在哪个算子上了。配合 EXPLAIN 看执行计划基本能定位问题。常见的慢查询原因有统计信息没收集该跑 ANALYZE TABLE 没跑、谓词下推失败、tablet 分布不均导致个别 BE 成为热点、内存超限触发落盘。ClickHouse 的排查路径是另一套。system.query_log表记录了每条查询的耗时、读取行数、内存峰值直接查它就能找到最慢的查询列表。再用system.parts看 part 数量是否异常用system.processes看正在跑的查询必要时用 EXPLAIN PIPELINE 看执行计划的并行度。常见慢查询原因有part 数过多、没有走分区裁剪、大字段读取过多、GROUP BY 时数据倾斜。两边的排查思路都有章可循但工具差异很大。Doris 更像传统数据库的体验Profile 细致适合 DBA 思维ClickHouse 则更依赖系统表熟悉之后也非常高效。7. 最后的选择题你的场景更适合哪一个7.1 按场景直接给结论根据这段时间的实战体验我自己的选型判断标准是这样选 Doris 的场景业务对数据更新和删除有实时要求不能接受“查不到最新值”。复杂多表 Join 是常态不想花大量精力优化 SQL 去绕开 Join。需要支撑较高并发的查询 API比如用户标签、订单详情这类点查。数据中包含大量 JSON/半结构化字段希望按字段直接查询。团队更熟悉 MySQL 生态想用 SQL 和 MySQL 客户端一把梭。选 ClickHouse 的场景核心场景是超大单表的聚合分析比如日志、事件流、监控指标。写入以批量导入为主数据基本不可变或者能接受最终一致。查询并发不高但单条 SQL 的数据扫描量极大需要极致吞吐。团队愿意接受一套独立的 ClickHouse 运维体系包括 part、分区键、分片键这些概念。7.2 也可以不二选一很多团队最终不是二选一而是双跑。Doris 负责实时写入、更新、高并发报表查询ClickHouse 负责日志明细的海量沉淀和 ad-hoc 分析。中间用管道做数据同步各取所长。这种方式初期会多一些运维成本但对业务侧体验确实最好。尤其是数仓链路已经比较成熟的情况下没有必要为了统一而强行把其中一个塞进所有场景。7.3 我个人实操后的最后建议如果让我重新做一次选型我不会再先问社区“谁更快”而是先列出三个高频查询和两个写入链路在两套环境上各压一遍。实测数据永远比任何人的经验都更贴近你的业务。另外有一个小提醒无论选哪个都要把监控体系提前搭好。Doris 重点盯 FE 元数据和 BE 的磁盘、内存、tablet 分布ClickHouse 重点盯 part 数量、merge 队列、副本同步延迟。这两个系统在监控上各有各的脾气提前把 system 表指标接到告警里后面能省掉无数个从睡梦中被叫醒的夜晚。
返回列表