Coverage Report

Created: 2026-07-20 15:57

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
24
#include "common/config.h"
25
#include "common/logging.h"
26
#include "util/cgroup_util.h"
27
28
namespace doris {
29
30
bvar::Adder<int64_t> s3_get_bytes_rate_limit_sleep_ns("s3_get_bytes_rate_limit_sleep_ns");
31
bvar::Adder<int64_t> s3_get_bytes_rate_limit_sleep_count("s3_get_bytes_rate_limit_sleep_count");
32
bvar::Adder<int64_t> s3_put_bytes_rate_limit_sleep_ns("s3_put_bytes_rate_limit_sleep_ns");
33
bvar::Adder<int64_t> s3_put_bytes_rate_limit_sleep_count("s3_put_bytes_rate_limit_sleep_count");
34
35
namespace {
36
37
2
std::function<void(int64_t)> bytes_rate_limiter_metric_func(S3RateLimitType type) {
38
2
    switch (type) {
39
1
    case S3RateLimitType::GET:
40
1
        return metric_func_factory(s3_get_bytes_rate_limit_sleep_ns,
41
1
                                   s3_get_bytes_rate_limit_sleep_count);
42
1
    case S3RateLimitType::PUT:
43
1
        return metric_func_factory(s3_put_bytes_rate_limit_sleep_ns,
44
1
                                   s3_put_bytes_rate_limit_sleep_count);
45
0
    default:
46
0
        return [](int64_t) {};
47
2
    }
48
2
}
49
50
// min(per_core * cores, cap) with overflow protection; cap == 0 means no cap.
51
9
int64_t cap_multiply(int64_t per_core, int64_t cores, int64_t cap) {
52
9
    cap = cap > 0 ? cap : std::numeric_limits<int64_t>::max();
53
9
    if (per_core > cap / cores) {
54
4
        return cap;
55
4
    }
56
5
    return per_core * cores;
57
9
}
58
59
221
size_t index_of(S3RateLimitType type) {
60
221
    CHECK(type == S3RateLimitType::GET || type == S3RateLimitType::PUT) << to_string(type);
61
221
    return static_cast<size_t>(type);
62
221
}
63
64
} // namespace
65
66
81
S3EffectiveRateLimit resolve_s3_rate_limit(S3RateLimitType type, int64_t cores) {
67
81
    const bool is_get = type == S3RateLimitType::GET;
68
81
    const int64_t qps_per_core = is_get ? config::s3_get_qps_per_core : config::s3_put_qps_per_core;
69
81
    const int64_t qps_max = is_get ? config::s3_get_qps_max : config::s3_put_qps_max;
70
81
    const int64_t bytes_per_core = is_get ? config::s3_get_bytes_per_second_per_core
71
81
                                          : config::s3_put_bytes_per_second_per_core;
72
81
    const int64_t bytes_max =
73
81
            is_get ? config::s3_get_bytes_per_second_max : config::s3_put_bytes_per_second_max;
74
81
    cores = std::max<int64_t>(1, cores);
75
76
81
    S3EffectiveRateLimit limit;
77
81
    if (qps_per_core < 0) {
78
        // Unset: the legacy absolute configs stay in charge, bit-for-bit compatible.
79
74
        limit.qps = is_get ? config::s3_get_token_per_second : config::s3_put_token_per_second;
80
74
        limit.burst = is_get ? config::s3_get_bucket_tokens : config::s3_put_bucket_tokens;
81
74
        limit.count_limit = is_get ? config::s3_get_token_limit : config::s3_put_token_limit;
82
74
    } else if (qps_per_core > 0) {
83
6
        limit.qps = cap_multiply(qps_per_core, cores, qps_max);
84
6
        limit.burst = limit.qps; // burst = 1 second worth of quota
85
6
    }                            // qps_per_core == 0: QPS limiting disabled, all fields stay 0.
86
87
81
    if (bytes_per_core > 0) {
88
3
        limit.bytes_per_second = cap_multiply(bytes_per_core, cores, bytes_max);
89
3
    }
90
81
    return limit;
91
81
}
92
93
0
int reset_s3_rate_limiter(S3RateLimitType type, size_t max_speed, size_t max_burst, size_t limit) {
94
0
    if (type == S3RateLimitType::UNKNOWN) {
95
0
        return -1;
96
0
    }
97
0
    return S3RateLimiterManager::instance().qps_limiter(type)->reset(max_speed, max_burst, limit);
98
0
}
99
100
38
int64_t s3_rate_limiter_cpu_cores() {
101
38
    if (int64_t overridden = config::s3_rate_limiter_cpu_cores; overridden > 0) {
102
6
        return overridden;
103
6
    }
104
32
    int physical = static_cast<int>(std::thread::hardware_concurrency());
105
    // Re-read the cgroup quota on every call: serverless BEs can be resized in place,
106
    // and the daemon refresh thread picks the change up through here.
107
32
    int limited = CGroupUtil::get_cgroup_limited_cpu_number(physical);
108
32
    return std::max(1, limited);
109
38
}
110
111
1
S3RateLimiterManager::S3RateLimiterManager() {
112
1
    const int64_t cores = s3_rate_limiter_cpu_cores();
113
2
    for (auto type : {S3RateLimitType::GET, S3RateLimitType::PUT}) {
114
2
        auto limit = resolve_s3_rate_limit(type, cores);
115
2
        _qps_limiters[index_of(type)] = std::make_unique<S3RateLimiterHolder>(
116
2
                limit.qps, limit.burst, limit.count_limit, s3_rate_limiter_metric_func(type));
117
2
        _bytes_limiters[index_of(type)] = std::make_unique<S3RateLimiterHolder>(
118
2
                limit.bytes_per_second, limit.bytes_per_second, limit.bytes_per_second,
119
2
                bytes_rate_limiter_metric_func(type));
120
2
    }
121
1
}
122
123
71
S3RateLimiterManager& S3RateLimiterManager::instance() {
124
71
    static S3RateLimiterManager ret;
125
71
    return ret;
126
71
}
127
128
119
S3RateLimiterHolder* S3RateLimiterManager::qps_limiter(S3RateLimitType type) {
129
119
    return _qps_limiters[index_of(type)].get();
130
119
}
131
132
98
S3RateLimiterHolder* S3RateLimiterManager::bytes_limiter(S3RateLimitType type) {
133
98
    return _bytes_limiters[index_of(type)].get();
134
98
}
135
136
35
void S3RateLimiterManager::refresh() {
137
35
    std::lock_guard guard(_refresh_lock);
138
35
    const int64_t cores = s3_rate_limiter_cpu_cores();
139
70
    for (auto type : {S3RateLimitType::GET, S3RateLimitType::PUT}) {
140
70
        const auto limit = resolve_s3_rate_limit(type, cores);
141
142
70
        auto* qps = qps_limiter(type);
143
70
        if (qps->get_max_speed() != static_cast<size_t>(limit.qps) ||
144
70
            qps->get_max_burst() != static_cast<size_t>(limit.burst) ||
145
70
            qps->get_limit() != static_cast<size_t>(limit.count_limit)) {
146
25
            qps->reset(limit.qps, limit.burst, limit.count_limit);
147
25
            LOG(INFO) << "reset S3 " << to_string(type) << " QPS rate limiter, qps=" << limit.qps
148
25
                      << ", burst=" << limit.burst << ", count_limit=" << limit.count_limit
149
25
                      << ", cores=" << cores;
150
25
        }
151
152
70
        auto* bytes = bytes_limiter(type);
153
70
        const auto bytes_per_second = static_cast<size_t>(limit.bytes_per_second);
154
70
        if (bytes->get_max_speed() != bytes_per_second ||
155
70
            bytes->get_max_burst() != bytes_per_second || bytes->get_limit() != bytes_per_second) {
156
7
            bytes->reset(bytes_per_second, bytes_per_second, bytes_per_second);
157
7
            LOG(INFO) << "reset S3 " << to_string(type)
158
7
                      << " bytes rate limiter, bytes_per_second=" << limit.bytes_per_second
159
7
                      << ", cores=" << cores;
160
7
        }
161
70
    }
162
35
}
163
164
293k
S3RateLimitGuard::S3RateLimitGuard(S3RateLimitType type, size_t estimated_bytes) {
165
293k
    if (!config::enable_s3_rate_limiter) {
166
293k
        return;
167
293k
    }
168
11
    auto& mgr = S3RateLimiterManager::instance();
169
170
11
    auto* qps = mgr.qps_limiter(type);
171
11
    if (qps->is_enabled() &&
172
12
        apply_s3_rate_limit(type, qps, config::s3_rate_limiter_log_interval) < 0) {
173
5
        _ok = false;
174
5
        return;
175
5
    }
176
177
7
    if (estimated_bytes == 0) {
178
7
        return;
179
7
    }
180
18.4E
    auto* bytes = mgr.bytes_limiter(type);
181
18.4E
    if (!bytes->is_enabled()) {
182
1
        return;
183
1
    }
184
    // Clamp the reservation to 1 second worth of bandwidth so a single oversized IO
185
    // (e.g. a whole-file read_at) cannot create unbounded upfront debt. The clamped
186
    // remainder is intentionally not accounted; effective quotas below the single-IO
187
    // upper bound are excluded by the config contract (see config.cpp).
188
18.4E
    _reserved = std::min(estimated_bytes, bytes->get_max_speed());
189
18.4E
    if (_reserved > 0) {
190
        // Pin the admitted bucket generation for settlement and count release.
191
17
        _charged_bucket = bytes->charge(_reserved);
192
17
        if (_charged_bucket == nullptr) {
193
5
            _ok = false;
194
5
        }
195
17
    }
196
18.4E
}
197
198
293k
S3RateLimitGuard::~S3RateLimitGuard() {
199
293k
    if (_charged_bucket != nullptr) {
200
12
        _charged_bucket->refund_count(_reserved);
201
12
    }
202
293k
}
203
204
116k
void S3RateLimitGuard::settle(size_t actual_bytes) {
205
116k
    if (_settled) {
206
0
        return;
207
0
    }
208
116k
    _settled = true;
209
116k
    if (_charged_bucket != nullptr && _reserved > actual_bytes) {
210
3
        _charged_bucket->refund_tokens(_reserved - actual_bytes);
211
3
    }
212
116k
}
213
214
} // namespace doris