You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by yi...@apache.org on 2024/09/04 01:55:34 UTC

(doris) branch master updated: [opt](scanner) Control the degree of parallelism of scanner when only limit involved (#39927)

This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 7b860577aeb [opt](scanner) Control the degree of parallelism of scanner when only limit involved (#39927)
7b860577aeb is described below

commit 7b860577aeb4d0dd4c56a0aa4613d94540813ade
Author: zhiqiang <se...@163.com>
AuthorDate: Wed Sep 4 09:55:26 2024 +0800

    [opt](scanner) Control the degree of parallelism of scanner when only limit involved (#39927)
    
    For queries like `select * from tbl limit 100;`, we should limit the
    parallelism to 1.
    
    We already have strategies in our code, but we can not control its
    behavior. So add session variables to control them.
    
    Previous pr like #33888 #36535 already has done something similar, this
    pr integrates them.
---
 be/src/pipeline/exec/scan_operator.cpp             |  36 ++++-
 .../java/org/apache/doris/planner/ScanNode.java    |  16 +-
 .../java/org/apache/doris/qe/SessionVariable.java  |  25 +++
 gensrc/thrift/PaloInternalService.thrift           |   3 +
 ...ptive_pipeline_task_serial_read_on_limit.groovy | 175 +++++++++++++++++++++
 5 files changed, 247 insertions(+), 8 deletions(-)

diff --git a/be/src/pipeline/exec/scan_operator.cpp b/be/src/pipeline/exec/scan_operator.cpp
index d7af0053944..73cd02b5a5d 100644
--- a/be/src/pipeline/exec/scan_operator.cpp
+++ b/be/src/pipeline/exec/scan_operator.cpp
@@ -45,6 +45,8 @@
 
 namespace doris::pipeline {
 
+const static int32_t ADAPTIVE_PIPELINE_TASK_SERIAL_READ_ON_LIMIT_DEFAULT = 10000;
+
 #define RETURN_IF_PUSH_DOWN(stmt, status)    \
     if (pdt == PushDownType::UNACCEPTABLE) { \
         status = stmt;                       \
@@ -1147,12 +1149,6 @@ ScanOperatorX<LocalStateType>::ScanOperatorX(ObjectPool* pool, const TPlanNode&
         : OperatorX<LocalStateType>(pool, tnode, operator_id, descs),
           _runtime_filter_descs(tnode.runtime_filters),
           _parallel_tasks(parallel_tasks) {
-    if (!tnode.__isset.conjuncts || tnode.conjuncts.empty()) {
-        // Which means the request could be fullfilled in a single segment iterator request.
-        if (tnode.limit > 0 && tnode.limit < 1024) {
-            _should_run_serial = true;
-        }
-    }
     if (tnode.__isset.push_down_count) {
         _push_down_count = tnode.push_down_count;
     }
@@ -1185,6 +1181,34 @@ Status ScanOperatorX<LocalStateType>::init(const TPlanNode& tnode, RuntimeState*
     if (tnode.__isset.topn_filter_source_node_ids) {
         topn_filter_source_node_ids = tnode.topn_filter_source_node_ids;
     }
+
+    // The first branch is kept for compatibility with the old version of the FE
+    if (!query_options.__isset.enable_adaptive_pipeline_task_serial_read_on_limit) {
+        if (!tnode.__isset.conjuncts || tnode.conjuncts.empty()) {
+            // Which means the request could be fullfilled in a single segment iterator request.
+            if (tnode.limit > 0 &&
+                tnode.limit <= ADAPTIVE_PIPELINE_TASK_SERIAL_READ_ON_LIMIT_DEFAULT) {
+                _should_run_serial = true;
+            }
+        }
+    } else {
+        DCHECK(query_options.__isset.adaptive_pipeline_task_serial_read_on_limit);
+        // The set of enable_adaptive_pipeline_task_serial_read_on_limit
+        // is checked in previous branch.
+        if (query_options.enable_adaptive_pipeline_task_serial_read_on_limit) {
+            int32_t adaptive_pipeline_task_serial_read_on_limit =
+                    ADAPTIVE_PIPELINE_TASK_SERIAL_READ_ON_LIMIT_DEFAULT;
+            if (query_options.__isset.adaptive_pipeline_task_serial_read_on_limit) {
+                adaptive_pipeline_task_serial_read_on_limit =
+                        query_options.adaptive_pipeline_task_serial_read_on_limit;
+            }
+
+            if (tnode.limit > 0 && tnode.limit <= adaptive_pipeline_task_serial_read_on_limit) {
+                _should_run_serial = true;
+            }
+        }
+    }
+
     return Status::OK();
 }
 
diff --git a/fe/fe-core/src/main/java/org/apache/doris/planner/ScanNode.java b/fe/fe-core/src/main/java/org/apache/doris/planner/ScanNode.java
index 50b0f5a0269..a92cac7b510 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/ScanNode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/ScanNode.java
@@ -739,8 +739,20 @@ public abstract class ScanNode extends PlanNode implements SplitGenerator {
     }
 
     public boolean shouldUseOneInstance(ConnectContext ctx) {
-        long limitRowsForSingleInstance = ctx == null ? 10000 : ctx.getSessionVariable().limitRowsForSingleInstance;
-        return hasLimit() && getLimit() < limitRowsForSingleInstance && conjuncts.isEmpty();
+        int adaptivePipelineTaskSerialReadOnLimit = 10000;
+
+        if (ctx != null) {
+            if (ctx.getSessionVariable().enableAdaptivePipelineTaskSerialReadOnLimit) {
+                adaptivePipelineTaskSerialReadOnLimit = ctx.getSessionVariable().adaptivePipelineTaskSerialReadOnLimit;
+            } else {
+                return false;
+            }
+        } else {
+            // No connection context, typically for broker load.
+        }
+
+        // For UniqueKey table, we will use multiple instance.
+        return hasLimit() && getLimit() <= adaptivePipelineTaskSerialReadOnLimit && conjuncts.isEmpty();
     }
 
     // In cloud mode, meta read lock is not enough to keep a snapshot of the partition versions.
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
index 4140835b657..c1d3c7a3efb 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
@@ -654,6 +654,11 @@ public class SessionVariable implements Serializable, Writable {
 
     public static final String IN_LIST_VALUE_COUNT_THRESHOLD = "in_list_value_count_threshold";
 
+    public static final String ENABLE_ADAPTIVE_PIPELINE_TASK_SERIAL_READ_ON_LIMIT =
+                                    "enable_adaptive_pipeline_task_serial_read_on_limit";
+    public static final String ADAPTIVE_PIPELINE_TASK_SERIAL_READ_ON_LIMIT =
+                                    "adaptive_pipeline_task_serial_read_on_limit";
+
     /**
      * If set false, user couldn't submit analyze SQL and FE won't allocate any related resources.
      */
@@ -2115,6 +2120,7 @@ public class SessionVariable implements Serializable, Writable {
     })
     public boolean enableFallbackOnMissingInvertedIndex = true;
 
+
     @VariableMgr.VarAttr(name = IN_LIST_VALUE_COUNT_THRESHOLD, description = {
         "in条件value数量大于这个threshold后将不会走fast_execute",
         "When the number of values in the IN condition exceeds this threshold,"
@@ -2122,6 +2128,22 @@ public class SessionVariable implements Serializable, Writable {
     })
     public int inListValueCountThreshold = 10;
 
+    @VariableMgr.VarAttr(name = ENABLE_ADAPTIVE_PIPELINE_TASK_SERIAL_READ_ON_LIMIT, needForward = true, description = {
+        "开启后将会允许自动调整 pipeline task 的并发数。当 scan 节点没有过滤条件,且 limit 参数小于"
+            + "adaptive_pipeline_task_serial_read_on_limit 中指定的行数时,scan 的并行度将会被设置为 1",
+        "When enabled, the pipeline task concurrency will be adjusted automatically. When the scan node has no filter "
+            + "conditions and the limit parameter is less than the number of rows specified in "
+            + "adaptive_pipeline_task_serial_read_on_limit, the parallelism of the scan will be set to 1."
+    })
+    public boolean enableAdaptivePipelineTaskSerialReadOnLimit = true;
+
+    @VariableMgr.VarAttr(name = ADAPTIVE_PIPELINE_TASK_SERIAL_READ_ON_LIMIT, needForward = true, description = {
+        "当 enable_adaptive_pipeline_task_serial_read_on_limit 开启时,scan 的并行度将会被设置为 1 的行数阈值",
+            "When enable_adaptive_pipeline_task_serial_read_on_limit is enabled, "
+            + "the number of rows at which the parallelism of the scan will be set to 1."
+    })
+    public int adaptivePipelineTaskSerialReadOnLimit = 10000;
+
     public void setEnableEsParallelScroll(boolean enableESParallelScroll) {
         this.enableESParallelScroll = enableESParallelScroll;
     }
@@ -3700,6 +3722,9 @@ public class SessionVariable implements Serializable, Writable {
         tResult.setKeepCarriageReturn(keepCarriageReturn);
 
         tResult.setEnableSegmentCache(enableSegmentCache);
+
+        tResult.setEnableAdaptivePipelineTaskSerialReadOnLimit(enableAdaptivePipelineTaskSerialReadOnLimit);
+        tResult.setAdaptivePipelineTaskSerialReadOnLimit(adaptivePipelineTaskSerialReadOnLimit);
         tResult.setInListValueCountThreshold(inListValueCountThreshold);
         return tResult;
     }
diff --git a/gensrc/thrift/PaloInternalService.thrift b/gensrc/thrift/PaloInternalService.thrift
index 85e4ade4ca4..9da87117154 100644
--- a/gensrc/thrift/PaloInternalService.thrift
+++ b/gensrc/thrift/PaloInternalService.thrift
@@ -335,6 +335,9 @@ struct TQueryOptions {
 
   127: optional i32 in_list_value_count_threshold = 10;
 
+  128: optional bool enable_adaptive_pipeline_task_serial_read_on_limit = true;
+  129: optional i32 adaptive_pipeline_task_serial_read_on_limit = 10000;
+
   // For cloud, to control if the content would be written into file cache
   // In write path, to control if the content would be written into file cache.
   // In read path, read from file cache or remote storage when execute query.
diff --git a/regression-test/suites/query_profile/adaptive_pipeline_task_serial_read_on_limit.groovy b/regression-test/suites/query_profile/adaptive_pipeline_task_serial_read_on_limit.groovy
new file mode 100644
index 00000000000..15481fe7c9a
--- /dev/null
+++ b/regression-test/suites/query_profile/adaptive_pipeline_task_serial_read_on_limit.groovy
@@ -0,0 +1,175 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+import groovy.json.StringEscapeUtils
+
+
+def getProfileList = {
+    def dst = 'http://' + context.config.feHttpAddress
+    def conn = new URL(dst + "/rest/v1/query_profile").openConnection()
+    conn.setRequestMethod("GET")
+    def encoding = Base64.getEncoder().encodeToString((context.config.feHttpUser + ":" + 
+            (context.config.feHttpPassword == null ? "" : context.config.feHttpPassword)).getBytes("UTF-8"))
+    conn.setRequestProperty("Authorization", "Basic ${encoding}")
+    return conn.getInputStream().getText()
+}
+
+
+def getProfile = { id ->
+        def dst = 'http://' + context.config.feHttpAddress
+        def conn = new URL(dst + "/api/profile/text/?query_id=$id").openConnection()
+        conn.setRequestMethod("GET")
+        def encoding = Base64.getEncoder().encodeToString((context.config.feHttpUser + ":" + 
+                (context.config.feHttpPassword == null ? "" : context.config.feHttpPassword)).getBytes("UTF-8"))
+        conn.setRequestProperty("Authorization", "Basic ${encoding}")
+        return conn.getInputStream().getText()
+}
+
+suite('adaptive_pipeline_task_serial_read_on_limit') {
+    sql """
+        DROP TABLE IF EXISTS adaptive_pipeline_task_serial_read_on_limit;
+    """
+    sql """
+        CREATE TABLE if not exists `adaptive_pipeline_task_serial_read_on_limit` (
+            `id` INT,
+            `name` varchar(32)
+        ) ENGINE=OLAP
+        DISTRIBUTED BY HASH(`id`) BUCKETS 10
+        PROPERTIES (
+            "replication_allocation" = "tag.location.default: 1"
+        );
+    """
+
+    // Insert data to table
+    sql """
+        insert into adaptive_pipeline_task_serial_read_on_limit values 
+        (1, "A"),(2, "B"),(3, "C"),(4, "D"),(5,"E"),(6,"F"),(7,"G"),(8,"H"),(9,"K");
+    """
+    sql """
+        insert into adaptive_pipeline_task_serial_read_on_limit values 
+        (10, "A"),(20, "B"),(30, "C"),(40, "D"),(50,"E"),(60,"F"),(70,"G"),(80,"H"),(90,"K");
+    """
+    sql """
+        insert into adaptive_pipeline_task_serial_read_on_limit values 
+        (101, "A"),(201, "B"),(301, "C"),(401, "D"),(501,"E"),(601,"F"),(701,"G"),(801,"H"),(901,"K");
+    """
+    sql """
+        insert into adaptive_pipeline_task_serial_read_on_limit values 
+        (1010, "A"),(2010, "B"),(3010, "C"),(4010, "D"),(5010,"E"),(6010,"F"),(7010,"G"),(8010,"H"),(9010,"K");
+    """
+    
+    def uuidString = UUID.randomUUID().toString()
+    sql "set enable_profile=true"
+    // set parallel_pipeline_task_num to 1 so that only one scan node,
+    // and we can check MaxScannerThreadNum in profile.
+    sql "set parallel_pipeline_task_num=1;"
+    // no limit, MaxScannerThreadNum = TabletNum
+    sql """
+        select "no_limit_1_${uuidString}", * from adaptive_pipeline_task_serial_read_on_limit;
+    """
+    sql "set parallel_pipeline_task_num=0;"
+    // With Limit, MaxScannerThreadNum = 1
+    sql """
+        select "with_limit_1_${uuidString}", * from adaptive_pipeline_task_serial_read_on_limit limit 10000;
+    """
+    // With Limit, but bigger then adaptive_pipeline_task_serial_read_on_limit,  MaxScannerThreadNum = TabletNum
+    sql """
+        select "with_limit_2_${uuidString}", * from adaptive_pipeline_task_serial_read_on_limit limit 10001;
+    """
+    sql """
+        set enable_adaptive_pipeline_task_serial_read_on_limit=false;
+    """
+    sql "set parallel_pipeline_task_num=1;"
+    // Forbid the strategy, with limit, MaxScannerThreadNum = TabletNum
+    sql """
+        select "not_enable_limit_${uuidString}", * from adaptive_pipeline_task_serial_read_on_limit limit 100;
+    """
+
+    sql "set parallel_pipeline_task_num=0;"
+
+    // Enable the strategy, with limit 20, MaxScannerThreadNum = 1
+    sql """
+        set enable_adaptive_pipeline_task_serial_read_on_limit=true;
+    """
+    sql """
+        set adaptive_pipeline_task_serial_read_on_limit=10;
+    """
+    sql """
+        select "modify_to_20_${uuidString}", * from adaptive_pipeline_task_serial_read_on_limit limit 15;
+    """
+
+    sql "set enable_profile=false"
+
+    def wholeString = getProfileList()
+    List profileData = new JsonSlurper().parseText(wholeString).data.rows
+    String queryIdNoLimit1 = "";
+    String queryIdWithLimit1 = "";
+    String queryIdWithLimit2 = "";
+    String queryIDNotEnableLimit = "";
+    String queryIdModifyTo20 = "";
+
+    logger.info("{}", uuidString)
+
+    for (def profileItem in profileData) {
+        if (profileItem["Sql Statement"].toString().contains("no_limit_1_${uuidString}")) {
+            queryIdNoLimit1 = profileItem["Profile ID"].toString()
+            logger.info("profileItem: {}", profileItem)
+        }
+        if (profileItem["Sql Statement"].toString().contains("with_limit_1_${uuidString}")) {
+            queryIdWithLimit1 = profileItem["Profile ID"].toString()
+            logger.info("profileItem: {}", profileItem)
+        }
+        if (profileItem["Sql Statement"].toString().contains("with_limit_2_${uuidString}")) {
+            queryIdWithLimit2 = profileItem["Profile ID"].toString()
+            logger.info("profileItem: {}", profileItem)
+        }
+        if (profileItem["Sql Statement"].toString().contains("not_enable_limit_${uuidString}")) {
+            queryIDNotEnableLimit = profileItem["Profile ID"].toString()
+            logger.info("profileItem: {}", profileItem)
+        }
+        if (profileItem["Sql Statement"].toString().contains("modify_to_20_${uuidString}")) {
+            queryIdModifyTo20 = profileItem["Profile ID"].toString()
+            logger.info("profileItem: {}", profileItem)
+        }
+    }
+    
+    logger.info("queryIdNoLimit1_${uuidString}: {}", queryIdNoLimit1)
+    logger.info("queryIdWithLimit1_${uuidString}: {}", queryIdWithLimit1)
+    logger.info("queryIdWithLimit2_${uuidString}: {}", queryIdWithLimit2)
+    logger.info("queryIDNotEnableLimit_${uuidString}: {}", queryIDNotEnableLimit)
+    logger.info("queryIdModifyTo20_${uuidString}: {}", queryIdModifyTo20)
+
+    assertTrue(queryIdNoLimit1 != "")
+    assertTrue(queryIdWithLimit1 != "")
+    assertTrue(queryIdWithLimit2 != "")
+    assertTrue(queryIDNotEnableLimit != "")
+    assertTrue(queryIdModifyTo20 != "")
+
+    def String profileNoLimit1 = getProfile(queryIdNoLimit1).toString()
+    def String profileWithLimit1 = getProfile(queryIdWithLimit1).toString()
+    def String profileWithLimit2 = getProfile(queryIdWithLimit2).toString()
+    def String profileNotEnableLimit = getProfile(queryIDNotEnableLimit).toString()
+    def String profileModifyTo20 = getProfile(queryIdModifyTo20).toString()
+    
+    assertTrue(profileNoLimit1.contains("- MaxScannerThreadNum: 10"))
+    assertTrue(profileWithLimit1.contains("- MaxScannerThreadNum: 1"))
+    assertTrue(profileWithLimit2.contains("- MaxScannerThreadNum: 10"))
+    assertTrue(profileNotEnableLimit.contains("- MaxScannerThreadNum: 10"))
+    assertTrue(profileModifyTo20.contains("- MaxScannerThreadNum: 1"))
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org