1. 为什么现代数据架构都在谈“中枢”这个概念先说个我自己的观察。早几年做数据平台大家聊的是“上数仓”一套Hive或者Spark作业跑批每天凌晨出报表架构简单直接问题也简单直接。但大概从2020年之后情况完全变了数据湖成了标配Iceberg、Hudi、Paimon一个接一个冒出来实时计算也不只是 Flink 的专利业务部门张口就要“实时大屏”“秒级看数”。这时候你会发现传统“一套数仓打天下”的思路根本撑不住——湖里数据越来越多查一次全表扫描能把集群跑冒烟实时链路和离线链路各搞一套口径对不上业务分析师想自己查数据还得找数仓团队排期提数。整个架构像一盘散沙缺的不是更多组件而是一个能把所有数据源统一收口、让查询变快、让实时和离线打通的高性能中枢。Apache Doris 最近几年在社区里热度一直很高核心原因就是它恰好卡在这个位置上。“数据湖加速”“实时数仓”“统一查询层”这三个词单独拎出来任何一个都有产品在做但 Doris 的特殊之处在于它把三件事收敛到一个系统里。对一线工程师来说这意味着少维护一套集群少写一套数据同步逻辑少头疼一次口径对不齐的问题。这篇文章我会结合自己的真实使用经历把 Doris 在这三个方向上的设计逻辑、实操配置和踩坑记录都摊开来讲希望能给正在做架构选型或者已经被湖仓割裂折磨得头疼的朋友一些参考。我先把结论放在前面Doris 能成为现代数据架构里的“高性能中枢”靠的不是某一个单点功能特别强而是它把“查询引擎 存储引擎 统一接入层”揉成了一个整体让数据湖的冷数据能加速查让实时数据能低延迟写入让所有业务方都通过同一个 SQL 入口拿到一致的数据。这个“一体”的思路才是它最值钱的地方。2. 数据湖加速Doris 不是取代数据湖而是给数据湖装上加速引擎2.1 为什么数据湖查询那么慢问题出在哪数据湖解决的是“存得下、存得便宜”的问题代价是“查得快”变成了短板。以 Hive 数仓为例数据以 Parquet/ORC 文件形式存在 HDFS 或对象存储上查询时 SparkSQL 或者 Presto 需要从远端拉取文件元数据、扫描列数据再把结果传到计算引擎。整个过程涉及大量的网络 I/O而且每个查询都要重复拉取一遍文件列表和统计信息。如果表的文件数量上万甚至几十万个光列举文件就能耗掉几秒钟更别提对象存储还有请求频率上限小文件一多查询直接被限流拖死。我之前维护过一套 Iceberg 湖表底层数据放在 S3 上每个月增量大概是 200GB文件数量累计到了 30 多万个。用 Trino 查一个简单的 count(*)第一次跑要 40 多秒其中大部分时间花在访问 metastore 和列举文件上。业务方根本没法接受这种延迟最后只能把高频查询的数据再同步回数仓等于湖和仓又变成了两套互相拷贝数据的系统。Doris 的思路完全不一样。它不把数据搬回自己的存储而是在 Doris 内部用 Multi-Catalog 机制挂载外部数据源借助 Doris 自己的向量化执行引擎去查湖里的数据。更关键的是Doris 会把“查询计划”做得很聪明——通过元数据缓存、分区裁剪、谓词下推这些手段把真正扫描的数据量压缩到最低。2.2 Multi-Catalog 是怎么做到“一份数据多处查询”的Multi-Catalog 是 Doris 1.2 之后正式稳定的功能现在 2.x 版本已经很成熟了。简单理解Catalog 就是 Doris 对外部数据源的注册中心。你创建一个 Hive CatalogDoris 就知道 Hive 的 metastore 地址、HDFS 配置、认证方式创建一个 Iceberg CatalogDoris 就和 Iceberg 的 catalog 服务建立连接。之后用户在 Doris 里执行查询SQL 里带上 catalog 名前缀比如SELECT * FROM hive_catalog.db.tableDoris 就会把这个查询翻译成对底层文件系统的读取操作。这种架构带来的最大好处是数据不需要拷贝一份进 Doris。湖里的原始数据继续保存在对象存储或 HDFS 上Doris 只负责“算”。这意味着数据湖作为单一数据源的地位保住了数据团队不需要为了查询性能而维护多份数据副本。对于已经投入了大量资源建设数据湖的公司来说用 Doris 做查询加速层比推翻原有架构重建一套数仓要现实得多。我给大家看一个实际创建 Catalog 的配置片段以 Hive Catalog 为例CREATE CATALOG hive_catalog PROPERTIES ( type hms, hive.metastore.uris thrift://metastore-host:9083, hadoop.username hive );这里type指定元数据服务类型hive.metastore.uris是 Hive Metastore 的地址hadoop.username是访问 HDFS 时的用户。如果底表数据在对象存储上还可以通过s3.endpoint、s3.access_key等参数配置认证信息。Doris 的多 Catalog 设计得很细它还支持在 Catalog 级别配置文件系统的访问协议比如 Hadoop 的fs.s3a.impl这类参数保证 Doris 能正确读取远端数据。2.3 File Cache让“冷湖”也能跑出“热仓”的速度光有 Catalog 还不够如果你每次查湖里数据都要从对象存储全量拉取性能依然上不去。Doris 真正的加速大招是 File Cache也叫本地文件缓存。它的原理并不复杂当 Doris 第一次从外部数据源读取某个文件时会把文件内容缓存在 BEBackend节点的本地磁盘上后续再查相同的数据直接从本地读盘不再走网络。这就像你家小区门口的自提柜快递员第一次把包裹放进去要花时间但之后你随时取件都是零等待。数据湖里的热数据经过第一次查询“加热”之后后续查询延迟能下降一个数量级。File Cache 的参数配置我放在下面供参考# be.conf 关键配置 file_cache_path [{path: /data1/doris/cache, total_size: 102400000000, query_limit: 10240000000}] file_cache_keep_last_sec 2592000file_cache_path里可以配多个目录total_size表示缓存空间上限query_limit限制单个查询最多使用多少缓存空间。file_cache_keep_last_sec控制缓存保留时间默认 30 天。这个字段比较考验规划能力如果你的数据湖里表比较多、数据更新频率高缓存空间设置太小会导致频繁淘汰命中率上不去设置太大又可能挤占 BE 节点的正常数据存储空间。我的建议是最开始按 BE 磁盘总容量的 20% 到 30% 分配跑一两周看命中率再动态调整。判断缓存是否生效最直接的方法是查看 FE 的审计日志或者 BE 的监控指标。Doris 的 BE 节点会暴露doris_be_file_cache_hit_rate这个指标你可以在 Grafana 里接上监控如果命中率长期低于 60%就说明缓存配置或者查询模式可能存在问题。2.4 数据湖加速的实际案例Paimon 表查询从 30 秒降到 2 秒说个我去年做的具体优化。当时业务方有一张 Paimon 表记录用户行为事件流水按日期分区每天 1.5 亿条左右。原来用 Spark 直接查这张表做用户路径分析跑一个 7 天周期的查询大概要 30 秒到 1 分钟业务同事抱怨“点一下等半天”。我把这张表通过 Paimon Catalog 挂到 Doris 之后在 be.conf 里开了 File Cache并给 BE 节点各挂载了一块 2TB 的 SSD 作为缓存盘。第一次查询还是需要把数据从对象存储拉过来耗时 25 秒左右但同一批数据第二次再查直接命中缓存查询时间降到 2 秒以内。业务方当时都惊了觉得“换了套查询工具而已怎么快了这么多”。这里面有个容易被忽略的细节Doris 对 Paimon 的读取做了并行化优化BE 节点越多拉取文件的并发度就越高。如果你的集群规模不小建议把文件缓存和节点扩展结合使用效果会叠加。另外Doris 2.x 对 Iceberg 和 Paimon 的支持已经非常完整了包括读取 MORMerge-on-Read表时的文件合并、读取向量化列数据都不用自己额外处理。3. 实时数仓从“T1”到“T0”Doris 在实时链路上做了什么3.1 实时数仓架构演进的思路我们为什么不再分 Lambda实时数仓这个概念被讲了这么多年真正落地的架构大多还是 Lambda 架构一套实时链路走 Flink Kafka 存储一套离线链路走 Hive/Spark两套逻辑两套代码最终在查询层做合并。Lambda 架构最大的问题不是技术而是“双份代码维护”。同一个指标实时任务里写一遍离线任务里再写一遍只要两边的口径稍有不一致业务方拿到手的数字就不一样。数据团队一大半的时间都在和“为什么实时和离线差一点”作斗争。Doris 在实时链路里的定位让 Lambda 架构有了被简化的可能。它支持从 Kafka 直接消费数据写入 Doris 表也支持通过 Flink CDC 把业务库的变更实时同步过来。这样一来实时数据和离线数据可以落在同一套 Doris 表里后续的查询逻辑完全统一。没有了两套口径的问题因为实时查询和离线查询本质上是在查同一份数据区别只是数据写入的时效性不同。我个人觉得这比在架构图上画一堆 Kafka、Flink、HBase、ClickHouse 的连线要清爽得多。Doris 表面上是“少了一个存储组件”实际上是“少了一层需要人为对齐的语义”。3.2 实时写入链路Routine Load 和 Stream Load 到底怎么选Doris 提供了多种数据导入方式日常用得最多的是 Stream Load 和 Routine Load两者适用场景不同。Stream Load 是一次的、手动的导入方式适合存量数据的批量回填或者定时调度。它的本质是通过 HTTP 协议把数据文件提交给 FEFE 再把数据分发到 BE。实际使用中命令行直接 curl 是最常见的姿势curl --location-trusted -u admin:your_password \ -H label:load_label_001 \ -H column_separator:, \ -H format:csv \ -T /path/to/your/data.csv \ http://doris-fe:8030/api/your_db/your_table/_stream_loadRoutine Load 则是常驻的、流式的导入方式适合持续消费 Kafka 数据。我举个例子假如你的业务数据在 Kafka 的user_eventstopic 里想要实时导入到 Doris 的dwd_user_event表CREATE ROUTINE LOAD your_db.user_event_load ON dwd_user_event PROPERTIES ( format json, jsonpaths [\$.user_id\,\$.event_type\,\$.event_time\] ) FROM KAFKA ( kafka_broker_list kafka-1:9092,kafka-2:9092, kafka_topic user_events, property.kafka_default_offsets OFFSET_BEGINNING );这里jsonpaths指定了 JSON 数据的字段映射如果不指定Doris 会默认按字段名自动匹配。启动 Routine Load 后Doris 会持续消费 Kafka 消息近实时地写入数据表。延迟通常在秒级到十几秒之间取决于你的 Kafka 分区数、Doris 导入并发度和单条消息的大小。两类导入方式的选择逻辑很直观链路持续在跑、数据源是消息队列选 Routine Load离线文件调度或者一次性大批量导入选 Stream Load。后面如果业务要求 Exactly-Once 语义可以在 Routine Load 开启enable_upsert配合 Unique 模型实现幂等写入。3.3 Unique 模型与 Aggregated 模型实时数仓的底层表结构设计Doris 表模型直接决定了实时数仓能不能按预期工作。最常用的是 Unique 模型和 Aggregate 模型。Unique 模型适合记录明细数据它保证同一主键下只有一条数据。比如订单表每天订单状态会变更你希望实时同步 MySQL 的变更到 Doris就建 Unique 模型表CREATE TABLE dwd_order ( order_id BIGINT, user_id BIGINT, status VARCHAR(20), update_time DATETIME ) UNIQUE KEY(order_id) DISTRIBUTED BY HASH(order_id) BUCKETS 16 PROPERTIES (replication_num 2);写入时如果有相同order_id的新数据Doris 会按版本覆盖旧数据。这里需要注意版本机制Doris 的 Unique 模型默认用“后写入覆盖先写入”的规则但实时场景下消息乱序很常见。如果你发现订单状态被旧数据覆盖了可以在表属性里开启function_column.sequence_type DATETIME指定用业务时间字段来作为版本比较依据而不是导入时间。Aggregate 模型则适合预聚合的报表场景。比如你要实时统计每个商品类目的 GMV就可以建一张以类目为维度、GMV 为 SUM 指标的表。Doris 在导入数据时就会按照维度做增量聚合查询时不需要再实时算 sum速度非常快。我流量低谷期的时候经常用 Aggregate 模型把 Flink 算好的分钟级聚合结果直接灌进 Doris查询端秒出结果压力很小。3.4 实时数仓的一个典型链路配置我把一套完整的实时数仓链路写出来大家可以直接参照这套结构来做设计业务库(MySQL) --Flink CDC-- Kafka --Routine Load-- Doris(DWD明细表) Kafka 用户行为日志 --Routine Load-- Doris(DWD事件表) Doris(DWD表) --异步物化视图/定时任务-- Doris(ADS表)在这个链路里Flink CDC 负责把 MySQL 的 binlog 实时捕获并写入 KafkaDoris 用 Routine Load 消费 Kafka 数据写入明细表。Doris 内部再用物化视图或者定时任务做轻量级的汇总计算直接产出报表数据。这套方案的优点是组件少链路短出问题的概率低。之前团队里 Flink 任务出错导致数据积压恢复要重启任务、追 offset现在把一部分计算下推到 Doris 之后Flink 只做纯同步挂了重启也很快Doris 这边只要把延迟追平就能恢复一致。4. 统一查询层让湖、仓、实时数据用一套 SQL 口径说话4.1 统一查询层到底在“统一”什么很多公司在建设数据平台时都会遇到一个很尴尬的场景数仓的数据在 Hive/Spark 里实时数据在 ClickHouse 里线上业务库在 MySQL 里数据湖又在 Iceberg 上。每个系统都有自己的表结构、权限体系和 SQL 方言业务方想综合查询时怎么办要么数据团队提前把数据同步到同一个系统要么业务自己写多个查询再在应用层合并。两种方式都又慢又容易出错。统一查询层Unified Query Layer解决的就是这个问题把多个数据源接入同一个 SQL 查询入口让用户感觉不到底层数据的物理位置用一套语法把分布在多个系统的数据关联起来。Doris 的 Multi-Catalog 功能加上外部表能力让它可以承担这个统一查询层的角色。它不仅能查数据湖里的表还能直接映射 MySQL、PostgreSQL、Elasticsearch 等外部数据源。查询的时候你可以在 Doris 里直接写一条跨源 joinSELECT u.user_id, u.user_name, e.event_type, e.event_time FROM doris_dim.user u JOIN external_es.es_log e ON u.user_id e.user_id WHERE e.event_time 2024-01-01 AND u.user_level VIP;这条 SQL 里doris_dim.user是 Doris 内部的维表external_es.es_log是对接 Elasticsearch 的外部表。Doris 会分别从 Doris 存储和 ES 索引里拉取数据在本地完成 join。用户完全不需要关心数据从哪来只管提交 SQL。4.2 在 Doris 里映射 MySQL 和 ES 外部表外部表的创建语法和 Catalog 有相似之处但作用范围更小、更聚焦。以 MySQL 外部表为例你可以直接查询甚至写入远端的 MySQL 库表CREATE EXTERNAL TABLE ext_mysql_order ( order_id BIGINT, amount DECIMAL(12, 2), status VARCHAR(20) ) ENGINEmysql PROPERTIES ( host mysql-host, port 3306, user read_user, password password, database app_db, table orders );ES 外部表也是这样只是在 ENGINE 上指定elasticsearch并配置index名称和nodes的地址。创建好之后你就可以在 Doris 里直接对 ES 里的索引跑聚合查询。这里有个小经验ES 外部表的查询下推能力受限于 ES 本身建议把过滤条件下推到 Doris 端尽可能减少从 ES 拉取的文档数量。比如先按事件时间过滤、只取需要的字段再交给 Doris 做 join 和聚合性能会好很多。4.3 跨源查询的性能陷阱与规避方案统一查询层听起来很美实际落地时最容易被坑的是性能。我整理几个典型的坑和对应的规避方案。第一小表join大表时不要全量搬运。Doris 跨源 join 时小表可以加载到内存做 broadcast join但大表如果也全量拉取查询基本就垮了。合理的做法是先用子查询把大表的数据裁剪到最小范围再参与 join。第二外部表的谓词下推能力参差不齐。并不是所有外部数据源都能很好地接收你 SQL 里的过滤条件不同数据源暴露给 Doris 的下推接口差别很大。比如 PostgreSQL 支持得很完整但 ES 的某些复杂条件就会退化成全量扫描。排查方法也很简单用EXPLAIN看执行计划如果发现本该过滤的谓词没出现在 scan 节点上就说明下推没生效需要调整 SQL 写法。第三外部表统计信息缺失导致优化器误判。Doris 的查询优化器依赖表的行数、列基数等统计信息来选最优执行计划。外部表如果没有手动收集统计信息优化器可能选一个非常差的 join 顺序导致查询耗时爆炸。解决办法是对外部表执行ANALYZE TABLE或者手动设置row_count属性让优化器有据可依。5. 高性能背后的核心支撑Doris 的存储引擎和查询引擎是怎么协同的5.1 列式存储 向量化执行Doris 性能的两大底座Doris 能在统一查询层的位置上把性能做上去底层靠的是列式存储和向量化执行引擎的组合。列式存储意味着数据按列存放查询只需要读取涉及的列。比如一张 100 列的表业务只查其中 5 列Doris 就只扫描这 5 列的数据文件I/O 消费直接砍掉 95%。这和数据湖里的 Parquet/ORC 列存思路一致Doris 的独特之处在于它把列存和自身的高性能写入链路做了深度集成实时导入的数据会直接落成列存文件不需要额外的转换过程。向量化执行引擎理解起来更简单传统的逐行执行模式每条记录都要走一遍函数调用、类型判断CPU 利用率很低向量化执行把一批数据比如 1024 行作为整体处理一次性对整批数据做运算充分发挥 CPU 的 SIMD 指令集能力计算吞吐大幅提升。Doris 从 1.2 开始默认开启向量化执行很多查询性能相比之前提升了好几倍靠的就是这个底层优化。5.2 数据模型的灵活性在生产中的实际价值Doris 支持 Duplicate、Aggregate、Unique 三种数据模型这个设计在实际生产中特别有用。Duplicate 模型适合日志明细不区分主键所有行都保留Aggregate 模型适合指标汇总导入时自动聚合Unique 模型适合主键更新应对业务库同步场景。三种模型可以在同一个集群里共存根据每张表的业务特点灵活选择。这就比单一模型系统强很多——你不用因为一张表要更新主键就把整集群设计成 Unique 模型也不用因为报表聚合需求就把所有表都搞成 Aggregate 模型。每种模型在底层会用不同的文件合并策略和索引结构Doris 表的 schema 设计自由度很高。5.3 物化视图统一查询层里的“预计算加速器”统一查询层把多源数据拉进来了但如果每次查询都要现场聚合海量明细数据响应时间还是控制不下来。物化视图是解决这个问题的关键手段。Doris 提供同步物化视图和异步物化视图两种。同步物化视图简称为 Rollup它在数据导入时自动维护对写入链路透明查询时优化器自动选择最合适的物化视图。异步物化视图类似传统数仓的预计算表可以通过调度任务定时刷新。我举个例子一张日增量千万级的用户行为明细表业务方频繁要按天、按小时统计 PV/UV。如果你在 Doris 里建一个按小时维度聚合的异步物化视图CREATE MATERIALIZED VIEW mv_user_event_hourly BUILD IMMEDIATE REFRESH AUTO AS SELECT event_date, event_hour, COUNT(*) AS pv, COUNT(DISTINCT user_id) AS uv FROM dwd_user_event GROUP BY event_date, event_hour;之后业务查询如果恰好命中这个维度优化器会直接读取物化视图的结果而不是重新扫明细表。我实测下来这种场景查询延迟能从十几秒降到几百毫秒。物化视图是 Doris 统一查询层实现“加速”的一个重要手段在运维上也比在应用层做 cache 简单得多。6. 常见问题与排查技巧实录我在生产环境踩过的坑6.1 数据湖查询缓存命中率低现象File Cache 配置了但查询还是很慢监控里doris_be_file_cache_hit_rate一直在 30% 以下。排查思路先看缓存空间是否足够。如果total_size设置得太小缓存被频繁淘汰命中率自然低。我调过一次从 100GB 调到 1TB命中率立刻上来了。再看 BE 的缓存目录是否落在机械硬盘上。SSD 和 HDD 的随机读性能差距很大缓存盘必须是 SSD 或者 NVMe。最后看查询模式如果业务大量使用SELECT *不带过滤条件每次扫的数据范围都不同缓存基本就是摆设。这种场景下要先优化 SQL 加过滤条件再指望缓存。6.2 Routine Load 消费延迟累积现象业务反馈实时报表延迟从秒级变成分钟级Routine Load 任务的LAG指标持续增大。排查步骤先看 Kafka 侧消费组是否有其他消费者占用分区再看 Doris 导入任务的并发度和批次大小。有时候是max_batch_interval设置得太长比如默认 10 秒一个批次如果单批数据量也不大导入吞吐上不去。可以调小max_batch_interval到 5 秒并把desire_task_concurrent_num提高。还要注意 BE 的 CPU 使用率如果数据导入把 CPU 打满了查询性能也会被拖累。6.3 大查询把 BE 内存打爆现象某个大查询把 BE 进程搞 OOM 了整个集群查询都变慢。排查思路Doris 的 BE 内存管理比较灵活但大查询会申请大量内存做 hash join 或聚合。建议在 FE 设置查询内存上限比如exec_mem_limit默认 8GB改成 4GB 或者更低防止单片查询过度占用资源。同时打开查询队列和资源组功能给不同业务设置不同的并发上限。Doris 2.x 的资源组功能很强大可以按查询用户或者标签把 CPU、内存、并发数隔离开。6.4 “SQL 能查到数但哪张表是哪个 Catalog 的搞混了”现象时间久了Catalog 多了业务同事不知道去哪张表查数。解决办法是给 Catalog 命名加上清晰的前缀比如hive_prod、iceberg_analytics、mysql_app并在创建 Catalog 时写好 COMMENT 说明用途。另外可以利用 Doris 的权限体系给不同团队分配不同的库级权限避免互相干扰。6.5 物化视图刷新不及时报表数据偏旧现象异步物化视图设置了REFRESH AUTO但刷新周期不可控报表数据出现延迟。解决办法不要把重要报表完全依赖自动刷新建议手动指定刷新周期。比如CREATE MATERIALIZED VIEW mv_report_daily BUILD IMMEDIATE REFRESH AUTO AS SELECT ...; REFRESH MATERIALIZED VIEW mv_report_daily;或者设置REFRESH COMPLETE和明确的 cron 调度比如每天凌晨两点刷新一次。这样数据更新的时点清晰可控运维排查也方便。7. 一些关于选型和落地的个人思考这篇文章写到这里基础的内容和配置思路都覆盖得差不多了。最后再分享一点我个人在多个项目里使用 Doris 的体会。数据架构里的组件从来不是越多越好而是越“贴”业务越好。Doris 摊开来看很多能力单拎出来都不是行业里最顶尖的论数据湖读取它没有 Trino 那么全面的连接器生态论实时写入它没有 ClickHouse 那种极端的高吞吐论数据湖管理它本身也不是一个湖存储。但它真正擅长的是把这些能力收拢在一套系统里让数据团队不用在多个系统之间来回倒腾数据让业务方用一套 SQL 口径就能拿到一致的结果。这个“减少问题的能力”在真实的生产环境里比单个指标跑多快更有价值。如果你现在的架构已经被“多套存储 多套引擎 无数数据拷贝”折磨得心力交瘁我建议可以分三步来验证 Doris 是否适合你先拿一两个高频查询的数据湖表接入 Doris 做加速跑两周观察性能和缓存命中率再建一套 Routine Load 消费真实 Kafka 数据流做一张实时明细表最后把常用的报表查询切到 Doris 的物化视图上看能不能真正取代原来的预计算任务。三步走完你心里基本就有答案了。数据架构没有银弹Doris 也一样。它把复杂留给了自己把简单交还给了使用者——这也是我越来越愿意在生产环境里推它的原因。祝大家在选型和落地的路上少踩坑多跑赢。
