feat(root-ubuntu): add zram + earlyoom so a memory spike stalls, not wedges - #51
Merged
Conversation
…wedges
configure_swap gives a box slack. Nothing decided what dies when the slack
runs out, and on its own the slack was bought in the most expensive
currency the box has: disk.
The failure this prevents reads as a hardware fault. RAM fills, the kernel
reclaims, reclaim goes to disk, every process blocks on that disk, load
climbs, and the kernel's OOM killer -- which only fires once the machine is
already thrashing -- shoots something large and arbitrary. From a terminal
it is indistinguishable from a reboot, and `uptime` afterwards says it
wasn't one, which gets believed over the person who watched it happen.
Two functions, fixing different halves. zram is a compressed block device
in RAM used as swap ahead of the file, so reclaim costs CPU instead of
seeks (~3:1 with zstd; a live box here holds 468M of pages in 170M). It
does not add memory, it postpones the wall. earlyoom kills the biggest
consumer while the box is still responsive -- the kernel's killer is not
wrong about what to kill, it is late.
Ordered after configure_swap, which returns early if any swap is active:
zram first would suppress the swapfile entirely. The file stays as the
deeper tier; zram sits above it at priority 100.
Both are idempotent. Config goes through write_if_changed, the service is
only bounced when something changed or is not running, and an already
correct zram device is never cycled -- doing that on every refresh would
drop whatever the box had paged out. Verified by running the two functions
twice on a live box.
Three traps handled, all of which fail silently or half-way:
- DigitalOcean images ship no zram module; modprobe fails and the unit
then blames dev-zram0.device, never naming the real cause. Installs
linux-modules-extra for the running kernel and retries.
- a zram0 that already has a disksize refuses reconfiguration with EBUSY
and quietly keeps its old size and algorithm, so a changed config
appears to apply and hasn't. Reset before restart.
- systemd expands $EARLYOOM_ARGS from EnvironmentFile by splitting on
whitespace with no shell quote removal, so a quoted --avoid regex
arrives with the quotes inside the pattern and matches nothing. The
service starts happily either way.
swappiness 10 is right for a disk swapfile and wrong once paging out is a
memcpy, so a 61- drop-in raises it to 100 -- but only where zram actually
came up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRvWqCAeKAvvHFmQom8iMT
ThreatCrush Security Scan10 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 4 | LOW: 4
Snippets are redacted; ThreatCrush never prints matched credential material. |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
configure_swapgives a box slack. Nothing decided what dies when the slack runs out, and on its own it bought that slack in the most expensive currency the box has: disk.The failure this prevents reads as a hardware fault. RAM fills → the kernel reclaims → reclaim goes to disk → every process blocks on that disk → load climbs → the kernel's OOM killer (which only fires once the machine is already thrashing) shoots something large and arbitrary. From a terminal it is indistinguishable from a reboot — and
uptimeafterwards says it wasn't one, which tends to get believed over the person who watched it happen.This is not hypothetical: it hit
devtwice in one night, and two of our public boxes were running with zero swap.What
Two functions, fixing different halves:
Ordered after
configure_swap, which returns early if any swap is active — zram first would suppress the swapfile entirely. The file stays as the deeper tier; zram sits above it at priority 100.Idempotent
Config goes through
write_if_changed; the service is bounced only when something changed or isn't running; and an already-correct zram device is never cycled — doing that on every--refreshwould drop whatever the box had paged out.Verified by running both functions twice on a live box:
Three traps handled, all of which fail silently or half-way
modprobe zramfails, and the unit then blamesdev-zram0.device— never naming the real cause. Installslinux-modules-extra-$(uname -r)and retries. (All four of our DO droplets needed this; netcup did not.)Device or resource busyand quietly keeps its old size and algorithm — so a changed config appears to apply and hasn't. Reset before restart.$EARLYOOM_ARGSfromEnvironmentFileby splitting on whitespace with no shell quote removal, so a quoted--avoid '^(sshd|...)$'arrives with the quote characters inside the pattern and matches nothing. The service starts happily either way. Documented in-line, verify withps -o args= -C earlyoom.Notes
vm.swappiness=10is right for a disk swapfile and wrong once paging out is a memcpy, so a61-drop-in raises it to 100 — but only where zram actually came up. A box with just a swapfile keeps the conservative value.EARLYOOM_PREFERis deliberately empty by default. A prefer list is right on a workstation (the answer is obviously "the browser"); on a server the biggest consumer is the app, and shooting it first only means it dies sooner than the box would have made it. Let size decide and let systemd restart it.bash -nclean; no new shellcheck warnings in the added section.🤖 Generated with Claude Code
https://claude.ai/code/session_01SRvWqCAeKAvvHFmQom8iMT