Coverage Report

Created: 2026-08-01 19:58

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/util/s3_rate_limiter_manager.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "util/s3_rate_limiter_manager.h"
19
20
#include <algorithm>
21
#include <limits>
22
#include <thread>
23
#include <utility>
24
25
#include "common/config.h"
26
#include "common/logging.h"
27
#include "util/cgroup_util.h"
28
29
namespace doris {
30
31
bvar::Adder<int64_t> s3_get_bytes_rate_limit_sleep_ns("s3_get_bytes_rate_limit_sleep_ns");
32
bvar::Adder<int64_t> s3_get_bytes_rate_limit_sleep_count("s3_get_bytes_rate_limit_sleep_count");
33
bvar::Adder<int64_t> s3_get_bytes_rate_limit_rejected_count(
34
        "s3_get_bytes_rate_limit_rejected_count");
35
bvar::Adder<int64_t> s3_put_bytes_rate_limit_sleep_ns("s3_put_bytes_rate_limit_sleep_ns");
36
bvar::Adder<int64_t> s3_put_bytes_rate_limit_sleep_count("s3_put_bytes_rate_limit_sleep_count");
37
bvar::Adder<int64_t> s3_put_bytes_rate_limit_rejected_count(
38
        "s3_put_bytes_rate_limit_rejected_count");
39
40
namespace {
41
42
constexpr int64_t kBytesRateLimitLongSleepNs = 500 * 1000 * 1000;
43
constexpr int kBytesRateLimitLongSleepLogInterval = 50;
44
45
std::function<void(int64_t)> bytes_rate_limiter_metric_func_with_log(
46
        S3RateLimitType type, bvar::Adder<int64_t>& sleep_ns_bvar,
47
2
        bvar::Adder<int64_t>& sleep_count_bvar, bvar::Adder<int64_t>& rejected_count_bvar) {
48
2
    auto metric_func = metric_func_factory(sleep_ns_bvar, sleep_count_bvar, &rejected_count_bvar);
49
49
    return [type, metric_func = std::move(metric_func)](int64_t sleep_ns) {
50
49
        metric_func(sleep_ns);
51
49
        LOG_IF_EVERY_N(WARNING, sleep_ns > kBytesRateLimitLongSleepNs,
52
1
                       kBytesRateLimitLongSleepLogInterval)
53
1
                << "S3 " << to_string(type) << " request is throttled by bytes rate limiter"
54
1
                << ", sleep_ms=" << sleep_ns / 1000000;
55
49
    };
56
2
}
57
58
2
std::function<void(int64_t)> bytes_rate_limiter_metric_func(S3RateLimitType type) {
59
2
    switch (type) {
60
1
    case S3RateLimitType::GET:
61
1
        return bytes_rate_limiter_metric_func_with_log(type, s3_get_bytes_rate_limit_sleep_ns,
62
1
                                                       s3_get_bytes_rate_limit_sleep_count,
63
1
                                                       s3_get_bytes_rate_limit_rejected_count);
64
1
    case S3RateLimitType::PUT:
65
1
        return bytes_rate_limiter_metric_func_with_log(type, s3_put_bytes_rate_limit_sleep_ns,
66
1
                                                       s3_put_bytes_rate_limit_sleep_count,
67
1
                                                       s3_put_bytes_rate_limit_rejected_count);
68
0
    default:
69
0
        return [](int64_t) {};
70
2
    }
71
2
}
72
73
// min(per_core * cores, cap) with overflow protection; cap <= 0 means no cap.
74
14
int64_t cap_multiply(int64_t per_core, int64_t cores, int64_t cap) {
75
14
    cap = cap > 0 ? cap : std::numeric_limits<int64_t>::max();
76
14
    if (per_core > cap / cores) {
77
6
        return cap;
78
6
    }
79
8
    return per_core * cores;
80
14
}
81
82
361
size_t index_of(S3RateLimitType type) {
83
361
    DCHECK(type == S3RateLimitType::GET || type == S3RateLimitType::PUT) << to_string(type);
84
361
    return static_cast<size_t>(type);
85
361
}
86
87
} // namespace
88
89
131
S3EffectiveRateLimit resolve_s3_rate_limit(S3RateLimitType type, int64_t cores) {
90
131
    const bool is_get = type == S3RateLimitType::GET;
91
131
    const int64_t qps_per_core = is_get ? config::s3_get_requests_per_second_per_core
92
131
                                        : config::s3_put_requests_per_second_per_core;
93
131
    const int64_t qps_max = is_get ? config::s3_get_requests_per_second_max
94
131
                                   : config::s3_put_requests_per_second_max;
95
131
    const int64_t bytes_per_core = is_get ? config::s3_get_bytes_per_second_per_core
96
131
                                          : config::s3_put_bytes_per_second_per_core;
97
131
    const int64_t bytes_max =
98
131
            is_get ? config::s3_get_bytes_per_second_max : config::s3_put_bytes_per_second_max;
99
131
    cores = std::max<int64_t>(1, cores);
100
101
131
    S3EffectiveRateLimit limit;
102
131
    if (qps_per_core < 0) {
103
        // Unset: the legacy absolute configs stay in charge, bit-for-bit compatible.
104
115
        limit.qps = is_get ? config::s3_get_token_per_second : config::s3_put_token_per_second;
105
115
        limit.burst = is_get ? config::s3_get_bucket_tokens : config::s3_put_bucket_tokens;
106
115
        limit.count_limit = is_get ? config::s3_get_token_limit : config::s3_put_token_limit;
107
115
    } else if (qps_per_core > 0) {
108
8
        limit.qps = cap_multiply(qps_per_core, cores, qps_max);
109
8
        limit.burst = limit.qps; // burst = 1 second worth of quota
110
8
    }                            // qps_per_core == 0: QPS limiting disabled, all fields stay 0.
111
112
131
    if (bytes_per_core > 0) {
113
6
        limit.bytes_per_second = cap_multiply(bytes_per_core, cores, bytes_max);
114
6
    }
115
131
    return limit;
116
131
}
117
118
0
int reset_s3_rate_limiter(S3RateLimitType type, size_t max_speed, size_t max_burst, size_t limit) {
119
0
    if (type == S3RateLimitType::UNKNOWN) {
120
0
        return -1;
121
0
    }
122
0
    return S3RateLimiterManager::instance().qps_limiter(type)->reset(max_speed, max_burst, limit);
123
0
}
124
125
60
int64_t s3_rate_limiter_cpu_cores() {
126
60
    if (int32_t overridden = config::s3_rate_limiter_cpu_cores_override; overridden > 0) {
127
10
        return overridden;
128
10
    }
129
50
    int physical = static_cast<int>(std::thread::hardware_concurrency());
130
    // Re-read the cgroup quota on every call: serverless BEs can be resized in place,
131
    // and the daemon refresh thread picks the change up through here.
132
50
    int limited = CGroupUtil::get_cgroup_limited_cpu_number(physical);
133
50
    return std::max(1, limited);
134
60
}
135
136
1
S3RateLimiterManager::S3RateLimiterManager() {
137
1
    const int64_t cores = s3_rate_limiter_cpu_cores();
138
2
    for (auto type : {S3RateLimitType::GET, S3RateLimitType::PUT}) {
139
2
        auto limit = resolve_s3_rate_limit(type, cores);
140
2
        _qps_limiters[index_of(type)] = std::make_unique<S3RateLimiterHolder>(
141
2
                limit.qps, limit.burst, limit.count_limit, s3_rate_limiter_metric_func(type));
142
2
        _bytes_limiters[index_of(type)] = std::make_unique<S3RateLimiterHolder>(
143
2
                limit.bytes_per_second, limit.bytes_per_second, 0,
144
2
                bytes_rate_limiter_metric_func(type));
145
2
    }
146
1
}
147
148
115
S3RateLimiterManager& S3RateLimiterManager::instance() {
149
115
    static S3RateLimiterManager ret;
150
115
    return ret;
151
115
}
152
153
195
S3RateLimiterHolder* S3RateLimiterManager::qps_limiter(S3RateLimitType type) {
154
195
    return _qps_limiters[index_of(type)].get();
155
195
}
156
157
162
S3RateLimiterHolder* S3RateLimiterManager::bytes_limiter(S3RateLimitType type) {
158
162
    return _bytes_limiters[index_of(type)].get();
159
162
}
160
161
57
void S3RateLimiterManager::refresh() {
162
57
    std::lock_guard guard(_refresh_lock);
163
57
    const int64_t cores = s3_rate_limiter_cpu_cores();
164
114
    for (auto type : {S3RateLimitType::GET, S3RateLimitType::PUT}) {
165
114
        const auto limit = resolve_s3_rate_limit(type, cores);
166
167
114
        auto* qps = qps_limiter(type);
168
114
        if (qps->get_max_speed() != static_cast<size_t>(limit.qps) ||
169
114
            qps->get_max_burst() != static_cast<size_t>(limit.burst) ||
170
114
            qps->get_limit() != static_cast<size_t>(limit.count_limit)) {
171
53
            qps->reset(limit.qps, limit.burst, limit.count_limit);
172
53
            LOG(INFO) << "reset S3 " << to_string(type) << " QPS rate limiter, qps=" << limit.qps
173
53
                      << ", burst=" << limit.burst << ", count_limit=" << limit.count_limit
174
53
                      << ", cores=" << cores;
175
53
        }
176
177
114
        auto* bytes = bytes_limiter(type);
178
114
        if (bytes->get_max_speed() != static_cast<size_t>(limit.bytes_per_second)) {
179
25
            bytes->reset(limit.bytes_per_second, limit.bytes_per_second, 0);
180
25
            LOG(INFO) << "reset S3 " << to_string(type)
181
25
                      << " bytes rate limiter, bytes_per_second=" << limit.bytes_per_second
182
25
                      << ", cores=" << cores;
183
25
        }
184
114
    }
185
57
}
186
187
98
S3RateLimitGuard::S3RateLimitGuard(S3RateLimitType type, size_t estimated_bytes) {
188
98
    if (!config::enable_s3_rate_limiter) {
189
56
        return;
190
56
    }
191
42
    auto& mgr = S3RateLimiterManager::instance();
192
193
42
    auto* qps = mgr.qps_limiter(type);
194
42
    if (qps->is_enabled() &&
195
42
        apply_s3_rate_limit(type, qps, config::s3_rate_limiter_log_interval) < 0) {
196
12
        _ok = false;
197
12
        _reject_reason = S3RateLimitRejectReason::QPS;
198
12
        return;
199
12
    }
200
201
30
    if (estimated_bytes == 0) {
202
15
        return;
203
15
    }
204
15
    auto* bytes = mgr.bytes_limiter(type);
205
15
    if (!bytes->is_enabled()) {
206
2
        return;
207
2
    }
208
    // Clamp the reservation to 1 second worth of bandwidth so a single oversized IO
209
    // (e.g. a whole-file read_at) cannot create unbounded upfront debt. The clamped
210
    // remainder is intentionally not accounted; effective quotas below the single-IO
211
    // upper bound are excluded by the config contract (see config.cpp).
212
13
    _reserved = std::min(estimated_bytes, bytes->get_max_speed());
213
13
    if (_reserved > 0) {
214
        // Pin the admitted bucket generation for settle().
215
13
        _charged_bucket = bytes->charge(_reserved);
216
13
        if (_charged_bucket == nullptr) {
217
2
            _ok = false;
218
2
            _reject_reason = S3RateLimitRejectReason::BYTES;
219
2
        }
220
13
    }
221
13
}
222
223
6
void S3RateLimitGuard::settle(size_t actual_bytes) {
224
6
    if (_settled) {
225
0
        return;
226
0
    }
227
6
    _settled = true;
228
6
    if (_charged_bucket != nullptr && _reserved > actual_bytes) {
229
3
        _charged_bucket->refund(_reserved - actual_bytes);
230
3
    }
231
6
}
232
233
} // namespace doris