Doris在ETL链路中的实战:部署、选型、导入分桶与问题排查

📅 发布时间:2026/10/3 3:39:52
Doris在ETL链路中的实战:部署、选型、导入分桶与问题排查
1. 为什么我会在ETL主链路里塞进一个Doris1.1 旧链路的三宗罪先交代一下我这边的情况。团队维护的离线数仓一直是“Hive存明细定时调度跑汇总报表平台直查”的经典结构数据量在几百TB级别不算特别大但业务方越来越多问题开始集中在三件事上。第一是时效。每天的调度链路从晚上十点开始排队凌晨四五点才能跑到最下游的任务业务方早上九点打开看板偶尔还能看到昨天数据没刷完的告警。ETL本身没有错错在整条链路所有环节都依赖大规模批处理中间任何一张表卡住下游全部堵车。第二是临时查询。数据分析师习惯直接用BI工具写SQL拉数但Hive的响应速度摆在那里一个稍复杂的多表关联查询动辄几十秒甚至两三分钟。业务方等不住就反复提需求让人工跑数ETL团队每天被零碎的取数任务消耗大量时间。第三是臃肿。为了满足下游的各种查询形态数仓里衍生出一大堆中间汇总表。每次业务口径调整要重新跑一大批任务中间表越堆越多调度依赖越来越乱。1.2 Doris在ETL里到底承担什么Doris在ETL流程中能承担的角色比很多人想象的要广。它不只是一个加速查询的OLAP引擎更是一个能承接“轻量清洗、实时导入、服务查询”的综合节点。我实际落地下来把它放在了三个位置一是轻量贴源层。业务库的变更数据和Kafka里的埋点日志通过Routine Load或者Stream Load直接进Doris替代原先“先落HDFS再跑Hive”的繁琐链路。很多明细数据根本不需要等批处理入库即可用。二是汇总加速层。原先跑在Hive上的日汇总任务改造成Spark或者Doris自身的INSERT INTO ... SELECT之后把结果同步到Doris里给报表直查。省掉了原先“Hive出结果-同步到MySQL/Redis-再被报表读”的中间过程。三是临时查询入口。分析师原来在BI工具上连Hive查数现在把Doris作为默认数据源之一。响应速度从几十秒降到几百毫秒临时取数需求自己就消化掉了。所以Doris在大数据ETL里的定位并不是要替代Hive和Spark而是把链路中那些“等不起、查不动、没必要动用批处理”的部分接过来让ETL主干更瘦让下游查询更快。1.3 重构后的链路长什么样我压测验证通过后的架构大体是这样一个形态ODS层少量实时明细进Doris大量历史明细继续留在HDFS/HiveDWD层实时数仓场景由Flink写Doris离线场景仍由Hive产出后通过Broker Load同步ADS层报表汇总表统一放Doris支撑BI直查和高并发接口查询查询层Presto继续保留用于跨Hive和Doris的联邦查询日常取数用户直连Doris。这套结构跑起来之后最明显的变化是下游看板数据在每天早上七点半之前就能刷完偶尔有补数需求也只需要在Doris里跑一条SQL不再需要重排整个调度。后面几节我会把从部署、选型、导入分桶到排错的过程完整拆开讲踩过的坑和最终沉淀的方法都会写清楚给正在做同样选型的团队做一个参考。2. 部署落地从单机验收跑到三个FE五个BE2.1 部署前先想清楚这几件事很多团队在部署Doris时容易犯一个毛病拿到安装包就开跑结果跑起来容易真正接业务的时候各种资源瓶颈暴露出来又要重建集群。我建议部署前先把下面这几个问题想明白第一FE和BE要不要分机部署。FE是负责元数据和查询规划的节点BE是负责数据存储和计算的节点。小规模测试环境可以混部一两台机器搞定但生产环境绝对不能混部。FE的内存和CPU虽然有上限但一旦遇到大量查询并发或者元数据操作很容易和BE抢资源。我的生产集群是单独的3台FE机器内存64GB起步BE用了5台物理机内存配到128GB。第二FE的角色怎么规划。FE分为Follower和Observer两种角色。Follower参与选举和写入元数据日志Observer只提供读服务不参与选举。生产环境最少要3个Follower这样挂掉一个还能选主加Observer纯粹为了分担查询请求如果查询压力不大可以先不加。我在生产环境先部署了3个Follower后面业务查询量上来了又加了2个Observer用于隔离不同业务的元数据读压力。第三磁盘怎么规划。BE的数据目录可以用多个盘。Doris不像HDFS那样要求所有节点磁盘统一但每块盘的大小差异不要太大否则数据分布不均。我这边用了一块SSD专门放热数据分区几块HDD放历史冷分区靠schema的partition冷热策略去控制。2.2 FE和BE的安装步骤Doris安装本身其实不复杂复杂的是初始化过程里的各种细节。下面是我完整跑通一次生产集群安装的步骤直接照着操作基本不会有问题。先准备一台机器作为第一个FE节点解压二进制包进入fe目录配置conf/fe.conf。这里我列出几个关键配置# FE JVM 参数默认只有4G生产建议按机器内存设置 JAVA_OPTS-Xmx32768m -Xms32768m # MySQL协议端口客户端用这个端口连接 query_port 9030 # FE的HTTP端口用于WebUI和Stream Load http_port 8030 # FE内部通信端口 rpc_port 9020初始化FE执行cd fe bin/start_fe.sh --daemon这里有个坑第一次启动FE时master只会出现在一个节点上。如果想把另外两个节点也变成Follower需要用MySQL客户端连接主FE执行ALTER SYSTEM ADD FOLLOWER fe2_host:9010; ALTER SYSTEM ADD FOLLOWER fe3_host:9010;注意这里的9010是FE的edit log端口不是query_port。然后回到另外两台FE节点分别启动bin/start_fe.sh --daemon之后在BE节点上解压BE安装包配置conf/be.conf# BE数据目录多个目录用分号分隔 storage_root_path /data1/doris;/data2/doris # BE内存上限建议为物理内存的70%左右 memory_limit 80g # BE的端口 be_port 9060 webserver_port 8040 heartbeat_service_port 9050启动单个BEcd be bin/start_be.sh --daemon然后在FE上通过MySQL协议执行ALTER SYSTEM ADD BACKEND be1_host:9050; ALTER SYSTEM ADD BACKEND be2_host:9050;加完之后用SHOW BACKENDS确认所有BE状态为true再用SHOW FRONTENDS确认Follower和Leader角色正常集群就跑起来了。2.3 关键参数和最容易踩的坑部署完成后有几组参数是我强烈建议在接业务之前就调好的否则上线后一定会回来找补。第一组是FE的max_running_txn_num_per_db默认值是100。ETL场景下并发导入任务非常多尤其是多个表同时做Stream Load或者Broker Load事务数很容易打满。我实际遇到过导入任务排队、延迟飙高的问题把该参数调到1000之后才缓解。修改方式ALTER SYSTEM SET max_running_txn_num_per_db 1000;第二组是BE的write_buffer_size。Doris导入时每个tablet都会有一个内存中的写缓冲区默认大小是104857600字节也就是100MB左右。如果单次导入的数据量特别大、并发又高BE内存会被打爆。我踩过一次BE宕机的坑后来根据BE内存总量把该参数调小了同时限制了导入并发数。第三组是query_timeout。默认查询超时是300秒但如果BI工具里的某些复杂查询需要更长时间尽早调大否则用户会看到莫名其妙的查询中断。部署阶段最典型的三个坑我单独列一下FE节点混部导致端口冲突。默认情况下FE会占用8030、9030、9010、9020等端口BE会占用9060、8040、9050等端口混部时只要端口没规划好就起不来。建议提前把所有端口列一张表按节点规划清楚。BE的目录权限问题。Doris启动时的用户需要具备storage_root_path里所有目录的读写权限经常有人用root装了包然后换普通用户启动BE数据目录创建失败报错还特别隐晦看日志才发现是Permission denied。FE的JVM堆开太小。默认配置下FE的JVM堆只有4G一旦元数据量大或者高并发提交导入事务Full GC频繁整个集群的查询和导入都会卡。生产环境建议堆内存至少16G起步。3. 选型时刻Doris和ClickHouse到底怎么选3.1 两个引擎差异最大的地方我们团队在确定引入Doris之前也认真对比了ClickHouse。两者都是OLAP领域的热门引擎网上讨论也很多。我的结论是选型不应该看“谁更快”而应该看“谁更适配自己的ETL场景”。Doris和ClickHouse差异最大的地方有四个。架构层面。Doris是标准的存储计算一体架构FE负责元数据管理和查询规划BE负责存储与计算两者通过心跳感知状态。表的数据分布、副本管理、查询路由都由Doris自己完成对外提供的是MySQL协议。ClickHouse虽然也分布式但它的分布式表本质上是一张逻辑表底层数据分散在各个节点副本同步和分布式DDL需要依赖ZooKeeper或者ClickHouse Keeper来协调。从使用体验上看Doris更像一个“开箱即用的数据库”ClickHouse则需要你理解更多的底层机制。SQL兼容性。Doris支持标准SQL语法习惯和MySQL非常接近业务方迁移成本几乎为零BI工具直连也省事。ClickHouse的SQL方言差别比较大尤其是一些函数名和语法细节分析师从Hive/MySQL迁过来需要一段适应期。我在对比时特意让数据分析师用同样的SQL在两套引擎上跑Doris这边基本改都不用改ClickHouse那边需要调整不少函数写法。数据导入机制。Doris提供了Stream Load、Broker Load、Routine Load三件套导入即事务数据可见性有保证秒级延迟。ClickHouse的导入主要依赖INSERT INTO SELECT和分布式表大批量导入通常还要借助外部工具把数据文件分发到各个节点过程更繁琐一些保障一致性的成本也更高。并发查询能力。Doris因为FE做了统一的查询规划对小查询并发支持更好适合大量报表用户同时查询的场景。ClickHouse在单条大聚合查询上的吞吐表现更突出压榨CPU的能力很强但高并发下线程调度消耗大。两者没有绝对好坏只看你面对的是“几百人同时点报表”还是“几十个跑数任务扫全表”。3.2 ETL场景下的选型建议这条“ETL链路”该怎么选我直接给结论。如果你的核心诉求是数据持续导入、频繁更新、多表JOIN、BI工具直连、标准SQL那Doris更合适。ETL的场景天然要求“表结构可以变、数据可以更新、导入任务多、失败能回滚”这几点Doris做得更舒服。如果你的核心诉求是海量日志写入、宽表大聚合、查询性能极致、一次性导入后基本不更新那ClickHouse更合适。比如用户行为日志、监控指标、IoT点位数据这些数据写入后就是只读的ClickHouse的列式压缩和向量化执行能把查询性能压榨到极致。我在做选型决策时做了这么一张对比表放在这里供参考对比维度DorisClickHouse分布式依赖自带FE/BE体系依赖ZooKeeper/KeeperSQL兼容性标准SQLMySQL协议SQL方言差异明显数据更新支持Unique模型更新更新成本高不建议频繁更新导入能力Stream/Broker/Routine Load主要INSERT导入高并发查询表现稳定高并发下线程开销大运维复杂度自带WebUI扩缩容简单需要维护额外依赖组件最终我选择Doris还有一个很重要的原因团队已经有比较成熟的Flink和Kafka链路Doris的Routine Load能直接订阅Kafka少了中间一环数据转发服务整条实时链路的运维负担会小不少。4. ETL链路中的数据导入与分桶设计4.1 四种导入方式怎么选Doris的导入方式很丰富很多人一开始会纠结到底用哪种。我按实际情况给出的选择逻辑是这样的导入方式适用场景数据源实时性核心注意点Stream Load实时/近实时小批量本地文件、程序流秒级通过HTTP接口适合接口调用和手工调试Broker Load离线大批量HDFS/S3/OSS分钟级异步任务吞吐大适合T1同步Routine Load流式持续导入Kafka分钟级常驻任务自动消费KafkaInsert Into小批量、ETL计算Doris内部表/外表秒级支持INSERT INTO SELECT适合表间流转实际落地中Stream Load是我用得最多的导入方式灵活、幂等性好、出错容易定位。一个典型的调用是这样curl -L --user root:admin123 --location-trusted \ -H label:etl_order_daily_20250101 \ -H column_separator:| \ -H columns:order_id,user_id,city_id,amount,order_time \ -T /data/order_20250101.txt \ http://fe_host:8030/api/ads/order_daily/_stream_load这段命令里有个关键参数label它是导入任务的唯一标识。Doris会记录成功导入的label如果任务失败需要重试可以用同一个label重新提交系统会识别为同一任务不会重复导入数据。这个幂等机制对ETL任务非常重要我在做调度系统对接时就是直接拿任务实例ID作为label天然保证重跑安全。Routine Load我主要用于Kafka流式数据。一个最小配置长这样CREATE ROUTINE LOAD ods_event_load ON ods_event_detail PROPERTIES ( desired_concurrent_number 3, max_batch_interval 20, format json ) FROM KAFKA ( kafka_broker_list kafka1:9092,kafka2:9092, kafka_topic ods_event_detail, property.kafka_default_offsets OFFSET_BEGINNING );需要特别注意Routine Load里的desired_concurrent_number参数它决定导入任务的并发度直接关系到Kafka消费速率和Doris写入压力的平衡。如果Kafka分区数不多这个参数设置得再高也没意义反而可能因为并发调度产生空转线程。4.2 分桶到底在做什么工作中经常有人把分区和分桶混在一起说其实这是两个层面的概念。分区是按时间或者枚举值做逻辑切分比如按天分一个区它主要用于数据管理和查询裁剪。分桶是在每个分区内部按某个列的哈希值再切成若干个TabletTablet是Doris底层物理存储和数据复制的基本单元。查询时BE会并行扫描各个Tablet所以Tablet数量直接决定了查询的并行度。一个分桶对应一个Tablet分桶数越少并行度越低但分桶数也不是越多越好。每个Tablet都有元数据管理和底层数据块的调度开销如果一张表的数据只有几百MB却分了50个分桶查询时要调度的Tablet数量远多于实际需要的并行度通常会等一堆空表或者极小的表全部就绪才能出结果得不偿失。经验上单个Tablet的数据量控制在1GB到5GB之间是比较舒服的区间。数据量太小会导致大量非必要的小文件开销数据量太大则单Tablet扫描时间过长影响查询性能。4.3 几MB的小表还需要分桶吗这个问题我专门拿出来说因为很多团队建表时就折在这里了。结论先行几MB的小表不需要刻意分很多桶甚至不需要手动指定分桶数。Doris从2.0版本开始支持自动分桶建表时直接写BUCKETS AUTO就行CREATE TABLE ods_city_mapping ( city_id INT, city_name VARCHAR(64), province_id INT ) DUPLICATE KEY(city_id) DISTRIBUTED BY HASH(city_id) BUCKETS AUTO PROPERTIES(replication_num 3);让系统根据表数据量自动选择一个合理的分桶数。如果不想用自动分桶手动建表时几MB的数据分配1到3个分桶足够了。我见过最夸张的例子是一张只有8000多行的维度表分桶数给了48个。结果每次查询这张表光调度Tablet的时间就占了大半join操作时更是要把48个小Tablet都扫描一遍。后来我把分桶数改成3后同样的查询耗时降了一个数量级。但要注意一个权衡点不要因为现在只有几MB就永远不给分桶留余地。如果这张表有可能在近期内快速增长建议分桶数按未来一两个月的容量估算。比如表可能涨到5GB那分桶数设3到5个就够了。最忌讳频繁为分桶数发版本Doris现在支持修改分桶数但毕竟是一次额外操作。还有一点非常容易踩坑分桶键的选取。分桶键必须是高基数列最好取值分布均匀。建表时如果你的分桶键是一个只有0和1两个值的字段那么哈希后数据会严重倾斜必然存在部分Tablet数据量远大于其他Tablet查询性能被那一个重Tablet拖死。ETL场景下常用订单ID、用户ID做分桶键这类字段天然高基数且均匀。4.4 建模和导入的几条实操建议第一区分明细表、汇总表和更新表的模型选择。ETL链路里一定会遇到这三类表。明细表用Duplicate模型保留所有原始数据汇总表用Aggregate模型让Doris在导入时自动聚合业务库同步过来的表如果存在更新操作用Unique模型。而且Doris 2.x推荐使用主键模型的Merge-On-Write模式也就是建表时加上enable_unique_key_merge_on_write true更新性能比旧模式好很多。第二导入任务的并发要限流。Stream Load本身是单并发任务但如果调度系统把几百个Stream Load同时打进来BE内存和磁盘IO都会成为瓶颈。我后来在调度代码里给Doris导入任务统一加了信号量控制在每个BE最多同时运行5个导入任务重试和排队的逻辑放在调度层完成。第三清洗逻辑尽量前移。ETL过程中最常见的问题是把脏数据打进Doris回查时才发现源头没过滤。Doris的Stream Load和Broker Load都支持在导入时做简单转换比如指定columns参数做列的映射和过滤但复杂清洗逻辑建议还是由前置的Spark或Flink处理。Doris更适合做“已清洗数据的快速入库和查询”而不是承担复杂的流式计算职责。5. 一个让我排查到凌晨的错误Presto连接Doris报missing5.1 报错现场机器上的ETL链路稳定跑了一段时间后有个分析师突然跑来找我说Presto上查Doris里的表报错错误信息大概长这样Query failed: line 1:15 Column order_id is missing in table ads_order_daily重点就是这个missing。分析师说这个SQL昨天还能跑通今天突然就报order_id字段不存在但他明明记得这张表里是有这个字段的。我先自己在Doris上用MySQL客户端执行同样的SQL结果完全正常order_id字段存在数据也能查出来。这就证明数据本身没问题问题大概率出在Presto和Doris的连接器这一层。5.2 完整排查链路第一步检查Presto里看到的表结构。执行SHOW COLUMNS FROM doris_catalog.ads.ads_order_daily发现返回的列名里确实没有order_id而且少了不少字段。Presto拉到的元数据跟Doris真实表结构对不上。第二步怀疑是元数据缓存问题。Presto的Connector一般会对表结构做缓存我先是在Presto上执行了refresh materialized view又尝试重启了Presto coordinator和worker。等集群恢复后问题依旧说明不是简单的缓存。第三步翻Presto的日志。日志里能看到Connector在初始化表元数据时调用了JDBC的DatabaseMetaData接口但返回的结果集中列数量比预期少。这时候我怀疑是连接器版本和Doris版本不兼容。第四步比对版本。查了一下Presto环境里用的MySQL Connector/J版本偏老而Doris集群最近从1.2升到了2.1。Doris在升级后的元数据返回格式上有变化老的Connector解析时丢了一部分列信息。第五步用测试脚本直接调JDBC接口验证。确认了DatabaseMetaData.getColumns在指定catalog参数时返回的TABLE_TYPE过滤条件匹配不上。简单说连接器查询元数据时带了一个表类型过滤条件Doris新版本返回的类型值和旧连接器期望的值不一致导致所有列都被过滤掉了。最终结果就是Presto认为这张表里没有order_id报出missing错误。整个排查链路走下来核心就是一句话跨引擎访问时字段元数据对不上先怀疑版本兼容性。5.3 根因和最终修复修复其实简单把Presto节点上的JDBC驱动和Doris连接器统一升级到与Doris 2.1兼容的版本然后Presto侧重新创建catalog定义让连接器重新拉取一遍元数据。之后执行同样的SQLorder_id正常返回问题彻底消失。除了这个查询链路的问题我还遇到过写入链路类似的报错。ETL脚本里用Presto查Hive结果然后通过JDBC写入Doris结果是INSERT语句报找不到某个目标列。排查后发现写入时的字段顺序和Doris表结构顺序不一致代码里显式指定的column列表错位了。这类问题在代码评审里很难发现只有真正跑到写入那一步才会暴露。后来我在ETL任务的写入逻辑里统一加了一步校验先用desc命令拉取目标表结构和写入字段列表做一次全量比对不一致就直接fail掉任务并告警。5.4 这类问题怎么预防一次凌晨排查的经验让我在团队里沉淀了几条规矩一是版本清单制度。所有接入Doris的组件包括Presto连接器、JDBC驱动、Flink Connector、BI工具连接串全部登记版本号Doris升级前先对照清单评估兼容性。这个清单在手很多问题一眼就能定位。二是元数据变更通知。Doris表结构变更后相关链路负责人要在群里同步消息Presto、BI、Flink任务涉及的表变更优先在测试环境验证一遍连通性再动生产。三是查询前自检。对于核心报表表我加了一个定时任务每天检查Presto侧的表结构和Doris侧的实际表结构是否一致不一致自动告警。很多潜在问题不等用户发现DBA先看到了。把这次排查的根因、修复过程和预防措施记录下来之后我最大的感受是ETL工程里的很多“灵异事件”到最后都是元数据不同步或版本不一致早一点把元数据管理纳入规范能省掉大量深夜排查时间。另外再分享一个我自己一直在用的习惯给Doris里的每个导入任务起一个有业务含义的label比如etl_order_daily_20250101_v1排查问题时能靠label快速定位是哪一批数据、哪一次重跑、对应用什么业务逻辑。这个习惯在踩坑排查时真的帮了大忙。