查询性能分析
在 Doris 执行查询时,当碰到查询性能未达预期时,建议做进一步分析情况。本文将全面阐述如何在 Doris 中对查询进行性能分析。
在 Doris 中,由于 Profile 收集会产生一定的开销,因此默认情况下它是关闭的。若要进行查询性能分析,我们首先需要将其开启,具体操作为在 MySQL Client 中执行以下命令:
set enable_profile = true;
QueryID 排查慢查询
查询性能问题分析的首要步骤是获取待分析查询的 QueryID。这个 QueryID 可以从fe/log/fe.audit.log
日志文件中找到。
以 TPC-H 中的某条特定查询为例,通过查看日志信息,我们可以发现该查询的 QueryID 为QueryId=704185c15570441b-98ad0634c88584f0
。
2024-08-20 14:37:23,729 [query] IClient=127.0.0.1:33570|User=root|Ctl=internal Db=regression_test_tpch_sf0_1_p1I|State=EOF|ErrorCode=0|ErrorMessage=|Time(ms)=153|ScanBytes=0|ScanRows=0|ReturnRows=1|StmtId=1191|QueryId=704185c15570441b-98ad0634c88584f0|IsQuery=true|isNereids=true|feIp=168.45.0.1|StmtType=SELECT|Stmt=SELECT sum(l_extendedprice) / 7.0 AS avg_yearly FROM lineitem, part WHERE p_partkey = l_partkey AND p_brand_ "Brand#23" AND p_container = "MED BOX" AND l_quantity < ( SELECT 0.2*avg(l_quantity) FROM lineitem WHERE l_partkey= p_partkey) |CpuTimeMS=401ShuffleSendBytes=0|ShuffleSendRows=0|SqlHash=ес2e14fac69b9711dc305e218f1e94b8|peakMemoryBytes=33792|SqlDigest=|cloudClusterName=UNKNOWN|TraceId=|WcorkloadGroup=normal|FuzzyVariables=|scanBytesFromLocalStorage=0|scanBytesFromRemoteStorage=0
Profile 分析查询性能
在获取 QueryID 后,可以通过访问对应 FE 的 WebUI 来检索 Profile 文本。例如,通过访问链接http://{fe_ip}:{http_port}/QueryProfile/704185c15570441b-98ad0634c88584f0
,即可获取到相应的 Profile 信息,查看更多详细信息,可以下载 profile_704185c15570441b-98ad0634c88584f0.txt 文件。
Profile 文件结构
Profile 文件中包含以下几个主要的部分:
查询基本信息:包括 ID,时间,数据库等
SQL 语句以及执行计划。
FE 的耗时(Plan Time, Schedule Time 等)。
BE 在执行过程中各个 Operator 的执行耗时(包括 Merged Profile、Execution Profile、以及 Execution Profile 中的每个 PipelineTask)。
在慢查询中,通常耗时主要集中在 BE 的执行过程,接下来将主要介绍这部分的分析过程。
通过 Merged Profile 进行 BE 执行分析
为了帮助用户更准确地分析性能瓶颈,Doris 提供了各个 Operator 聚合后的 Profile 结果。
以 EXCHANGE_OPERATOR(id=4)为例:
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 等待自身执行条件依赖的时间 |
在 Doris 中,每个 Operator 根据用户设置的并发数并发执行。因此,Merged Profile 对每个执行并发的每个指标都计算出了 Max、Avg 和 Min 的值。
其中,WaitForDependencyTime 指标在不同 Operator 对应有不同的值,因为每个 Operator 执行的条件依赖不同。例如,在这个 EXCHANGE_OPERATOR 的例子中,条件依赖是有数据被上游的算子通过 RPC 发送过来。因此,这里的 WaitForDependencyTime 实际上就是在等待上游算子发送数据的时间。
通过 Execution Profile 进行 BE 执行分析
区别于 Merged Profile,Execution Profile 展示的是具体的某个并发中的详细指标。
还是以 EXCHANGE_OPERATOR(id=4)为例:
EXCHANGE_OPERATOR (id=4):(ExecTime: 706.351us)
- BlocksProduced: 0
- CloseTime: 38.287us
- DataArrivalWaitTime: 0ns
- DecompressBytes: 0.00
- DecompressTime: 0ns
- DeserializeRowBatchTimer: 0ns
- ExecTime: 706.351us
- FirstBatchArrivalWaitTime: 0ns
- InitTime: 647.605us
- LocalBytesReceived: 0.00
- MemoryUsage:
- PeakMemoryUsage: 0.00
- OpenTime: 5.943us
- ProjectionTime: 0ns
- RemoteBytesReceived: 0.00
- RowsProduced: 0
- SendersBlockedTotalTimer(*): 0ns
- WaitForDependencyTime: 0ns
- WaitForData0: 9.476ms
在该 Profile 中,LocalBytesReceived 是 Exchange Operator 特有的一个指标,其他 Operator 中并不存在,因此它也没有被包含在 Merged Profile 中。
PipelineTask 执行时间分析
在 Doris 中,一个 PipelineTask 由多个 Operator 组成。当分析一个 PipelineTask 的执行耗时时,需要重点关注以下几个方面:
ExecuteTime:表示整个 PipelineTask 的实际执行时间,它大约等于该 Task 中所有 Operator 的 ExecTime 之和。
WaitWorkerTime:表示 Task 等待执行 Worker 的时间。当 Task 处于
runnable
状态时,它需要等待一个空闲的 Worker 来执行,该耗时主要取决于集群的负载情况。等待执行依赖的时间:一个 Task 可以执行的依赖条件是每个 Operator 的 Dependency 全部满足执行条件,而 Task 等待执行依赖的时间就是将这些依赖的等待时间相加。
以上述例子中的其中一个 Task 为例:
PipelineTask (index=1):(ExecTime: 4.773ms)
- ExecuteTime: 1.656ms
- CloseTime: 90.402us
- GetBlockTime: 11.235us
- OpenTime: 1.448ms
- PrepareTime: 1.555ms
- SinkTime: 14.228us
- WaitWorkerTime: 63.868us
DATA_STREAM_SINK_OPERATOR (id=8,dst_id=8):(ExecTime: 1.688ms)
- WaitForDependencyTime: 0ns
- WaitForBroadcastBuffer: 0ns
- WaitForRpcBufferQueue: 0ns
AGGREGATION_OPERATOR (id=7 , nereids_id=648):(ExecTime: 398.12us)
- WaitForDependency[AGGREGATION_OPERATOR_DEPENDENCY]Time: 10.495ms
该 task 包含了 DATA_STREAM_SINK_OPERATOR
和 AGGREGATION_OPERATOR
两个 Operator。其中:
DATA_STREAM_SINK_OPERATOR
有两个依赖,分别是WaitForBroadcastBuffer
和WaitForRpcBufferQueue
AGGREGATION_OPERATOR
有一个依赖,为AGGREGATION_OPERATOR_DEPENDENCY
。
因此,当前 Task 的耗时分布如下:
ExecuteTime(执行总时间):1.656ms(约等于两个 Operator 的 ExecTime 总和)
WaitWorkerTime(等待 Worker 的时间):63.868us(说明当前集群负载不高,Task 就绪以后立即就有 Worker 来执行)
等待执行依赖的时间:10.495ms(
WaitForBroadcastBuffer
+WaitForRpcBufferQueue
+WaitForDependency[AGGREGATION_OPERATOR_DEPENDENCY]Time
)即当前 task 的所有 Dependency 相加得到的总的等待时间。
性能问题通用排查思路
在 Doris 执行查询的过程中,通常可以依据以下四个步骤来排查性能问题:
1 定位算子执行性能问题
算子执行缓慢是日常生产环境中较为常见的一类问题。在定位过程中,可以根据 Merged Profile 中的 Plan Tree,梳理出每个 Operator 的 ExecTime
和 WaitForDependencyTime
。
若
ExecTime
较慢,则表明当前算子存在性能问题,这可能是算子本身执行性能不佳,也可能是执行规划的 Plan 不够优化所导致的。若
ExecTime
很快,但WaitForDependencyTime
很长,则说明性能瓶颈不在当前算子,需沿着 Plan Tree 继续查找其子节点。
2 定位数据倾斜问题
在定位算子性能问题的过程中,若发现某个算子的 ExecTime
的最小值(Min)和最大值(Max)相差悬殊,则需观察该算子的数据量(RowsProduced
)是否同样存在显著差异。若是,则说明发生了数据倾斜。
3 定位 RPC 延迟过大的问题
当遍历完整个 Plan Tree 之后,若未能找到任何执行缓慢的算子,接下来需排查是否因 RPC 延迟过大而导致的性能问题。
在此过程中,需找到 Execution Profile 中的每个 DATA_STREAM_SINK_OPERATOR
,并检查其中的 RpcMaxTime
是否存在异常值。该指标指明了 RPC 过程中耗时最长的一次调用,若其值过大,则代表 RPC 延迟较高,可能是网络问题所致。
4 定位集群负载过高导致的性能问题
在 Doris 的执行引擎中,执行线程数量是固定的。因此,当集群负载很高时,每个 Task 需等待空闲的执行 Worker 来执行。可以通过 Execution Profile 中的每个 PipelineTask 下,查看WaitWorkerTime
指标来获取等待时间的信息,以进一步判断。