三台 BE 跑一条 Join,比单机还慢。继续加 BE 后,CPU 没有吃满,网络却先打满。MPP 没失效,它只是忠实地把一个错误的数据搬运方案并行执行了。
Doris 的性能来自优化器选对数据位置、Fragment 控制跨机边界、Pipeline 填满单机核心。MPP 只表示可以并行,不保证并行有收益。
一条 SQL 会经历三次完全不同的拆分
SELECTd.region,SUM(f.pay_amount)FROMfact_order fJOINdim_shop dONf.shop_id=d.shop_idWHEREf.pay_date='2026-08-27'GROUPBYd.region;执行链不是 SQL 直接广播到所有 BE:
SQL → Nereids 选择扫描、Join 顺序和数据分布 → FE 在 Shuffle 边界切成 PlanFragment → Coordinator 把 Fragment 实例调度到 BE → BE 把 Fragment 拆成 Pipeline DAG → PipelineTask 在有界线程池中执行 → Exchange 传输中间 Block → Coordinator 汇总结果MPP 官方文档 和 Pipeline 官方文档 分别覆盖跨 BE 和单 BE 两层。把它们混成 MPP 会漏掉性能最关键的边界。
MySQL、Spark 与 Doris 的差异不在会不会 Join
固定同一条件:十亿行事实表关联百万行维表,按地区聚合,结果用于交互式看板。
| 引擎 | 执行重心 | 优势 | 主要边界 |
|---|---|---|---|
| MySQL | 单实例索引、Nested Loop 与行式执行 | 高选择性点查和事务 | 大范围扫描与并行分析受单机限制 |
| Spark SQL | Stage、Task 与 Shuffle | 大批量容错计算、弹性资源 | 调度和物化开销不适合高频交互 |
| Doris | 常驻 BE、Fragment、向量化 Pipeline | 低调度开销的并行分析与查询服务 | 错误 Shuffle、倾斜和估算仍会拖垮查询 |
Doris 的优势是常驻执行引擎把分析查询拆成列式 Block,在 BE 内继续流水并行;代价是需要正确统计信息、数据分布和内存治理。
Fragment 决定数据什么时候跨机器
物理计划遇到跨节点数据交换时插入DataStreamSink与ExchangeNode,并在这里切开 Fragment。小维表可能 Broadcast 到每个事实表节点;两张大表通常按 Join Key Shuffle。
Broadcast dim_shop 全量 → BE1、BE2、BE3 fact_order 本地扫描 → 本地 Join Shuffle fact_order 按 shop_id 重分布 ↘ 对齐后 Join dim_shop 按 shop_id 重分布 ↗Broadcast 减少事实表搬运,却把维表复制到每个实例;Shuffle 能处理大表,却产生网络、序列化和接收端内存。选错策略时,增加节点会增加复制份数或交换连接,并不必然更快。
Pipeline 解决的是等待,不是消灭阻塞
Join Build、Aggregation 和 Sort 需要阶段性物化,无法组成一条永不停止的流水线。Pipeline 把阻塞算子拆成 Sink 与 Source,再用 Dependency 连接:
Pipeline 0:Scan dim → JoinBuildSink ↓ hash table ready Pipeline 1:Scan fact → JoinProbe → AggSink ↓ aggregation ready Pipeline 2:AggSource → ResultSink等待 Hash Table 时,PipelineTask 释放执行线程,而不是让一个 OS 线程空等。线程数受 BE 线程池约束,所以并发查询增加时不会按 Fragment 数无限膨胀。
但 Build 侧过大仍会吃内存,热点 Key 仍会造成单实例长尾,Sort 仍可能 Spill。Pipeline 改善调度,不改变算法复杂度。
用 EXPLAIN 和 Profile 分别证明两件事
EXPLAIN回答计划准备怎么做:分区裁剪、Join 类型、数据分布和 Fragment 边界。Query Profile 回答实际上做了多少:扫描行数、Exchange 字节、各实例耗时和峰值内存。
最小验证顺序:
EXPLAINVERBOSESELECTd.region,SUM(f.pay_amount)FROMfact_order fJOINdim_shop dONf.shop_id=d.shop_idWHEREf.pay_date='2026-08-27'GROUPBYd.region;SETenable_profile=true;-- 执行目标 SQLSHOWQUERY PROFILE;判断只看四个差值:
| 证据 | 正常含义 | 异常含义 |
|---|---|---|
| 估算行数 vs 实际行数 | CBO 基数可靠 | 统计信息失真,Join 策略可能错误 |
| 各实例 ScanRows | 数据分布均匀 | Tablet 或谓词倾斜 |
| ExchangeBytes | 只搬必要数据 | Shuffle 放大或裁剪失败 |
| 最大实例耗时 vs 中位数 | 并行任务接近 | 单个长尾决定总耗时 |
这个实验的淘汰条件很明确:增加 BE 后 ExchangeBytes 与长尾实例继续上升,却没有减少有效扫描和总耗时,说明当前拆分方式没有获得 MPP 收益。
源码只追四个入口
在 Apache Doris4.0.8中,源码阅读的最短路径是:
- FE
NereidsPlanner:逻辑与物理计划入口; - FE
Coordinator:Fragment 实例调度和结果协调; - BE Pipeline Fragment Context:把 Fragment 建成 Pipeline DAG;
- BE Task Scheduler:在有界线程池中推进 PipelineTask。
阅读时围绕一条 SQL 对照EXPLAIN,定位哪个规则选择了 Join 分布、哪个 Exchange 切开 Fragment、哪个 Dependency 阻塞 Probe。脱离具体计划浏览类文件,只会重新变成架构名词。
面试里真正有区分度的回答
Doris 查询快不是单靠 MPP。Nereids 先决定 Join 顺序和数据分布,FE 在 Shuffle 边界形成 Fragment,Coordinator 将实例放到持有 Tablet 的 BE,BE 再用 PipelineTask 在有界线程池中推进向量化算子。性能最终由扫描裁剪、Exchange 数据量、实例倾斜和阻塞算子内存共同决定。
并行只能缩短被正确拆分的工作;拆错以后,MPP 只是让更多机器一起搬错数据。
官方资料
- MPP Architecture
- Pipeline Execution Engine
- Query Profile
- Apache Doris 4.0.8 Source