Skip to content

Commit e48604e

Browse files
reactor: Auto enable overprovisioned in k8s on 6.12-7.0
We are seeing system lock ups when running Redpanda on k8s and kernel versions [6.12-7.0]. This is caused by scheduler starvation bugs in those kernel ranges. As with previous similar issues (k8s perf issues, RHEL8 tuned issues) it can be worked around with by enabling overprovisioned and allowing the scheduler to move threads around and avoid busy spinning when not needed. We limit the enablement to explicit detection of kubernetes (via env var). The more accurate check would be to see whether our top-most cgroup has raised cpu.weight (this is what causes the contention and deadlock). However this very check fails on kubernetes as it uses private cgroup namespaces which makes it look like a pod is running in the cgroup root. Hence we can't reliably read the top-most cgroup weight. Given that we effectively only run into this problem in kubernetes we go with the k8s env var check. We opt out of this auto overprovisioned mode when `--poll-aio=1` is set explicitly.
1 parent 1da9462 commit e48604e

1 file changed

Lines changed: 70 additions & 6 deletions

File tree

src/core/reactor.cc

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <chrono>
2929
#include <cmath>
3030
#include <coroutine>
31+
#include <cstdlib>
3132
#include <exception>
3233
#include <filesystem>
3334
#include <fstream>
@@ -4523,6 +4524,64 @@ unsigned smp::adjust_max_networking_aio_io_control_blocks(unsigned network_iocbs
45234524
return network_iocbs;
45244525
}
45254526

