Files
doris/cloud/test/s3_rate_limiter_test.cpp
Gavin Chouandgavinchou 68938c5ca3 [fix](s3) Add S3 rate limit observability (#64038)
### What problem does this PR solve?

Add observability for S3 local rate limiting and S3 429 responses so
throttling-related performance issues can be diagnosed and alerted more
directly.

### What is changed?

- Add explicit S3 local rate limiter bvars for sleep duration, sleep
count, and rejected count.
- Rename the old ambiguous S3 local rate limiter bvars so their meaning
is clear.
- Log the first and every N local S3 rate limiter throttled/rejected
requests without reading bvar values for decisions.
- Add S3 429 retry/failure bvars for S3/Azure object clients.
- Update S3 rate limiter unit coverage for sleep and rejected metrics.

### New or renamed bvar metrics

Renamed local S3 GET limiter metrics:

- `get_rate_limit_ns` -> `s3_get_rate_limit_sleep_ns`
- `get_rate_limit_exceed_req_num` -> `s3_get_rate_limit_sleep_count`

Renamed local S3 PUT limiter metrics:

- `put_rate_limit_ns` -> `s3_put_rate_limit_sleep_ns`
- `put_rate_limit_exceed_req_num` -> `s3_put_rate_limit_sleep_count`

New local S3 limiter rejection metrics:

- `s3_get_rate_limit_rejected_count`
- `s3_put_rate_limit_rejected_count`

New S3 429 metrics:

- `s3_request_retry_too_many_requests_count`
- `s3_request_failed_too_many_requests_count`

Metric examples:

```text
s3_get_rate_limit_sleep_ns : 123456789
s3_get_rate_limit_sleep_count : 42
s3_get_rate_limit_rejected_count : 3
s3_put_rate_limit_sleep_ns : 987654321
s3_put_rate_limit_sleep_count : 27
s3_put_rate_limit_rejected_count : 1
s3_request_retry_too_many_requests_count : 8
s3_request_failed_too_many_requests_count : 2
```

Example local limiter logs:

```text
S3 GET request is throttled by local rate limiter, sleep_ms=12, sleep_count=1000, token_per_second=1000, bucket_tokens=2000, token_limit=5000
S3 PUT request is rejected by local rate limiter, rejected_count=1, token_per_second=1000, bucket_tokens=2000, token_limit=5000
```

### Tests

- `sh run-cloud-ut.sh --run --filter=s3_rate_limiter_test:*`
- `sh run-be-ut.sh --run --filter=S3FileWriterTest.*`

Note: an earlier BE run used an incorrect lowercase filter
(`s3_file_writer_test:*`), which matched no suite and started running
the full BE suite; it was stopped and replaced by the correct
`S3FileWriterTest.*` run above.

---------

Co-authored-by: gavinchou <gavinchou@apache.org>
2026-07-15 21:51:38 +08:00

190 lines
6.6 KiB
C++

// 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.
#include <bvar/bvar.h>
#include <cpp/token_bucket_rate_limiter.h>
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <thread>
#include <vector>
#include "common/configbase.h"
#include "common/logging.h"
using namespace doris::cloud;
namespace doris {
extern bvar::Adder<int64_t> s3_put_rate_limit_rejected_count;
} // namespace doris
int main(int argc, char** argv) {
auto conf_file = "doris_cloud.conf";
if (!doris::cloud::config::init(conf_file, true)) {
std::cerr << "failed to init config file, conf=" << conf_file << std::endl;
return -1;
}
if (!doris::cloud::init_glog("util")) {
std::cerr << "failed to init glog" << std::endl;
return -1;
}
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
// #define S3RateLimiterTest DISABLED_S3RateLimiterTest
TEST(S3RateLimiterTest, normal) {
auto rate_limiter = doris::S3RateLimiter(1, 5, 10);
std::atomic_int64_t failed;
std::atomic_int64_t succ;
std::atomic_int64_t sleep_thread_num;
std::atomic_int64_t sleep;
auto request_thread = [&]() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto ms = rate_limiter.add(1);
if (ms < 0) {
failed++;
} else if (ms == 0) {
succ++;
} else {
sleep += ms;
sleep_thread_num++;
}
};
{
std::vector<std::thread> threads;
for (size_t i = 0; i < 20; i++) {
threads.emplace_back(request_thread);
}
for (auto& t : threads) {
if (t.joinable()) {
t.join();
}
}
}
EXPECT_EQ(failed, 10);
EXPECT_EQ(succ, 5);
EXPECT_EQ(sleep_thread_num, 5);
}
TEST(S3RateLimiterTest, BasicRateLimit) {
auto rate_limiter = doris::S3RateLimiter(100, 100, 200);
int64_t sleep_time = rate_limiter.add(50);
EXPECT_EQ(sleep_time, 0);
sleep_time = rate_limiter.add(60);
EXPECT_GT(sleep_time, 0);
}
TEST(S3RateLimiterTest, BurstCapacity) {
auto rate_limiter = doris::S3RateLimiter(125, 250, 1000);
int64_t sleep_time = rate_limiter.add(250);
EXPECT_EQ(sleep_time, 0);
sleep_time = rate_limiter.add(1);
EXPECT_GT(sleep_time, 0);
}
TEST(S3RateLimiterTest, ExceedLimit) {
auto rate_limiter = doris::S3RateLimiter(125, 250, 250);
int64_t sleep_time = rate_limiter.add(250);
EXPECT_EQ(sleep_time, 0);
sleep_time = rate_limiter.add(1);
EXPECT_EQ(sleep_time, -1);
}
TEST(S3RateLimiterHolderTest, BvarMetric) {
bvar::Adder<int64_t> rate_limit_sleep_ns("rate_limit_sleep_ns");
bvar::Adder<int64_t> rate_limit_sleep_count("rate_limit_sleep_count");
bvar::Adder<int64_t> rate_limit_rejected_count("rate_limit_rejected_count");
auto rate_limiter_holder = doris::S3RateLimiterHolder(
125, 250, 251,
doris::metric_func_factory(rate_limit_sleep_ns, rate_limit_sleep_count,
&rate_limit_rejected_count));
int64_t sleep_time = rate_limiter_holder.add(250);
EXPECT_EQ(sleep_time, 0);
EXPECT_EQ(rate_limit_sleep_ns.get_value(), 0);
EXPECT_EQ(rate_limit_sleep_count.get_value(), 0);
EXPECT_EQ(rate_limit_rejected_count.get_value(), 0);
sleep_time = rate_limiter_holder.add(1);
EXPECT_GT(sleep_time, 0);
EXPECT_GT(rate_limit_sleep_ns.get_value(), 0);
EXPECT_EQ(rate_limit_sleep_count.get_value(), 1);
EXPECT_EQ(rate_limit_rejected_count.get_value(), 0);
sleep_time = rate_limiter_holder.add(1);
EXPECT_EQ(sleep_time, -1);
EXPECT_EQ(rate_limit_rejected_count.get_value(), 1);
}
TEST(S3RateLimiterHolderTest, ApplyS3RateLimitRecordsRejectedMetric) {
auto rate_limiter_holder = doris::S3RateLimiterHolder(
125, 250, 1, doris::s3_rate_limiter_metric_func(doris::S3RateLimitType::PUT));
auto rejected_count = doris::s3_put_rate_limit_rejected_count.get_value();
int64_t sleep_time =
doris::apply_s3_rate_limit(doris::S3RateLimitType::PUT, &rate_limiter_holder, 1);
EXPECT_EQ(sleep_time, 0);
EXPECT_EQ(doris::s3_put_rate_limit_rejected_count.get_value(), rejected_count);
sleep_time = doris::apply_s3_rate_limit(doris::S3RateLimitType::PUT, &rate_limiter_holder, 1);
EXPECT_EQ(sleep_time, -1);
EXPECT_EQ(doris::s3_put_rate_limit_rejected_count.get_value(), rejected_count + 1);
}
TEST(S3RateLimiterHolderTest, ConcurrentResetReturnsConsistentConfig) {
constexpr size_t config_a_speed = 101;
constexpr size_t config_a_burst = 102;
constexpr size_t config_a_limit = 103;
constexpr size_t config_b_speed = 201;
constexpr size_t config_b_burst = 202;
constexpr size_t config_b_limit = 203;
doris::S3RateLimiterHolder rate_limiter_holder(config_a_speed, config_a_burst, config_a_limit,
[](int64_t) {});
std::atomic<bool> start {false};
std::thread reset_thread([&]() {
while (!start.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
for (size_t i = 0; i < 10000; ++i) {
if (i % 2 == 0) {
rate_limiter_holder.reset(config_b_speed, config_b_burst, config_b_limit);
} else {
rate_limiter_holder.reset(config_a_speed, config_a_burst, config_a_limit);
}
}
});
start.store(true, std::memory_order_release);
for (size_t i = 0; i < 10000; ++i) {
auto result = rate_limiter_holder.add_with_config(0);
bool is_config_a = result.max_speed == config_a_speed &&
result.max_burst == config_a_burst && result.limit == config_a_limit;
bool is_config_b = result.max_speed == config_b_speed &&
result.max_burst == config_b_burst && result.limit == config_b_limit;
EXPECT_TRUE(is_config_a || is_config_b);
}
reset_thread.join();
}