Repository navigation
Conversation
Sets mem_limit and memswap_limit on all seven running services. Seven, not the six #114 names — blackbox-exporter joined the stack after it was written. #114 was blocked on data, and docs/roadmap.md had already narrowed the gate: not "wait for more history" but "explain the 2026-08-29 hour in which loki and alloy both peaked". That hour is explained. At 06:18:14-15 both processes tore down internal components in the same second — alloy logged 35 "node exited without error" lines in three minutes and re-registered its collectors at 06:21, loki's compactor stopped and cycled start/stop five more times through 06:34, logging "instance not found in the ring" each time and returning 400s while alloy dropped batches. Both were at baseline by 06:40 and neither container restarted. It is component churn, not a leak and not a deploy, and it has not recurred since. The method needed changing too, and for a reason worth recording: "3x every peak" did not fit the machine because it was being read off the working set, which includes reclaimable page cache. mem_limit bounds a cgroup the kernel reclaims cache from before it kills anything, so what a limit must clear is the anonymous high-water mark. alloy is the worked example — a 512 MiB working-set peak is 221 MiB of RSS and the rest is cache from walking /rootfs. Sized from container_memory_rss over 2026-08-21 -> 09-04, with the multiplier chosen per service rather than flat, the seven sum to 5120 MiB against 7816 MiB of RAM; read off working set the same rule gives 7602 MiB, which would bound nothing in aggregate. loki gets the loosest ratio and the largest number. #114 had listed it among the low-risk services to start with, on a 152 MiB peak from a six-hour window; fourteen days make it the riskiest of the seven, and 1536M is set to survive a recurrence of the 06:20 event rather than to clip it. memswap_limit equals mem_limit everywhere, so the stack cannot page into the unencrypted /swap.img. This is the decision #114 asked to be made deliberately rather than defaulted, and the default is the wrong way round: unset, it is twice mem_limit, so adding memory limits alone would have made that exposure worse. Measured cost is 70 MiB of swap for prometheus and less for the rest. docs/security.md and SECURITY.md record the narrowing without overstating it. Enforcement and detection stay uncoupled, which is why #63 was a separate issue in the first place. ContainerHighMemory still measures against machine_memory_bytes. ContainerNearMemoryLimit is the limit-relative complement the limits make possible, on RSS for the same reason the limits were sized from it, with promtool cases that fail if the numerator is "simplified" to working set or the divide-by-zero guard is dropped — both mutations were run and both fail. That the rule can fire against real data rather than only fixtures was checked end to end with a throwaway container: compose mem_limit -> memory.max, memswap_limit -> memory.swap.max = 0, cAdvisor reporting a non-zero container_spec_memory_limit_bytes, and the expression producing a series. Not applied to stacks/lab: it runs on its own guest with its own Prometheus and no series reaching this one, so it has nothing of its own to size from. Recorded there as a difference rather than left as an omission. scripts/validate.sh passes. The stack has not been restarted onto these limits yet — that is the converge timer's job once this is on main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gerrrt
force-pushed
the
gerrrt/memory-limits-services-c6ba60
branch
from
September 4, 2026 22:17
42eca85 to
2e6cf06
Compare
Owner
Author
|
Closing as superseded by #298, which is merged and deployed. This is not a rejection of the work in here — #298 was built from this
Every limit merged unchanged: Three things were corrected on the way in, recorded here because the reasoning
Some smaller numbers were re-measured too (medians were means; alertmanager's Deploy verification is on #298. The branch is being deleted along with its |
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.
Closes #114.
Sets
mem_limitandmemswap_limiton all seven running services — seven, notthe six #114 names, because
blackbox-exporterjoined the stack after the issuewas written.
The gate, and how it was cleared
#114 was blocked on data, and
docs/roadmap.mdhad already narrowed what wouldunblock it: not "wait for more history" but explain the 2026-08-29 hour in
which
lokiandalloyboth peaked, because a peak two services share iseither real correlated load or an artefact, and only one of those is worth
sizing a ceiling around.
It is the artefact. At 06:18:14–15 both processes tore down internal components
in the same second — Alloy logged 35
node exited without errorlines in three minutes (210 across the hour) andre-registered its cAdvisor and unix collectors at 06:21; Loki's compactor module
stopped and then cycled start/stop five more times through 06:34, logging
instance not found in the ringeach time and returning 400s while Alloy droppedbatches. Both were back at baseline by 06:40, and neither container restarted —
creation times either side are unchanged. In-process component churn: not a
crash, not a deploy, not a leak, and not recurred in the six days since.
There are now 14.2 days of
container_memory_*history (2026-08-21 → 09-04),so the issue's first "Done when" box is satisfied on its own terms as well.
The method had to change too
3x every peakdid not fit the machine — but the reason is not that the machineis small. It is that the peaks were being read off the working set, which
includes reclaimable page cache.
mem_limitbounds a cgroup the kernel reclaimscache from before it OOM-kills anything, so what a limit must actually clear is
the anonymous high-water mark.
alloyis the worked example: a 512 MiBworking-set peak is 221 MiB of RSS and the rest is cache from walking
/rootfs.Sized from RSS with the multiplier chosen per service, the seven sum to
5120 MiB against 7816 MiB of RAM. The same
3xrule read off theworking-set column gives 7602 MiB — the limits would sum to the whole machine
and bound nothing in aggregate, which is the one thing they exist to do.
lokigets the loosest ratio and the largest number. #114 listed it amongthe low-risk services to start with, on a 152 MiB peak from a six-hour window.
Fourteen days invert that: it has by far the widest dynamic range here, and
1536M is set to survive a recurrence of the 06:20 event rather than to clip it.
OOM-killing the log store during component churn turns a self-healing blip into
an outage and takes the record of it along too.
Swap: decided, not defaulted
memswap_limitequalsmem_limiteverywhere, somemory.swap.maxis 0 and thestack cannot page out at all. The default is the wrong way round — unset, it is
twice
mem_limit, so adding memory limits alone would have made theunencrypted-
/swap.imgexposure worse, by giving constrained containers areason to spill into it. Measured cost of switching it off: 70 MiB of peak swap
for Prometheus, 17 for Alloy, 12 for Loki, under 5 for the rest.
docs/security.mdandSECURITY.mdrecord the narrowing without overstatingit — the disk is unchanged, the host still swaps, and anything already paged out
is still there.
Enforcement stays uncoupled from detection
Which is why #63 was a separate issue in the first place.
ContainerHighMemorystill divides by
machine_memory_bytes.ContainerNearMemoryLimitis thelimit-relative complement, added with its own
promtool test rulescases, andit measures RSS for the same reason the limits were sized from it.
Two mutations were run against the new tests, and both fail as they should:
changing the numerator to working set fails the page-cache case, and dropping
the
> 0guard fails the unlimited-container case with+Inf%.That the rule can fire against real data rather than only fixtures was
checked end to end with a throwaway container: compose
mem_limit→memory.max,memswap_limit→memory.swap.max = 0, cAdvisor reporting anon-zero
container_spec_memory_limit_bytes, and the expression producing aseries. That check is the one #63 never had.
Not applied to
stacks/labIt runs on its own guest with its own Prometheus and no series reaching this
one, so it has nothing of its own to size from. Copying these numbers would be a
limit derived from a different machine. Recorded there as a difference rather
than left as an omission.
What is not done
scripts/validate.shpasses (2 skips, both "not the deployment checkout").The stack has not been restarted onto these limits — this worktree is not
the deployment checkout, so the third "Done when" box is the converge timer's
job once this is on
main. Worth watching after it deploys:ContainerOomKilledquiet, and no service settling near its ceiling.
Separately, while measuring:
container_start_time_secondsdid not move whenthe stack was stopped and started on 09-04 — it still reports each container's
creation time. If that also holds for Docker's restart policy, then
ContainerRestartLoopcannot fire for the crash loop it exists to catch, whichis the #63 shape again. Not touched here; flagged for its own issue.
🤖 Generated with Claude Code