Doris统一OLAP架构解析:从MPP+LSM-Tree原理到实时数仓实战 1. 从“大杂烩”到“统一分析”为什么我们需要Doris如果你在数据团队待过几年肯定经历过这样的场景业务部门要一个实时看板你吭哧吭哧用FlinkKafkaClickHouse搭了一套好不容易上线了第二天他们又说要跑一个复杂的多表关联历史分析ClickHouse扛不住你又得去折腾Hive或者Spark SQL。等到报表需求稳定了产品经理跑过来说能不能支持高并发的点查询给用户做实时画像你看着手头这一堆“烟囱式”的系统心里只有一个想法这日子什么时候是个头这就是过去几年很多公司数据架构的真实写照。OLAP联机分析处理领域长期处于“一个场景一个引擎”的碎片化状态。ClickHouse擅长单表极速查询但对多表Join和更新支持弱Kylin预计算快但模型僵化灵活性差传统MPP数据库扩展性和成本又是问题。数据工程师和架构师们不得不像“救火队员”一样在各种引擎间做权衡、做集成、做数据同步维护成本高数据一致性也难以保证。而Doris的出现正是在尝试解决这个核心痛点。我第一次接触Doris当时还叫Palo大概是在2018年它给我的第一印象是“野心不小”——它想用一个系统同时搞定实时数据更新、交互式即席查询、离线批量分析和高并发点查。这听起来有点像“既要、又要、还要”但在深入使用和参与了几个大型项目后我发现它并不是空谈。它通过一套融合的架构设计确实在相当多的场景下把我们从多引擎运维的泥潭里拉了出来。简单来说Doris的目标是成为大数据分析领域的“瑞士军刀”一把工具应对多种分析任务降低技术栈的复杂度。对于追求敏捷和效率的现代数据团队来说这种“统一分析”的价值远比某个单点性能指标提升10%更有吸引力。2. Doris核心架构解析如何做到“多面手”Doris能做到“多面手”其根本在于它独特的设计哲学和架构实现。它不是简单地把几个引擎拼在一起而是从存储和计算两个层面进行了深度整合。2.1 融合架构MPP与LSM-Tree的巧妙结合Doris的底层架构可以概括为“MPP计算框架 类LSM-Tree的存储引擎”。这个组合是它能力的基石。MPP大规模并行处理大家应该不陌生它意味着查询任务被拆分成多个子任务在多个节点上并行执行最后汇总结果。这保证了Doris在处理海量数据扫描和聚合时能充分利用集群资源获得线性的性能扩展。这是它胜任交互式即席查询和离线批量分析的基础。更有意思的是它的存储引擎。Doris没有采用ClickHouse的MergeTree系列引擎而是自研了一套存储引擎其核心思想借鉴了LSM-Tree日志结构合并树。LSM-Tree是HBase、Cassandra等NoSQL数据库的核心以出色的写入性能闻名。Doris对其进行了改造使其适应分析型场景。数据写入Doris时会先写入内存缓冲区MemTable写满后刷到磁盘上形成一个不可变的数据段Segment。后台有专门的合并线程定期将多个小的Segment合并成大的Segment。这个过程有几个关键优势高吞吐写入写入几乎全是顺序I/O写内存、刷盘避免了传统B树随机写带来的磁盘寻址开销非常适合日志、埋点等流式数据的实时摄入。高效的批量删除与更新Doris通过“删除标记Delete Vector”和“部分列更新”来实现。对于删除它不会立即物理删除数据而是记录一个标记在后续查询或合并时过滤。对于更新它支持只更新指定列而不是整行重写。这比一些只能追加或整行更新的分析引擎要灵活得多。这种存储设计让Doris在保持高吞吐数据摄入能力的同时还能支持一定的数据更新操作为其承接实时数仓场景打下了基础。2.2 数据组织分区、分桶与物化视图理解了底层存储我们再看Doris如何组织数据这直接决定了查询效率。分区Partitioning和大多数数据库一样Doris支持按时间如PARTITION BY RANGE(dt)或枚举值进行分区。分区是粗粒度的数据管理单元主要用于数据生命周期管理TTL、批量数据操作如删除旧分区和查询时的分区裁剪。例如查询最近7天的数据优化器可以只扫描对应的7个分区极大减少数据读取量。分桶Bucketing这是Doris查询性能的关键。在分区内数据会进一步被水平切分成多个桶Bucket。分桶的规则是基于分桶键一个或多个列的Hash值。数据写入时同一分桶键的数据会落在同一个桶内。分桶是数据在集群中物理分布的最小单元也是数据并行处理和数据均衡的基本单位。分桶键的选择至关重要它应该选择查询中最常作为过滤条件WHERE或关联条件JOIN的列。例如如果大量查询以user_id为条件那么用user_id做分桶键就能保证相同user_id的数据在同一个桶内。这样进行user_id上的等值过滤或JOIN时可以极大减少数据在节点间的网络传输即避免Shuffle实现本地化计算性能提升显著。分桶数量建议每个表的分桶数量是集群节点数量的整数倍以保证数据均匀分布。通常单个桶的数据量建议在100MB到1GB之间。物化视图Materialized View这是Doris应对复杂聚合查询和加速固定模式查询的“王牌”。物化视图本质上是一张预计算好的表。Doris支持在基础表上创建多种物化视图如聚合视图、连接视图等查询时优化器会自动判断是否能够路由到更合适的物化视图上对用户完全透明。注意物化视图虽然好但它是“空间换时间”的典型。它会占用额外的存储空间并在数据导入时增加计算开销。因此它更适合查询模式固定、且查询性能瓶颈明显的场景。不要为了用而用。2.3 计算模型向量化与查询优化在计算层Doris全面采用了向量化执行引擎。传统的行式执行引擎一次处理一行数据CPU缓存利用率低现代CPU的SIMD指令集也无法发挥。向量化引擎一次处理一批数据比如1024行在内存中以列式块Column Block的形式组织使得更好的CPU缓存局部性。能够利用SIMD指令进行并行计算如一次对1024个数值进行加法。减少了虚函数调用等开销。这使得Doris在CPU密集型的扫描和聚合运算上效率比传统行存引擎有数量级的提升。此外Doris的查询优化器也在不断进化支持基于成本的优化CBO能够根据数据统计信息如行数、NDV、数据分布选择最优的执行计划包括Join顺序、是否启用本地Shuffle等。3. 从零到一Doris核心功能实操指南理论讲再多不如动手试一下。我们以一个典型的用户行为日志分析场景为例看看如何从零开始使用Doris。3.1 环境准备与集群部署部署是第一步。Doris提供多种部署方式对于生产环境我强烈推荐使用手动部署而非Docker以便更好地控制资源和管理生命周期。硬件建议FEFrontend节点负责元数据管理、查询解析与规划。需要较好的CPU和内存。建议至少2核4GB生产环境建议3个或以上奇数个组成高可用。磁盘不需要很大但需要稳定SSD最佳。BEBackend节点负责数据存储和查询执行。这是资源消耗大户。需要大量的CPU、内存和磁盘I/O。内存建议64GB起步磁盘建议使用高性能SSD或NVMe网络建议万兆。BE节点可以水平扩展。部署步骤精要下载与解压从Apache官网下载最新稳定版二进制包解压到所有节点的相同目录例如/opt/doris。配置FE修改fe/conf/fe.conf。关键配置# 元数据目录确保有足够空间和权限 meta_dir /path/to/doris-meta # 绑定IP改为节点内网IP priority_networks 192.168.1.0/24 # JVM参数根据内存调整 JAVA_OPTS -Xmx4096m -Xms4096m启动第一个FE./bin/start_fe.sh --daemon。通过MySQL客户端连接端口9030并初始化集群ALTER SYSTEM ADD FOLLOWER fe_host:9010;添加其他FE。配置BE修改be/conf/be.conf。# 数据存储目录可配置多个用分号隔开 storage_root_path /path1/to/storage;/path2/to/storage # 绑定IP priority_networks 192.168.1.0/24 # 其他资源限制 mem_limit 80% # BE进程内存上限建议物理内存的80%启动BE./bin/start_be.sh --daemon。在FE节点上通过MySQL客户端将BE加入集群ALTER SYSTEM ADD BACKEND be_host:9050;。验证通过SHOW PROC /frontends;和SHOW PROC /backends;查看FE/BE状态确保Alive列为true。实操心得部署时最容易出问题的是网络和端口。务必确保所有节点间的9030, 9010, 9050, 9060, 8040等端口互通。建议先用telnet命令逐一测试。另外生产环境一定要部署多个FE至少3个以实现高可用避免单点故障导致整个集群不可用。3.2 数据建模与表创建实战假设我们要分析用户页面访问日志表名为user_page_views。建表语句是发挥Doris性能的关键。CREATE TABLE IF NOT EXISTS example_db.user_page_views ( dt DATE NOT NULL COMMENT 数据分区-天, event_time DATETIME NOT NULL COMMENT 事件时间, user_id BIGINT NOT NULL COMMENT 用户ID, page_id VARCHAR(256) NOT NULL COMMENT 页面ID, device_type VARCHAR(32) COMMENT 设备类型, province VARCHAR(32) COMMENT 省份, duration INT COMMENT 停留时长(秒), event_type VARCHAR(32) COMMENT 事件类型如click, view ) -- 1. 分区按天分区便于管理历史数据 PARTITION BY RANGE(dt) ( PARTITION p202401 VALUES [(2024-01-01), (2024-02-01)), PARTITION p202402 VALUES [(2024-02-01), (2024-03-01)), PARTITION p202403 VALUES [(2024-03-01), (2024-04-01)) ) -- 2. 分桶这是性能核心我们选择最常查询的user_id作为分桶键。 -- 假设我们计划有16个BE节点每个节点初始4个桶总桶数64。 DISTRIBUTED BY HASH(user_id) BUCKETS 64 -- 3. 表属性 PROPERTIES ( replication_num 3, -- 副本数通常设为3保证高可用 storage_medium SSD, -- 存储介质SSD或HDD dynamic_partition.enable true, -- 启用动态分区自动创建新分区 dynamic_partition.time_unit DAY, dynamic_partition.start -30, -- 保留最近30天分区 dynamic_partition.end 3, -- 提前创建未来3天的分区 dynamic_partition.prefix p, dynamic_partition.buckets 64 -- 动态分区的分桶数 );关键点解析分区键dt我们选择了事件日期这是最常见的时间维度。通过动态分区属性我们无需手动管理每天分区的创建和删除Doris会自动维护最近30天到未来3天的分区非常省心。分桶键user_id这是经过深思熟虑的。我们的业务查询很可能围绕用户展开例如“查询某个用户最近的行为”、“计算用户活跃度”。以user_id分桶能保证同一个用户的数据尽可能落在同一个BE节点上在进行user_id过滤或GROUP BY user_id时能最大程度避免数据跨节点传输实现本地聚合。分桶数64这是一个经验值。我们假设未来集群有16个BE每个BE承载4个桶负载比较均衡。单个桶的数据量会随着数据增长而变大后续如果单个桶超过10GB可以考虑增加分桶数Doris支持在线增加分桶数但减少比较麻烦所以初期可以稍微设大一点。副本数3这是生产环境的标配。数据会在不同BE上存储3份即使同时坏掉2个节点数据依然可用查询也可以由剩下的副本提供服务。3.3 数据导入多种方式的选择与配置数据进来了模型才有价值。Doris支持丰富的数据导入方式我们需要根据数据源和时效性要求来选择。1. 批量导入Broker Load适用于从HDFS、S3等外部存储系统导入TB/PB级历史数据。LOAD LABEL example_db.label_20240328_01 ( DATA INFILE(hdfs://namenode:8020/path/to/log/dt2024-03-28/*.parquet) INTO TABLE user_page_views FORMAT AS parquet (dt, event_time, user_id, page_id, device_type, province, duration, event_type) ) WITH BROKER hdfs_broker PROPERTIES ( timeout 3600, max_filter_ratio 0.1 );Broker Load是异步作业提交后可以通过SHOW LOAD WHERE LABEL label_20240328_01;查看状态。它利用集群资源进行分布式读取和导入效率非常高。2. 流式导入Routine Load这是实现实时数仓的关键。它持续消费Kafka中的消息并导入Doris。CREATE ROUTINE LOAD example_db.kafka_load_job ON user_page_views COLUMNS(dt, event_time, user_id, page_id, device_type, province, duration, event_type) PROPERTIES ( desired_concurrent_number 5, -- 并发任务数根据BE数量和Kafka分区数调整 max_batch_interval 10, -- 最大间隔10秒提交一批 max_batch_rows 200000, -- 每批最多20万行 max_batch_size 104857600 -- 每批最大100MB ) FROM KAFKA ( kafka_broker_list broker1:9092,broker2:9092, kafka_topic user_page_view_topic, property.group.id doris_consumer_group, property.security.protocol SASL_PLAINTEXT, -- ... 其他Kafka认证配置 );创建成功后Doris会自动启动多个子任务从Kafka拉取数据。你可以通过SHOW ROUTINE LOAD\G监控消费进度和错误信息。这是将Doris作为实时查询层最常用的方式。3. 实时插入INSERT INTO适用于小批量、低延迟的插入或者从其他Doris表导入数据。INSERT INTO user_page_views VALUES (2024-03-28, 2024-03-28 10:00:00, 10001, home, iOS, 北京, 30, view), (2024-03-28, 2024-03-28 10:00:01, 10002, product, Android, 上海, 45, click);注意事项INSERT INTO是同步操作且每次都会产生一个新的数据版本频繁的小批量插入会产生大量小文件影响查询性能。绝对不要用它在生产环境进行高频的单条或少量数据插入那是OLTP数据库的用法。对于实时流请务必使用Routine Load。3.4 查询优化与物化视图应用表建好了数据也进来了我们来看看如何高效查询。基础查询示例-- 查询当天PV/UV SELECT dt, COUNT(*) as pv, COUNT(DISTINCT user_id) as uv FROM user_page_views WHERE dt 2024-03-28 GROUP BY dt; -- 查询用户行为路径利用窗口函数 SELECT user_id, page_id, event_time, LAG(page_id) OVER (PARTITION BY user_id ORDER BY event_time) as last_page FROM user_page_views WHERE dt 2024-03-27 ORDER BY user_id, event_time; -- 多表关联假设我们有一张用户维度表user_profile SELECT u.province, p.page_id, COUNT(*) as visit_count FROM user_page_views p JOIN user_profile u ON p.user_id u.user_id WHERE p.dt 2024-03-28 AND u.city 杭州 GROUP BY u.province, p.page_id ORDER BY visit_count DESC LIMIT 10;当发现某些聚合查询如按province和device_type统计UV频繁执行且较慢时就是物化视图出场的时候了。创建物化视图CREATE MATERIALIZED VIEW province_device_uv_mv AS SELECT dt, province, device_type, COUNT(DISTINCT user_id) as uv FROM user_page_views GROUP BY dt, province, device_type;创建完成后Doris会自动异步构建这个物化视图。之后所有符合该聚合模式的查询优化器都会自动将查询重写到物化视图上。例如执行SELECT dt, province, COUNT(DISTINCT user_id) FROM user_page_views WHERE dt2024-03-28 GROUP BY dt, province;虽然查询没用到device_type但因为物化视图province_device_uv_mv的数据粒度更细包含device_type优化器依然能利用它来加速只需在物化视图结果上再做一次聚合即可速度会快很多。实操心得物化视图不是银弹。创建前一定要用EXPLAIN命令查看原始查询的执行计划确认瓶颈确实在聚合计算上。同时要监控物化视图的存储开销和导入延迟。对于维度组合非常多高基数的列创建物化视图可能导致“维度爆炸”存储暴增需要谨慎评估。4. 生产环境运维与常见问题排查将Doris用于生产除了功能稳定性和可运维性同样重要。下面分享一些实战中积累的经验和常见问题的排查思路。4.1 集群监控与告警没有监控的系统就是在“裸奔”。Doris提供了丰富的监控指标主要通过Frontend的Web UI端口8030和Prometheus导出。关键监控项集群健康度所有FE/BE节点是否Alive。查询统计fe_query_latency查询延迟、fe_qps、fe_query_err_rate错误率。关注P95/P99延迟。导入监控routine_load_rowsRoutine Load导入行数、load_finish导入作业成功率。BE节点资源be_mem_usage内存使用率、be_disk_usage磁盘使用率、be_cpu_usage。内存是BE最关键的资源长时间超过90%需要警惕。Compaction状态be_base_compaction_score和be_cumulative_compaction_score。这个分数如果持续很高比如超过1000说明数据合并Compaction跟不上写入速度会导致查询变慢。这是需要重点关注的“健康指标”。建议将上述指标接入到Prometheus Grafana中并设置告警规则例如BE节点宕机、内存使用率85%、Compaction Score500持续10分钟等。4.2 常见问题与解决方案实录这里记录了几个我踩过的坑和解决方案。问题一查询突然变慢SHOW PROC /backends发现某个BE的LastStartTime很近且TabletNum远少于其他BE。排查这通常是某个BE节点宕机后重启或者因网络问题被FE判定为宕机后其上的数据副本开始在其他BE上修复。修复期间该BE没有数据查询负载会倾斜到其他BE。同时数据修复本身会消耗大量网络和磁盘I/O影响集群整体性能。解决首先检查该BE节点的日志be/log/be.INFO看是否有OOM内存溢出或磁盘错误的记录。如果节点已恢复等待其数据副本自动修复完成。可以通过SHOW PROC /backends查看TabletNum是否逐渐接近其他节点。如果修复速度太慢可以适当调大BE配置中的clone_task_thread_num和clone_worker_count但要注意对正常查询的影响。根本预防确保服务器硬件特别是内存和磁盘稳定网络延迟低且无丢包。设置合理的JVM参数防止OOM。问题二数据导入尤其是Routine Load出现“-235”错误Data quality not good enough。排查这个错误意味着导入的数据因某些原因如类型转换失败、数据格式不符、列数不匹配被过滤的行数超过了max_filter_ratio默认0即不允许任何错误。使用SHOW ROUTINE LOAD WHERE namejob_name\G查看错误样例。解决检查数据源最常见的原因是Kafka中的JSON数据格式与表结构不匹配或者存在NULL值传入了非空列。仔细对比Kafka消息体和表结构。调整导入容错如果确认少数脏数据可以忽略可以在创建Routine Load时设置max_filter_ratio 0.1允许10%的错误率。使用严格模式对于要求精确的场景可以设置strict_mode true但这样遇到任何错误整个批次都会失败。预处理数据最可靠的方法是在数据进入Kafka之前或者在Doris外部如使用Flink进行清洗和格式化。问题三磁盘空间增长过快或者Compaction Score持续高位。排查这通常是写入量过大或者数据模型如分桶数设置不合理导致产生大量小文件Segment。每个Segment都会占用元数据内存且过多的Segment会严重影响Compaction效率和查询性能。解决检查分桶数通过SHOW PARTITIONS FROM table_name;查看每个分区下每个桶的数据量。如果单个桶数据量很小如小于100MB但Segment数量很多说明分桶数可能过多了。可以考虑对新建分区减少分桶数历史分区无法直接修改。调整Compaction参数可以尝试调优BE的Compaction参数如增加cumulative_compaction_num_threads_per_disk和base_compaction_num_threads_per_disk来加快合并速度。但这是治标需监控CPU和I/O。优化导入频率对于Stream Load或Routine Load适当调大max_batch_interval和max_batch_size/rows让每批次导入的数据量更大减少小批次产生的Segment数量。定期清理对于明确不再查询的历史冷数据使用ALTER TABLE table_name DROP PARTITION partition_name;删除整个分区这是最直接的释放空间方式。问题四高并发点查询如根据user_id查最新行为响应不稳定。排查Doris虽然擅长分析但也支持点查。点查性能取决于能否利用前缀索引Short Key Index和是否触发谓词下推。使用EXPLAIN查看查询计划确认是否使用了PREDICATES谓词下推。解决优化前缀索引Doris表的排序键默认是建表语句中前36个字节的列会构建前缀索引。确保点查的过滤条件列如user_id在排序键中尽可能靠前。使用物化视图为高频点查场景创建只包含关键列的物化视图甚至可以是DUPLICATE模型不聚合专门服务这类查询。增加查询缓存Doris支持结果缓存针对完全相同的SQL和分区缓存。对于极少变动的维度表查询可以尝试启用。连接池与负载均衡在应用层使用连接池避免频繁建立连接开销。对多个FE节点配置负载均衡分散查询压力。4.3 性能调优 checklist在系统上线前或遇到性能问题时可以按以下清单自查检查项目标/建议检查命令/方法分桶键选择是否为高频过滤或JOIN的列分析业务SQL的WHERE和JOIN条件分桶数量是否约为BE节点数的整数倍单个桶数据量是否在100MB-1GBSHOW PARTITIONS FROM tbl;计算DataSize/BucketNum前缀索引点查条件列是否在排序键前36字节内查看建表语句的前几列Compaction健康Score是否长期处于低位100SHOW PROC /backends\G查看CompactionScore内存使用BE内存使用率是否长期低于80%监控be_mem_usage指标查询计划复杂查询是否使用了本地Shuffle或正确的Join顺序对慢SQL使用EXPLAIN或EXPLAIN GRAPH查看数据倾斜各BE节点磁盘使用率和Tablet数量是否均衡SHOW PROC /backends;对比DataUsedCapacity和TabletNum5. 选型思考Doris vs. 其他主流OLAP引擎最后我们来聊聊实际选型。Doris不是万能的清楚它的边界才能用好它。我将它和几个常见的对手做个对比这源于我们团队在多个项目中的实际测试和选型评估。Doris vs. ClickHouseDoris优势真正的实时更新支持Update/Delete、更好的多表关联能力、更友好的SQL兼容性对MySQL协议支持极好、更完善的管理功能Web UI、统一的用户权限。在需要实时维表关联、数据有更新需求的场景如电商订单状态更新Doris是更自然的选择。ClickHouse优势极致单表查询性能、在宽表、大聚合场景下速度往往更快更强的数据压缩能力社区生态在某些特定领域如向量计算更活跃。如果你的场景是海量日志分析且数据模型是纯粹的追加写入宽表查询模式固定且复杂ClickHouse可能更有优势。选型建议需要实时更新和复杂关联 -Doris。单表海量数据扫描聚合查询模式固定 -ClickHouse。Doris vs. StarRocks这里需要说明StarRocks是Doris的一个分支两者同根同源。目前StarRocks在商业化推进、云原生部署、以及某些企业级功能如更完善的资源隔离、存算分离架构上更为激进。社区原版的Apache Doris则更遵循Apache基金会的开源治理模式。功能上两者高度重叠具体选型可能更多考虑社区生态、商业支持需求和技术栈匹配度。Doris vs. 云数仓如Snowflake, BigQueryDoris优势成本可控自建集群硬件成本通常低于云数仓的按扫描/存储付费模式尤其对于稳定且大量的查询负载数据自主可控深度定制化能力更强。云数仓优势免运维弹性伸缩极致简单生态集成好与云上其他服务无缝对接按需付费对于波动大的负载可能更划算。选型建议有强运维团队追求极致成本和可控性业务模型稳定 -自建Doris。追求快速启动、零运维业务变化快且预算充足 -云数仓。从我个人的经验来看Doris最适合的角色是作为公司内部的“实时与离线融合的统一分析层”。它下游对接Kafka、Flink等实时流以及Hive/S3上的离线数据上游服务BI报表、即席查询、数据服务接口等多种应用。它用一套系统简化了架构降低了开发和运维的复杂度这对于中型互联网公司和快速发展的业务团队来说价值巨大。当然没有完美的系统理解它的强项统一、实时、易用和弱项超大规模单表纯扫描性能可能略逊于专精引擎把它放在正确的场景里它就能成为你数据架构中最得力的核心组件之一。