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

mymeiyi 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 c345e92d93d [feature](fe) Add meta service RPC rate limiting in FE 
(#65694)
c345e92d93d is described below

commit c345e92d93d086da11326d449bc1935829a8d072
Author: meiyi <[email protected]>
AuthorDate: Thu Jul 23 15:17:01 2026 +0800

    [feature](fe) Add meta service RPC rate limiting in FE (#65694)
    
    Add configurable FE-side rate limiting for meta service RPCs. The change
    introduces per-method Resilience4j rate limiters, dynamic config
    validation, weighted get-version limiting for batch requests,
    rate-limit-specific metrics, and profile/audit visibility for
    get-version limiter wait time.
---
 .../main/java/org/apache/doris/common/Config.java  |  33 +++
 .../MetaServiceRpcRateLimitConfigValidator.java    | 103 ++++++++
 fe/fe-core/pom.xml                                 |   4 +
 .../apache/doris/cloud/rpc/MetaServiceProxy.java   |  97 ++++---
 .../cloud/rpc/MetaServiceRateLimitException.java   |  27 ++
 .../doris/cloud/rpc/MetaServiceRpcRateLimiter.java | 278 +++++++++++++++++++++
 .../doris/common/profile/SummaryProfile.java       |  44 +++-
 .../java/org/apache/doris/metric/CloudMetrics.java |  17 ++
 .../doris/cloud/rpc/MetaServiceProxyTest.java      | 190 ++++++++++++++
 .../cloud/rpc/MetaServiceRpcRateLimiterTest.java   | 275 ++++++++++++++++++++
 .../doris/common/profile/SummaryProfileTest.java   |  30 +++
 fe/pom.xml                                         |   6 +
 12 files changed, 1064 insertions(+), 40 deletions(-)

diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java 
b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
index 440e5599735..655af570cfb 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
@@ -3489,6 +3489,38 @@ public class Config extends ConfigBase {
             "In cloud mode, the retry count when the FE request to meta 
service times out. Default is 1."})
     public static int meta_service_rpc_timeout_retry_times = 1;
 
+    @ConfField(mutable = true, description = {
+            "Whether to enable QPS rate limit for RPC requests to meta 
service."})
+    public static boolean meta_service_rpc_rate_limit_enabled = false;
+
+    @ConfField(mutable = true, description = {
+            "Default QPS limit for each method (requests per second) in each 
cpu core, "
+                    + "non-positive value (<= 0) means no limit"})
+    public static int meta_service_rpc_rate_limit_default_qps_per_core = 50;
+
+    @ConfField(mutable = true,
+            callback = 
MetaServiceRpcRateLimitConfigValidator.QpsConfigHandler.class,
+            description = {
+                "QPS limit config per rpc method to meta service in per cpu 
core, "
+                    + "format: method1:qps1;method2:qps2, "
+                    + "e.g.: 
getPartitionVersion:100;getTableVersion:100;getTabletStats:50, "
+                    + "non-positive value (<= 0) means no limit"})
+    public static String meta_service_rpc_rate_limit_qps_per_core_config
+            = 
"getPartitionVersion:500;getTableVersion:500;getTabletStats:50;beginTxn:50";
+
+    @ConfField(mutable = true,
+            callback = 
MetaServiceRpcRateLimitConfigValidator.PositiveIntConfigHandler.class,
+            description = {
+                "Burst window for meta service RPC rate limit in seconds. "
+                    + "The long-term average QPS is unchanged, while calls can 
burst within this window."})
+    public static int meta_service_rpc_rate_limit_burst_seconds = 2;
+
+    @ConfField(mutable = true, callback = 
MetaServiceRpcRateLimitConfigValidator.NonNegativeLongConfigHandler.class,
+            description = {
+                "Max wait time in milliseconds when meta service RPC is rate 
limited, "
+                    + "zero means fail fast."})
+    public static long meta_service_rpc_rate_limit_wait_timeout_ms = 1000;
+
     @ConfField(mutable = true, description = {
             "In cloud mode, the auto start and stop ignores the databases used 
by internal jobs, "
                     + "such as those used for statistics. "
@@ -3735,4 +3767,5 @@ public class Config extends ConfigBase {
                     + "(持有主副本的桶),并在单个 tablet 写入量超过阈值(默认 200 MB)后在本地桶之间轮转。"
                     + "可降低导入内存压力并提升随机分桶表的吞吐量,覆盖所有导入类型。"})
     public static boolean enable_adaptive_random_bucket_load = true;
+
 }
diff --git 
a/fe/fe-common/src/main/java/org/apache/doris/common/MetaServiceRpcRateLimitConfigValidator.java
 
b/fe/fe-common/src/main/java/org/apache/doris/common/MetaServiceRpcRateLimitConfigValidator.java
new file mode 100644
index 00000000000..54ac359934c
--- /dev/null
+++ 
b/fe/fe-common/src/main/java/org/apache/doris/common/MetaServiceRpcRateLimitConfigValidator.java
@@ -0,0 +1,103 @@
+// 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.
+
+package org.apache.doris.common;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public final class MetaServiceRpcRateLimitConfigValidator {
+    private MetaServiceRpcRateLimitConfigValidator() {
+    }
+
+    public static Map<String, Integer> parseQpsPerCoreConfig(String config) 
throws ConfigException {
+        if (config == null || config.trim().isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, Integer> qpsPerCore = new HashMap<>();
+        Set<String> methods = new HashSet<>();
+        for (String item : config.split(";")) {
+            String trimmedItem = item.trim();
+            if (trimmedItem.isEmpty()) {
+                continue;
+            }
+            int separatorIndex = trimmedItem.indexOf(':');
+            if (separatorIndex <= 0 || separatorIndex != 
trimmedItem.lastIndexOf(':')
+                    || separatorIndex == trimmedItem.length() - 1) {
+                throw new ConfigException("Invalid format, expected 
method1:qps1;method2:qps2");
+            }
+
+            String methodName = trimmedItem.substring(0, 
separatorIndex).trim();
+            String qpsText = trimmedItem.substring(separatorIndex + 1).trim();
+            if (methodName.isEmpty() || qpsText.isEmpty()) {
+                throw new ConfigException("Invalid format, expected 
method1:qps1;method2:qps2");
+            }
+            if (!methods.add(methodName)) {
+                throw new ConfigException("Duplicate method: " + methodName);
+            }
+            try {
+                qpsPerCore.put(methodName, Integer.parseInt(qpsText));
+            } catch (NumberFormatException e) {
+                throw new ConfigException("Invalid qps for method: " + 
methodName, e);
+            }
+        }
+        return qpsPerCore;
+    }
+
+    public static void validatePositive(String fieldName, int value) throws 
ConfigException {
+        if (value <= 0) {
+            throw new ConfigException(fieldName + " must be positive");
+        }
+    }
+
+    public static void validateNonNegative(String fieldName, long value) 
throws ConfigException {
+        if (value < 0) {
+            throw new ConfigException(fieldName + " must be non-negative");
+        }
+    }
+
+    public static class QpsConfigHandler extends ConfigBase.DefaultConfHandler 
{
+        @Override
+        public void handle(Field field, String confVal) throws Exception {
+            parseQpsPerCoreConfig(confVal);
+            super.handle(field, confVal);
+        }
+    }
+
+    public static class PositiveIntConfigHandler extends 
ConfigBase.DefaultConfHandler {
+        @Override
+        public void handle(Field field, String confVal) throws Exception {
+            String trimmedVal = confVal.trim();
+            validatePositive(field.getName(), Integer.parseInt(trimmedVal));
+            super.handle(field, trimmedVal);
+        }
+    }
+
+    public static class NonNegativeLongConfigHandler extends 
ConfigBase.DefaultConfHandler {
+        @Override
+        public void handle(Field field, String confVal) throws Exception {
+            String trimmedVal = confVal.trim();
+            validateNonNegative(field.getName(), Long.parseLong(trimmedVal));
+            super.handle(field, trimmedVal);
+        }
+    }
+}
diff --git a/fe/fe-core/pom.xml b/fe/fe-core/pom.xml
index ec53a24a75e..d7a7acb8387 100644
--- a/fe/fe-core/pom.xml
+++ b/fe/fe-core/pom.xml
@@ -228,6 +228,10 @@ under the License.
             <artifactId>guava-testlib</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.github.resilience4j</groupId>
+            <artifactId>resilience4j-ratelimiter</artifactId>
+        </dependency>
         <!-- 
https://mvnrepository.com/artifact/com.googlecode.java-ipv6/java-ipv6 -->
         <dependency>
             <groupId>com.googlecode.java-ipv6</groupId>
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceProxy.java 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceProxy.java
index 25fcfbcd0d7..6b81f084717 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceProxy.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceProxy.java
@@ -19,6 +19,7 @@ package org.apache.doris.cloud.rpc;
 
 import org.apache.doris.cloud.proto.Cloud;
 import org.apache.doris.common.Config;
+import org.apache.doris.common.profile.SummaryProfile;
 import org.apache.doris.metric.CloudMetrics;
 import org.apache.doris.metric.MetricRepo;
 import org.apache.doris.rpc.RpcException;
@@ -39,6 +40,7 @@ import java.util.function.Function;
 
 public class MetaServiceProxy {
     private static final Logger LOG = 
LogManager.getLogger(MetaServiceProxy.class);
+    private static final MetaServiceRpcRateLimiter 
META_SERVICE_RPC_RATE_LIMITER = new MetaServiceRpcRateLimiter();
 
     // use exclusive lock to make sure only one thread can add or remove 
client from
     // serviceMap.
@@ -105,6 +107,7 @@ public class MetaServiceProxy {
         }
 
         try {
+            acquireRateLimit(methodName);
             final MetaServiceClient client = getProxy();
             Cloud.GetInstanceResponse response = client.getInstance(request);
             if (MetricRepo.isInit && Config.isCloudMode()) {
@@ -112,17 +115,63 @@ public class MetaServiceProxy {
                         .update(System.currentTimeMillis() - startTime);
             }
             return response;
+        } catch (MetaServiceRateLimitException e) {
+            recordRpcRateLimited(methodName);
+            throw e;
         } catch (Exception e) {
-            if (MetricRepo.isInit && Config.isCloudMode()) {
-                CloudMetrics.META_SERVICE_RPC_ALL_FAILED.increase(1L);
-                
CloudMetrics.META_SERVICE_RPC_FAILED.getOrAdd(methodName).increase(1L);
-                CloudMetrics.META_SERVICE_RPC_LATENCY.getOrAdd(methodName)
-                        .update(System.currentTimeMillis() - startTime);
-            }
+            recordRpcFailed(methodName, startTime);
             throw new RpcException("", e.getMessage(), e);
         }
     }
 
+    private static long acquireRateLimit(String methodName) throws 
RpcException {
+        return META_SERVICE_RPC_RATE_LIMITER.acquire(methodName);
+    }
+
+    private static long acquireRateLimit(String methodName, int permits) 
throws RpcException {
+        return META_SERVICE_RPC_RATE_LIMITER.acquire(methodName, permits);
+    }
+
+    private static int getGetVersionRateLimitPermits(Cloud.GetVersionRequest 
request) {
+        if (!request.getBatchMode()) {
+            return 1;
+        }
+        int permits = request.hasIsTableVersion() && 
request.getIsTableVersion()
+                ? request.getTableIdsCount()
+                : request.getPartitionIdsCount();
+        return Math.max(permits, 1);
+    }
+
+    static void resetMetaServiceRpcRateLimitForTest() {
+        META_SERVICE_RPC_RATE_LIMITER.reset();
+    }
+
+    private static void recordRpcFailed(String methodName, long startTime) {
+        if (MetricRepo.isInit && Config.isCloudMode()) {
+            CloudMetrics.META_SERVICE_RPC_ALL_FAILED.increase(1L);
+            
CloudMetrics.META_SERVICE_RPC_FAILED.getOrAdd(methodName).increase(1L);
+            CloudMetrics.META_SERVICE_RPC_LATENCY.getOrAdd(methodName)
+                    .update(System.currentTimeMillis() - startTime);
+        }
+    }
+
+    private static void recordRpcRateLimited(String methodName) {
+        if (MetricRepo.isInit && Config.isCloudMode()) {
+            CloudMetrics.META_SERVICE_RPC_ALL_RATE_LIMITED.increase(1L);
+            
CloudMetrics.META_SERVICE_RPC_RATE_LIMITED.getOrAdd(methodName).increase(1L);
+        }
+    }
+
+    private static void recordGetVersionRateLimitWait(long waitNs) {
+        if (waitNs <= 0) {
+            return;
+        }
+        SummaryProfile profile = SummaryProfile.getSummaryProfile(null);
+        if (profile != null) {
+            profile.addGetMetaVersionRateLimitWaitTime(waitNs);
+        }
+    }
+
     public void removeProxy(String address) {
         LOG.warn("begin to remove proxy: {}", address);
         MetaServiceClient service;
@@ -207,6 +256,7 @@ public class MetaServiceProxy {
                 MetaServiceClient client = null;
                 boolean requestFailed = false;
                 try {
+                    acquireRateLimit(methodName, 1);
                     client = proxy.getProxy();
                     if (tried > 1 && MetricRepo.isInit && 
Config.isCloudMode()) {
                         CloudMetrics.META_SERVICE_RPC_ALL_RETRY.increase(1L);
@@ -289,13 +339,11 @@ public class MetaServiceProxy {
                         .update(System.currentTimeMillis() - startTime);
             }
             return response;
+        } catch (MetaServiceRateLimitException e) {
+            recordRpcRateLimited(methodName);
+            throw e;
         } catch (RpcException e) {
-            if (MetricRepo.isInit && Config.isCloudMode()) {
-                CloudMetrics.META_SERVICE_RPC_ALL_FAILED.increase(1L);
-                
CloudMetrics.META_SERVICE_RPC_FAILED.getOrAdd(methodName).increase(1L);
-                CloudMetrics.META_SERVICE_RPC_LATENCY.getOrAdd(methodName)
-                        .update(System.currentTimeMillis() - startTime);
-            }
+            recordRpcFailed(methodName, startTime);
             throw e;
         }
     }
@@ -313,6 +361,7 @@ public class MetaServiceProxy {
         }
 
         try {
+            recordGetVersionRateLimitWait(acquireRateLimit(methodName, 
getGetVersionRateLimitPermits(request)));
             client = getProxy();
             Future<Cloud.GetVersionResponse> future = 
client.getVisibleVersionAsync(request);
             if (future instanceof 
com.google.common.util.concurrent.ListenableFuture) {
@@ -331,12 +380,7 @@ public class MetaServiceProxy {
 
                             @Override
                             public void onFailure(Throwable t) {
-                                if (MetricRepo.isInit && Config.isCloudMode()) 
{
-                                    
CloudMetrics.META_SERVICE_RPC_ALL_FAILED.increase(1L);
-                                    
CloudMetrics.META_SERVICE_RPC_FAILED.getOrAdd(methodName).increase(1L);
-                                    
CloudMetrics.META_SERVICE_RPC_LATENCY.getOrAdd(methodName)
-                                            .update(System.currentTimeMillis() 
- startTime);
-                                }
+                                recordRpcFailed(methodName, startTime);
                                 if (finalClient != null) {
                                     finalClient.shutdown(true);
                                 }
@@ -344,13 +388,11 @@ public class MetaServiceProxy {
                         }, 
com.google.common.util.concurrent.MoreExecutors.directExecutor());
             }
             return future;
+        } catch (MetaServiceRateLimitException e) {
+            recordRpcRateLimited(methodName);
+            throw e;
         } catch (Exception e) {
-            if (MetricRepo.isInit && Config.isCloudMode()) {
-                CloudMetrics.META_SERVICE_RPC_ALL_FAILED.increase(1L);
-                
CloudMetrics.META_SERVICE_RPC_FAILED.getOrAdd(methodName).increase(1L);
-                CloudMetrics.META_SERVICE_RPC_LATENCY.getOrAdd(methodName)
-                        .update(System.currentTimeMillis() - startTime);
-            }
+            recordRpcFailed(methodName, startTime);
             if (client != null) {
                 client.shutdown(true);
             }
@@ -358,13 +400,6 @@ public class MetaServiceProxy {
         }
     }
 
-    public Cloud.GetVersionResponse getVersion(Cloud.GetVersionRequest 
request) throws RpcException {
-        String methodName = request.hasIsTableVersion() && 
request.getIsTableVersion() ? "getTableVersion"
-                : "getPartitionVersion";
-        return executeWithMetrics(methodName, (client) -> 
client.getVersion(request),
-                Cloud.GetVersionResponse::getStatus);
-    }
-
     public Cloud.CreateTabletsResponse 
createTablets(Cloud.CreateTabletsRequest request) throws RpcException {
         return executeWithMetrics("createTablets", (client) -> 
client.createTablets(request),
                 Cloud.CreateTabletsResponse::getStatus);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceRateLimitException.java
 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceRateLimitException.java
new file mode 100644
index 00000000000..219a43d467b
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceRateLimitException.java
@@ -0,0 +1,27 @@
+// 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.
+
+package org.apache.doris.cloud.rpc;
+
+import org.apache.doris.rpc.RpcException;
+
+class MetaServiceRateLimitException extends RpcException {
+    MetaServiceRateLimitException(String methodName, long waitTimeoutMs) {
+        super("", "meta service rpc rate limited, method: " + methodName
+                + ", wait timeout: " + waitTimeoutMs + "ms");
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceRpcRateLimiter.java
 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceRpcRateLimiter.java
new file mode 100644
index 00000000000..cfcd9407641
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/rpc/MetaServiceRpcRateLimiter.java
@@ -0,0 +1,278 @@
+// 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.
+
+package org.apache.doris.cloud.rpc;
+
+import org.apache.doris.common.Config;
+import org.apache.doris.common.ConfigException;
+import org.apache.doris.common.MetaServiceRpcRateLimitConfigValidator;
+import org.apache.doris.metric.CloudMetrics;
+import org.apache.doris.metric.MetricRepo;
+import org.apache.doris.rpc.RpcException;
+
+import com.google.common.collect.Maps;
+import io.github.resilience4j.ratelimiter.RateLimiter;
+import io.github.resilience4j.ratelimiter.RateLimiterConfig;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+class MetaServiceRpcRateLimiter {
+    private static final Logger LOG = 
LogManager.getLogger(MetaServiceRpcRateLimiter.class);
+
+    private static final int CPU_CORES = 
Runtime.getRuntime().availableProcessors();
+    private static final RateLimitConfigSnapshot EMPTY_SNAPSHOT =
+            new RateLimitConfigSnapshot(0, "", 1, 0);
+
+    private final ReentrantLock configLock = new ReentrantLock();
+    private final ConcurrentMap<String, RateLimiterHolder> rateLimiters = 
Maps.newConcurrentMap();
+    private volatile RateLimitConfigSnapshot currentSnapshot = EMPTY_SNAPSHOT;
+    private volatile Map<String, Integer> methodQpsPerCore = 
Collections.emptyMap();
+
+    long acquire(String methodName) throws RpcException {
+        return acquire(methodName, 1);
+    }
+
+    long acquire(String methodName, int permits) throws RpcException {
+        if (!Config.meta_service_rpc_rate_limit_enabled) {
+            return 0;
+        }
+
+        RateLimiterHolder holder = getRateLimiter(methodName);
+        if (holder == null) {
+            return 0;
+        }
+
+        int permitsToAcquire = Math.min(Math.max(permits, 1), 
holder.maxPermitsInTimeout);
+        // Resilience4j returns negative when the estimated wait exceeds the 
configured timeout.
+        // Otherwise the returned wait time is within 
meta_service_rpc_rate_limit_wait_timeout_ms.
+        long nanosToWait = 
holder.rateLimiter.reservePermission(permitsToAcquire);
+        if (nanosToWait < 0) {
+            throw new MetaServiceRateLimitException(methodName,
+                    Config.meta_service_rpc_rate_limit_wait_timeout_ms);
+        }
+        if (nanosToWait == 0) {
+            return 0;
+        }
+
+        long waitMs = TimeUnit.NANOSECONDS.toMillis(nanosToWait);
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("meta service rpc rate limiter waits before acquiring 
permission, method: {}, permits: {}, "
+                            + "original permits: {}, max permits in timeout: 
{}, limit for period: {}, "
+                            + "burst seconds: {}, wait ms: {}",
+                    methodName, permitsToAcquire, permits, 
holder.maxPermitsInTimeout, holder.limitForPeriod,
+                    holder.burstSeconds,
+                    waitMs);
+        }
+        long waitStartNs = System.nanoTime();
+        try {
+            TimeUnit.NANOSECONDS.sleep(nanosToWait);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RpcException("", e.getMessage(), e);
+        }
+        long actualWaitNs = System.nanoTime() - waitStartNs;
+        if (MetricRepo.isInit && Config.isCloudMode()) {
+            
CloudMetrics.META_SERVICE_RPC_RATE_LIMIT_WAIT_LATENCY.getOrAdd(methodName)
+                    .update(TimeUnit.NANOSECONDS.toMillis(actualWaitNs));
+        }
+        return actualWaitNs;
+    }
+
+    private RateLimiterHolder getRateLimiter(String methodName) throws 
RpcException {
+        refreshConfigIfNeeded();
+        RateLimitConfigSnapshot snapshot = currentSnapshot;
+        int qpsPerCore = methodQpsPerCore.getOrDefault(methodName, 
snapshot.defaultQpsPerCore);
+        if (qpsPerCore <= 0) {
+            rateLimiters.remove(methodName);
+            return null;
+        }
+
+        int limitForPeriod = getLimitForPeriod(methodName, qpsPerCore, 
snapshot.burstSeconds);
+        int maxPermitsInTimeout = getMaxPermitsInTimeout(methodName, 
limitForPeriod, snapshot.burstSeconds,
+                snapshot.waitTimeoutMs);
+        RateLimiterHolder holder = rateLimiters.compute(methodName, (name, 
existingHolder) -> {
+            if (existingHolder != null && 
existingHolder.matches(limitForPeriod, snapshot.burstSeconds,
+                    snapshot.waitTimeoutMs)) {
+                return existingHolder;
+            }
+            return new RateLimiterHolder(createRateLimiter(methodName, 
limitForPeriod,
+                    snapshot.burstSeconds, snapshot.waitTimeoutMs), 
limitForPeriod,
+                    maxPermitsInTimeout, snapshot.burstSeconds, 
snapshot.waitTimeoutMs);
+        });
+        return holder;
+    }
+
+    private void refreshConfigIfNeeded() throws RpcException {
+        RateLimitConfigSnapshot latestSnapshot = 
RateLimitConfigSnapshot.current();
+        if (latestSnapshot.equals(currentSnapshot)) {
+            return;
+        }
+
+        configLock.lock();
+        try {
+            latestSnapshot = RateLimitConfigSnapshot.current();
+            if (latestSnapshot.equals(currentSnapshot)) {
+                return;
+            }
+            validateConfig(latestSnapshot);
+            methodQpsPerCore = 
parseMethodQpsPerCore(latestSnapshot.qpsPerCoreConfig);
+            currentSnapshot = latestSnapshot;
+        } finally {
+            configLock.unlock();
+        }
+    }
+
+    private void validateConfig(RateLimitConfigSnapshot snapshot) throws 
RpcException {
+        try {
+            
MetaServiceRpcRateLimitConfigValidator.validatePositive("meta_service_rpc_rate_limit_burst_seconds",
+                    snapshot.burstSeconds);
+            
MetaServiceRpcRateLimitConfigValidator.validateNonNegative("meta_service_rpc_rate_limit_wait_timeout_ms",
+                    snapshot.waitTimeoutMs);
+        } catch (ConfigException e) {
+            throw new RpcException("", e.getMessage(), e);
+        }
+    }
+
+    private Map<String, Integer> parseMethodQpsPerCore(String config) throws 
RpcException {
+        try {
+            return 
MetaServiceRpcRateLimitConfigValidator.parseQpsPerCoreConfig(config);
+        } catch (ConfigException e) {
+            throw new RpcException("", "invalid 
meta_service_rpc_rate_limit_qps_per_core_config: " + config, e);
+        }
+    }
+
+    private int getLimitForPeriod(String methodName, int qpsPerCore, int 
burstSeconds) throws RpcException {
+        long limitForPeriod = (long) qpsPerCore * CPU_CORES * burstSeconds;
+        if (limitForPeriod > Integer.MAX_VALUE) {
+            throw new RpcException("", "meta service rpc rate limit is too 
large, method: " + methodName
+                    + ", qps per core: " + qpsPerCore + ", cpu cores: " + 
CPU_CORES
+                    + ", burst seconds: " + burstSeconds);
+        }
+        return (int) limitForPeriod;
+    }
+
+    private int getMaxPermitsInTimeout(String methodName, int limitForPeriod, 
int burstSeconds, long waitTimeoutMs)
+            throws RpcException {
+        if (waitTimeoutMs <= 0) {
+            return limitForPeriod;
+        }
+
+        long refreshPeriodMs = TimeUnit.SECONDS.toMillis(burstSeconds);
+        // Keep a small margin from the timeout boundary, so a capped large 
batch does not fail repeatedly
+        // because of scheduling jitter or an already slow permit reservation 
path.
+        long maxPermitsInTimeout = Math.max(limitForPeriod,
+                (long) Math.floor(limitForPeriod * (double) waitTimeoutMs / 
refreshPeriodMs * 0.95D));
+        if (maxPermitsInTimeout > Integer.MAX_VALUE) {
+            throw new RpcException("", "meta service rpc rate limit permits in 
timeout is too large, method: "
+                    + methodName + ", limit for period: " + limitForPeriod
+                    + ", burst seconds: " + burstSeconds + ", wait timeout ms: 
" + waitTimeoutMs);
+        }
+        return (int) maxPermitsInTimeout;
+    }
+
+    private RateLimiter createRateLimiter(String methodName, int 
limitForPeriod, int burstSeconds,
+            long waitTimeoutMs) {
+        RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom()
+                .limitRefreshPeriod(Duration.ofSeconds(burstSeconds))
+                .limitForPeriod(limitForPeriod)
+                .timeoutDuration(Duration.ofMillis(waitTimeoutMs))
+                .build();
+        return RateLimiter.of("meta_service_rpc_" + methodName, 
rateLimiterConfig);
+    }
+
+    void reset() {
+        configLock.lock();
+        try {
+            rateLimiters.clear();
+            methodQpsPerCore = Collections.emptyMap();
+            currentSnapshot = EMPTY_SNAPSHOT;
+        } finally {
+            configLock.unlock();
+        }
+    }
+
+    private static class RateLimiterHolder {
+        private final RateLimiter rateLimiter;
+        private final int limitForPeriod;
+        private final int maxPermitsInTimeout;
+        private final int burstSeconds;
+        private final long waitTimeoutMs;
+
+        private RateLimiterHolder(RateLimiter rateLimiter, int limitForPeriod, 
int maxPermitsInTimeout,
+                int burstSeconds, long waitTimeoutMs) {
+            this.rateLimiter = rateLimiter;
+            this.limitForPeriod = limitForPeriod;
+            this.maxPermitsInTimeout = maxPermitsInTimeout;
+            this.burstSeconds = burstSeconds;
+            this.waitTimeoutMs = waitTimeoutMs;
+        }
+
+        private boolean matches(int limitForPeriod, int burstSeconds, long 
waitTimeoutMs) {
+            return this.limitForPeriod == limitForPeriod && this.burstSeconds 
== burstSeconds
+                    && this.waitTimeoutMs == waitTimeoutMs;
+        }
+    }
+
+    private static class RateLimitConfigSnapshot {
+        private final int defaultQpsPerCore;
+        private final String qpsPerCoreConfig;
+        private final int burstSeconds;
+        private final long waitTimeoutMs;
+
+        private RateLimitConfigSnapshot(int defaultQpsPerCore, String 
qpsPerCoreConfig, int burstSeconds,
+                long waitTimeoutMs) {
+            this.defaultQpsPerCore = defaultQpsPerCore;
+            this.qpsPerCoreConfig = qpsPerCoreConfig == null ? "" : 
qpsPerCoreConfig;
+            this.burstSeconds = burstSeconds;
+            this.waitTimeoutMs = waitTimeoutMs;
+        }
+
+        private static RateLimitConfigSnapshot current() {
+            return new 
RateLimitConfigSnapshot(Config.meta_service_rpc_rate_limit_default_qps_per_core,
+                    Config.meta_service_rpc_rate_limit_qps_per_core_config,
+                    Config.meta_service_rpc_rate_limit_burst_seconds,
+                    Config.meta_service_rpc_rate_limit_wait_timeout_ms);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof RateLimitConfigSnapshot)) {
+                return false;
+            }
+            RateLimitConfigSnapshot that = (RateLimitConfigSnapshot) o;
+            return defaultQpsPerCore == that.defaultQpsPerCore && burstSeconds 
== that.burstSeconds
+                    && waitTimeoutMs == that.waitTimeoutMs
+                    && Objects.equals(qpsPerCoreConfig, that.qpsPerCoreConfig);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(defaultQpsPerCore, qpsPerCoreConfig, 
burstSeconds, waitTimeoutMs);
+        }
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/common/profile/SummaryProfile.java 
b/fe/fe-core/src/main/java/org/apache/doris/common/profile/SummaryProfile.java
index 124be52b05b..7011e28df7e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/common/profile/SummaryProfile.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/common/profile/SummaryProfile.java
@@ -106,6 +106,7 @@ public class SummaryProfile {
     public static final String FETCH_RESULT_TIME = "Fetch Result Time";
     public static final String WRITE_RESULT_TIME = "Write Result Time";
     public static final String GET_META_VERSION_TIME = "Get Meta Version Time";
+    public static final String GET_META_VERSION_RATE_LIMIT_WAIT_TIME = "Get 
Meta Version Rate Limit Wait Time";
     public static final String GET_PARTITION_VERSION_TIME = "Get Partition 
Version Time";
     public static final String GET_PARTITION_VERSION_COUNT = "Get Partition 
Version Count";
     public static final String GET_PARTITION_VERSION_BY_HAS_DATA_COUNT = "Get 
Partition Version Count (hasData)";
@@ -218,6 +219,7 @@ public class SummaryProfile {
             PAIMON_SCAN_METRICS,
             NEREIDS_DISTRIBUTE_TIME,
             GET_META_VERSION_TIME,
+            GET_META_VERSION_RATE_LIMIT_WAIT_TIME,
             GET_PARTITION_VERSION_TIME,
             GET_PARTITION_VERSION_BY_HAS_DATA_COUNT,
             GET_PARTITION_VERSION_COUNT,
@@ -275,6 +277,7 @@ public class SummaryProfile {
             .put(CREATE_SCAN_RANGE_TIME, 2)
             .put(ICEBERG_SCAN_METRICS, 3)
             .put(PAIMON_SCAN_METRICS, 3)
+            .put(GET_META_VERSION_RATE_LIMIT_WAIT_TIME, 1)
             .put(GET_PARTITION_VERSION_TIME, 1)
             .put(GET_PARTITION_VERSION_COUNT, 1)
             .put(GET_PARTITION_VERSION_BY_HAS_DATA_COUNT, 1)
@@ -403,6 +406,8 @@ public class SummaryProfile {
     private long getTableVersionTime = 0;
     @SerializedName(value = "getTableVersionCount")
     private long getTableVersionCount = 0;
+    @SerializedName(value = "getMetaVersionRateLimitWaitTime")
+    private long getMetaVersionRateLimitWaitTime = 0;
     @SerializedName(value = "transactionCommitBeginTime")
     private long transactionCommitBeginTime = -1;
     @SerializedName(value = "transactionCommitEndTime")
@@ -678,6 +683,8 @@ public class SummaryProfile {
 
         if (Config.isCloudMode()) {
             executionSummaryProfile.addInfoString(GET_META_VERSION_TIME, 
getPrettyGetMetaVersionTime());
+            
executionSummaryProfile.addInfoString(GET_META_VERSION_RATE_LIMIT_WAIT_TIME,
+                    getPrettyGetMetaVersionRateLimitWaitTime());
             executionSummaryProfile.addInfoString(GET_PARTITION_VERSION_TIME, 
getPrettyGetPartitionVersionTime());
             executionSummaryProfile.addInfoString(GET_PARTITION_VERSION_COUNT, 
getPrettyGetPartitionVersionCount());
             
executionSummaryProfile.addInfoString(GET_PARTITION_VERSION_BY_HAS_DATA_COUNT,
@@ -882,6 +889,10 @@ public class SummaryProfile {
         this.getTableVersionCount += 1;
     }
 
+    public void addGetMetaVersionRateLimitWaitTime(long ns) {
+        this.getMetaVersionRateLimitWaitTime += ns;
+    }
+
     public void incGetPartitionVersionByHasDataCount() {
         this.getPartitionVersionByHasDataCount += 1;
     }
@@ -1065,6 +1076,13 @@ public class SummaryProfile {
         return RuntimeProfile.printCounter(getMetaVersionTime, TUnit.TIME_NS);
     }
 
+    private String getPrettyGetMetaVersionRateLimitWaitTime() {
+        if (getMetaVersionRateLimitWaitTime == 0) {
+            return "N/A";
+        }
+        return RuntimeProfile.printCounter(getMetaVersionRateLimitWaitTime, 
TUnit.TIME_NS);
+    }
+
     private String getPrettyGetPartitionVersionTime() {
         if (getPartitionVersionTime == 0) {
             return "N/A";
@@ -1095,8 +1113,8 @@ public class SummaryProfile {
         return RuntimeProfile.printCounter(getTableVersionCount, TUnit.UNIT);
     }
 
-    public long getGetPartitionVersionTime() {
-        return getPartitionVersionTime;
+    public long getGetPartitionVersionTimeMs() {
+        return TimeUnit.NANOSECONDS.toMillis(getPartitionVersionTime);
     }
 
     public long getGetPartitionVersionCount() {
@@ -1107,14 +1125,18 @@ public class SummaryProfile {
         return getPartitionVersionByHasDataCount;
     }
 
-    public long getGetTableVersionTime() {
-        return getTableVersionTime;
+    public long getGetTableVersionTimeMs() {
+        return TimeUnit.NANOSECONDS.toMillis(getTableVersionTime);
     }
 
     public long getGetTableVersionCount() {
         return getTableVersionCount;
     }
 
+    public long getGetMetaVersionRateLimitWaitTime() {
+        return getMetaVersionRateLimitWaitTime;
+    }
+
     private String getPrettyTime(long end, long start, TUnit unit) {
         if (start == -1 || end == -1) {
             return "N/A";
@@ -1383,13 +1405,17 @@ public class SummaryProfile {
     }
 
     public String getMetaTime() {
-        return "{"
-                + "\"get_partition_version_time_ms\"" + ":" + 
this.getGetPartitionVersionTime() + ","
+        String metaTime = "{"
+                + "\"get_partition_version_time_ms\"" + ":" + 
this.getGetPartitionVersionTimeMs() + ","
                 + "\"get_partition_version_count_has_data\"" + ":" + 
this.getGetPartitionVersionByHasDataCount() + ","
                 + "\"get_partition_version_count\"" + ":" + 
this.getGetPartitionVersionCount() + ","
-                + "\"get_table_version_time_ms\"" + ":" + 
this.getGetTableVersionTime() + ","
-                + "\"get_table_version_count\"" + ":" + 
this.getGetTableVersionCount()
-                + "}";
+                + "\"get_table_version_time_ms\"" + ":" + 
this.getGetTableVersionTimeMs() + ","
+                + "\"get_table_version_count\"" + ":" + 
this.getGetTableVersionCount();
+        if (this.getGetMetaVersionRateLimitWaitTime() > 0) {
+            metaTime += ",\"get_meta_version_rate_limit_wait_time_ms\"" + ":"
+                    + 
TimeUnit.NANOSECONDS.toMillis(this.getGetMetaVersionRateLimitWaitTime());
+        }
+        return metaTime + "}";
     }
 
     public String getScheduleTime() {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/metric/CloudMetrics.java 
b/fe/fe-core/src/main/java/org/apache/doris/metric/CloudMetrics.java
index b88780a9f4c..7649b3b1c67 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/metric/CloudMetrics.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/metric/CloudMetrics.java
@@ -58,13 +58,16 @@ public class CloudMetrics {
     // Per-method meta-service RPC metrics
     public static AutoMappedMetric<LongCounterMetric> META_SERVICE_RPC_TOTAL;
     public static AutoMappedMetric<LongCounterMetric> META_SERVICE_RPC_FAILED;
+    public static AutoMappedMetric<LongCounterMetric> 
META_SERVICE_RPC_RATE_LIMITED;
     public static AutoMappedMetric<LongCounterMetric> META_SERVICE_RPC_RETRY;
     public static AutoMappedMetric<GaugeMetricImpl<Double>> 
META_SERVICE_RPC_PER_SECOND;
     public static AutoMappedMetric<HistogramMetric> META_SERVICE_RPC_LATENCY;
+    public static AutoMappedMetric<HistogramMetric> 
META_SERVICE_RPC_RATE_LIMIT_WAIT_LATENCY;
 
     // Aggregate meta-service metrics
     public static LongCounterMetric META_SERVICE_RPC_ALL_TOTAL;
     public static LongCounterMetric META_SERVICE_RPC_ALL_FAILED;
+    public static LongCounterMetric META_SERVICE_RPC_ALL_RATE_LIMITED;
     public static LongCounterMetric META_SERVICE_RPC_ALL_RETRY;
     public static GaugeMetricImpl<Double> META_SERVICE_RPC_ALL_PER_SECOND;
 
@@ -155,6 +158,9 @@ public class CloudMetrics {
         META_SERVICE_RPC_FAILED = MetricRepo.addLabeledMetrics("method", () ->
             new LongCounterMetric("meta_service_rpc_failed", MetricUnit.NOUNIT,
                 "failed meta service RPC calls"));
+        META_SERVICE_RPC_RATE_LIMITED = MetricRepo.addLabeledMetrics("method", 
() ->
+            new LongCounterMetric("meta_service_rpc_rate_limited", 
MetricUnit.NOUNIT,
+                "rate limited meta service RPC calls"));
         META_SERVICE_RPC_RETRY = MetricRepo.addLabeledMetrics("method", () ->
             new LongCounterMetric("meta_service_rpc_retry", MetricUnit.NOUNIT,
                 "meta service RPC retry attempts"));
@@ -170,6 +176,14 @@ public class CloudMetrics {
         });
         MetricRepo.DORIS_METRIC_REGISTER.addHistogramMetrics(
                 "meta_service_rpc_latency", META_SERVICE_RPC_LATENCY, 
Config::isCloudMode);
+        META_SERVICE_RPC_RATE_LIMIT_WAIT_LATENCY = new 
AutoMappedMetric<>(methodName -> {
+            List<MetricLabel> labels = Collections.singletonList(new 
MetricLabel("method", methodName));
+            return new HistogramMetric(MetricRegistry.name("meta_service", 
"rpc", "rate_limit_wait",
+                    "latency", "ms"), labels);
+        });
+        MetricRepo.DORIS_METRIC_REGISTER.addHistogramMetrics(
+                "meta_service_rpc_rate_limit_wait_latency", 
META_SERVICE_RPC_RATE_LIMIT_WAIT_LATENCY,
+                Config::isCloudMode);
 
         // Aggregate meta-service metrics
         META_SERVICE_RPC_ALL_TOTAL = new 
LongCounterMetric("meta_service_rpc_all_total",
@@ -178,6 +192,9 @@ public class CloudMetrics {
         META_SERVICE_RPC_ALL_FAILED = new 
LongCounterMetric("meta_service_rpc_all_failed",
             MetricUnit.NOUNIT, "total failed meta service RPC calls");
         
MetricRepo.DORIS_METRIC_REGISTER.addMetrics(META_SERVICE_RPC_ALL_FAILED);
+        META_SERVICE_RPC_ALL_RATE_LIMITED = new 
LongCounterMetric("meta_service_rpc_all_rate_limited",
+            MetricUnit.NOUNIT, "total rate limited meta service RPC calls");
+        
MetricRepo.DORIS_METRIC_REGISTER.addMetrics(META_SERVICE_RPC_ALL_RATE_LIMITED);
         META_SERVICE_RPC_ALL_RETRY = new 
LongCounterMetric("meta_service_rpc_all_retry",
             MetricUnit.NOUNIT, "total meta service RPC retry attempts");
         
MetricRepo.DORIS_METRIC_REGISTER.addMetrics(META_SERVICE_RPC_ALL_RETRY);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/rpc/MetaServiceProxyTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/rpc/MetaServiceProxyTest.java
index c37bd8a76cd..7638bfa774d 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/rpc/MetaServiceProxyTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/rpc/MetaServiceProxyTest.java
@@ -34,19 +34,37 @@ import java.util.Queue;
 import java.util.concurrent.atomic.AtomicInteger;
 
 public class MetaServiceProxyTest {
+    private static final int CPU_CORES = 
Runtime.getRuntime().availableProcessors();
+
     private String originEndpoint;
     private long originReconnectIntervalMs;
     private long originRetryCnt;
+    private boolean originRateLimitEnabled;
+    private int originRateLimitDefaultQpsPerCore;
+    private String originRateLimitQpsPerCoreConfig;
+    private int originRateLimitBurstSeconds;
+    private long originRateLimitWaitTimeoutMs;
 
     @Before
     public void setUp() {
         originEndpoint = Config.meta_service_endpoint;
         originReconnectIntervalMs = 
Config.meta_service_rpc_reconnect_interval_ms;
         originRetryCnt = Config.meta_service_rpc_retry_cnt;
+        originRateLimitEnabled = Config.meta_service_rpc_rate_limit_enabled;
+        originRateLimitDefaultQpsPerCore = 
Config.meta_service_rpc_rate_limit_default_qps_per_core;
+        originRateLimitQpsPerCoreConfig = 
Config.meta_service_rpc_rate_limit_qps_per_core_config;
+        originRateLimitBurstSeconds = 
Config.meta_service_rpc_rate_limit_burst_seconds;
+        originRateLimitWaitTimeoutMs = 
Config.meta_service_rpc_rate_limit_wait_timeout_ms;
 
         Config.meta_service_endpoint = "127.0.0.1:12345";
         Config.meta_service_rpc_reconnect_interval_ms = 0;
         Config.meta_service_rpc_retry_cnt = 1;
+        Config.meta_service_rpc_rate_limit_enabled = false;
+        Config.meta_service_rpc_rate_limit_default_qps_per_core = 10;
+        Config.meta_service_rpc_rate_limit_qps_per_core_config = "";
+        Config.meta_service_rpc_rate_limit_burst_seconds = 1;
+        Config.meta_service_rpc_rate_limit_wait_timeout_ms = 0;
+        MetaServiceProxy.resetMetaServiceRpcRateLimitForTest();
     }
 
     @After
@@ -54,6 +72,12 @@ public class MetaServiceProxyTest {
         Config.meta_service_endpoint = originEndpoint;
         Config.meta_service_rpc_reconnect_interval_ms = 
originReconnectIntervalMs;
         Config.meta_service_rpc_retry_cnt = originRetryCnt;
+        Config.meta_service_rpc_rate_limit_enabled = originRateLimitEnabled;
+        Config.meta_service_rpc_rate_limit_default_qps_per_core = 
originRateLimitDefaultQpsPerCore;
+        Config.meta_service_rpc_rate_limit_qps_per_core_config = 
originRateLimitQpsPerCoreConfig;
+        Config.meta_service_rpc_rate_limit_burst_seconds = 
originRateLimitBurstSeconds;
+        Config.meta_service_rpc_rate_limit_wait_timeout_ms = 
originRateLimitWaitTimeoutMs;
+        MetaServiceProxy.resetMetaServiceRpcRateLimitForTest();
     }
 
     @Test
@@ -216,6 +240,172 @@ public class MetaServiceProxyTest {
         Mockito.verify(client, Mockito.never()).shutdown(Mockito.anyBoolean());
     }
 
+    @Test
+    public void testExecuteRequestRateLimitedWithoutRetryOrShutdown() throws 
RpcException {
+        enableRateLimit(1, "", 1, 0);
+        MetaServiceProxy proxy = new MetaServiceProxy();
+        MetaServiceClient client = mockNormalClient();
+        putClient(proxy, client);
+
+        MetaServiceProxy.MetaServiceClientWrapper wrapper = 
Deencapsulation.getField(proxy, "w");
+        AtomicInteger callCount = new AtomicInteger();
+        Cloud.GetVersionResponse okResponse = okGetVersionResponse();
+        for (int i = 0; i < CPU_CORES; i++) {
+            wrapper.executeRequest("limited", (ignored) -> {
+                callCount.incrementAndGet();
+                return okResponse;
+            }, Cloud.GetVersionResponse::getStatus);
+        }
+
+        try {
+            wrapper.executeRequest("limited", (ignored) -> {
+                callCount.incrementAndGet();
+                return okResponse;
+            }, Cloud.GetVersionResponse::getStatus);
+            Assert.fail("should throw RpcException");
+        } catch (RpcException e) {
+            Assert.assertTrue(e.getMessage().contains("meta service rpc rate 
limited"));
+        }
+
+        Assert.assertEquals(CPU_CORES, callCount.get());
+        Mockito.verify(client, Mockito.never()).shutdown(Mockito.anyBoolean());
+    }
+
+    @Test
+    public void testRateLimitSharedBetweenProxies() throws RpcException {
+        enableRateLimit(1, "", 1, 0);
+        MetaServiceProxy firstProxy = new MetaServiceProxy();
+        MetaServiceProxy secondProxy = new MetaServiceProxy();
+        putClient(firstProxy, mockNormalClient());
+        putClient(secondProxy, mockNormalClient());
+
+        MetaServiceProxy.MetaServiceClientWrapper firstWrapper = 
Deencapsulation.getField(firstProxy, "w");
+        MetaServiceProxy.MetaServiceClientWrapper secondWrapper = 
Deencapsulation.getField(secondProxy, "w");
+        Cloud.GetVersionResponse okResponse = okGetVersionResponse();
+        for (int i = 0; i < CPU_CORES; i++) {
+            firstWrapper.executeRequest("shared", (ignored) -> okResponse, 
Cloud.GetVersionResponse::getStatus);
+        }
+
+        try {
+            secondWrapper.executeRequest("shared", (ignored) -> okResponse, 
Cloud.GetVersionResponse::getStatus);
+            Assert.fail("should throw RpcException");
+        } catch (RpcException e) {
+            Assert.assertTrue(e.getMessage().contains("meta service rpc rate 
limited"));
+        }
+    }
+
+    @Test
+    public void testGetInstanceRateLimitedBeforeRpc() throws RpcException {
+        enableRateLimit(1, "", 1, 0);
+        MetaServiceProxy proxy = new MetaServiceProxy();
+        MetaServiceClient client = mockNormalClient();
+        putClient(proxy, client);
+        consumeRateLimitPermits(proxy, "getInstance");
+
+        try {
+            proxy.getInstance(Cloud.GetInstanceRequest.newBuilder().build());
+            Assert.fail("should throw RpcException");
+        } catch (RpcException e) {
+            Assert.assertTrue(e.getMessage().contains("meta service rpc rate 
limited"));
+        }
+
+        Mockito.verify(client, Mockito.never()).getInstance(Mockito.any());
+        Mockito.verify(client, Mockito.never()).shutdown(Mockito.anyBoolean());
+    }
+
+    @Test
+    public void testGetVisibleVersionAsyncRateLimitedBeforeRpc() throws 
RpcException {
+        enableRateLimit(1, "", 1, 0);
+        MetaServiceProxy proxy = new MetaServiceProxy();
+        MetaServiceClient client = mockNormalClient();
+        putClient(proxy, client);
+        consumeRateLimitPermits(proxy, "getPartitionVersion");
+
+        try {
+            
proxy.getVisibleVersionAsync(Cloud.GetVersionRequest.newBuilder().build());
+            Assert.fail("should throw RpcException");
+        } catch (RpcException e) {
+            Assert.assertTrue(e.getMessage().contains("meta service rpc rate 
limited"));
+        }
+
+        Mockito.verify(client, 
Mockito.never()).getVisibleVersionAsync(Mockito.any());
+        Mockito.verify(client, Mockito.never()).shutdown(Mockito.anyBoolean());
+    }
+
+    @Test
+    public void 
testBatchGetVisibleVersionAsyncConsumesMultipleRateLimitPermits() throws 
RpcException {
+        enableRateLimit(1, "", 1, 0);
+        MetaServiceProxy proxy = new MetaServiceProxy();
+        MetaServiceClient client = mockNormalClient();
+        putClient(proxy, client);
+        SettableFuture<Cloud.GetVersionResponse> future = 
SettableFuture.create();
+        
Mockito.when(client.getVisibleVersionAsync(Mockito.any())).thenReturn(future);
+
+        proxy.getVisibleVersionAsync(buildBatchTableVersionRequest(CPU_CORES));
+
+        try {
+            proxy.getVisibleVersionAsync(Cloud.GetVersionRequest.newBuilder()
+                    .setIsTableVersion(true)
+                    .build());
+            Assert.fail("should throw RpcException");
+        } catch (RpcException e) {
+            Assert.assertTrue(e.getMessage().contains("meta service rpc rate 
limited"));
+        }
+        Mockito.verify(client, 
Mockito.times(1)).getVisibleVersionAsync(Mockito.any());
+    }
+
+    private void enableRateLimit(int defaultQpsPerCore, String 
qpsPerCoreConfig, int burstSeconds,
+            long waitTimeoutMs) {
+        Config.meta_service_rpc_rate_limit_enabled = true;
+        Config.meta_service_rpc_rate_limit_default_qps_per_core = 
defaultQpsPerCore;
+        Config.meta_service_rpc_rate_limit_qps_per_core_config = 
qpsPerCoreConfig;
+        Config.meta_service_rpc_rate_limit_burst_seconds = burstSeconds;
+        Config.meta_service_rpc_rate_limit_wait_timeout_ms = waitTimeoutMs;
+        MetaServiceProxy.resetMetaServiceRpcRateLimitForTest();
+    }
+
+    private void putClient(MetaServiceProxy proxy, MetaServiceClient client) {
+        Map<String, MetaServiceClient> serviceMap = 
Deencapsulation.getField(proxy, "serviceMap");
+        serviceMap.put(Config.meta_service_endpoint, client);
+    }
+
+    private void consumeRateLimitPermits(MetaServiceProxy proxy, String 
methodName) throws RpcException {
+        MetaServiceProxy.MetaServiceClientWrapper wrapper = 
Deencapsulation.getField(proxy, "w");
+        Cloud.GetVersionResponse okResponse = okGetVersionResponse();
+        for (int i = 0; i < CPU_CORES; i++) {
+            wrapper.executeRequest(methodName, (ignored) -> okResponse, 
Cloud.GetVersionResponse::getStatus);
+        }
+    }
+
+    private Cloud.GetVersionRequest buildBatchPartitionVersionRequest(int 
partitionNum) {
+        Cloud.GetVersionRequest.Builder builder = 
Cloud.GetVersionRequest.newBuilder()
+                .setBatchMode(true);
+        for (int i = 0; i < partitionNum; i++) {
+            builder.addDbIds(1L);
+            builder.addTableIds(1L);
+            builder.addPartitionIds(i);
+        }
+        return builder.build();
+    }
+
+    private Cloud.GetVersionRequest buildBatchTableVersionRequest(int 
tableNum) {
+        Cloud.GetVersionRequest.Builder builder = 
Cloud.GetVersionRequest.newBuilder()
+                .setBatchMode(true)
+                .setIsTableVersion(true);
+        for (int i = 0; i < tableNum; i++) {
+            builder.addDbIds(1L);
+            builder.addTableIds(i);
+        }
+        return builder.build();
+    }
+
+    private Cloud.GetVersionResponse okGetVersionResponse() {
+        return Cloud.GetVersionResponse.newBuilder()
+                .setStatus(Cloud.MetaServiceResponseStatus.newBuilder()
+                        .setCode(Cloud.MetaServiceCode.OK))
+                .build();
+    }
+
     private MetaServiceClient mockNormalClient() {
         MetaServiceClient client = Mockito.mock(MetaServiceClient.class);
         Mockito.when(client.isNormalState()).thenReturn(true);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/rpc/MetaServiceRpcRateLimiterTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/rpc/MetaServiceRpcRateLimiterTest.java
new file mode 100644
index 00000000000..e54db40f595
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/rpc/MetaServiceRpcRateLimiterTest.java
@@ -0,0 +1,275 @@
+// 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.
+
+package org.apache.doris.cloud.rpc;
+
+import org.apache.doris.common.Config;
+import org.apache.doris.common.ConfigBase;
+import org.apache.doris.common.MetaServiceRpcRateLimitConfigValidator;
+import org.apache.doris.rpc.RpcException;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.TimeUnit;
+
+public class MetaServiceRpcRateLimiterTest {
+    private static final int CPU_CORES = 
Runtime.getRuntime().availableProcessors();
+
+    private boolean originRateLimitEnabled;
+    private int originRateLimitDefaultQpsPerCore;
+    private String originRateLimitQpsPerCoreConfig;
+    private int originRateLimitBurstSeconds;
+    private long originRateLimitWaitTimeoutMs;
+
+    private MetaServiceRpcRateLimiter rateLimiter;
+
+    @Before
+    public void setUp() {
+        originRateLimitEnabled = Config.meta_service_rpc_rate_limit_enabled;
+        originRateLimitDefaultQpsPerCore = 
Config.meta_service_rpc_rate_limit_default_qps_per_core;
+        originRateLimitQpsPerCoreConfig = 
Config.meta_service_rpc_rate_limit_qps_per_core_config;
+        originRateLimitBurstSeconds = 
Config.meta_service_rpc_rate_limit_burst_seconds;
+        originRateLimitWaitTimeoutMs = 
Config.meta_service_rpc_rate_limit_wait_timeout_ms;
+
+        rateLimiter = new MetaServiceRpcRateLimiter();
+        enableRateLimit(1, "", 1, 0);
+    }
+
+    @After
+    public void tearDown() {
+        Config.meta_service_rpc_rate_limit_enabled = originRateLimitEnabled;
+        Config.meta_service_rpc_rate_limit_default_qps_per_core = 
originRateLimitDefaultQpsPerCore;
+        Config.meta_service_rpc_rate_limit_qps_per_core_config = 
originRateLimitQpsPerCoreConfig;
+        Config.meta_service_rpc_rate_limit_burst_seconds = 
originRateLimitBurstSeconds;
+        Config.meta_service_rpc_rate_limit_wait_timeout_ms = 
originRateLimitWaitTimeoutMs;
+        rateLimiter.reset();
+    }
+
+    @Test
+    public void testMethodRateLimitCanBeDisabledByConfig() throws RpcException 
{
+        enableRateLimit(1, "unlimited:0", 1, 0);
+
+        for (int i = 0; i <= CPU_CORES; i++) {
+            rateLimiter.acquire("unlimited");
+        }
+    }
+
+    @Test
+    public void testRateLimitDisabledBySwitch() throws RpcException {
+        consumePermits("disabledSwitch", CPU_CORES);
+        assertRateLimited("disabledSwitch");
+
+        Config.meta_service_rpc_rate_limit_enabled = false;
+        rateLimiter.acquire("disabledSwitch");
+    }
+
+    @Test
+    public void testMethodOverrideQpsTakesEffect() throws RpcException {
+        enableRateLimit(1, "fast:2", 1, 0);
+
+        consumePermits("normal", CPU_CORES);
+        assertRateLimited("normal");
+
+        consumePermits("fast", CPU_CORES * 2);
+        assertRateLimited("fast");
+    }
+
+    @Test
+    public void testRateLimitIsIsolatedBetweenMethods() throws RpcException {
+        consumePermits("firstMethod", CPU_CORES);
+        assertRateLimited("firstMethod");
+
+        rateLimiter.acquire("secondMethod");
+    }
+
+    @Test
+    public void testWeightedAcquireConsumesMultiplePermits() throws 
RpcException {
+        rateLimiter.acquire("weighted", CPU_CORES);
+
+        assertRateLimited("weighted");
+    }
+
+    @Test
+    public void 
testWeightedAcquireLargerThanTimeoutCapacityIsCappedAndSucceeds() throws 
RpcException {
+        rateLimiter.acquire("largeWeighted", CPU_CORES + 1);
+
+        assertRateLimited("largeWeighted");
+    }
+
+    @Test
+    public void testWeightedAcquireUsesTimeoutCapacity() throws RpcException {
+        enableRateLimit(1, "", 1, 3000);
+
+        long waitNs = rateLimiter.acquire("timeoutCapacity", CPU_CORES * 2);
+
+        Assert.assertTrue(waitNs > 0);
+    }
+
+    @Test
+    public void testDynamicConfigUpdate() throws RpcException {
+        consumePermits("dynamic", CPU_CORES);
+        assertRateLimited("dynamic");
+
+        Config.meta_service_rpc_rate_limit_default_qps_per_core = 0;
+        rateLimiter.acquire("dynamic");
+    }
+
+    @Test
+    public void testBurstWindow() throws RpcException {
+        enableRateLimit(1, "burst:1", 2, 0);
+
+        consumePermits("burst", CPU_CORES * 2);
+        assertRateLimited("burst");
+    }
+
+    @Test
+    public void testWaitTimeoutRejectsWithoutWaitingForNextRefreshPeriod() 
throws RpcException {
+        enableRateLimit(1, "", 3, 1);
+        consumePermits("waitTimeout", CPU_CORES * 3);
+
+        long startTimeMs = System.currentTimeMillis();
+        assertRateLimited("waitTimeout");
+        Assert.assertTrue(System.currentTimeMillis() - startTimeMs < 
TimeUnit.SECONDS.toMillis(1));
+    }
+
+    @Test
+    public void testAcquireReturnsActualWaitTime() throws RpcException {
+        enableRateLimit(1, "", 1, 2000);
+        consumePermits("wait", CPU_CORES);
+
+        long waitNs = rateLimiter.acquire("wait");
+
+        Assert.assertTrue(waitNs > 0);
+    }
+
+    @Test
+    public void testInvalidQpsConfig() throws Exception {
+        Field field = 
Config.class.getField("meta_service_rpc_rate_limit_qps_per_core_config");
+        MetaServiceRpcRateLimitConfigValidator.QpsConfigHandler handler =
+                new MetaServiceRpcRateLimitConfigValidator.QpsConfigHandler();
+
+        assertConfigHandlerRejects(handler, field, "invalid", "Invalid");
+        assertConfigHandlerRejects(handler, field, "method:", "Invalid");
+        assertConfigHandlerRejects(handler, field, ":1", "Invalid");
+        assertConfigHandlerRejects(handler, field, "method:abc", "Invalid");
+        assertConfigHandlerRejects(handler, field, "method:1;method:2", 
"Duplicate");
+
+        Config.meta_service_rpc_rate_limit_qps_per_core_config = "invalid";
+        assertRpcException("invalid 
meta_service_rpc_rate_limit_qps_per_core_config",
+                () -> rateLimiter.acquire("invalid"));
+
+        Config.meta_service_rpc_rate_limit_qps_per_core_config = 
"method:1;method:2";
+        assertRpcException("invalid 
meta_service_rpc_rate_limit_qps_per_core_config",
+                () -> rateLimiter.acquire("method"));
+    }
+
+    @Test
+    public void testInvalidBurstSecondsConfigHandler() throws Exception {
+        Field field = 
Config.class.getField("meta_service_rpc_rate_limit_burst_seconds");
+        MetaServiceRpcRateLimitConfigValidator.PositiveIntConfigHandler 
handler =
+                new 
MetaServiceRpcRateLimitConfigValidator.PositiveIntConfigHandler();
+
+        assertConfigHandlerRejects(handler, field, "0", "must be positive");
+        assertConfigHandlerRejects(handler, field, "-1", "must be positive");
+
+        handler.handle(field, " 2 ");
+        Assert.assertEquals(2, 
Config.meta_service_rpc_rate_limit_burst_seconds);
+    }
+
+    @Test
+    public void testInvalidBurstSeconds() throws RpcException {
+        enableRateLimit(1, "", 0, 0);
+
+        assertRpcException("meta_service_rpc_rate_limit_burst_seconds must be 
positive",
+                () -> rateLimiter.acquire("invalidBurst"));
+    }
+
+    @Test
+    public void testInvalidWaitTimeoutMsConfigHandler() throws Exception {
+        Field field = 
Config.class.getField("meta_service_rpc_rate_limit_wait_timeout_ms");
+        MetaServiceRpcRateLimitConfigValidator.NonNegativeLongConfigHandler 
handler =
+                new 
MetaServiceRpcRateLimitConfigValidator.NonNegativeLongConfigHandler();
+
+        assertConfigHandlerRejects(handler, field, "-1", "must be 
non-negative");
+
+        handler.handle(field, " 0 ");
+        Assert.assertEquals(0, 
Config.meta_service_rpc_rate_limit_wait_timeout_ms);
+    }
+
+    @Test
+    public void testInvalidWaitTimeoutMs() throws RpcException {
+        enableRateLimit(1, "", 1, -1);
+
+        assertRpcException("meta_service_rpc_rate_limit_wait_timeout_ms must 
be non-negative",
+                () -> rateLimiter.acquire("invalidWaitTimeout"));
+    }
+
+    @Test
+    public void testLimitForPeriodOverflow() throws RpcException {
+        enableRateLimit(Integer.MAX_VALUE, "", 2, 0);
+
+        assertRpcException("meta service rpc rate limit is too large",
+                () -> rateLimiter.acquire("overflow"));
+    }
+
+    private void enableRateLimit(int defaultQpsPerCore, String 
qpsPerCoreConfig, int burstSeconds,
+            long waitTimeoutMs) {
+        Config.meta_service_rpc_rate_limit_enabled = true;
+        Config.meta_service_rpc_rate_limit_default_qps_per_core = 
defaultQpsPerCore;
+        Config.meta_service_rpc_rate_limit_qps_per_core_config = 
qpsPerCoreConfig;
+        Config.meta_service_rpc_rate_limit_burst_seconds = burstSeconds;
+        Config.meta_service_rpc_rate_limit_wait_timeout_ms = waitTimeoutMs;
+        rateLimiter.reset();
+    }
+
+    private void consumePermits(String methodName, int permitNum) throws 
RpcException {
+        for (int i = 0; i < permitNum; i++) {
+            rateLimiter.acquire(methodName);
+        }
+    }
+
+    private void assertConfigHandlerRejects(ConfigBase.ConfHandler handler, 
Field field, String config,
+            String expectedMessage) throws Exception {
+        try {
+            handler.handle(field, config);
+            Assert.fail("should throw exception");
+        } catch (Exception e) {
+            Assert.assertTrue(e.getMessage().contains(expectedMessage));
+        }
+    }
+
+    private void assertRateLimited(String methodName) throws RpcException {
+        assertRpcException("meta service rpc rate limited", () -> 
rateLimiter.acquire(methodName));
+    }
+
+    private void assertRpcException(String expectedMessage, RpcCall rpcCall) 
throws RpcException {
+        try {
+            rpcCall.run();
+            Assert.fail("should throw RpcException");
+        } catch (RpcException e) {
+            Assert.assertTrue(e.getMessage().contains(expectedMessage));
+        }
+    }
+
+    private interface RpcCall {
+        void run() throws RpcException;
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/profile/SummaryProfileTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/common/profile/SummaryProfileTest.java
index afe40a68dc8..f6e8d5a677d 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/common/profile/SummaryProfileTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/common/profile/SummaryProfileTest.java
@@ -17,6 +17,8 @@
 
 package org.apache.doris.common.profile;
 
+import org.apache.doris.common.Config;
+
 import com.google.common.collect.ImmutableMap;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -78,6 +80,34 @@ public class SummaryProfileTest {
         Assertions.assertEquals("20ms", 
profile.getPrettyNereidsPreloadExternalMetadataTime());
     }
 
+    @Test
+    public void testMetaVersionRateLimitWaitTime() {
+        String originalCloudUniqueId = Config.cloud_unique_id;
+        Config.cloud_unique_id = "test_cloud";
+        try {
+            SummaryProfile profile = new SummaryProfile();
+            profile.addGetPartitionVersionTime(1_000_000);
+            profile.addGetTableVersionTime(2_000_000);
+            profile.addGetMetaVersionRateLimitWaitTime(1_000_000);
+            profile.addGetMetaVersionRateLimitWaitTime(2_000_000);
+
+            profile.update(ImmutableMap.of());
+
+            RuntimeProfile executionSummary = profile.getExecutionSummary();
+            Assertions.assertEquals(3_000_000, 
profile.getGetMetaVersionRateLimitWaitTime());
+            Assertions.assertEquals("3.0ms", executionSummary.getInfoString(
+                    SummaryProfile.GET_META_VERSION_RATE_LIMIT_WAIT_TIME));
+            String metaTime = profile.getMetaTime();
+            
Assertions.assertTrue(metaTime.contains("\"get_partition_version_time_ms\":1"));
+            
Assertions.assertTrue(metaTime.contains("\"get_table_version_time_ms\":2"));
+            
Assertions.assertTrue(metaTime.contains("\"get_meta_version_rate_limit_wait_time_ms\":3"));
+            Assertions.assertFalse(new SummaryProfile().getMetaTime().contains(
+                    "get_meta_version_rate_limit_wait_time_ms"));
+        } finally {
+            Config.cloud_unique_id = originalCloudUniqueId;
+        }
+    }
+
     @Test
     public void testExternalTableMetaSummary() {
         SummaryProfile profile = new SummaryProfile();
diff --git a/fe/pom.xml b/fe/pom.xml
index 2b44718723b..fb9b8088f69 100644
--- a/fe/pom.xml
+++ b/fe/pom.xml
@@ -291,6 +291,7 @@ under the License.
         <mqtt.version>1.2.5</mqtt.version>
         <slf4j.version>2.0.17</slf4j.version>
         <metrics-core.version>4.0.2</metrics-core.version>
+        <resilience4j.version>2.4.0</resilience4j.version>
         <!-- Keep Netty compatible with Arrow Flight SQL 19 and other 
transitive Netty users. -->
         <netty-all.version>4.2.15.Final</netty-all.version>
         <!--The dependence of transitive dependence cannot be ruled out, only 
Saving the nation through twisted ways.-->
@@ -918,6 +919,11 @@ under the License.
                 <artifactId>cel</artifactId>
                 <version>${cel.version}</version>
             </dependency>
+            <dependency>
+                <groupId>io.github.resilience4j</groupId>
+                <artifactId>resilience4j-ratelimiter</artifactId>
+                <version>${resilience4j.version}</version>
+            </dependency>
             <!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
             <dependency>
                 <groupId>org.javassist</groupId>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to