Skip to main content

handle-machine-shared-memory

Use when a shared multi-user Linux box keeps freezing or going unresponsive from out-of-memory (OOM) events, when one user's process is eating all the RAM, or when someone needs to cap memory so the machine stays usable without starving legitimate work. Covers cgroup-v2 / systemd memory guard rails (MemoryMax / MemoryMin), giving Docker its own capped slice, a preflight that refuses over-budget eval runs, and the reasoning that makes the caps safe. Triggers on "the server keeps freezing", "OOM killed my process / dbus / the session", "someone is hogging all the memory", "put a memory limit on docker/users so the machine never dies", "cgroup memory caps", "MemoryMax", "the box becomes unusable under load". Assumes you have root but NOT hardware control (can't add RAM).

Zur Installation springen

Quellinformationen

Repository
AMindToThink/claude-code-settings
Letzte Quellaktivität
1. September 2026 um 12:47
Erkannte Sprache von SKILL.md
Englisch
Sterne
4
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
handle-machine-shared-memory
description
Use when a shared multi-user Linux box keeps freezing or going unresponsive from out-of-memory (OOM) events, when one user's process is eating all the RAM, or when someone needs to cap memory so the machine stays usable without starving legitimate work. Covers cgroup-v2 / systemd memory guard rails (MemoryMax / MemoryMin), giving Docker its own capped slice, a preflight that refuses over-budget eval runs, and the reasoning that makes the caps safe. Triggers on "the server keeps freezing", "OOM killed my process / dbus / the session", "someone is hogging all the memory", "put a memory limit on docker/users so the machine never dies", "cgroup memory caps", "MemoryMax", "the box becomes unusable under load". Assumes you have root but NOT hardware control (can't add RAM).
# Handling shared-machine memory (stop the OOM freezes) **Goal:** the machine never becomes globally unusable, *and* each user (and Docker) can still use a lot of memory. The failure you're fixing is not "a process died" — it's "a process died and took `dbus`/`sshd` with it, freezing the whole box." One process OOMing is fine and even desirable (fail fast); the session going down is not. The whole approach is one idea: **make a machine-wide OOM structurally impossible (the rail), and make the split between users and Docker cheap to change (the dial)** — so you never have to forecast the "right" numbers. **Boundary rule:** these commands change a shared machine's global state and need root. Produce them, explain exactly how to run them, and let the human run them — do NOT execute `systemctl` / `set-property` / `docker restart` / swap changes yourself unless the user has explicitly told you to. If they say "I'll run those," believe them and just hand over precise, copy-pasteable commands. --- ## 0. Diagnose first (read-only) — and don't trust an empty grep Before changing anything, prove what actually happened. - **Find the OOM victims and the collateral.** `journalctl -k` / `dmesg` for `Out of memory`, `oom-kill`, `Killed process`. Identify the hog (PID, UID, RSS, runtime) *and* whether the kernel killed anything load-bearing (`dbus-daemon`, `sshd`, `systemd`) — that collateral is *why the machine froze*, not the hog itself. - **Empty grep output on a shared box often means permission-denied, not absence.** If you can't read `/var/log` or another user's `journalctl`, you'll get nothing back and wrongly conclude "no OOMs." Confirm you can actually read the source (try with `sudo`, check exit codes) before reporting "none found." This is a recurring self-inflicted false conclusion. - **`memory.current` includes reclaimable page cache.** A slice showing 39 GiB "used" may be mostly cache from reading large files; a cap it hits triggers *reclaim*, not an OOM. Don't size caps off a cache-inflated number, and don't size them off an *idle-moment snapshot* taken after the hog was already killed — that measures a coffee break, not the workload. Record physical RAM, cgroup version, whether `systemd-oomd` is installed, and the current caps: ```bash grep MemTotal /proc/meminfo systemctl show user.slice -p MemoryMax -p MemoryMin -p MemoryCurrent systemctl show system.slice -p MemoryMax -p MemoryMin -p MemoryCurrent cat /sys/fs/cgroup/user.slice/memory.max 2>/dev/null ``` --- ## 1. The rail — the one invariant that never moves **Caps are permissions, not reservations.** `MemoryMax=80G` means "you may never exceed 80", NOT "80 is set aside for you." So two caps that each look safe can still over-commit the machine: ``` user.slice = 80G + docker.slice = 20G = 100G on a 94 GiB box ``` Both slices sit "within limits" right until physical RAM is exhausted → global OOM → dbus dies → freeze returns. **The rail: `user.slice + docker.slice ≤ MemTotal − RESERVE`.** That sum, under physical RAM, is the only thing that makes a machine-wide OOM impossible. Everything else is a dial; this is the rail. **Derive RESERVE** (everything charged to *neither* user nor docker slice): - `system.slice` — sshd, dbus, dockerd, containerd, journald, plus `containerd-shim` processes (~15 MB each × containers). Budget a few GiB. - Kernel memory charged to no cgroup — slab, page tables, network buffers: ~2–3 GiB. - A responsiveness cushion so `ssh`/`htop`/`ls` always work: ~1–2 GiB. A reserve of ~8 GiB is a reasonable starting default on a ~90 GiB box. **Compute the rail as `floor(MemTotal_GiB − RESERVE_GIB)`; never hardcode the result** — flooring rounds toward a larger reserve (conservative), and on a new machine the number follows automatically. --- ## 2. `MemoryMax` vs `MemoryMin` — use both, for different jobs - **`MemoryMax` = ceiling** ("you may never exceed this"). A *permission*. Summing these is the rail; it makes a global OOM impossible. - **`MemoryMin` = floor** ("this much is yours unconditionally; never reclaimed"). A *grant*. This keeps the machine *responsive* — sshd/dbus can't be reclaimed out from under you. **`MemoryMin` alone is NOT enough, and this is the subtle part:** it protects against *reclaim*, not against the *OOM killer*. Per the kernel cgroup-v2 docs, if no unprotected reclaimable memory remains, the OOM killer still fires — and victim selection never consults `memory.min`. So you need both: summing caps make a global OOM impossible; `MemoryMin` grants keep the survivors resident. Neither substitutes for the other. **Trap: "uncapped" is not "protected."** A slice with `MemoryMax=infinity`, `MemoryMin=0` (the usual default for `system.slice`) can grow *and* be fully reclaimed under pressure — it looks protective and reserves nothing. Give the must-survive slice a real `MemoryMin`. --- ## 3. Docker needs its own slice (containers escape `user.slice`) `docker run` is **not a fork — it's an RPC to `dockerd`.** The CLI asks the daemon (root, in `system.slice`) to create the container; a process inherits its cgroup from its *parent*, and the parent is the daemon, not your shell. Under the default `cgroupfs` driver the container lands at top-level `/sys/fs/cgroup/docker/<id>`, **outside `user.slice` entirely**. So a per-user cap does nothing to containers, and Docker needs its own top-level `docker.slice`, capped directly. **One-time plumbing** (the only disruptive/irreversible step — do it while zero containers exist, when it's free): ```bash sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json >/dev/null <<'EOF' { "exec-opts": ["native.cgroupdriver=systemd"], "cgroup-parent": "docker.slice" } EOF sudo systemctl restart docker ``` If `daemon.json` already exists with other settings, MERGE these two keys in — don't overwrite (a second write silently drops the old content). Verify it's valid, non-empty JSON before restarting (`python3 -c "import json; json.load(open('/etc/docker/daemon.json'))"`). **Trap: capping `docker.service` caps nothing.** `set-property docker.service MemoryMax=…` limits only the ~36 MB *daemon* — containers aren't its cgroup children. It looks like a working Docker limit and enforces nothing. Cap `docker.slice`. --- ## 4. Apply the caps ```bash # Rail: docker + user must sum under MemTotal - RESERVE. Example on a 94 GiB box # (rail = 86): pick a starting split, e.g. docker 36 / user 50. sudo systemctl set-property docker.slice MemoryMax=36G sudo systemctl set-property user.slice MemoryMax=50G # rail - docker sudo systemctl set-property user-1000.slice MemoryMax=40G # per-user, under the parent sudo systemctl set-property user-1001.slice MemoryMax=40G # Reservations — the "simple commands always work" part (often ZERO by default). sudo systemctl set-property system.slice MemoryMin=4G sudo systemctl set-property user-0.slice MemoryMin=1G # root's shell lives in user.slice ``` - Per-user caps that sum ABOVE the parent (40+40 > 50) are intentional: either user can burst to most of the user budget, neither can take it all. They're inert until the parent `user.slice` cap is set (a child can't exceed its parent). - `set-property` persists to `/etc/systemd/system.control/<unit>.d/` and takes effect **live, no restart**. Undo one with `sudo systemctl revert <unit>`. - The starting split is a *starting position, not a prediction*. Don't justify it with a measured "real need" — today's heaviness may be a bug, and tomorrow's Docker need is larger. Make it cheap to change instead (next section). --- ## 5. The dial + a preflight (so you never forecast) Turning the dial is `user.slice = rail − docker.slice`, two `set-property` calls. Do NOT build a "rebalance script" that prints commands for a human to run — **a script that emits commands enforces nothing** (same shape as the `docker.service` trap: looks like a guard rail, isn't). Put the reasoning in a short doc, and put enforcement where it can actually enforce: 1. **The kernel** — `MemoryMax` is the rail. 2. **A launch-path preflight** — refuse over-budget runs before they spend anything. **Make container cost arithmetic, not a guess.** Container memory is bounded: each sample's compose file declares `mem_limit`s (e.g. BashArena ≈ 2.25 GiB/sample across ~5 containers). So `docker.slice ÷ per_sample = concurrent samples`, and any desired concurrency names its own budget. A preflight that (a) sums `mem_limit`s from the compose file, (b) reads `/sys/fs/cgroup/docker.slice/memory.max`, (c) refuses if `max_samples × per_sample > budget` — and whose refusal **prints the exact `set-property` commands to widen the budget** (derived from `MemTotal − RESERVE`, not a magic constant) — *is* the dial. No separate calculator; it already computed the number. **The preflight must:** - **Never auto-rebalance.** Silently shrinking the other user's budget so your run fits is exactly the quiet action to avoid. Refuse, print, let a human decide. - **Fail loudly on unreadable inputs** (missing/uncapped `docker.slice`, unreadable compose) rather than assuming a default and pressing on — a check computed from a guessed number is worse than no check, because it looks like it passed. **Why over-budget is worse than a freeze once capped:** with `docker.slice` capped, an over-committed run no longer freezes the box — the kernel just OOM-kills containers mid-eval, producing trajectories where *infrastructure* failures wear *agent-failure* clothing. Bad data type-compatible with good data. The preflight exists to stop that silent corruption, not just the freeze. --- ## 6. Deliberately skipped / deferred - **Swap: skip it** (for a box where big memory is usually a bug). At `MemoryMax` the kernel reclaims first and OOM-kills only if reclaim fails; a big Python heap is nearly all *anonymous* memory, so with no swap there's nothing to reclaim and the kill is immediate and clean. Adding swap turns that clean kill into disk thrash that saturates the shared disk — reintroducing "unusable" via the I/O path. - **`MemoryHigh` (soft throttle): defer** until you have a *measured* profile of legitimate large runs, and add it *with* swap (it can't throttle anon-heavy Python with nowhere to reclaim to). Size it just below `MemoryMax`, as a brake — set it to "worst bug so far" and a legitimate large run sits permanently throttled into uselessness. - **`systemd-oomd`: optional** — PSI-based proactive killing with better victim selection than the kernel's, if installed. --- ## 7. Verify (don't infer from config) 1. **Containers really land in the slice.** After the restart + a fresh login, start a container and confirm it's under `/sys/fs/cgroup/docker.slice/…`, NOT top-level `/sys/fs/cgroup/docker/`. If top-level, `cgroup-parent` didn't take and the cap enforces nothing. 2. **The rail holds.** As one user: `python3 -c "x=bytearray(60*2**30)"`. Confirm it's killed at the per-user cap, the **other user is unaffected**, **dbus survives**, `ssh` still connects, and the machine never freezes. 3. **The dial works.** Run the preflight over-budget → it refuses and prints the commands; apply them; re-run → passes. That loop is what replaces forecasting. **Docker-socket gotcha before any of this is testable:** `docker ps` can fail with `permission denied … /var/run/docker.sock` even when `getent group docker` lists your user — a login session predating the group-add carries stale supplementary groups. `newgrp docker` or a fresh login clears it. (Corollary: the `docker` group is root-equivalent — `-v /:/host` — so per-container `--memory` is unenforceable by anyone but the requester, but an aggregate *slice* cap is enforced by the kernel regardless.) --- ## 8. Communicating the commands (recurring pain points) When you hand root commands to a human, be explicit enough that they never have to guess — this genuinely tripped the user up more than once: - **State who and where.** "Run as your normal user (`sudo` elevates per-command); directory doesn't matter for these." Don't make them wonder if they must `su` to root. - **`sudo` = root privileges, NOT root's folders.** A plain `sudo <cmd>` keeps your current working directory; only `sudo -i` moves you to `/root`. Since these commands use absolute paths (`/etc/docker/…`) or no path (`systemctl`), cwd is irrelevant — say so, so they don't second-guess. - **Relative-path commands are the exception.** The preflight uses `scripts/…`, so it *must* run from the repo root, as the normal user, no sudo. Call this out separately. - **"Create a file with content" ≠ `touch`.** `touch` makes an empty file; an empty `daemon.json` is invalid JSON and breaks dockerd. Give the full `sudo tee <<'EOF'` heredoc, and a `cat` + JSON-validate step before the restart. - **Flag when a command isn't available yet.** If the preflight lives on an unmerged branch, it won't be on disk on `main` — say that, so they don't run it into a "file not found."
Auf GitHub ansehen