Coverage Report

Created: 2026-08-13 12:06

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
29
    return [type, metric_func = std::move(metric_func)](int64_t sleep_ns) {
50
29
        metric_func(sleep_ns);
51
29
        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
29
    };
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
236
size_t index_of(S3RateLimitType type) {
83
236
    DCHECK(type == S3RateLimitType::GET || type == S3RateLimitType::PUT) << to_string(type);
84
236
    return static_cast<size_t>(type);
85
236
}
86
87
} // namespace
88
89
99
S3EffectiveRateLimit resolve_s3_rate_limit(S3RateLimitType type, int64_t cores) {
90
99
    const bool is_get = type == S3RateLimitType::GET;
91
99
    const int64_t qps_per_core = is_get ? config::s3_get_requests_per_second_per_core
92
99
                                        : config::s3_put_requests_per_second_per_core;
93
99
    const int64_t qps_max = is_get ? config::s3_get_requests_per_second_max
94
99
                                   : config::s3_put_requests_per_second_max;
95
99
    const int64_t bytes_per_core = is_get ? config::s3_get_bytes_per_second_per_core
96
99
                                          : config::s3_put_bytes_per_second_per_core;
97
99
    const int64_t bytes_max =
98
99
            is_get ? config::s3_get_bytes_per_second_max : config::s3_put_bytes_per_second_max;
99
99
    cores = std::max<int64_t>(1, cores);
100
101
99
    S3EffectiveRateLimit limit;
102
99
    if (qps_per_core < 0) {
103
        // Unset: the legacy absolute configs stay in charge, bit-for-bit compatible.
104
83
        limit.qps = is_get ? config::s3_get_token_per_second : config::s3_put_token_per_second;
105
83
        limit.burst = is_get ? config::s3_get_bucket_tokens : config::s3_put_bucket_tokens;
106
83
        limit.count_limit = is_get ? config::s3_get_token_limit : config::s3_put_token_limit;
107
83
    } 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
99
    if (bytes_per_core > 0) {
113
6
        limit.bytes_per_second = cap_multiply(bytes_per_core, cores, bytes_max);
114
6
    }
115
99
    return limit;
116
99
}
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
44
int64_t s3_rate_limiter_cpu_cores() {
126
44
    if (int32_t overridden = config::s3_rate_limiter_cpu_cores_override; overridden > 0) {
127
10
        return overridden;
128
10
    }
129
34
    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
34
    int limited = CGroupUtil::get_cgroup_limited_cpu_number(physical);
133
34
    return std::max(1, limited);
134
44
}
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
66
S3RateLimiterManager& S3RateLimiterManager::instance() {
149
66
    static S3RateLimiterManager ret;
150
66
    return ret;
151
66
}
152
153
121
S3RateLimiterHolder* S3RateLimiterManager::qps_limiter(S3RateLimitType type) {
154
121
    return _qps_limiters[index_of(type)].get();
155
121
}
156
157
111
S3RateLimiterHolder* S3RateLimiterManager::bytes_limiter(S3RateLimitType type) {
158
111
    return _bytes_limiters[index_of(type)].get();
159
111
}
160
161
41
void S3RateLimiterManager::refresh() {
162
41
    std::lock_guard guard(_refresh_lock);
163
41
    const int64_t cores = s3_rate_limiter_cpu_cores();
164
82
    for (auto type : {S3RateLimitType::GET, S3RateLimitType::PUT}) {
165
82
        const auto limit = resolve_s3_rate_limit(type, cores);
166
167
82
        auto* qps = qps_limiter(type);
168
82
        if (qps->get_max_speed() != static_cast<size_t>(limit.qps) ||
169
82
            qps->get_max_burst() != static_cast<size_t>(limit.burst) ||
170
82
            qps->get_limit() != static_cast<size_t>(limit.count_limit)) {
171
33
            qps->reset(limit.qps, limit.burst, limit.count_limit);
172
33
            LOG(INFO) << "reset S3 " << to_string(type) << " QPS rate limiter, qps=" << limit.qps
173
33
                      << ", burst=" << limit.burst << ", count_limit=" << limit.count_limit
174
33
                      << ", cores=" << cores;
175
33
        }
176
177
82
        auto* bytes = bytes_limiter(type);
178
82
        if (bytes->get_max_speed() != static_cast<size_t>(limit.bytes_per_second)) {
179
15
            bytes->reset(limit.bytes_per_second, limit.bytes_per_second, 0);
180
15
            LOG(INFO) << "reset S3 " << to_string(type)
181
15
                      << " bytes rate limiter, bytes_per_second=" << limit.bytes_per_second
182
15
                      << ", cores=" << cores;
183
15
        }
184
82
    }
185
41
}
186
187
480k
S3RateLimitGuard::S3RateLimitGuard(S3RateLimitType type, size_t estimated_bytes) {
188
480k
    if (!config::enable_s3_rate_limiter) {
189
480k
        return;
190
480k
    }
191
18.4E
    auto& mgr = S3RateLimiterManager::instance();
192
193
18.4E
    auto* qps = mgr.qps_limiter(type);
194
18.4E
    if (qps->is_enabled() &&
195
18.4E
        apply_s3_rate_limit(type, qps, config::s3_rate_limiter_log_interval) < 0) {
196
3
        _ok = false;
197
3
        _reject_reason = S3RateLimitRejectReason::QPS;
198
3
        return;
199
3
    }
200
201
18.4E
    if (estimated_bytes == 0) {
202
4
        return;
203
4
    }
204
18.4E
    auto* bytes = mgr.bytes_limiter(type);
205
18.4E
    if (!bytes->is_enabled()) {
206
0
        return;
207
0
    }
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
18.4E
    _reserved = std::min(estimated_bytes, bytes->get_max_speed());
213
18.4E
    if (_reserved > 0) {
214
        // Pin the admitted bucket generation for settle().
215
6
        _charged_bucket = bytes->charge(_reserved);
216
6
        if (_charged_bucket == nullptr) {
217
0
            _ok = false;
218
0
            _reject_reason = S3RateLimitRejectReason::BYTES;
219
0
        }
220
6
    }
221
18.4E
}
222
223
300k
void S3RateLimitGuard::settle(size_t actual_bytes) {
224
300k
    if (_settled) {
225
0
        return;
226
0
    }
227
300k
    _settled = true;
228
300k
    if (_charged_bucket != nullptr && _reserved > actual_bytes) {
229
2
        _charged_bucket->refund(_reserved - actual_bytes);
230
2
    }
231
300k
}
232
233
} // namespace doris