4527+
// Series 6+ kernels saw the new EEVDF scheduler introduced and heavy code churn
4528+
// in that
4529+
//
4530+
// [6.12, 7.1) has one or many bugs that cause the scheduler to completely
4531+
// deadlock the system if there is a busy spinning thread (like seastar).
4532+
// The fixes have been backported to the LTS version 6.12 and 6.18
4533+
// so later series are exempted again.
4534+
// The root trigger for this is having a (root) cgroup with raised cpu weight
4535+
// which makes them directly compete with kernel threads. We have seen issues
4536+
// because of this repeatedly in the past.
4537+
// This is reliably observed when running in kubernetes which bumps its
4538+
// root/top-level cgroup cpu weight by some (weird) formula.
4539+
// While this scenario could theoretically also happen otherwise we in practice
4540+
// only see it in kubernetes (even changed systemd services don't touch the root
4541+
// slice) and hence we limit our detection to kubernetes. An alternative would
4542+
// be to just check the cpu weight of our top most cgroup slice. However, this
4543+
// fails on k8s (and elsewhere) as they use private cgroup namespaces which
4544+
// makes the pod only see its own weight.
4545+
static bool k8s_eevdf_deadlock() {
4546+
const auto ku = internal::kernel_uname();
4547+
if (!ku.whitelisted({"6.12"}) || ku.whitelisted({"7.1"})) {
4548+
return false;
4549+
}
4550+
// LTS series with the fixes backported. Note a three-component version only
4551+
// matches the same version.patchlevel series, so these exemptions don't
4552+
// apply to any other series.
4553+
if (ku.whitelisted({"6.12.94", "6.18.38"})) {
4554+
return false;
4555+
}
4556+
return std::getenv("KUBERNETES_SERVICE_HOST") != nullptr;
4557+
}
4558+
4559+
// We have repeatedly seen issues where seastar's shard-pinning and busy
4560+
// spinning has caused scheduling issues with other kernel threads (most often
4561+
// the DIO thread).
4562+
// Usually this often requires an additional ingredient such as kubernetes
4563+
// or scheduling tuning (e.g.: from tuned - handled in a different place).
4564+
// If we detect these conditions and affected kernel versions, we automatically
4565+
// enable overdirectly provisioning to avoid the issue.
4566+
static bool should_auto_overprovision(const reactor_options& reactor_opts) {
4567+
// Treat --poll-aio as a cop-out. Alternative would be to make
4568+
// --overprovisioned a proper tri-state but there is a risk people have just
4569+
// set it to false by default explicitly.
4570+
// --poll-aio is safer from the POV as you can't set it with a special flag
4571+
// via rpk (like you can do with overprovisioned)
4572+
if (!reactor_opts.poll_aio.defaulted() && reactor_opts.poll_aio.get_value()) {
4573+
return false;
4574+
}
4575+
4576+
if (k8s_eevdf_deadlock()) {
4577+
seastar_logger.info("Automatically enabling --overprovisioned: running in k8s on "
4578+
"kernel versions with known scheduling issues");
4579+
return true;
4580+
}
4581+
4582+
return false;
4583+
}
4584+
45264585
void smp::configure(const smp_options& smp_opts, const reactor_options& reactor_opts)
45274586
{
45284587
#ifdef SEASTAR_TLS_DUAL_BACKEND
@@ -4535,7 +4594,12 @@ void smp::configure(const smp_options& smp_opts, const reactor_options& reactor_
45354594
internal::crypto::set_provider(reactor_opts.crypto_provider.get_selected_candidate()());
45364595
#endif
45374596

4538-
bool use_transparent_hugepages = !reactor_opts.overprovisioned;
4597+
bool overprovisioned = bool(reactor_opts.overprovisioned);
4598+
if (!overprovisioned && should_auto_overprovision(reactor_opts)) {
4599+
overprovisioned = true;
4600+
}
4601+
4602+
bool use_transparent_hugepages = !overprovisioned;
45394603

45404604
#ifndef SEASTAR_NO_EXCEPTION_HACK
45414605
if (smp_opts.enable_glibc_exception_scaling_workaround.get_value()) {
@@ -4569,7 +4633,7 @@ void smp::configure(const smp_options& smp_opts, const reactor_options& reactor_
45694633
_using_dpdk = native_stack && native_stack->dpdk_pmd;
45704634
#endif
45714635
auto thread_affinity = smp_opts.thread_affinity.get_value();
4572-
if (reactor_opts.overprovisioned
4636+
if (overprovisioned
45734637
&& smp_opts.thread_affinity.defaulted()) {
45744638
thread_affinity = false;
45754639
}
@@ -4583,7 +4647,7 @@ void smp::configure(const smp_options& smp_opts, const reactor_options& reactor_
45834647

45844648
resource::configuration rc;
45854649

4586-
rc.overcommit = reactor_opts.overprovisioned;
4650+
rc.overcommit = overprovisioned;
45874651

45884652
smp::_tmain = std::this_thread::get_id();
45894653
resource::cpuset cpu_set = get_current_cpuset();
@@ -4743,10 +4807,10 @@ void smp::configure(const smp_options& smp_opts, const reactor_options& reactor_
47434807

47444808
reactor_config reactor_cfg = {
47454809
.task_quota = std::chrono::duration_cast<sched_clock::duration>(reactor_opts.task_quota_ms.get_value() * 1ms),
4746-
.max_poll_time = [&reactor_opts] () -> std::chrono::nanoseconds {
4810+
.max_poll_time = [&] () -> std::chrono::nanoseconds {
47474811
if (reactor_opts.poll_mode) {
47484812
return std::chrono::nanoseconds::max();
4749-
} else if (reactor_opts.overprovisioned && reactor_opts.idle_poll_time_us.defaulted()) {
4813+
} else if (overprovisioned && reactor_opts.idle_poll_time_us.defaulted()) {
47504814
return 0us;
47514815
} else {
47524816
return reactor_opts.idle_poll_time_us.get_value() * 1us;
@@ -4761,7 +4825,7 @@ void smp::configure(const smp_options& smp_opts, const reactor_options& reactor_
47614825
.max_task_backlog = reactor_opts.max_task_backlog.get_value(),
47624826
.strict_o_direct = !reactor_opts.relaxed_dma,
47634827
.bypass_fsync = reactor_opts.unsafe_bypass_fsync.get_value(),
4764-
.no_poll_aio = !reactor_opts.poll_aio.get_value() || (reactor_opts.poll_aio.defaulted() && reactor_opts.overprovisioned),
4828+
.no_poll_aio = !reactor_opts.poll_aio.get_value() || (reactor_opts.poll_aio.defaulted() && overprovisioned),
47654829
.aio_nowait_works = reactor_opts.linux_aio_nowait.defaulted() ? std::optional<bool>(std::nullopt) : std::optional<bool>(reactor_opts.linux_aio_nowait.get_value()), // Mixed in with filesystem-provided values later
47664830
.abort_on_too_long_task_queue = reactor_opts.abort_on_too_long_task_queue.get_value(),
47674831
};

0 commit comments

Comments
 (0)