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 |