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/18 10:35:22 UTC

(doris) branch spill_and_reserve updated: [feature](workloadgroup) use slot num to control memory distribution among queries (#40935)

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

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


The following commit(s) were added to refs/heads/spill_and_reserve by this push:
     new ccc47f32c4c [feature](workloadgroup) use slot num to control memory distribution among queries (#40935)
ccc47f32c4c is described below

commit ccc47f32c4c37d9b3fae3d53d68e05f76d3b4bed
Author: yiguolei <67...@qq.com>
AuthorDate: Wed Sep 18 18:35:12 2024 +0800

    [feature](workloadgroup) use slot num to control memory distribution among queries (#40935)
    
    ## Proposed changes
    
    Issue Number: close #xxx
    
    <!--Describe your changes.-->
    
    ---------
    
    Co-authored-by: yiguolei <yi...@gmail.com>
---
 be/src/runtime/fragment_mgr.cpp                    |  3 +-
 be/src/runtime/memory/mem_tracker_limiter.cpp      |  4 +-
 be/src/runtime/memory/mem_tracker_limiter.h        |  3 +-
 be/src/runtime/query_context.cpp                   | 16 ++--
 be/src/runtime/query_context.h                     | 18 ++--
 be/src/runtime/thread_context.cpp                  | 16 +++-
 be/src/runtime/workload_group/workload_group.cpp   | 12 ++-
 be/src/runtime/workload_group/workload_group.h     | 16 +++-
 .../workload_group/workload_group_manager.cpp      | 97 +++++++++++++++-------
 be/test/runtime/memory/mem_tracker_test.cpp        |  6 --
 .../main/java/org/apache/doris/qe/Coordinator.java |  2 +-
 .../java/org/apache/doris/qe/SessionVariable.java  | 26 ++++++
 .../doris/resource/workloadgroup/QueryQueue.java   | 36 ++++++--
 .../doris/resource/workloadgroup/QueueToken.java   |  8 +-
 .../resource/workloadgroup/WorkloadGroup.java      |  5 ++
 gensrc/thrift/BackendService.thrift                |  1 +
 gensrc/thrift/PaloInternalService.thrift           |  6 +-
 17 files changed, 204 insertions(+), 71 deletions(-)

diff --git a/be/src/runtime/fragment_mgr.cpp b/be/src/runtime/fragment_mgr.cpp
index 6324cd31b7e..b304a8d33a7 100644
--- a/be/src/runtime/fragment_mgr.cpp
+++ b/be/src/runtime/fragment_mgr.cpp
@@ -753,8 +753,7 @@ Status FragmentMgr::_get_query_ctx(const Params& params, TUniqueId query_id, boo
         // temp query ctx now. For example, the query id maybe removed from workload group's queryset.
         _query_ctx_map.insert(std::make_pair(query_ctx->query_id(), query_ctx));
         LOG(INFO) << "Register query/load memory tracker, query/load id: "
-                  << print_id(query_ctx->query_id())
-                  << " limit: " << PrettyPrinter::print(query_ctx->mem_limit(), TUnit::BYTES);
+                  << print_id(query_ctx->query_id());
     }
     return Status::OK();
 }
diff --git a/be/src/runtime/memory/mem_tracker_limiter.cpp b/be/src/runtime/memory/mem_tracker_limiter.cpp
index 78e66b6a579..3a30ae1c5bf 100644
--- a/be/src/runtime/memory/mem_tracker_limiter.cpp
+++ b/be/src/runtime/memory/mem_tracker_limiter.cpp
@@ -709,7 +709,7 @@ int64_t MemTrackerLimiter::free_top_overcommit_query(
                     seek_num++;
                     // 32M small query does not cancel
                     if (tracker->consumption() <= 33554432 ||
-                        tracker->consumption() < tracker->limit()) {
+                        tracker->consumption() < tracker->_limit) {
                         small_num++;
                         continue;
                     }
@@ -719,7 +719,7 @@ int64_t MemTrackerLimiter::free_top_overcommit_query(
                         continue;
                     }
                     auto overcommit_ratio = int64_t(
-                            (static_cast<double>(tracker->consumption()) / tracker->limit()) *
+                            (static_cast<double>(tracker->consumption()) / tracker->_limit) *
                             10000);
                     max_pq.emplace(overcommit_ratio, tracker->label());
                     query_consumption[tracker->label()] = tracker->consumption();
diff --git a/be/src/runtime/memory/mem_tracker_limiter.h b/be/src/runtime/memory/mem_tracker_limiter.h
index faf354cca4c..bc7e966e27f 100644
--- a/be/src/runtime/memory/mem_tracker_limiter.h
+++ b/be/src/runtime/memory/mem_tracker_limiter.h
@@ -120,6 +120,7 @@ public:
     bool limit_exceeded() const { return _limit >= 0 && _limit < consumption(); }
     Status check_limit(int64_t bytes = 0);
     bool is_overcommit_tracker() const { return type() == Type::QUERY || type() == Type::LOAD; }
+    void set_limit(int64_t new_mem_limit) { _limit = new_mem_limit; }
     bool is_query_cancelled() { return _is_query_cancelled; }
     void set_is_query_cancelled(bool is_cancelled) { _is_query_cancelled.store(is_cancelled); }
 
@@ -340,7 +341,7 @@ private:
     MemCounter _reserved_counter;
 
     // Limit on memory consumption, in bytes.
-    int64_t _limit;
+    std::atomic<int64_t> _limit;
 
     // Group number in mem_tracker_limiter_pool and mem_tracker_pool, generated by the timestamp.
     int64_t _group_num;
diff --git a/be/src/runtime/query_context.cpp b/be/src/runtime/query_context.cpp
index 072443729b2..47d750978be 100644
--- a/be/src/runtime/query_context.cpp
+++ b/be/src/runtime/query_context.cpp
@@ -117,26 +117,26 @@ QueryContext::QueryContext(TUniqueId query_id, ExecEnv* exec_env,
 
 void QueryContext::_init_query_mem_tracker() {
     bool has_query_mem_limit = _query_options.__isset.mem_limit && (_query_options.mem_limit > 0);
-    int64_t _bytes_limit = has_query_mem_limit ? _query_options.mem_limit : -1;
-    if (_bytes_limit > MemInfo::mem_limit()) {
-        VLOG_NOTICE << "Query memory limit " << PrettyPrinter::print(_bytes_limit, TUnit::BYTES)
+    int64_t bytes_limit = has_query_mem_limit ? _query_options.mem_limit : -1;
+    if (bytes_limit > MemInfo::mem_limit()) {
+        VLOG_NOTICE << "Query memory limit " << PrettyPrinter::print(bytes_limit, TUnit::BYTES)
                     << " exceeds process memory limit of "
                     << PrettyPrinter::print(MemInfo::mem_limit(), TUnit::BYTES)
                     << ". Using process memory limit instead";
-        _bytes_limit = MemInfo::mem_limit();
+        bytes_limit = MemInfo::mem_limit();
     }
     if (_query_options.query_type == TQueryType::SELECT) {
         query_mem_tracker = MemTrackerLimiter::create_shared(
                 MemTrackerLimiter::Type::QUERY, fmt::format("Query#Id={}", print_id(_query_id)),
-                _bytes_limit);
+                bytes_limit);
     } else if (_query_options.query_type == TQueryType::LOAD) {
         query_mem_tracker = MemTrackerLimiter::create_shared(
                 MemTrackerLimiter::Type::LOAD, fmt::format("Load#Id={}", print_id(_query_id)),
-                _bytes_limit);
+                bytes_limit);
     } else { // EXTERNAL
         query_mem_tracker = MemTrackerLimiter::create_shared(
                 MemTrackerLimiter::Type::LOAD, fmt::format("External#Id={}", print_id(_query_id)),
-                _bytes_limit);
+                bytes_limit);
     }
     if (_query_options.__isset.is_report_success && _query_options.is_report_success) {
         query_mem_tracker->enable_print_log_usage();
@@ -152,7 +152,7 @@ QueryContext::~QueryContext() {
     std::string mem_tracker_msg;
     if (query_mem_tracker->peak_consumption() != 0) {
         mem_tracker_msg = fmt::format(
-                ", deregister query/load memory tracker, queryId={}, Limit={}, CurrUsed={}, "
+                ", deregister query/load memory tracker, queryId={}, CurrUsed={}, "
                 "PeakUsed={}",
                 print_id(_query_id), MemCounter::print_bytes(query_mem_tracker->limit()),
                 MemCounter::print_bytes(query_mem_tracker->consumption()),
diff --git a/be/src/runtime/query_context.h b/be/src/runtime/query_context.h
index 0a19cd61f94..73d34fc0116 100644
--- a/be/src/runtime/query_context.h
+++ b/be/src/runtime/query_context.h
@@ -207,8 +207,6 @@ public:
 
     std::vector<TUniqueId> get_fragment_instance_ids() const { return fragment_instance_ids; }
 
-    int64_t mem_limit() const { return _bytes_limit; }
-
     void set_merge_controller_handler(
             std::shared_ptr<RuntimeFilterMergeControllerEntity>& handler) {
         _merge_controller_handler = handler;
@@ -238,8 +236,19 @@ public:
                             bool* has_running_task) const;
     size_t get_revocable_size() const;
 
-    void set_spill_threshold(int64_t spill_threshold) { _spill_threshold = spill_threshold; }
-    int64_t spill_threshold() { return _spill_threshold; }
+    void set_mem_limit(int64_t new_mem_limit) { query_mem_tracker->set_limit(new_mem_limit); }
+
+    std::shared_ptr<MemTrackerLimiter>& get_mem_tracker() { return query_mem_tracker; }
+
+    int32_t get_slot_count() {
+        return _query_options.__isset.query_slot_count ? _query_options.query_slot_count : 1;
+    }
+
+    bool enable_query_slot_hard_limit() {
+        return _query_options.__isset.enable_query_slot_hard_limit
+                       ? _query_options.enable_query_slot_hard_limit
+                       : false;
+    }
     DescriptorTbl* desc_tbl = nullptr;
     bool set_rsc_info = false;
     std::string user;
@@ -319,7 +328,6 @@ private:
     TUniqueId _query_id;
     ExecEnv* _exec_env = nullptr;
     MonotonicStopWatch _query_watcher;
-    int64_t _bytes_limit = 0;
     bool _is_pipeline = false;
     bool _is_nereids = false;
     std::atomic<int> _running_big_mem_op_num = 0;
diff --git a/be/src/runtime/thread_context.cpp b/be/src/runtime/thread_context.cpp
index c89f532e592..55c5273ef71 100644
--- a/be/src/runtime/thread_context.cpp
+++ b/be/src/runtime/thread_context.cpp
@@ -46,10 +46,18 @@ AttachTask::AttachTask(const std::shared_ptr<MemTrackerLimiter>& mem_tracker) {
 
 AttachTask::AttachTask(RuntimeState* runtime_state) {
     signal::set_signal_is_nereids(runtime_state->is_nereids());
-    QueryThreadContext query_thread_context = {runtime_state->query_id(),
-                                               runtime_state->query_mem_tracker(),
-                                               runtime_state->get_query_ctx()->workload_group()};
-    init(query_thread_context);
+    // RuntimeState not always has query ctx.
+    // For example during push handler or schema change
+    if (runtime_state->get_query_ctx() == nullptr) {
+        QueryThreadContext query_thread_context = {runtime_state->query_id(),
+                                                   runtime_state->query_mem_tracker()};
+        init(query_thread_context);
+    } else {
+        QueryThreadContext query_thread_context = {
+                runtime_state->query_id(), runtime_state->query_mem_tracker(),
+                runtime_state->get_query_ctx()->workload_group()};
+        init(query_thread_context);
+    }
 }
 
 AttachTask::AttachTask(const QueryThreadContext& query_thread_context) {
diff --git a/be/src/runtime/workload_group/workload_group.cpp b/be/src/runtime/workload_group/workload_group.cpp
index 4d53ffc9686..dac52e1def4 100644
--- a/be/src/runtime/workload_group/workload_group.cpp
+++ b/be/src/runtime/workload_group/workload_group.cpp
@@ -49,6 +49,8 @@ const static bool ENABLE_MEMORY_OVERCOMMIT_DEFAULT_VALUE = true;
 const static int CPU_HARD_LIMIT_DEFAULT_VALUE = -1;
 const static int SPILL_LOW_WATERMARK_DEFAULT_VALUE = 50;
 const static int SPILL_HIGH_WATERMARK_DEFAULT_VALUE = 80;
+// This is a invalid value, and should ignore this value during usage
+const static int TOTAL_QUERY_SLOT_COUNT_DEFAULT_VALUE = 0;
 
 WorkloadGroup::WorkloadGroup(const WorkloadGroupInfo& tg_info)
         : _id(tg_info.id),
@@ -141,6 +143,7 @@ void WorkloadGroup::check_and_update(const WorkloadGroupInfo& tg_info) {
             _spill_high_watermark = tg_info.spill_high_watermark;
             _scan_bytes_per_second = tg_info.read_bytes_per_second;
             _remote_scan_bytes_per_second = tg_info.remote_read_bytes_per_second;
+            _total_query_slot_count = tg_info.total_query_slot_count;
         } else {
             return;
         }
@@ -403,6 +406,12 @@ WorkloadGroupInfo WorkloadGroupInfo::parse_topic_info(
         remote_read_bytes_per_second = tworkload_group_info.remote_read_bytes_per_second;
     }
 
+    // 16 total slots
+    int total_query_slot_count = TOTAL_QUERY_SLOT_COUNT_DEFAULT_VALUE;
+    if (tworkload_group_info.__isset.total_query_slot_count) {
+        total_query_slot_count = tworkload_group_info.total_query_slot_count;
+    }
+
     return {.id = tg_id,
             .name = name,
             .cpu_share = cpu_share,
@@ -417,7 +426,8 @@ WorkloadGroupInfo WorkloadGroupInfo::parse_topic_info(
             .spill_low_watermark = spill_low_watermark,
             .spill_high_watermark = spill_high_watermark,
             .read_bytes_per_second = read_bytes_per_second,
-            .remote_read_bytes_per_second = remote_read_bytes_per_second};
+            .remote_read_bytes_per_second = remote_read_bytes_per_second,
+            .total_query_slot_count = total_query_slot_count};
 }
 
 void WorkloadGroup::upsert_task_scheduler(WorkloadGroupInfo* tg_info, ExecEnv* exec_env) {
diff --git a/be/src/runtime/workload_group/workload_group.h b/be/src/runtime/workload_group/workload_group.h
index 2fbb4dd3030..fa7fc787870 100644
--- a/be/src/runtime/workload_group/workload_group.h
+++ b/be/src/runtime/workload_group/workload_group.h
@@ -94,12 +94,22 @@ public:
     int spill_threshold_low_water_mark() const {
         return _spill_low_watermark.load(std::memory_order_relaxed);
     }
-    int spill_threashold_high_water_mark() const {
+
+    int spill_threshold_high_water_mark() const {
         return _spill_high_watermark.load(std::memory_order_relaxed);
     }
 
-    void set_weighted_memory_ratio(double ratio);
+    int total_query_slot_count() const {
+        return _total_query_slot_count.load(std::memory_order_relaxed);
+    }
+
     bool add_wg_refresh_interval_memory_growth(int64_t size) {
+        // If a group is enable memory overcommit, then not need check the limit
+        // It is always true, and it will only fail when process memory is not
+        // enough.
+        if (_enable_memory_overcommit) {
+            return true;
+        }
         auto realtime_total_mem_used =
                 _total_mem_used + _wg_refresh_interval_memory_growth.load() + size;
         if ((realtime_total_mem_used >
@@ -232,6 +242,7 @@ private:
     std::atomic<int> _spill_high_watermark;
     std::atomic<int64_t> _scan_bytes_per_second {-1};
     std::atomic<int64_t> _remote_scan_bytes_per_second {-1};
+    std::atomic<int> _total_query_slot_count = 0;
 
     // means workload group is mark dropped
     // new query can not submit
@@ -275,6 +286,7 @@ struct WorkloadGroupInfo {
     const int spill_high_watermark = 0;
     const int read_bytes_per_second = -1;
     const int remote_read_bytes_per_second = -1;
+    const int total_query_slot_count = 0;
     // log cgroup cpu info
     uint64_t cgroup_cpu_shares = 0;
     int cgroup_cpu_hard_limit = 0;
diff --git a/be/src/runtime/workload_group/workload_group_manager.cpp b/be/src/runtime/workload_group/workload_group_manager.cpp
index da3a9313836..c17b55f8956 100644
--- a/be/src/runtime/workload_group/workload_group_manager.cpp
+++ b/be/src/runtime/workload_group/workload_group_manager.cpp
@@ -146,7 +146,6 @@ struct WorkloadGroupMemInfo {
     std::list<std::shared_ptr<MemTrackerLimiter>> tracker_snapshots =
             std::list<std::shared_ptr<MemTrackerLimiter>>();
 };
-
 void WorkloadGroupMgr::refresh_wg_weighted_memory_limit() {
     std::shared_lock<std::shared_mutex> r_lock(_group_mutex);
 
@@ -195,59 +194,99 @@ void WorkloadGroupMgr::refresh_wg_weighted_memory_limit() {
             doris::GlobalMemoryArbitrator::sys_mem_available_details_str(),
             PrettyPrinter::print(all_workload_groups_mem_usage, TUnit::BYTES),
             weighted_memory_limit_ratio);
-    LOG_EVERY_T(INFO, 10) << debug_msg;
-
+    LOG_EVERY_T(INFO, 60) << debug_msg;
     for (auto& wg : _workload_groups) {
-        // 3.1 calculate query spill threshold of task group
-        auto wg_weighted_mem_limit =
-                int64_t(wg.second->memory_limit() * weighted_memory_limit_ratio);
+        auto wg_mem_limit = wg.second->memory_limit();
+        auto wg_weighted_mem_limit = int64_t(wg_mem_limit * weighted_memory_limit_ratio);
         wg.second->set_weighted_memory_limit(wg_weighted_mem_limit);
-
-        // 3.2 set workload groups weighted memory limit and all query spill threshold.
-        auto wg_query_count = wgs_mem_info[wg.first].tracker_snapshots.size();
-        int64_t query_spill_threshold =
-                wg_query_count ? (wg_weighted_mem_limit + wg_query_count) / wg_query_count
-                               : wg_weighted_mem_limit;
-        for (const auto& query : wg.second->queries()) {
-            auto query_ctx = query.second.lock();
-            if (!query_ctx) {
-                continue;
-            }
-            query_ctx->set_spill_threshold(query_spill_threshold);
-        }
-
-        // 3.3 only print debug logs, if workload groups is_high_wartermark or is_low_wartermark.
+        auto all_query_ctxs = wg.second->queries();
         bool is_low_wartermark = false;
         bool is_high_wartermark = false;
         wg.second->check_mem_used(&is_low_wartermark, &is_high_wartermark);
+        int64_t wg_high_water_mark_limit =
+                wg_mem_limit * wg.second->spill_threshold_high_water_mark() / 100;
+        int64_t weighted_high_water_mark_limit =
+                wg_weighted_mem_limit * wg.second->spill_threshold_high_water_mark() / 100;
         std::string debug_msg;
         if (is_high_wartermark || is_low_wartermark) {
             debug_msg = fmt::format(
                     "\nWorkload Group {}: mem limit: {}, mem used: {}, weighted mem limit: {}, "
-                    "used "
-                    "ratio: {}, query count: {}, query spill threshold: {}",
+                    "high water mark mem limit: {}, used ratio: {}",
                     wg.second->name(),
                     PrettyPrinter::print(wg.second->memory_limit(), TUnit::BYTES),
                     PrettyPrinter::print(wgs_mem_info[wg.first].total_mem_used, TUnit::BYTES),
                     PrettyPrinter::print(wg_weighted_mem_limit, TUnit::BYTES),
-                    (double)wgs_mem_info[wg.first].total_mem_used / wg_weighted_mem_limit,
-                    wg_query_count, PrettyPrinter::print(query_spill_threshold, TUnit::BYTES));
+                    PrettyPrinter::print(weighted_high_water_mark_limit, TUnit::BYTES),
+                    (double)wgs_mem_info[wg.first].total_mem_used / wg_weighted_mem_limit);
 
             debug_msg += "\n  Query Memory Summary:";
             // check whether queries need to revoke memory for task group
             for (const auto& query_mem_tracker : wgs_mem_info[wg.first].tracker_snapshots) {
                 debug_msg += fmt::format(
-                        "\n    MemTracker Label={}, Used={}, SpillThreshold={}, "
+                        "\n    MemTracker Label={}, Used={}, MemLimit={}, "
                         "Peak={}",
                         query_mem_tracker->label(),
                         PrettyPrinter::print(query_mem_tracker->consumption(), TUnit::BYTES),
-                        PrettyPrinter::print(query_spill_threshold, TUnit::BYTES),
+                        PrettyPrinter::print(query_mem_tracker->limit(), TUnit::BYTES),
                         PrettyPrinter::print(query_mem_tracker->peak_consumption(), TUnit::BYTES));
             }
-            LOG_EVERY_T(INFO, 1) << debug_msg;
-        } else {
             continue;
         }
+
+        int32_t total_used_slot_count = 0;
+        int32_t total_slot_count = wg.second->total_query_slot_count();
+        // calculate total used slot count
+        for (const auto& query : all_query_ctxs) {
+            auto query_ctx = query.second.lock();
+            if (!query_ctx) {
+                continue;
+            }
+            total_used_slot_count += query_ctx->get_slot_count();
+        }
+        // calculate per query weighted memory limit
+        debug_msg = "Query Memory Summary:";
+        for (const auto& query : all_query_ctxs) {
+            auto query_ctx = query.second.lock();
+            if (!query_ctx) {
+                continue;
+            }
+            int64_t query_weighted_mem_limit = 0;
+            // If the query enable hard limit, then it should not use the soft limit
+            if (query_ctx->enable_query_slot_hard_limit()) {
+                if (total_slot_count < 1) {
+                    LOG(WARNING)
+                            << "query " << print_id(query_ctx->query_id())
+                            << " enabled hard limit, but the slot count < 1, could not take affect";
+                } else {
+                    // If the query enable hard limit, then not use weighted info any more, just use the settings limit.
+                    query_weighted_mem_limit =
+                            (wg_high_water_mark_limit * query_ctx->get_slot_count()) /
+                            total_slot_count;
+                }
+            } else {
+                // If low water mark is not reached, then use process memory limit as query memory limit.
+                // It means it will not take effect.
+                if (!is_low_wartermark) {
+                    query_weighted_mem_limit = process_memory_limit;
+                } else {
+                    query_weighted_mem_limit =
+                            total_used_slot_count > 0
+                                    ? (wg_high_water_mark_limit + total_used_slot_count) *
+                                              query_ctx->get_slot_count() / total_used_slot_count
+                                    : wg_high_water_mark_limit;
+                }
+            }
+            debug_msg += fmt::format(
+                    "\n    MemTracker Label={}, Used={}, Limit={}, Peak={}",
+                    query_ctx->get_mem_tracker()->label(),
+                    PrettyPrinter::print(query_ctx->get_mem_tracker()->consumption(), TUnit::BYTES),
+                    PrettyPrinter::print(query_weighted_mem_limit, TUnit::BYTES),
+                    PrettyPrinter::print(query_ctx->get_mem_tracker()->peak_consumption(),
+                                         TUnit::BYTES));
+
+            query_ctx->set_mem_limit(query_weighted_mem_limit);
+        }
+        LOG_EVERY_T(INFO, 60) << debug_msg;
     }
 }
 
diff --git a/be/test/runtime/memory/mem_tracker_test.cpp b/be/test/runtime/memory/mem_tracker_test.cpp
index 49f6aa3bf0c..5104cf8a40d 100644
--- a/be/test/runtime/memory/mem_tracker_test.cpp
+++ b/be/test/runtime/memory/mem_tracker_test.cpp
@@ -27,30 +27,24 @@ namespace doris {
 
 TEST(MemTrackerTest, SingleTrackerNoLimit) {
     auto t = MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::GLOBAL);
-    EXPECT_FALSE(t->has_limit());
     t->consume(10);
     EXPECT_EQ(t->consumption(), 10);
     t->consume(10);
     EXPECT_EQ(t->consumption(), 20);
     t->release(15);
     EXPECT_EQ(t->consumption(), 5);
-    EXPECT_FALSE(t->limit_exceeded());
     t->release(5);
 }
 
 TEST(MemTrackerTest, SingleTrackerWithLimit) {
     auto t = std::make_unique<MemTrackerLimiter>(MemTrackerLimiter::Type::GLOBAL, "limit tracker",
                                                  11);
-    EXPECT_TRUE(t->has_limit());
     t->consume(10);
     EXPECT_EQ(t->consumption(), 10);
-    EXPECT_FALSE(t->limit_exceeded());
     t->consume(10);
     EXPECT_EQ(t->consumption(), 20);
-    EXPECT_TRUE(t->limit_exceeded());
     t->release(15);
     EXPECT_EQ(t->consumption(), 5);
-    EXPECT_FALSE(t->limit_exceeded());
     t->release(5);
 }
 
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java b/fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java
index 5e3a59d9a54..a4be2ad03ef 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java
@@ -677,7 +677,7 @@ public class Coordinator implements CoordInterface {
                         // throw exception during workload group manager.
                         throw new UserException("could not find query queue");
                     }
-                    queueToken = queryQueue.getToken();
+                    queueToken = queryQueue.getToken(context.getSessionVariable().wgQuerySlotCount);
                     queueToken.get(DebugUtil.printId(queryId),
                             this.queryOptions.getExecutionTimeout() * 1000);
                 }
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 14deb3b755b..3d0e1d8e2f8 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
@@ -628,6 +628,10 @@ public class SessionVariable implements Serializable, Writable {
 
     public static final String BYPASS_WORKLOAD_GROUP = "bypass_workload_group";
 
+    public static final String QUERY_SLOT_COUNT = "query_slot_count";
+
+    public static final String ENABLE_QUERY_SLOT_HARD_LIMIT = "enable_query_slot_hard_limit";
+
     public static final String MAX_COLUMN_READER_NUM = "max_column_reader_num";
 
     public static final String USE_MAX_LENGTH_OF_VARCHAR_IN_CTAS = "use_max_length_of_varchar_in_ctas";
@@ -781,6 +785,25 @@ public class SessionVariable implements Serializable, Writable {
             "whether bypass workload group's limitation, currently only support bypass query queue"})
     public boolean bypassWorkloadGroup = false;
 
+    @VariableMgr.VarAttr(name = QUERY_SLOT_COUNT, needForward = true, checker = "checkQuerySlotCount",
+            description = {
+            "每个查询占用的slot的数量,workload group的query slot的总数等于设置的最大并发数",
+            "Number of slots occupied by each query, the total number of query slots "
+            + "of the workload group equals the maximum number of concurrent requests"})
+    public int wgQuerySlotCount = 1;
+
+    public void checkQuerySlotCount(String slotCnt) {
+        Long slotCount = Long.valueOf(slotCnt);
+        if (slotCount < 1 || slotCount > 1025) {
+            throw new InvalidParameterException("query_slot_count should be between 1 and 1024)");
+        }
+    }
+
+    @VariableMgr.VarAttr(name = ENABLE_QUERY_SLOT_HARD_LIMIT, needForward = true, description = {
+            "是否通过硬限的方式来计算每个Slot的内存资源",
+            "Whether to calculate the memory resources of each Slot by hard limit"})
+    public boolean enableQuerySlotHardLimit = false;
+
     @VariableMgr.VarAttr(name = MAX_COLUMN_READER_NUM)
     public int maxColumnReaderNum = 20000;
 
@@ -3783,6 +3806,9 @@ public class SessionVariable implements Serializable, Writable {
         tResult.setEnableFallbackOnMissingInvertedIndex(enableFallbackOnMissingInvertedIndex);
         tResult.setHiveOrcUseColumnNames(hiveOrcUseColumnNames);
         tResult.setHiveParquetUseColumnNames(hiveParquetUseColumnNames);
+        tResult.setQuerySlotCount(wgQuerySlotCount);
+        tResult.setEnableQuerySlotHardLimit(enableQuerySlotHardLimit);
+
         tResult.setKeepCarriageReturn(keepCarriageReturn);
 
         tResult.setEnableSegmentCache(enableSegmentCache);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/QueryQueue.java b/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/QueryQueue.java
index ba2d2526f2e..0da702ad3f9 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/QueryQueue.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/QueryQueue.java
@@ -26,6 +26,7 @@ import org.apache.doris.resource.workloadgroup.QueueToken.TokenState;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.PriorityQueue;
 import java.util.Queue;
@@ -98,24 +99,42 @@ public class QueryQueue {
                 + runningQueryQueue.size() + ", currentWaitingQueryNum=" + waitingQueryQueue.size();
     }
 
-    public QueueToken getToken() throws UserException {
+    public int usedSlotCount() {
+        int cnt = 0;
+        for (Iterator iterator = runningQueryQueue.iterator(); iterator.hasNext();) {
+            QueueToken queueToken = (QueueToken) iterator.next();
+            cnt += queueToken.getQuerySlotCount();
+        }
+        return cnt;
+    }
+
+    public QueueToken getToken(int querySlotCount) throws UserException {
+        if (maxConcurrency > 0 && (querySlotCount > maxConcurrency || querySlotCount < 1)) {
+            throw new UserException("query slot count " + querySlotCount
+                    + " should be smaller than workload group's max concurrency "
+                    + maxConcurrency + " and > 0");
+        }
         AdmissionControl admissionControl = Env.getCurrentEnv().getAdmissionControl();
         queueLock.lock();
         try {
             if (LOG.isDebugEnabled()) {
                 LOG.info(this.debugString());
             }
-            QueueToken queueToken = new QueueToken(queueTimeout, this);
+            QueueToken queueToken = new QueueToken(queueTimeout, querySlotCount, this);
 
             boolean isReachMaxCon = runningQueryQueue.size() >= maxConcurrency;
+            boolean hasFreeSlot = queueToken.getQuerySlotCount() <= maxConcurrency - usedSlotCount();
             boolean isResourceAvailable = admissionControl.checkResourceAvailable(queueToken);
-            if (!isReachMaxCon && isResourceAvailable) {
+            if (!isReachMaxCon && isResourceAvailable && hasFreeSlot) {
                 runningQueryQueue.offer(queueToken);
                 queueToken.complete();
                 return queueToken;
             } else if (waitingQueryQueue.size() >= maxQueueSize) {
                 throw new UserException("query waiting queue is full, queue length=" + maxQueueSize);
             } else {
+                if (!hasFreeSlot) {
+                    queueToken.setQueueMsg("NO_FREE_SLOT");
+                }
                 if (isReachMaxCon) {
                     queueToken.setQueueMsg("WAIT_IN_QUEUE");
                 }
@@ -145,14 +164,19 @@ public class QueryQueue {
         AdmissionControl admissionControl = Env.getCurrentEnv().getAdmissionControl();
         queueLock.lock();
         try {
-            runningQueryQueue.remove(releaseToken);
-            waitingQueryQueue.remove(releaseToken);
-            admissionControl.removeQueueToken(releaseToken);
+            if (releaseToken != null) {
+                runningQueryQueue.remove(releaseToken);
+                waitingQueryQueue.remove(releaseToken);
+                admissionControl.removeQueueToken(releaseToken);
+            }
             while (runningQueryQueue.size() < maxConcurrency) {
                 QueueToken queueToken = waitingQueryQueue.peek();
                 if (queueToken == null) {
                     break;
                 }
+                if (queueToken.getQuerySlotCount() > maxConcurrency - usedSlotCount()) {
+                    break;
+                }
                 if (admissionControl.checkResourceAvailable(queueToken)) {
                     queueToken.complete();
                     runningQueryQueue.offer(queueToken);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/QueueToken.java b/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/QueueToken.java
index 748c0c21bda..67afcca1340 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/QueueToken.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/QueueToken.java
@@ -54,6 +54,7 @@ public class QueueToken implements Comparable<QueueToken> {
 
     private long queueStartTime = -1;
     private long queueEndTime = -1;
+    private final int querySlotCount;
 
     private volatile String queueMsg = "";
 
@@ -62,10 +63,11 @@ public class QueueToken implements Comparable<QueueToken> {
     // Object is just a placeholder, it's meaningless now
     private CompletableFuture<Object> future;
 
-    public QueueToken(long queueWaitTimeout, QueryQueue queryQueue) {
+    public QueueToken(long queueWaitTimeout, int querySlotCount, QueryQueue queryQueue) {
         this.tokenId = tokenIdGenerator.addAndGet(1);
         this.queueWaitTimeout = queueWaitTimeout;
         this.queueStartTime = System.currentTimeMillis();
+        this.querySlotCount = querySlotCount;
         this.queryQueue = queryQueue;
         this.future = new CompletableFuture<>();
     }
@@ -146,4 +148,8 @@ public class QueueToken implements Comparable<QueueToken> {
     public long getTokenId() {
         return tokenId;
     }
+
+    public int getQuerySlotCount() {
+        return querySlotCount;
+    }
 }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/WorkloadGroup.java b/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/WorkloadGroup.java
index 44fb98e10ef..108c48778fd 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/WorkloadGroup.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/resource/workloadgroup/WorkloadGroup.java
@@ -620,6 +620,11 @@ public class WorkloadGroup implements Writable, GsonPostProcessable {
         if (!StringUtils.isEmpty(tagStr)) {
             tWorkloadGroupInfo.setTag(tagStr);
         }
+        
+        String totalQuerySlotCountStr = properties.get(MAX_CONCURRENCY);
+        if (totalQuerySlotCountStr != null) {
+            tWorkloadGroupInfo.setTotalQuerySlotCount(Integer.parseInt(totalQuerySlotCountStr));
+        }
 
         TopicInfo topicInfo = new TopicInfo();
         topicInfo.setWorkloadGroupInfo(tWorkloadGroupInfo);
diff --git a/gensrc/thrift/BackendService.thrift b/gensrc/thrift/BackendService.thrift
index ed0ae243a1d..4acd4602432 100644
--- a/gensrc/thrift/BackendService.thrift
+++ b/gensrc/thrift/BackendService.thrift
@@ -270,6 +270,7 @@ struct TWorkloadGroupInfo {
   14: optional i64 read_bytes_per_second
   15: optional i64 remote_read_bytes_per_second
   16: optional string tag
+  17: optional i32 total_query_slot_count
 }
 
 enum TWorkloadMetricType {
diff --git a/gensrc/thrift/PaloInternalService.thrift b/gensrc/thrift/PaloInternalService.thrift
index 4dfe26be250..66d0ccfd3f1 100644
--- a/gensrc/thrift/PaloInternalService.thrift
+++ b/gensrc/thrift/PaloInternalService.thrift
@@ -332,10 +332,7 @@ struct TQueryOptions {
   125: optional bool enable_segment_cache = true;
 
   126: optional i32 runtime_bloom_filter_max_size = 16777216;
-
   127: optional i32 in_list_value_count_threshold = 10;
-
-
   // We need this two fields to make sure thrift id on master is compatible with other branch.
   128: optional bool enable_verbose_profile = false;
   129: optional i32 rpc_verbose_profile_max_instance_count = 0;
@@ -348,6 +345,9 @@ struct TQueryOptions {
   // The minimum memory that an operator required to run.
   133: optional i32 minimum_operator_memory_required_kb = 1024;
 
+  134: optional bool enable_query_slot_hard_limit = false;
+  135: optional i32 query_slot_count = 0;
+
   // 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.


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