Apache Doris Profile 深度解析:从获取到分析,解锁查询性能优化密码
在 Doris 数据库中,高效的查询性能是数据处理的关键。当我们遇到查询缓慢、资源消耗异常等问题时,Doris 提供的 Profile 工具就如同一位 “性能侦探”,能帮我们抽丝剥茧,找到问题根源。今天,我们就来深入聊聊如何分析 Profile,让 Doris 的查询性能更上一层楼。
一、Profile:解决查询性能的利器
Profile 是 Doris 用于记录和展示查询执行详细信息的强大工具,它以树状结构将查询计划的每个阶段(Operator)的执行时间、资源消耗等指标清晰呈现。通过 Profile,我们可以直观地了解查询的性能表现,判断哪些操作耗时过长,是否存在数据倾斜、网络延迟等问题,从而为优化查询性能、排查问题提供重要依据。无论是慢查询分析、性能调优,还是高并发场景下的瓶颈定位,Profile 的分析都贯穿其中,是 Doris 性能调优的核心依据。
对于用户而言,使用 Profile 有三个核心目标:精准找到查询的瓶颈点,进行基础的调优操作,以及基于瓶颈点判断需要哪些专业人员介入分析(找社区大佬支持)。掌握 Profile 的分析方法,能让你成为真正懂Doris性能优化的 “行家”。
二、获取 Profile:不同环境下的操作指南
想要利用 Profile 优化查询性能,首先要学会如何获取它。在不同的网络环境下,获取 Profile 的方法有所不同。
(一)Doris 集群能够正常访问外网
-
开启 Profile 收集参数
enable_profile
,该参数为 session 变量,不建议全局开启。在 mysql 客户端执行set enable_profile=true;
,并通过show variables like '%profile%';
确认变量是否正常开启(如果是压测时候,可以全局开启)。 -
执行对应 Query。需要注意的是,在集群有多个 FE 的情况下,要在开启 Profile 上报参数的 FE 上执行 Query,因为该参数并非全局生效(session变量,不加global,只在当前环境下生效)。
-
获取 Profile。访问执行对应 Query 的 FE HTTP 界面(
HTTP://FE_IP:HTTP_PORT
)的 QueryProfile 页面,点击对应 Profile ID 查看 Profile,还可以在 Profile 界面下载对应 Profile。
3.0.3 版本开始支持auto_profile_threshold_ms 变量,这样就不需要收集所有的query profile了,只需要收集超过指定时间的慢查询的profile
(二)Doris 集群访问外网受到限制
当集群不能正常访问外网时,需要通过 API 的方式获取 Profile(HTTP://FE_IP:HTTP_PORT/API/Profile?Query_ID=
),IP 和端口是指执行对应 Query 的 FE 对应 IP 和端口。具体步骤如下:
-
前两步与正常访问外网时相同,即开启
enable_profile
参数并执行对应 Query。 -
找到对应 Query ID,通过
show query profile "/";
根据对应 Query 找到 Profile ID。
+-----------------------------------+-----------+---------------------+---------------------+-------+------------+------+------------+-------------------------------------------------------+
| Profile ID | Task Type | Start Time | End Time | Total | Task State | User | Default Db | Sql Statement |
+-----------------------------------+-----------+---------------------+---------------------+-------+------------+------+------------+-------------------------------------------------------+
| 1b0bb22689734d30-bbe56e17c2ff21dc | QUERY | 2024-02-28 11:00:17 | 2024-02-28 11:00:17 | 7ms | EOF | root | | select id,name from test.test where name like "%RuO%" |
| 202fb174510c4772-965289e8f7f0cf10 | QUERY | 2024-02-25 19:39:20 | 2024-02-25 19:39:20 | 19ms | EOF | root | | select id,name from test.test where name like "%KJ%" |
+-----------------------------------+-----------+---------------------+---------------------+-------+------------+------+------------+-------------------------------------------------------+
2 rows in set (0.00 sec)
-
查询 Profile 并将 Profile 重定向到一个文本中,使用命令
CURL -X GET -u user:password http://fe_ip:http_port/api/profile?query_id=1b0bb22689734d30-bbe56e17c2ff21dc > test.profile
。 -
返回的 Profile 换行符为
n
,分析起来不方便,可以在文本编辑工具中将n
替换为n
,以便更好地查看和分析。
三、Profile 基础信息总览:快速定位问题方向
先来看一个 Profile 的基础信息示例:
Profile ID: 26ec8a4369b46ba-9af43a31cc4f5102- Task Type: QUERY- Start Time: 2025-03-31 17:23:47- End Time: 2025-03-31 17:25:51- Total: 2min4sec [总的耗时]- Task State: OK- User: root- Default Catalog: internal- Default Db: fine_bi_crossdata- Sql Statement: select * from view1 limit 100;
Execution Summary:- Parse SQL Time: 15ms- Nereids Analysis Time: 10ms- Nereids Rewrite Time: 51ms- Nereids Optimize Time: 30ms- Nereids Translate Time: 3ms- Workload Group: normal- Analysis Time: 10ms- Plan Time: 99ms [优化器的耗时,如果这个时间占据单个查询的比例过大,比如超过20%,需要联系社区同学分析]- JoinReorder Time: N/A- CreateSingleNode Time: N/A- QueryDistributed Time: N/A- Init Scan Node Time: N/A- Finalize Scan Node Time: N/A- Get Splits Time: N/A [如果是这块的占比时间比较高的情况,大概率是外表的问题]- Get Partitions Time: N/A- Get Partition Files Time: N/A- Create Scan Range Time: N/A- Get Partition Version Time: 9.561ms- Get Partition Version Count (hasData): 0- Get Partition Version Count: 1- Get Table Version Time: N/A- Get Table Version Count: 0- Schedule Time: 98ms- Fragment Assign Time: 1ms- Fragment Serialize Time: 21ms [这里也类似,如果耗时过多,请也得联系社区的同学分析了。很可能是有GC了]- Fragment RPC Phase1 Time: 75ms- Fragment RPC Phase2 Time: 1ms- Fragment Compressed Size: 3.92 MB- Fragment RPC Count: 24- Schedule Time Of BE: {...}- Wait and Fetch Result Time: 2min3sec- Fetch Result Time: 2min3sec [BE具体执行的耗时]- Write Result Time: 1ms [回写mysql的耗时,如果这块耗时过多,可能是客户端的瓶颈,分析一下结果集是不是过大]
从这些信息中,我们可以快速判断问题可能出在 FE 还是 BE 。通常情况下,95% 以上的主要耗时集中在Wait and Fetch Result Time
。若 FE 出现问题,常见原因有 GC 问题、锁问题、CPU 打满等,这类问题与 FE 的集群负载密切相关,一旦确认是 FE 的问题,需要收集当时的 CPU、内存监控数据进一步分析。
四、定位瓶颈点:Merged Profile 汇总信息
为了更准确地分析性能瓶颈,Doris 提供了各个 operator 聚合后的 Merged Profile 结果。以EXCHANGE_OPERATOR
为例:
EXCHANGE_OPERATOR (id=4):- BlocksProduced: sum 0, avg 0, max 0, min 0- CloseTime: avg 34.133us, max 38.287us, min 29.979us- ExecTime: avg 700.357us, max 706.351us, min 694.364us- InitTime: avg 648.104us, max 648.604us, min 647.605us- MemoryUsage: sum , avg , max , min - PeakMemoryUsage: sum 0.00 , avg 0.00 , max 0.00 , min 0.00 - OpenTime: avg 4.541us, max 5.943us, min 3.139us- ProjectionTime: avg 0ns, max 0ns, min 0ns- RowsProduced: sum 0, avg 0, max 0, min 0- WaitForDependencyTime: avg 0ns, max 0ns, min 0ns- WaitForData0: avg 9.434ms, max 9.476ms, min 9.391ms
Merged Profile 对每个 operator 的核心指标做了合并,核心指标和含义包括:
指标名称 | 指标含义 |
---|---|
BlocksProduced | 产生的 Data Block 数量 |
CloseTime | Operator 在 close 阶段的耗时 |
ExecTime | Operator 在各个阶段执行的总耗时 |
InitTime | Operator 在 Init 阶段的耗时 |
MemoryUsage | Operator 在执行阶段的内存用量 |
OpenTime | Operator 在 Open 阶段的耗时 |
ProjectionTime | Operator 在做 projection 的耗时 |
RowsProduced | Operator 返回的行数 |
WaitForDependencyTime | Operator 等待自身执行的条件依赖的时间 |
我们可以通过以下简单方法快速定位瓶颈点:
cat profile_b112de34c7a94d2e-a6b773cc1db43602.txt |grep ExecTime | grep max
找到max
耗时最久的时间后,再回头在 Profile 中查找对应的算子,就能确定查询的性能瓶颈所在。
例如,通过上述方法找出慢的算子是AGG算子
,后续就可以针对AGG算子
进行深入分析和调优。
五、基础调优:对症下药,提升性能
Doris 中的查询主要分为两类,针对不同类型的查询,调优方法也有所不同。
(一)单表大宽表分析
对于单表的大宽表分析,优化方法可参考 Doris 查询优化秘籍(上篇):关键优化策略剖析,通过合理设计表结构、选择合适的索引等方式提升查询性能。
(二)多表关联复杂分析
多表关联的复杂分析中,70% - 80% 的性能问题是由 RuntimeFilter 和 JoinReorder 不合适导致的。
-
RuntimeFilter 调优:在 Profile 中搜索
RuntimeFilterState
关键字,如果发现IsPushDown = false
或RuntimeFilter
的状态为NOT_READY
,可以通过设置set runtime_filter_wait_infinitely = true
,重新验证是否是 RF 导致的性能问题。 -
Join 方式调优:在 Doris 中,
shuffle
和broadcast
是常见的数据分发方式,选择不当会影响查询性能。例如,当出现数据倾斜导致Shuffle
性能极差时,可以通过指定broadcast join hint
来优化,如SELECT COUNT(*) FROM orders o JOIN [broadcast] customer c ON o.customer_number = c.customer_number;
。 -
Join 顺序调优:Doris 中 JOIN 操作建议左表大于右表(或至少左表不小于右表),以优化查询性能。如果发现
max
的耗时主要在join
上,可以查看join
顺序是否合理。通过对比HASH_JOIN_OPERATOR
(左表)和HASH_JOIN_SINK_OPERATOR
(右表)的数据量情况,判断join
顺序是否存在问题。若InputRows > ProbeRows
,大概率join
顺序不合理,此时可以请优化器的专家进行分析处理。 -
并行度调整:当
Schedule time
占据大量查询耗时,通常是instance
过多,导致rpc
、序列化处理的数据量过大。这种情况下,可以考虑调小pipeline
的并行度,如设置set global parallel_pipeline_task_num = 1
,观察性能是否改善。
六、深入分析 Profile:常见问题与处理方案
通过 Execution Summary 部分,我们可以初步确定查询的瓶颈在 FE 还是 BE。若在 BE,我们可以重点分析各个算子的情况。在 2.1 版本的 Profile 经过 merge 后,界面较为精简,我们可以通过查看每个算子的查询时间来定位问题。一般来说,ExecTime
最大的节点就是最耗时的节点,顺着这个 id 继续分析。如果各个算子耗时相差不大,说明没有明确的热点。
以下是一些常见问题的处理方案:
-
热点在 nested loop join 上:尝试修改 sql,将 nested loop join 改为 hash join,可显著提升性能。
-
热点在 join 上:
-
检查 join order 顺序,从条目数上判断是否合理。若右表条目数显著大于左表,或左右表数据量相近但右表列数显著大于左表,可能需要优化器同学介入。
-
排查 Runtime filter,检查 rf 是否生效、等待时间是否合理,以及是否存在多余或无用的 rf。
-
优化 join 列,尽量将 join 列和 group by 列建为非 null,能用定长列就不用变长列。
-
评估 join 的 shuffle 方式是否合理,考虑使用
Broadcast
、colocate
或优化Bucket shuffle
。
-
-
热点在 agg 上:参考 join 列的优化方式,调整表结构,如将 group by 的列作为表的分桶列或分区列,可对单表聚合性能提升有显著效果。
-
scan 上慢:
-
检查表引擎类型,优先选择查询性能较好的
duplicate key
引擎。 -
使用
select 分桶列,count(*) from table group by 分桶列 order by count(*) desc limit 100;
检查分桶是否有数据倾斜问题。 -
注意 key 列的顺序,因为 Doris 底层存储引擎按 key 列排序存储,合理的 key 列顺序有助于快速二分查找。
-
观察表的 compaction 情况,找到慢 scan 的 tablet,查看 compaction 链接,若存在慢副本,可通过
set use_fix_replica
绕过,并联系官方同学分析原因。
-
Profile 是 Doris 性能优化的关键利器,掌握它的获取、分析和调优方法,能让我们在面对查询性能问题时游刃有余。希望通过本文的分享,大家都能成为 Doris 性能优化的高手!如果你在使用 Profile 的过程中还有其他问题或经验,欢迎在评论区留言交流~