ClickHouse + DolphinScheduler:两个组件搞定轻量离线数仓,谁还堆 Hadoop 全家桶?

发布时间:2026/9/15 15:04:26
ClickHouse + DolphinScheduler:两个组件搞定轻量离线数仓,谁还堆 Hadoop 全家桶? 摘要只用 ClickHouse DolphinScheduler 两个组件搭轻量离线数仓告别 Hadoop 全家桶适合日增千万级以下中小团队。用 Database 划分 ODS/DWD/DWS/DIM/ADS 五层数据同步不引入 DataX直接用 CK 原生 MySQL 表引擎挂载远端表配 ReplacingMergeTree 按源表 update_time 去重实现全量初始化加幂等增量再用 DolphinScheduler 的 SQL/SHELL/DEPENDENT 节点做定时增量与层间依赖调度。搭数仓一定要上 Hadoop 全家桶吗本文手把手带你用 ClickHouse DolphinScheduler 搭建一套轻量离线数仓从建库到调度一条龙中小团队也能拥有自己的数据底座。前言你是不是也被全家桶劝退过提到离线数仓很多人脑子里自动弹出一套豪华套餐Hadoop Hive Spark DolphinScheduler Sqoop/DataX 可能还要再来个 OLAP 引擎……光是把这些组件部署完运维同学的头发已经掉了一半项目还没正式开始。但现实是不是每个团队都需要日均 PB 级的处理能力也不是每个数仓都要搞得像 NASA 指挥中心一样复杂。对于日增数据量在千万级以下的中小团队来说有一条轻装上阵的路线 ——ClickHouse DolphinScheduler两个组件就能撑起一套五脏俱全的离线数仓。两条路线对比维度全家桶方案轻量方案技术栈Hadoop Hive Spark 调度 ETL OLAPClickHouse DolphinScheduler部署复杂度⭐⭐⭐⭐⭐建议先买防脱洗发水⭐⭐Docker 一把梭运维成本高N 个组件 N 种报错姿势低两个组件心里有数适用场景大规模离线计算、数据湖中小规模数仓、快速交付的 BI 场景查询性能依赖引擎组合ClickHouse 原生 OLAP天然快不是说全家桶不好而是杀鸡别用牛刀—— 如果你的数据量还没大到需要 Hadoop 来扛先试试轻量方案省下来的时间可以多写几个需求好吧这可能不算好处 。一、前置准备在开始之前你需要先把两个核心组件部署好组件参考文档ClickHouseClickHouse 25.4 基于 Docker 单机部署实战指南DolphinSchedulerDolphinScheduler 单机部署实战Docker 一把梭中小团队的调度神器部署完这两个我们就可以正式搬砖了。二、数仓分层先把房间隔好数仓的经典分层模型即使是轻量方案也不能含糊。在 ClickHouse 中我们直接用Database来划分各层CREATEDATABASEIFNOTEXISTSods;-- 原始数据层贴源层CREATEDATABASEIFNOTEXISTSdwd;-- 明细数据层CREATEDATABASEIFNOTEXISTSdws;-- 汇总数据层CREATEDATABASEIFNOTEXISTSdim;-- 维度层CREATEDATABASEIFNOTEXISTSads;-- 应用数据层直接给 BI 用-- 还要一个放「远端数据源映射表」的库它不算数仓的一层-- 但下一节的 MySQL 表引擎要建在这里别漏。CREATEDATABASEIFNOTEXISTSsource_mapping;六句 SQL数仓的骨架就搭好了。是的就这么朴实无华。source_mapping单独拎出来是因为它里面放的不是数据是「连接」——建在 ODS 里会让人误以为那是已经落库的数据。小贴士分层不是为了好看而是为了让数据流转有据可循 —— ODS 管搬进来DWD 管洗干净DWS 管聚合好ADS 管端上桌。每一层各司其职后续排查问题的时候你会感谢自己。三、数据同步把数据搬进 ClickHouse3.1 同步方案选型数据从 MySQL、MongoDB 等业务库同步到 ClickHouse常见方案分两大类方案思路代表工具特点ETL/ELT 中间件部署独立的同步工具DataX、Sqoop、Flink CDC功能强大但多一个组件多一份运维CK 原生表引擎ClickHouse 直接连数据源MySQL 引擎、MongoDB 引擎、JDBC 引擎等零额外部署SQL 即同步ClickHouse 内置了丰富的集成表引擎支持 MySQL、MongoDB、PostgreSQL、Kafka、S3、HDFS 等几十种数据源基本覆盖了常见的数据接入场景。我们的选择既然目标是轻量那就贯彻到底 —— 直接用ClickHouse 原生 MySQL 表引擎不引入额外组件SQL 写完数据就过来了。原则能少一个组件就少一个少一个组件就少一种凌晨三点被叫醒的可能。3.2 创建数据源映射MySQL 表引擎在 ClickHouse 中通过 MySQL 表引擎直接挂载远端 MySQL 表CREATETABLEsource_mapping.member_profileENGINEMySQL(your-mysql-host:3306,your_database,member_profile,your_username,your_password);⚠️注意这里只是创建了一个映射数据仍然存储在 MySQL 中。每次查询这张表ClickHouse 都会实时去 MySQL 拉数据。所以它的定位是同步的桥梁而不是最终存储。3.3 创建 ODS 层目标表接下来在 ODS 层创建真正存储数据的本地表CREATETABLEods.ods_member_profile(id UInt64COMMENT主键ID,member_id StringCOMMENT会员编号,avatar_url Nullable(String)COMMENT头像地址,login_key StringCOMMENT登录唯一标识,phone Nullable(String)COMMENT手机号码,member_type Int8DEFAULT1COMMENT会员类型: 1-普通, 2-内部,statusNullable(Int8)COMMENT状态: 0-禁用, 1-启用,secret_key Nullable(String)COMMENT密钥已加密,nickname Nullable(String)COMMENT昵称,avatar_large Nullable(String)COMMENT大头像地址,create_timeDateTimeCOMMENT创建时间,update_timeDateTimeCOMMENT更新时间取源表同名字段同时兼任版本列)ENGINEReplacingMergeTree(update_time)PARTITIONBYtoYYYYMM(create_time)ORDERBY(member_id)SETTINGS index_granularity256-- 默认 8192这里调小是为点查优化代价见下COMMENTODS层 - 会员信息表;几个关键设计决策解释一下ReplacingMergeTree这是重点ODS 层会反复同步增量数据同一条记录可能被多次写入。ReplacingMergeTree会在后台 Merge 时按版本列去重机制细节见 ClickHouse 官方 · ReplacingMergeTree。如果用普通MergeTree你的数据会越来越膨胀最终查出来一堆重复行场面一度非常混乱 PARTITION BY toYYYYMM(create_time)按月分区方便管理和清理历史数据。⚠️ 这里有个前提见下面那条注意ORDER BY (member_id)按业务主键排序查询效率拉满index_granularity 256默认值是8192这里调小到 1/32。好处是稀疏索引更密、按member_id点查时要扫的数据块更小代价是主键索引本身占的内存和磁盘涨约 32 倍。会员表这种「行数不算多、但经常按 ID 捞单条」的场景划算换成日志类大宽表就别照抄了先用默认值。版本列为什么必须取源表的update_time而不是now()这一步选错会静默弄脏数据。官方文档写得很明确merge 时保留的是版本列取值最大的那一行。所以版本列的含义必须是「这行数据在业务上有多新」而不是「这行是什么时候被灌进来的」。如果写成_version DateTime DEFAULT now()平时顺序跑没问题一补数就出事10:00 那个窗口跑失败了某会员的旧资料没同步进来11:00 正常跑同步到该会员 10:30 修改后的新资料版本 11:00下午 15:00 你去补跑 10:00 那个窗口捞到的是该会员修改前的旧资料而版本 now() 15:0015:00 11:00 ⇒ClickHouse 会认为旧资料更新把新资料覆盖掉。而且它不报错对数时才会发现。改用update_time当版本列补数捞到的旧行版本天然更小覆盖不了新行——补几次都一样这才叫幂等。顺带这样做还白拿一个好处update_time作为真实列留在了 ODS 里下游 DWD 想按它做增量、或者排查「这行到底啥时候变的」都有据可查。⚠️另一个前提分区键那一列这里是create_time在业务上必须是不变的。后台合并只在分区内进行所以同一个member_id的新旧两行一旦落到不同分区就永远不会被合并掉。我实测过这个行为ClickHouse 25.4脚本与完整输出见 ods-sync-bench同一个member_id造两行、create_time分别落在 1 月和 2 月OPTIMIZE TABLE ... FINAL跑完之后——场景合并后剩几行查询时加FINALcreate_time跨月2 行新旧都在✅ 1 行正确create_time同月✅ 1 行✅ 1 行两个结论都要记住① 跨分区的重复磁盘上会一直存着OPTIMIZE也清不掉② 但查询时写FINAL能跨分区拿到正确结果——后台合并和查询FINAL的行为不一样别混为一谈。正常业务里create_time是创建时间、不会变所以这个坑不会触发。但如果你做过数据订正、或者从别的系统迁移过来改写过创建时间就要留意受影响的记录会留下永久的存储膨胀而且任何漏写FINAL的查询都会把它重复计一遍。3.4 初始化存量数据首次同步需要把 MySQL 中的全量数据灌进来INSERTINTOods.ods_member_profile(id,member_id,avatar_url,login_key,phone,member_type,status,secret_key,nickname,avatar_large,create_time,update_time)SELECTid,member_id,ifNull(avatar_url,)ASavatar_url,login_key,ifNull(phone,)ASphone,member_type,ifNull(status,0)ASstatus,ifNull(secret_key,)ASsecret_key,ifNull(nickname,)ASnickname,ifNull(avatar_large,)ASavatar_large,create_time,update_time-- ← 版本列必须来自源表别用 now()FROMsource_mapping.member_profile;执行完毕后验证一下数据量SELECTcount(*)FROMods.ods_member_profile FINAL;为什么用FINAL因为ReplacingMergeTree的去重发生在后台 Merge 过程中如果 Merge 还没完成直接count(*)可能会看到重复数据。加上FINAL关键字ClickHouse 会在查询时强制去重得到准确结果。当然FINAL有性能开销生产环境大表慎用验证的时候用用无妨。顺便说一下同步性能 —— 你可能会担心全量灌数据会不会跑到天荒地老结论是不用担心但这个数字强依赖环境我把两组来源不同的数据都摆出来来源MySQL → ClickHouse说明我们生产环境多张业务表的同步统计~35w 条/sMySQL 与 CK跨机表更宽含网络往返本地压测台可复现见下~98 万 ~ 118 万条/sMySQL 与 CK同机容器、11 列窄表是上限值压测台那组的完整条件Ubuntu 22.04.5 / 4 核 7 GiB / Docker 29.6.1表结构与本文 ODS 表一致。200 万行耗时 1.70 秒、500 万行 5.12 秒峰值内存 692 → 744 MiB增长很缓说明是流式写入没把整表读进内存。⚠️两组差了三倍多不是谁测错了而是它们量的本来就不是一回事。同机容器没有跨机网络往返窄表每行的字节数也小得多。⇒别拿任何一个当自己的容量规划依据。要自己环境的真实数字用下面这条 SQL 在你的 CK 上查——written_rows和query_duration_ms是 ClickHouse 自己记的比掐秒表准也和上面压测台是同一口径SELECTevent_time,tables[1]AStbl,written_rows,round(query_duration_ms/1000,2)ASsecs,round(written_rows/(query_duration_ms/1000))ASrows_per_secFROMsystem.query_logWHEREtypeQueryFinishANDquery_kindInsertANDwritten_rows100000ANDevent_datetoday()-30ORDERBYwritten_rowsDESCLIMIT20;压测台可直接跑造数 → 灌入 → 出速率跑完自动清理ods-sync-bench一行sh bench.sh。完整执行输出、两个踩过的坑都在里面。四、增量调度让 DolphinScheduler 帮你打工存量数据搞定了但数仓是要活的 —— 业务库每天都有新数据进来我们需要定时增量同步。这时候就轮到 DolphinScheduler 登场了。4.1 创建调度工作流操作路径很简单创建项目 → 新建工作流 → 添加 SQL 节点 → 上线定时DolphinScheduler 的 UI 交互比较直观这里不赘述基础操作如有不熟悉的同学可留言评论后续补充分享相关内容的文章。我们重点关注数仓场景下常用的三种节点类型模块组件用途通用组件SQL执行 MySQL / ClickHouse SQL数仓的主力通用组件SHELL发通知飞书/钉钉、执行脚本等辅助操作逻辑节点DEPENDENT依赖上游工作流控制层级间的执行顺序4.2 增量同步 SQL核心在工作流中添加一个 SQL 节点填写增量同步逻辑 ——这才是本章的重头戏INSERTINTOods.ods_member_profile(id,member_id,avatar_url,login_key,phone,member_type,status,secret_key,nickname,avatar_large,create_time,update_time)SELECTid,member_id,ifNull(avatar_url,)ASavatar_url,login_key,ifNull(phone,)ASphone,member_type,ifNull(status,0)ASstatus,ifNull(secret_key,)ASsecret_key,ifNull(nickname,)ASnickname,ifNull(avatar_large,)ASavatar_large,create_time,update_time-- ← 版本列补数能幂等全靠它FROMsource_mapping.member_profileWHEREupdate_timetoDateTime($[yyyy-MM-dd HH:00:00-1/24])ANDupdate_timetoDateTime($[yyyy-MM-dd HH:00:00]);增量逻辑说明$[yyyy-MM-dd HH:00:00-1/24]是 DolphinScheduler 的内置时间参数表示上一个整点$[yyyy-MM-dd HH:00:00]表示当前整点每次只同步过去一小时内有更新的数据配合ReplacingMergeTree的去重机制实现幂等增量同步WHERE 条件拆解update_time 开始时间捞出该时段内新增或修改过的记录新增时 update_time create_time修改时 update_time 会被刷新update_time 结束时间排除掉窗口之后才发生的变更两个条件都卡在update_time上构成一个干净的半开区间[上一整点, 当前整点)⚡性能提示MySQL 源表务必给update_time建索引否则每小时一次的增量查询会变成全表扫描慢查询警告了解一下 上界为什么必须卡update_time不能卡create_time这直接决定补数能不能还原当时的窗口。本文早期版本上界写的是create_time 结束时间理由是排除当前整点之后才创建的数据。听着没错但它漏了一种情况一条 9:00 就创建、却在 11:02 被修改的老记录——create_time是 9:00怎么卡都在界内于是它会被[10:00, 11:00)这个窗口捞走尽管它的变更明明发生在 11:02。后果不是丢数据下个窗口会再捞一次ReplacingMergeTree也会去重而是窗口内容不再只由窗口决定还取决于你什么时候跑。同一个窗口11:05 跑和第二天补数时跑捞到的东西不一样 ⇒下面那条补数能正确还原当时的窗口的建议就不成立了。改成两头都卡update_time窗口才是真正封闭的跑多少次、什么时候跑结果都一样。⚠️这个窗口方案有个必须知道的前提它不会「回捞」。每次执行只处理[上一整点, 当前整点)这一个固定窗口窗口是按调度时间算的不是按上次成功时间算的。所以一旦某次执行失败又没人补跑那一小时的数据就永久留在了 MySQL 里——下一次执行只管它自己那个窗口不会回头补。而且它不会报错ODS 表照常有新数据进来只是中间缺了一段往往要等对数时才发现。三条建议在 DolphinScheduler 里给这个工作流配上失败重试 告警通知它本身支持别让失败静默过去补数时用它的「补数」功能按调度时间重跑$[yyyy-MM-dd HH:00:00]这类参数会跟着补数的时间走能正确还原当时的窗口窗口留一点重叠比如下界用-2/24往前多捞一小时——ReplacingMergeTree会按update_time去重多捞不会脏数据少捞才会丢数据。⚠️ 这条成立的前提是版本列取自源表update_time若沿用now()重叠和补数反而会拿旧数据盖掉新数据见上一节那段红框。4.3 上线定时调度保存工作流后上线并配置定时策略比如每小时执行一次调度时间小技巧建议将执行时间比整点延后 3~5 分钟比如每小时的第 5 分钟触发。业务库通常有 MySQL 主从复制延迟卡在整点跑可能漏掉最后几秒写入的数据。稍微慢半拍数据反而更完整。4.4 验证同步结果调度跑完后查一下最新数据是否已经同步过来SELECTmember_id,create_time,update_timeFROMods.ods_member_profile FINALORDERBYupdate_timeDESCLIMIT10;这里按update_time排序不是create_time——增量同步捞的是「有变更」的记录。一条三个月前创建、今天被改过的老数据按create_time排永远翻不到你会误以为增量没生效。看到最新的业务变更已经出现在 ODS 层说明整个链路已经打通 五、后续建设从 ODS 到 ADS 的数据流转ODS 层只是起点完整的数仓还需要继续向上构建每一层之间的 ETL 逻辑同样通过 DolphinScheduler 的 SQL 任务节点来调度利用DEPENDENT 节点控制层级间的依赖关系确保上游跑完下游才启动。往上一层怎么做增量这里就能收到前面那个决定的利息了——我们把update_time当成真实列留在了 ODS所以 DWD 可以照抄同一套窗口写法只是数据源从 MySQL 映射表换成 ODS 表-- 形状示意SELECT 里换成你自己的清洗逻辑INSERTINTOdwd.dwd_member_profileSELECT/* 你的字段与清洗表达式 */FROMods.ods_member_profile FINALWHEREupdate_timetoDateTime($[yyyy-MM-dd HH:00:00-1/24])ANDupdate_timetoDateTime($[yyyy-MM-dd HH:00:00]);⚠️这里的FINAL和前面第三节那句「生产大表慎用FINAL」是有张力的得说清楚怎么取舍不加FINAL同一条记录在 ODS 里可能还留着多个未合并的版本会被一起带到 DWDDWD 就脏了。加FINAL正确但代价是 ClickHouse 要对相关 part 做合并处理ODS 表越大越慢。实务上的分界ODS 还不大时直接用FINAL简单省事等它慢到影响调度了换成显式去重把开销限制在窗口内SELECT*FROMods.ods_member_profileWHEREupdate_timetoDateTime($[yyyy-MM-dd HH:00:00-1/24])ANDupdate_timetoDateTime($[yyyy-MM-dd HH:00:00])ORDERBYmember_id,update_timeDESCLIMIT1BYmember_id;LIMIT 1 BY member_id是 ClickHouse 特有的写法按ORDER BY排好之后每个member_id只留第一行。比套一层row_number()子查询短得多也更好读。 这一段是写法示意不是我实测过的基准——两种写法的性能拐点在哪取决于你 ODS 的体量和 part 数量自己拿system.query_log量一把方法见第三节那条 SQL。另外一条建议是确定的DWD 表同样用ReplacingMergeTree(update_time)让去重逻辑逐层一致——每层各搞一套版本规则对数时会非常痛苦。至此一套轻量但完整的离线数仓就搭建完成了。六、总结回顾一下我们做了什么步骤内容关键技术点1部署 ClickHouse DolphinSchedulerDocker 部署开箱即用2创建数仓分层ODS/DWD/DWS/DIM/ADSClickHouse Database 划分3配置数据源映射MySQL 表引擎零组件接入4全量初始化 增量调度ReplacingMergeTree版本列取源表update_timeupdate_time时间窗口5逐层构建数仓DolphinScheduler 工作流编排这套方案的核心优势极简部署只有两个组件Docker 一把梭半天搞定零额外 ETL 组件利用 ClickHouse 原生表引擎直连数据源不用再折腾 DataX、Sqoop天然 OLAP 能力ClickHouse 本身就是 OLAP 引擎数仓查询不需要再套一层调度灵活DolphinScheduler 支持 DAG 编排、依赖管理、失败重试、告警通知麻雀虽小五脏俱全三个别踩的坑正文各有展开这里汇总一遍都是不报错但会静默出问题的坑后果怎么避版本列用now()补数时用旧数据覆盖新数据版本列取源表update_time增量窗口上界卡create_time同一窗口不同时刻跑捞到的数据不一样补数还原不了上下界都卡update_time分区键那列在业务上会变同主键的行落到不同分区后台永远合并不掉分区键选不可变的列查询记得带FINAL当然这套方案也有其适用边界—— 如果你的数据量级已经到了 TB/PB 级别或者需要复杂的流批一体处理那还是老老实实上大数据全家桶吧。工具没有高低之分只有合不合适。 如果这篇文章帮你少踩了一个坑或者让你对轻量数仓有了新的思路欢迎点赞 收藏 ⭐ 关注你的支持是我持续输出的动力有任何问题也欢迎评论区交流我们一起把数仓这件事搞得明明白白 延伸阅读ClickHouse Flink DolphinScheduler中小厂三件套搞定离线实时数仓告别 Hadoop 全家桶 —— 本文离线数仓的升级版加上 Flink CDC 补齐秒级实时链路。trade 是数据域还是主题域数仓分层里最容易搞混的一对概念一篇讲透 —— 分好 ODS/DWD/DWS 只是第一步再厘清数据域与主题域这对易混概念。ClickHouse 内存爆了一次增量 SQL 从 8.6 GiB 干到 115 MiB 的实战复盘 —— 数仓跑起来后增量同步 SQL 把内存打爆的真实优化案例。️ 标签ClickHouseDolphinScheduler离线数仓数仓分层MySQL表引擎ReplacingMergeTree最后更新2026-09-14订正三处照抄就会出问题的地方另补两条实测结论跟着早期版本搭过的建议回头看一眼版本列从_version DateTime DEFAULT now()改成直接用源表的update_time。ReplacingMergeTree保留的是版本列最大的那一行用now()意味着版本表示「什么时候灌进来的」而不是「数据有多新」——一旦补数或窗口重叠就会用旧数据静默覆盖新数据而本文第四节恰恰建议了这两件事。改用update_time后补数才真正幂等并且update_time作为真实列留在 ODS下游可以拿它做增量。补上CREATE DATABASE source_mapping。第三节的 MySQL 表引擎建在这个库里而第二节只建了 ODS/DWD/DWS/DIM/ADS 五层照抄会在第一步就报库不存在。§3.3 补一条实测发现的注意事项ReplacingMergeTree的后台合并只在分区内做同一主键的行若因create_time被订正而落到不同分区就永远合并不掉但查询时的FINAL能跨分区。两者行为不同实测脚本与输出在ods-sync-bench目录。增量窗口的上界从create_time 结束时间改成update_time 结束时间。原写法会把「早就创建、但在窗口之后才被修改」的老记录捞进本窗口导致同一个窗口在不同时刻跑捞到的数据不一样——而本文恰恰建议「补数时按调度时间重跑能正确还原当时的窗口」这个承诺在原写法下不成立。两头都卡update_time后窗口才真正封闭。§4.4 的验证 SQL 同理从ORDER BY create_time改成ORDER BY update_time按创建时间排永远看不到被修改的老数据会误判增量没生效。§5 顺带补了 DWD 层的增量写法。同步速率那组数字补上了可复现的来源。原文只有「~35w 条/s」加一句「基于多张业务表的实际同步统计」读者无从验证。现在并列生产与本地压测两组差三倍多因为一个跨机一个同机并附上可直接跑的压测台ods-sync-bench 与一条在自己生产 CK 上取数的 SQL。另index_granularity 256补上代价说明默认 8192调小换点查、代价是索引开销涨约 32 倍大宽表别照抄、正文顶部补引封面。