1. 项目概述:从MPP数据库到现代数据仓库的演进
如果你在过去几年里关注过大数据领域,尤其是数据仓库和实时分析这个赛道,那么“Apache Doris”这个名字你一定不会陌生。它最初以“百度 Palo”的名字在内部孵化,后来开源并捐赠给了 Apache 软件基金会,最终成为了今天的 Apache Doris。简单来说,Doris 是一个基于 MPP(大规模并行处理)架构的、高性能、实时的分析型数据库。但如果你只把它理解为一个“数据库”,那就有点小看它了。在我实际的项目经历中,Doris 更像是一个“数据服务层”,它承接了从 Kafka、Flink 等流处理系统过来的实时数据,也对接了 Hive、HDFS 等离线数据湖,然后以亚秒级的响应速度,支撑着从业务报表、用户行为分析到实时大屏、即席查询等几乎所有对时效性有要求的场景。
为什么 Doris 能火起来?我觉得核心在于它在一个正确的时间点,用一套相对简单的技术栈,解决了一个非常普遍的痛点:既要实时,又要快,还要能扛住高并发。在它之前,很多团队可能用着 HBase 做点实时查询,但复杂的 SQL 支持是个问题;或者用着 Presto/Trino 做交互式分析,但实时数据导入又得折腾一套链路。Doris 试图把这两件事都做好,它内置了高效的列式存储引擎、向量化执行引擎,以及一套自己的类 SQL 查询语言,让你用 MySQL 协议就能直接连接和操作,对业务开发来说几乎没有额外的学习成本。这听起来是不是有点像 ClickHouse?没错,它们经常被拿来比较,但 Doris 在数据模型(支持聚合模型和更新模型)、物化视图的易用性以及 MySQL 协议的兼容性上,有着自己鲜明的特色,特别适合从 MySQL/OLTP 体系迁移过来的团队。
2. 核心架构与设计哲学解析
要真正用好 Doris,不能只停留在“怎么建表、怎么写 SQL”的层面,必须理解其底层的设计哲学。这决定了你在数据建模、集群规划和问题排查时的思路是否正确。
2.1 MPP 架构与数据分布策略
Doris 采用经典的 MPP 架构,这意味着查询任务会被分解成多个子任务,在多个节点上并行执行,最后汇总结果。集群中的节点主要分为两类:Frontend(FE)和 Backend(BE)。FE 负责元数据管理、查询解析与规划、集群协调;BE 负责数据存储和计算。这种分离架构的好处是显而易见的:扩展性强。当计算资源不足时,可以单独扩容 BE;当连接数或规划压力大时,可以扩容 FE。
这里有一个非常关键的设计点:数据分布。Doris 的数据表必须指定分桶(Bucket)和分区(Partition)。分区通常按时间(比如按天)划分,用于实现数据的生命周期管理(TTL)和查询时的分区裁剪。分桶则是将分区内的数据进一步打散到不同 BE 上的机制,分桶键的选择至关重要,它直接决定了数据倾斜和查询效率。
注意:分桶键的选择是建模的核心。一个常见的误区是直接使用用户ID这类高基数列。如果用户ID分布不均,会导致数据严重倾斜,某些BE负载过高。更优的做法是选择像“城市+用户ID后几位”这样的组合键,或者直接使用“随机分桶”。但随机分桶在涉及分桶键的等值或范围查询时,无法进行桶裁剪,会损失一部分性能。这需要根据查询模式做权衡。
2.2 数据模型:聚合、唯一与重复
Doris 提供了三种数据模型,这是它区别于很多同类产品的亮点,也直接对应了不同的业务场景。
聚合模型(Aggregate Model):这是为预聚合场景设计的。比如你有一张表,字段是
用户ID, 日期, 城市, 访问次数, 消费金额。你可以将用户ID, 日期, 城市设为聚合键(Unique Key),将访问次数和消费金额设为 SUM 聚合类型。当导入相同聚合键的数据时,Doris 会自动在后台进行 SUM 操作。这非常适合报表类场景,数据在入库时即完成聚合,查询速度极快。但缺点是,你无法查询到原始的、未聚合的明细数据。唯一模型(Unique Model):可以理解为聚合模型的一个特例,它只为每个唯一键保留最新版本的数据。这本质上实现了主键更新的能力。比如用户属性表,以
用户ID为唯一键,后续导入的新数据会自动覆盖旧数据。这在承接 CDC(Change Data Capture)数据,构建实时用户维度表时非常有用。重复模型(Duplicate Model):就是普通的明细表,没有唯一键约束,存储所有导入的原始数据。适合存储日志、行为流水等需要完整追溯的场景。
选择哪种模型,是 Doris 应用设计的第一个重大决策。我个人的经验是:优先考虑聚合模型,因为它的性能收益最大。只有当业务明确需要点查最新状态(如用户画像)时,才用唯一模型;只有当业务需要全量明细(如审计、溯源)时,才用重复模型。
2.3 存储引擎:列式、索引与物化视图
Doris 的存储引擎是自研的,采用列式存储。列存的好处对于分析查询不言而喻:压缩率高,IO效率高,特别适合聚合查询。在此基础上,Doris 提供了丰富的索引来加速查询:
- 前缀索引:基于表指定的排序列(默认是前36个字节)自动构建。对于等值或范围查询过滤条件包含这些前列的查询,效率极高。
- Bloom Filter 索引:适用于高基数列的等值过滤,能快速判断数据块中是否包含目标值,减少不必要的IO。
- ZoneMap 索引:自动为每列数据块记录 min/max 值,对于范围查询过滤非常有效。
除了索引,物化视图(Materialized View)是 Doris 另一个“性能大杀器”。你可以基于一张基表,创建若干张具有不同维度聚合和排序方式的物化视图。查询时,优化器会自动判断是否能够路由到某个物化视图上执行,从而避免对原始海量数据的扫描。比如,你的基表是按秒的明细,可以创建一个按小时、按省份聚合的物化视图。当查询每小时各省的统计时,Doris 会自动从这个小得多的物化视图中读取数据,查询速度可能有数量级的提升。
实操心得:物化视图虽好,但不宜滥用。每创建一个物化视图,就相当于增加了一张物理表,会占用存储空间,并在数据导入时增加计算开销。我的建议是,根据最核心、最耗时的几个查询模式,有针对性地创建物化视图。不要试图为所有可能的查询组合都创建视图。
3. 从零到一:集群部署与数据接入实战
理论讲得再多,不如动手搭一个。下面我将以一个典型的实时数仓场景为例,带你走一遍从集群部署、数据建模到数据接入的完整流程。假设我们的业务是电商,需要实时分析用户的点击流和订单数据。
3.1 集群规划与部署要点
假设我们规划一个用于生产环境的小规模集群:3个FE(1个Leader,2个Follower),5个BE。
硬件配置:
- FE:对CPU和内存要求相对较高,因为要处理查询规划和元数据。建议8核16GB内存以上。磁盘不需要很大,主要用于存储元数据镜像和日志。
- BE:这是计算和存储的主力。需要大量的CPU、内存和磁盘。建议16核64GB内存起步。磁盘推荐使用SSD,因为随机读性能对列存引擎至关重要。网络带宽要足,BE之间数据交换频繁。
部署步骤精简版:
- 环境准备:所有节点安装 JDK 8 或 11,关闭防火墙或设置好端口规则(FE: 8030, 9020, 9030; BE: 9060, 8040, 9050)。
- 下载解压:从官网下载最新稳定版二进制包,解压到
/opt/doris之类目录。 - 配置FE:修改
fe/conf/fe.conf,重点设置priority_networks(指定用于内部通信的IP段)和meta_dir(元数据路径)。启动第一个FE:./bin/start_fe.sh --daemon。 - 通过MySQL客户端连接FE:
mysql -h FE_HOST -P 9030 -uroot。初始密码为空。执行ALTER SYSTEM ADD FOLLOWER "follower1:9010";和ALTER SYSTEM ADD OBSERVER "observer1:9010";添加其他FE节点(在生产环境,OBSERVER只用于扩展读连接,不参与选举)。 - 配置并启动BE:修改
be/conf/be.conf,同样设置priority_networks和storage_root_path(数据存储路径,可配置多个,用分号隔开)。启动BE:./bin/start_be.sh --daemon。 - 在FE中添加BE:通过MySQL客户端连接FE,执行
ALTER SYSTEM ADD BACKEND "be1:9050";。 - 验证集群状态:执行
SHOW PROC '/frontends';和SHOW PROC '/backends';查看节点状态是否为Alive。
3.2 数据建模与建表示例
根据电商场景,我们创建两张表:一张用户行为明细表(重复模型),一张实时订单聚合表(聚合模型)。
-- 1. 创建数据库 CREATE DATABASE IF NOT EXISTS realtime_ec; USE realtime_ec; -- 2. 用户点击流明细表(重复模型,用于详细行为分析) CREATE TABLE IF NOT EXISTS user_click_log ( `user_id` BIGINT NOT NULL COMMENT "用户ID", `item_id` BIGINT NOT NULL COMMENT "商品ID", `category_id` INT COMMENT "品类ID", `action` VARCHAR(20) COMMENT "行为类型,如click, cart, buy", `ts` DATETIME NOT NULL COMMENT "行为时间", `device` VARCHAR(50) COMMENT "设备", `province` VARCHAR(20) COMMENT "省份" ) DUPLICATE KEY(user_id, item_id, ts) -- 重复模型,指定排序列 PARTITION BY RANGE(ts) -- 按时间范围分区 ( PARTITION p202405 VALUES LESS THAN ("2024-06-01"), PARTITION p202406 VALUES LESS THAN ("2024-07-01") ) DISTRIBUTED BY HASH(user_id) BUCKETS 10 -- 按user_id哈希分桶,10个桶 PROPERTIES ( "replication_num" = "3", -- 副本数,通常与BE数量匹配或更少 "storage_medium" = "SSD" ); -- 3. 实时订单聚合表(聚合模型,用于快速出报表) CREATE TABLE IF NOT EXISTS order_agg_daily ( `dt` DATE NOT NULL COMMENT "日期", `province` VARCHAR(20) NOT NULL COMMENT "省份", `category_id` INT NOT NULL COMMENT "品类ID", `order_count` BIGINT SUM DEFAULT "0" COMMENT "订单数", `gmv` DECIMAL(20, 2) SUM DEFAULT "0.0" COMMENT "总交易额", `user_count` BIGINT BITMAP_UNION COMMENT "下单用户数(使用Bitmap去重计数)" ) AGGREGATE KEY(dt, province, category_id) -- 聚合键 PARTITION BY RANGE(dt) ( PARTITION p202405 VALUES LESS THAN ("2024-06-01"), PARTITION p202406 VALUES LESS THAN ("2024-07-01") ) DISTRIBUTED BY HASH(province, category_id) BUCKETS 8 PROPERTIES ( "replication_num" = "3", "storage_medium" = "SSD" );建表语句中有几个关键点:
DISTRIBUTED BY HASH(...) BUCKETS N:这是数据分布的核心。桶数(BUCKETS)建议设置为 BE 节点数量的整数倍,以保证数据均匀分布。桶数一旦确定,后期修改非常麻烦(需要重导数据),所以初期规划要谨慎。replication_num:副本数,用于高可用。通常设置为3,但如果你只有3个BE,设置为3意味着每个数据块有3个副本,会占用3倍存储。在小集群中,可以设置为2作为权衡。BITMAP_UNION:这是 Doris 提供的高级聚合函数,用于精确去重计数,比基于COUNT(DISTINCT)的查询性能高得多。
3.3 数据导入:流式与批量接入
数据导入是 Doris 生产链路的关键一环。它支持多种方式,这里介绍最常用的两种:Routine Load(流式)和 Broker Load(批量)。
1. Routine Load:从 Kafka 实时接入这是实现实时数仓的核心。假设用户行为日志已经发往 Kafka 的user_click_topic。
-- 创建例行导入作业 CREATE ROUTINE LOAD realtime_ec.user_click_load ON user_click_log COLUMNS(user_id, item_id, category_id, action, ts, device, province) PROPERTIES ( "desired_concurrent_number"="3", -- 并发任务数 "max_batch_interval" = "10", -- 最大间隔10秒消费一次 "max_batch_rows" = "200000", -- 每批最多20万行 "max_batch_size" = "104857600" -- 每批最大100MB ) FROM KAFKA ( "kafka_broker_list" = "kafka1:9092,kafka2:9092", "kafka_topic" = "user_click_topic", "property.group.id" = "doris_click_group", "property.security.protocol" = "SASL_PLAINTEXT", -- 如果有认证,还需配置 property.sasl.mechanism, property.sasl.jaas.config 等 "format" = "json" -- 假设数据是JSON格式 );创建后,可以通过SHOW ROUTINE LOAD;和SHOW ROUTINE LOAD TASK;监控导入状态。Routine Load 会自动管理消费位点,保证至少一次语义。
2. Broker Load:从 HDFS/S3 批量导入用于初始化历史数据导入或定时的批量数据补充。需要提前配置好 Broker(Doris 用于访问外部存储的组件)。
LOAD LABEL realtime_ec.label_order_20240501 -- 导入任务标签,需唯一 ( DATA INFILE("hdfs://namenode:8020/path/to/order_data_20240501.parquet") INTO TABLE order_agg_daily FORMAT AS "parquet" (dt, province, category_id, order_count, gmv, user_id) -- 映射字段,user_id用于BITMAP ) WITH BROKER "broker_name" PROPERTIES ( "timeout" = "3600" );提交后,通过SHOW LOAD WHERE LABEL = 'label_order_20240501';查看导入进度和结果。
4. 性能调优与运维核心要点
Doris 开箱即用性能就不错,但要发挥其最大潜力,尤其是在数据量巨大、查询复杂的生产环境,调优必不可少。
4.1 查询优化:执行计划解读与调优
当遇到慢查询时,第一反应应该是查看执行计划。使用EXPLAIN命令:
EXPLAIN SELECT province, category_id, SUM(gmv) as total_gmv FROM order_agg_daily WHERE dt >= '2024-05-01' AND dt <= '2024-05-07' GROUP BY province, category_id ORDER BY total_gmv DESC LIMIT 10;执行计划输出会很长,关键看几点:
- 分区裁剪(PartitionPrune):是否有效过滤了分区。如果扫描的分区数远大于实际需要的,说明分区键设置或查询条件有问题。
- 桶裁剪(BucketPrune):是否利用了分桶键进行过滤。这取决于你的查询条件是否包含分桶键的等值条件。
- 聚合节点(AGGREGATE):是单级聚合还是两级聚合(
AGGREGATE (merge finalize))。对于大数据集,两级聚合(先在BE本地聚合,再在FE或单个BE上全局聚合)效率更高。可以通过设置set parallel_fragment_exec_instance_num = 4;来调整并行度,促使两级聚合发生。 - 物化视图选择:计划中是否显示
SCAN MATERIALIZED VIEW。如果没有,可以尝试使用EXPLAIN命令查看是否命中了你期望的物化视图,有时需要手动 Hint 或优化查询写法。
4.2 资源隔离与并发控制
在多人使用的分析平台,资源争用是常态。Doris 提供了资源标签(Resource Tag)功能来实现物理资源的隔离。
- 给 BE 打标签:在 BE 配置文件
be.conf中设置tag.location = "group_a"。 - 创建资源组:
CREATE RESOURCE GROUP group_high PROPERTIES ( "cpu_share"="100", "mem_limit"="30%", "tag.location"="group_a" ); - 将用户或查询绑定到资源组:
SET PROPERTY FOR 'analyst_user' 'resource_tags' = 'group_high'; -- 或者针对单个查询 SET resource_group = 'group_high'; SELECT ...
这样,来自analyst_user的查询只会调度到带有group_a标签的 BE 上执行,避免影响其他关键任务。
4.3 监控与告警体系搭建
生产系统离不开监控。Doris 提供了丰富的 Metrics 接口(通过 FE/BE 的/metricsAPI)和系统表(如information_schema下的BE_TABLETS,FE_PROC等)。
- 核心监控项:
- 集群健康度:FE/BE 节点存活状态、副本健康度(
tablet_health)。 - 资源使用:BE 的 CPU、内存、磁盘使用率、网络 IO。FE 的 JVM 内存和元数据数量。
- 查询性能:查询延迟(
query_latency)、QPS、失败率。可以按用户、资源组细分。 - 导入性能:Routine Load/Broker Load 的导入速率、延迟、错误率。
- Compaction 状态:Doris 后台通过 Compaction 合并数据文件,如果积压(
cumulative_compaction_score,base_compaction_score过高),会影响查询和导入性能。
- 集群健康度:FE/BE 节点存活状态、副本健康度(
建议使用 Prometheus 抓取 Doris 的 Metrics,用 Grafana 制作监控大盘,并针对关键指标(如 BE 宕机、磁盘使用率 >85%、查询 P99 延迟 > 10s)设置告警规则,接入钉钉、企业微信等。
5. 典型问题排查与实战经验
最后,分享几个我在运维 Doris 集群时踩过的坑和解决方法,这些在官方文档里不一定找得到。
5.1 问题一:数据导入变慢,Routine Load 出现堆积
现象:Kafka 中的数据产生速度正常,但 Doris 消费延迟(Lag)越来越大,BE 监控显示写盘 IO 很高。
排查思路:
- 检查
SHOW ROUTINE LOAD;看是否有错误信息。 - 检查 BE 日志,是否有大量的
WARN或ERROR,特别是与compaction相关的。 - 使用
SHOW PROC '/backends'\G查看各个 BE 的LastStreamLoadTime和LastStreamLoadTime,判断是否某个 BE 特别慢。 - 查看
SHOW TABLET FROM table_name,关注VersionCount列。如果这个值持续增长且很大(比如超过100),说明 Compaction 速度跟不上数据导入速度,导致数据版本过多,每次查询需要合并大量版本,性能下降。
解决方案:
- 短期:适当调低 Routine Load 的
max_batch_rows和max_batch_size,降低单批数据量,给 Compaction 喘息之机。 - 长期:
- 增加 BE 节点的磁盘 IOPS(换用更好的 SSD)。
- 调整 Compaction 参数(需谨慎)。在 BE 的
be.conf中,可以适当调高cumulative_compaction_num_threads_per_disk和base_compaction_num_threads_per_disk,增加并发线程数。 - 检查数据模型。如果使用了唯一模型或聚合模型,且更新/聚合非常频繁,会加剧 Compaction 压力。评估是否可以用更粗粒度的聚合,或者将实时更新改为微批处理。
5.2 问题二:查询内存超限(Memory Exceeded)
现象:执行复杂聚合或包含COUNT(DISTINCT)的查询时,报错Memory exceed limit。
原因分析:Doris 的查询内存控制是在 BE 端进行的。像GROUP BY、ORDER BY、COUNT(DISTINCT)、窗口函数这样的操作,都需要在内存中维护中间状态(哈希表、排序区等)。如果数据量过大或分组键基数太高,就容易爆内存。
解决方案:
- 优化查询:
- 将
COUNT(DISTINCT user_id)改为使用聚合表中的BITMAP_UNION预计算列。这是最有效的办法。 - 增加过滤条件,减少参与计算的数据量。
- 尝试将大查询拆分成多个小查询。
- 将
- 调整参数:
- 在会话级别设置更大的内存限制:
SET exec_mem_limit = 8589934592;(8GB)。但这只是治标,且可能影响其他查询。 - 启用 Spill to Disk 功能(如果版本支持)。对于
ORDER BY和AGGREGATE,可以设置set enable_spill=true;和set spill_mode=“auto”;,让中间结果在内存不足时溢写到磁盘。
- 在会话级别设置更大的内存限制:
- 审视数据模型:检查是否可以通过创建更合适的物化视图,将计算提前完成,从而避免在查询时进行重计算。
5.3 问题三:FE 元数据写入失败导致选主失败
现象:FE Follower 节点日志频繁报错,无法与 Leader 同步元数据,甚至整个集群出现不可用。
排查:这通常是底层存储(FE 的meta_dir所在磁盘)出现问题,或者 FE JVM 内存不足导致 Full GC 频繁,进程僵死。
解决与预防:
- 确保元数据目录高可用:
meta_dir最好配置在可靠的共享存储上(如基于 SSD 的云盘,并做好快照备份),或者至少确保有定期的元数据备份策略(使用sh backup_meta.sh)。 - 监控 FE JVM:为 FE 配置合适的堆内存(
-Xmx和-Xms),通过监控观察 GC 情况。如果老年代使用率持续很高,需要考虑升级 FE 机器配置。 - 使用 Observer 节点:对于读多写少的场景,可以多用 Observer FE 来分担查询的请求解析和规划压力,避免 Follower 节点过载影响元数据同步。
5.4 关于数据备份与恢复
虽然 Doris 通过多副本保证了数据的高可用(一份数据损坏,自动从其他副本恢复),但无法防范逻辑错误(如误删数据)或机房级灾难。因此,定期的全量备份是必须的。
Doris 提供了BACKUP和RESTORE命令,可以将指定数据库或表的数据和元数据备份到远端对象存储(如 S3、HDFS)。
-- 创建备份 BACKUP SNAPSHOT realtime_ec.snapshot_20240520 TO `hdfs_repo` -- 需提前通过 CREATE REPOSITORY 创建 ON (order_agg_daily, user_click_log) PROPERTIES ("type" = "full"); -- 查看备份任务 SHOW BACKUP FROM realtime_ec; -- 恢复数据(到新表) RESTORE SNAPSHOT realtime_ec.snapshot_20240520 FROM `hdfs_repo` ON ( `order_agg_daily` AS `order_agg_daily_restored`) PROPERTIES ( "backup_timestamp"="2024-05-20-15-30-00", "replication_num" = "3" );备份恢复是最后一道防线,建议结合业务重要性,制定合理的备份周期(如每日全备)和保留策略。
经过这些年的实践,我的体会是,Doris 的成功在于它在“性能”、“易用性”和“功能完备性”之间找到了一个很好的平衡点。它不像一些系统那样需要极其专业的调优才能用,也不像另一些系统为了易用牺牲了极限性能。它的学习曲线相对平缓,但上限很高,足以支撑起一个中等规模公司核心的实时数据分析需求。当然,它也不是银弹,在超大规模(PB级以上)数据、极端复杂的多表关联场景下,可能还是会遇到挑战。但在它所擅长的领域——即席查询、实时报表、用户行为分析——它无疑是一把趁手的好武器。最后一个小建议,多关注社区的邮件列表和 GitHub Issues,很多你遇到的坑,可能已经有人踩过并给出了解决方案。