第3章 Doris深度剖析:MPP架构、分区与分桶、Rollup与物化视图、Bitmap精确去重、Stream Load与Broker Load
Doris 这个项目,我关注了挺长时间。从早期的 Palo 到现在的 Apache Doris,它在大规模 OLAP 场景下的表现,确实让人印象深刻。这一章,咱们就把它拆开看看,聊聊几个最核心的设计。
核心观点:Doris 的成功,在于它把 MPP 架构、智能索引(Rollup/物化视图)和高效的导入机制,组合成了一个对用户非常友好的整体。你不需要像用某些系统那样,为了性能去手动调优底层存储格式。
3.1 MPP架构:不只是“分而治之”
Doris 的 MPP(大规模并行处理)架构,说白了就是让很多台机器一起干活。但它的设计里,有几个细节值得琢磨。
它把节点分成了两类:Frontend(FE) 和 Backend(BE)。FE 负责接收 SQL、解析、生成执行计划;BE 负责真正干活——存数据、算数据。
我个人觉得,Doris 的 MPP 做得好的地方在于它的向量化执行引擎。我记得在 1.0 版本之前,Doris 还是逐行处理的,性能瓶颈很明显。后来全面转向向量化,一次处理一批数据(比如 1024 行),CPU 的 Cache 命中率大幅提升。我在一个日活千万的报表场景里做过对比,同样的 SQL,向量化开启后查询耗时直接降了 60%。
为什么会这样?因为现代 CPU 的瓶颈往往不在计算,而在内存访问。向量化执行让数据在 CPU 流水线上跑得更顺畅,减少了分支预测失败和 Cache Miss。
我的建议:在部署 Doris 集群时,FE 节点建议至少 3 台,用奇数台保证选举高可用。BE 节点则根据数据量来,一般建议 10 台起步,单台 BE 的磁盘建议用 SSD,尤其是热数据。
3.2 分区与分桶:数据组织的艺术
分区和分桶,是 Doris 数据分布的核心。很多新手容易搞混,我简单说说我的理解。
- 分区(Partition): 逻辑上的数据划分,通常按时间。比如按天分区,每天的数据放在一个独立的目录里。这主要是为了管理方便——删除旧数据直接 Drop 分区,比 Delete 快得多。
- 分桶(Bucket): 物理上的数据分布,在每个分区内部,再按某个字段的 Hash 值,把数据打散到不同的 BE 节点上。这主要是为了并行计算——查询时可以多个 BE 同时扫描不同的桶。
我曾经遇到过一个案例:用户把一张 10 亿行的表只分了 3 个桶,结果查询时只有 3 个线程在跑,其他 BE 节点都在围观。这就是典型的“分桶不足”。
避坑指南:分桶数不是越多越好。我建议每个桶的数据量控制在 1GB ~ 10GB 之间。桶太多会导致元数据膨胀,而且小文件过多会影响导入性能。比如一张表 100GB,分 20 个桶就挺合适。
下面这张图,展示了 Doris 中分区与分桶的层级关系:
3.3 Rollup与物化视图:空间换时间的智慧
Rollup 和物化视图,是 Doris 加速查询的两大利器。很多人觉得它们差不多,其实侧重点不同。
- Rollup(上卷): 本质上是基表的一个预聚合副本。比如基表按“用户+时间+商品”存了明细,你可以建一个 Rollup 只按“用户+时间”做 SUM。查询时,如果 SQL 的粒度匹配 Rollup,Doris 会自动路由到 Rollup 上,扫描的数据量会少很多。
- 物化视图: 比 Rollup 更灵活。它允许你定义任意的 SQL 查询(包括 JOIN、过滤),然后把结果物化下来。Doris 的物化视图是透明改写的——你查基表,系统自动判断能不能用物化视图替代。
我记得有一次,一个用户有个查询要跑 30 秒,里面有个复杂的 COUNT(DISTINCT user_id)。我建议他建了一个物化视图,把去重结果预计算好。改完之后,查询降到 0.5 秒。嗯,这就是物化视图的魅力。
我的经验:Rollup 适合“维度上卷”这种固定模式的加速。物化视图适合更复杂的场景,比如多表 JOIN 后的预计算。但注意,物化视图会占用额外的存储空间,而且导入时会有额外的计算开销。别建太多,挑最慢的几个查询来优化就行。
3.4 Bitmap精确去重:大基数场景的救星
说到 COUNT(DISTINCT),Doris 提供了两种方式:近似去重(HyperLogLog) 和 精确去重(Bitmap)。
近似去重速度快,但结果有误差(通常 1%~2%)。精确去重用 Bitmap,每个用户 ID 映射到一个 Bit 位上,然后做位运算。对于千万级甚至亿级的去重,Bitmap 的优势非常明显。
为什么?因为 Bitmap 的 COUNT 操作本质上是计算 1 的个数,现代 CPU 有专门的 POPCNT 指令,一个时钟周期就能算 64 位。你想想看,用传统方式去重,需要维护一个巨大的 HashSet,内存占用高,而且比较操作慢。
使用 Bitmap 去重,需要先把用户 ID 转换成整数(RoaringBitmap 要求整数类型)。Doris 提供了 BITMAP_HASH 函数来做这个转换。
-- 建表时定义 Bitmap 列
CREATE TABLE user_visit (
dt DATE,
user_id INT,
visit_bitmap BITMAP BITMAP_UNION
)
AGGREGATE KEY(dt)
DISTRIBUTED BY HASH(dt) BUCKETS 10;
-- 导入数据时,用 BITMAP_HASH 转换
INSERT INTO user_visit
SELECT dt, BITMAP_HASH(user_id)
FROM source_table;
-- 查询精确 UV
SELECT dt, BITMAP_COUNT(visit_bitmap) AS uv
FROM user_visit
GROUP BY dt;
避坑指南:我曾经遇到一个坑——用户 ID 是字符串,直接用 BITMAP_HASH 转换后,发现去重结果比实际少了。原因是 BITMAP_HASH 返回的是 64 位整数,但 RoaringBitmap 内部用 32 位整数存储,高位被截断了。解决方案是先用 CRC32 或 MURMUR_HASH 把字符串转成 32 位整数,再存入 Bitmap。
3.5 Stream Load与Broker Load:导入的两种姿势
Doris 的导入方式很多,但最常用的就两个:Stream Load 和 Broker Load。
| 特性 | Stream Load | Broker Load |
|---|---|---|
| 触发方式 | HTTP 协议,客户端直接推送 | SQL 命令,异步拉取 |
| 数据源 | 本地文件、内存数据流 | HDFS、S3、OSS 等对象存储 |
| 适用场景 | 实时小批量(MB ~ GB 级) | 离线大批量(GB ~ TB 级) |
| 事务保证 | 单次导入原子性 | 单次导入原子性 |
| 导入速度 | 快(直接写入 BE) | 中等(需要 Broker 中转) |
我个人习惯:实时数据用 Stream Load,离线数据用 Broker Load。比如 Kafka 里的实时日志,我用 Flink 消费后,通过 Stream Load 批量写入 Doris(每 10 秒一次,每次几 MB)。而每天凌晨的 T+1 报表数据,几百 GB 存在 HDFS 上,就用 Broker Load 一次性导入。
Stream Load 的使用非常简单,一个 curl 命令就能搞定:
curl --location-trusted -u root: \
-H "label:my_label_001" \
-H "column_separator:," \
-T /data/my_data.csv \
http://fe_host:8030/api/my_db/my_table/_stream_load
这里有个关键参数 label,它保证了导入的幂等性。如果导入失败重试,Doris 会检查 label 是否已经存在,避免数据重复。嗯,这个设计很实用。
我的建议:Stream Load 的 label 建议用“业务名_时间戳_UUID”的格式,确保全局唯一。Broker Load 则建议用 SQL 方式提交,方便在调度系统里管理。
最后,我把这一章的核心知识点整理成了一张脑图,方便你回顾: