blob: d553a5ca63b57e103837ed4348ff2c6d9ee31824 [file]
// 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 "util/s3_rate_limiter_manager.h"
#include <algorithm>
#include <limits>
#include <thread>
#include <utility>
#include "common/config.h"
#include "common/logging.h"
#include "util/cgroup_util.h"
namespace doris {
bvar::Adder<int64_t> s3_get_bytes_rate_limit_sleep_ns("s3_get_bytes_rate_limit_sleep_ns");
bvar::Adder<int64_t> s3_get_bytes_rate_limit_sleep_count("s3_get_bytes_rate_limit_sleep_count");
bvar::Adder<int64_t> s3_get_bytes_rate_limit_rejected_count(
"s3_get_bytes_rate_limit_rejected_count");
bvar::Adder<int64_t> s3_put_bytes_rate_limit_sleep_ns("s3_put_bytes_rate_limit_sleep_ns");
bvar::Adder<int64_t> s3_put_bytes_rate_limit_sleep_count("s3_put_bytes_rate_limit_sleep_count");
bvar::Adder<int64_t> s3_put_bytes_rate_limit_rejected_count(
"s3_put_bytes_rate_limit_rejected_count");
namespace {
constexpr int64_t kBytesRateLimitLongSleepNs = 500 * 1000 * 1000;
constexpr int kBytesRateLimitLongSleepLogInterval = 50;
std::function<void(int64_t)> bytes_rate_limiter_metric_func_with_log(
S3RateLimitType type, bvar::Adder<int64_t>& sleep_ns_bvar,
bvar::Adder<int64_t>& sleep_count_bvar, bvar::Adder<int64_t>& rejected_count_bvar) {
auto metric_func = metric_func_factory(sleep_ns_bvar, sleep_count_bvar, &rejected_count_bvar);
return [type, metric_func = std::move(metric_func)](int64_t sleep_ns) {
metric_func(sleep_ns);
LOG_IF_EVERY_N(WARNING, sleep_ns > kBytesRateLimitLongSleepNs,
kBytesRateLimitLongSleepLogInterval)
<< "S3 " << to_string(type) << " request is throttled by bytes rate limiter"
<< ", sleep_ms=" << sleep_ns / 1000000;
};
}
std::function<void(int64_t)> bytes_rate_limiter_metric_func(S3RateLimitType type) {
switch (type) {
case S3RateLimitType::GET:
return bytes_rate_limiter_metric_func_with_log(type, s3_get_bytes_rate_limit_sleep_ns,
s3_get_bytes_rate_limit_sleep_count,
s3_get_bytes_rate_limit_rejected_count);
case S3RateLimitType::PUT:
return bytes_rate_limiter_metric_func_with_log(type, s3_put_bytes_rate_limit_sleep_ns,
s3_put_bytes_rate_limit_sleep_count,
s3_put_bytes_rate_limit_rejected_count);
default:
return [](int64_t) {};
}
}
// min(per_core * cores, cap) with overflow protection; cap <= 0 means no cap.
int64_t cap_multiply(int64_t per_core, int64_t cores, int64_t cap) {
cap = cap > 0 ? cap : std::numeric_limits<int64_t>::max();
if (per_core > cap / cores) {
return cap;
}
return per_core * cores;
}
size_t index_of(S3RateLimitType type) {
DCHECK(type == S3RateLimitType::GET || type == S3RateLimitType::PUT) << to_string(type);
return static_cast<size_t>(type);
}
} // namespace
S3EffectiveRateLimit resolve_s3_rate_limit(S3RateLimitType type, int64_t cores) {
const bool is_get = type == S3RateLimitType::GET;
const int64_t qps_per_core = is_get ? config::s3_get_requests_per_second_per_core
: config::s3_put_requests_per_second_per_core;
const int64_t qps_max = is_get ? config::s3_get_requests_per_second_max
: config::s3_put_requests_per_second_max;
const int64_t bytes_per_core = is_get ? config::s3_get_bytes_per_second_per_core
: config::s3_put_bytes_per_second_per_core;
const int64_t bytes_max =
is_get ? config::s3_get_bytes_per_second_max : config::s3_put_bytes_per_second_max;
cores = std::max<int64_t>(1, cores);
S3EffectiveRateLimit limit;
if (qps_per_core < 0) {
// Unset: the legacy absolute configs stay in charge, bit-for-bit compatible.
limit.qps = is_get ? config::s3_get_token_per_second : config::s3_put_token_per_second;
limit.burst = is_get ? config::s3_get_bucket_tokens : config::s3_put_bucket_tokens;
limit.count_limit = is_get ? config::s3_get_token_limit : config::s3_put_token_limit;
} else if (qps_per_core > 0) {
limit.qps = cap_multiply(qps_per_core, cores, qps_max);
limit.burst = limit.qps; // burst = 1 second worth of quota
} // qps_per_core == 0: QPS limiting disabled, all fields stay 0.
if (bytes_per_core > 0) {
limit.bytes_per_second = cap_multiply(bytes_per_core, cores, bytes_max);
}
return limit;
}
int reset_s3_rate_limiter(S3RateLimitType type, size_t max_speed, size_t max_burst, size_t limit) {
if (type == S3RateLimitType::UNKNOWN) {
return -1;
}
return S3RateLimiterManager::instance().qps_limiter(type)->reset(max_speed, max_burst, limit);
}
int64_t s3_rate_limiter_cpu_cores() {
if (int32_t overridden = config::s3_rate_limiter_cpu_cores_override; overridden > 0) {
return overridden;
}
int physical = static_cast<int>(std::thread::hardware_concurrency());
// Re-read the cgroup quota on every call: serverless BEs can be resized in place,
// and the daemon refresh thread picks the change up through here.
int limited = CGroupUtil::get_cgroup_limited_cpu_number(physical);
return std::max(1, limited);
}
S3RateLimiterManager::S3RateLimiterManager() {
const int64_t cores = s3_rate_limiter_cpu_cores();
for (auto type : {S3RateLimitType::GET, S3RateLimitType::PUT}) {
auto limit = resolve_s3_rate_limit(type, cores);
_qps_limiters[index_of(type)] = std::make_unique<S3RateLimiterHolder>(
limit.qps, limit.burst, limit.count_limit, s3_rate_limiter_metric_func(type));
_bytes_limiters[index_of(type)] = std::make_unique<S3RateLimiterHolder>(
limit.bytes_per_second, limit.bytes_per_second, 0,
bytes_rate_limiter_metric_func(type));
}
}
S3RateLimiterManager& S3RateLimiterManager::instance() {
static S3RateLimiterManager ret;
return ret;
}
S3RateLimiterHolder* S3RateLimiterManager::qps_limiter(S3RateLimitType type) {
return _qps_limiters[index_of(type)].get();
}
S3RateLimiterHolder* S3RateLimiterManager::bytes_limiter(S3RateLimitType type) {
return _bytes_limiters[index_of(type)].get();
}
void S3RateLimiterManager::refresh() {
std::lock_guard guard(_refresh_lock);
const int64_t cores = s3_rate_limiter_cpu_cores();
for (auto type : {S3RateLimitType::GET, S3RateLimitType::PUT}) {
const auto limit = resolve_s3_rate_limit(type, cores);
auto* qps = qps_limiter(type);
if (qps->get_max_speed() != static_cast<size_t>(limit.qps) ||
qps->get_max_burst() != static_cast<size_t>(limit.burst) ||
qps->get_limit() != static_cast<size_t>(limit.count_limit)) {
qps->reset(limit.qps, limit.burst, limit.count_limit);
LOG(INFO) << "reset S3 " << to_string(type) << " QPS rate limiter, qps=" << limit.qps
<< ", burst=" << limit.burst << ", count_limit=" << limit.count_limit
<< ", cores=" << cores;
}
auto* bytes = bytes_limiter(type);
if (bytes->get_max_speed() != static_cast<size_t>(limit.bytes_per_second)) {
bytes->reset(limit.bytes_per_second, limit.bytes_per_second, 0);
LOG(INFO) << "reset S3 " << to_string(type)
<< " bytes rate limiter, bytes_per_second=" << limit.bytes_per_second
<< ", cores=" << cores;
}
}
}
S3RateLimitGuard::S3RateLimitGuard(S3RateLimitType type, size_t estimated_bytes) {
if (!config::enable_s3_rate_limiter) {
return;
}
auto& mgr = S3RateLimiterManager::instance();
auto* qps = mgr.qps_limiter(type);
if (qps->is_enabled() &&
apply_s3_rate_limit(type, qps, config::s3_rate_limiter_log_interval) < 0) {
_ok = false;
_reject_reason = S3RateLimitRejectReason::QPS;
return;
}
if (estimated_bytes == 0) {
return;
}
auto* bytes = mgr.bytes_limiter(type);
if (!bytes->is_enabled()) {
return;
}
// Clamp the reservation to 1 second worth of bandwidth so a single oversized IO
// (e.g. a whole-file read_at) cannot create unbounded upfront debt. The clamped
// remainder is intentionally not accounted; effective quotas below the single-IO
// upper bound are excluded by the config contract (see config.cpp).
_reserved = std::min(estimated_bytes, bytes->get_max_speed());
if (_reserved > 0) {
// Pin the admitted bucket generation for settle().
_charged_bucket = bytes->charge(_reserved);
if (_charged_bucket == nullptr) {
_ok = false;
_reject_reason = S3RateLimitRejectReason::BYTES;
}
}
}
void S3RateLimitGuard::settle(size_t actual_bytes) {
if (_settled) {
return;
}
_settled = true;
if (_charged_bucket != nullptr && _reserved > actual_bytes) {
_charged_bucket->refund(_reserved - actual_bytes);
}
}
} // namespace doris