From f8b49c00a1777872289f8b374de38345110bf3c1 Mon Sep 17 00:00:00 2001 From: Max Bohomolov Date: Tue, 18 Aug 2026 12:27:13 +0000 Subject: [PATCH] finalize v2 scenatios --- README.md | 3 +++ scenario-helpers.sh | 1 + scenarios/docker-nested-subgroup.sh | 26 ++++++++++++++++++++++++++ scenarios/systemd-memory-above-host.sh | 16 ++++++++++++++++ scenarios/systemd-own.sh | 5 ++++- scenarios/systemd-quota-above-host.sh | 18 ++++++++++++++++++ 6 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 scenarios/docker-nested-subgroup.sh create mode 100644 scenarios/systemd-memory-above-host.sh create mode 100644 scenarios/systemd-quota-above-host.sh diff --git a/README.md b/README.md index 1cfd1ef..fd13727 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,13 @@ scenario_exec() { container_probe "$1" -m "$SET_MEMORY_BYTES" --cpus "$QUOTA" -- | `bare` | no limits at all, so every reading should fall back to host values | | `systemd-own` | deep chain, limits on the process's own cgroup, cpuset delegated | | `systemd-ancestor` | limit on an ancestor while the leaf carries no memory controller files | +| `systemd-quota-above-host` | a cpu quota larger than the machine has cores, which is a permission, not a resource | +| `systemd-memory-above-host` | the same on the memory axis: a limit larger than the machine has RAM | | `docker-private` | private cgroupns (docker's default), all three axes at once | | `docker-memory-only` | memory alone, CPU falls back to host | | `docker-cpuset-only` | cpuset alone, memory falls back to host | | `docker-host-ns` | `--cgroupns=host`, where the container sees the full chain instead of its own root | +| `docker-nested-subgroup` | a tighter cgroup made inside the container: two levels, two different limits | | `docker-cgroup-parent` | limit on a parent cgroup, set outside the container (needs docker's systemd driver) | | `k8s-limits` | a pod with container limits, as kubelet writes them | | `k8s-no-limits` | a pod with nothing set, so every reading should fall back to the node's values | diff --git a/scenario-helpers.sh b/scenario-helpers.sh index 481987a..a02912c 100644 --- a/scenario-helpers.sh +++ b/scenario-helpers.sh @@ -21,6 +21,7 @@ have_sudo() { sudo -n true 2>/dev/null; } have_systemd_user() { systemd-run --user --scope -q true 2>/dev/null; } cgroup_driver() { "$ENGINE" info -f '{{.CgroupDriver}}' 2>/dev/null; } ncpus() { nproc 2>/dev/null || getconf _NPROCESSORS_ONLN; } +mem_total_bytes() { awk '/^MemTotal:/ {print $2 * 1024}' /proc/meminfo; } # Prints the first unmet requirement from $REQUIRES, or nothing when all are met. unmet_requirement() { diff --git a/scenarios/docker-nested-subgroup.sh b/scenarios/docker-nested-subgroup.sh new file mode 100644 index 0000000..81d17dd --- /dev/null +++ b/scenarios/docker-nested-subgroup.sh @@ -0,0 +1,26 @@ +# shellcheck shell=bash +SCENARIO_DESC="a tighter cgroup created inside the container, so two levels carry different limits" +REQUIRES="engine" +INNER_STYLE=container + +OUTER_MEMORY_BYTES=$((512 * 1024 * 1024)) +SET_MEMORY_BYTES=$((256 * 1024 * 1024)) + +# The container gets one limit and the probe then puts itself under a tighter one, the shape kubelet produces +# and the only scenario here where two levels hold different limits - so the tightest one has to win. +# +# The dance is the kernel's "no internal processes" rule: a cgroup cannot both hold processes and delegate +# controllers to its children, so the shell first vacates the namespace root, then enables the memory +# controller for children, and only then moves into the tighter cgroup it created. +NESTED_SETUP=' +mount -o remount,rw /sys/fs/cgroup 2>/dev/null || true +mkdir -p /sys/fs/cgroup/init /sys/fs/cgroup/inner +echo $$ > /sys/fs/cgroup/init/cgroup.procs +echo +memory > /sys/fs/cgroup/cgroup.subtree_control +echo '"$SET_MEMORY_BYTES"' > /sys/fs/cgroup/inner/memory.max +echo $$ > /sys/fs/cgroup/inner/cgroup.procs +' + +scenario_exec() { + container_probe "$NESTED_SETUP $1" --privileged -m "$OUTER_MEMORY_BYTES" +} diff --git a/scenarios/systemd-memory-above-host.sh b/scenarios/systemd-memory-above-host.sh new file mode 100644 index 0000000..ad6c4d6 --- /dev/null +++ b/scenarios/systemd-memory-above-host.sh @@ -0,0 +1,16 @@ +# shellcheck shell=bash +SCENARIO_DESC="a memory limit larger than the machine has RAM" +REQUIRES="systemd-user" +INNER_STYLE=local + +# The memory twin of systemd-quota-above-host, and the axis where the library already guards itself: a limit at +# or above the host's total is treated as no limit, so the derived figures fall back to host values while the +# sensor still reports the number the file holds. Realistic wherever a container or pod is given a limit larger +# than the machine it landed on. +SET_MEMORY_BYTES=$(($(mem_total_bytes) + 1024 * 1024 * 1024)) + +UNIT="bench-memory-above-host-$$" + +scenario_exec() { + systemd-run --user --scope -q --unit "$UNIT" -p "MemoryMax=$SET_MEMORY_BYTES" sh -c "$1" +} diff --git a/scenarios/systemd-own.sh b/scenarios/systemd-own.sh index 8cd1ae8..a16cfbb 100644 --- a/scenarios/systemd-own.sh +++ b/scenarios/systemd-own.sh @@ -4,8 +4,11 @@ REQUIRES="sudo systemd-run min${BENCH_CPUS}cpu" INNER_STYLE=local # A system scope, not a user one: user slices do not delegate cpuset, so AllowedCPUs would be silently ignored. -# The quota is a core looser than the cpuset here, the mirror of docker-private, where it is tighter. +# The quota is a core looser than the cpuset here, the mirror of docker-private, where it is tighter. It stops +# at the machine's core count: a quota above that is a different question, asked on purpose by +# systemd-quota-above-host, and letting this row drift into it would just duplicate that one. QUOTA_CORES=$((BENCH_CPUS + 1)) +[ "$QUOTA_CORES" -gt "$(ncpus)" ] && QUOTA_CORES=$(ncpus) SET_MEMORY_BYTES=$((512 * 1024 * 1024)) SET_CPU_CORES=$QUOTA_CORES diff --git a/scenarios/systemd-quota-above-host.sh b/scenarios/systemd-quota-above-host.sh new file mode 100644 index 0000000..81eea0e --- /dev/null +++ b/scenarios/systemd-quota-above-host.sh @@ -0,0 +1,18 @@ +# shellcheck shell=bash +SCENARIO_DESC="a cpu quota larger than the machine has cores" +REQUIRES="systemd-user" +INNER_STYLE=local + +# Nothing rejects a quota above the machine's core count: systemd writes it as asked, and a kubernetes limit +# above node capacity does the same whenever the requests are small enough to schedule. Docker is the exception, +# it validates --cpus against nproc. The cores that exist are the real ceiling, so a derived "allowed cores" +# above them means the utilization ratio is divided by more than can ever be consumed. +QUOTA_CORES=$(($(ncpus) + 1)) + +SET_CPU_CORES=$QUOTA_CORES + +UNIT="bench-quota-above-host-$$" + +scenario_exec() { + systemd-run --user --scope -q --unit "$UNIT" -p "CPUQuota=$((QUOTA_CORES * 100))%" sh -c "$1" +}