Skip to content

fix(monitoring-agents): give kube-state-metrics a sized memory limit - #3846

Open
myasnikovdaniil wants to merge 2 commits into
mainfrom
fix/kube-state-metrics-resources
Open

fix(monitoring-agents): give kube-state-metrics a sized memory limit#3846
myasnikovdaniil wants to merge 2 commits into
mainfrom
fix/kube-state-metrics-resources

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

kube-state-metrics ran BestEffort with no resources at all: the subchart ships resources: {} and our wrapper never set any. So it put no memory.max on its pod cgroup, and the Talos userspace OOM handler only ever selects cgroups that lack one, which left it permanently in the victim set. Any tenant workload driving node memory pressure could evict the source of every kube-state metric we alert on. Background is in docs/operations/system-memory-limits.md.

The numbers are measured

On a three node cluster with 322 pods and roughly 25.6k series, which is the smallest we run: 14 day peak working set 307Mi, peak RSS 280Mi, mean 176Mi.

So the request sits above the mean at 192Mi rather than at the peak, and the limit carries wide headroom at 2Gi. The headroom is deliberate: this component's memory scales with the number of objects in the cluster, and the --metric-labels-allowlist=pods=[*],deployments=[*] already in this file widens every pod and deployment series further, so a cluster larger than the one measured lands well above what was seen here. 2Gi is 6.7x that peak, which read as linear in object count is roughly 2100 pods and 170k series, so the limit stays a ceiling rather than a bound well past the largest cluster this sizing was drawn from.

No CPU limit, following the node DaemonSet sizing in c9ba7a3cc, which set one zero times across nineteen files. CPU is irrelevant to the OOM ranking, and a ceiling that binds on a scraper turns into scrape timeouts rather than backpressure.

This supersedes #2632

#2632 proposes 256Mi, which is below the 307Mi peak measured on the smallest cluster we have, so it would convert an eviction risk into a reliable OOM kill. It also adds a cpu: 200m limit, which is the half that buys nothing. The diagnosis there was right and it sat since May; the numbers are what needed replacing.

Checks

packages/system/monitoring-agents/tests/kube_state_metrics_resources_test.yaml pins the limit, both requests, and the absence of a CPU limit. Mutation checked: dropping the limit to 256Mi turns it red. The chart's suites are 3 files and 6 tests, all green.

Release note

fix(monitoring-agents): kube-state-metrics now carries a memory request and limit sized from a measured peak, so it is no longer a permanent candidate for the node OOM handler

Summary by CodeRabbit

  • New Features
    • Added resource settings for kube-state-metrics, including CPU and memory requests and a 2Gi memory limit.
    • Added validation to ensure the configured resource values are applied correctly and no CPU limit is set.

It ran BestEffort with no resources at all, so it set no memory.max on
its pod cgroup and the Talos userspace OOM handler kept it permanently
in the victim set: any tenant workload driving node memory pressure
could evict it, and it is the source of every kube-state metric we
alert on.

Numbers are measured rather than guessed. A 14d peak on a 3-node
cluster with 322 pods and ~25.6k series is 307Mi working set, 280Mi
RSS, 176Mi mean. The request sits above the mean and the limit carries
wide headroom, because this component scales with object count and
--metric-labels-allowlist widens every pod and deployment series
further, so a larger cluster lands well above what was measured.

No CPU limit, following the node DaemonSet sizing in c9ba7a3: CPU is
irrelevant to the OOM ranking, and a binding ceiling on a scraper turns
into scrape timeouts rather than backpressure.

Supersedes #2632, whose 256Mi limit sits below the measured peak.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
@github-actions github-actions Bot added size/M This PR changes 30-99 lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug labels Aug 16, 2026
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e70d652-e7f1-4585-b9ac-7746cd65e55e

📥 Commits

Reviewing files that changed from the base of the PR and between 9e251dd and c2bc0ac.

📒 Files selected for processing (1)
  • packages/system/monitoring-agents/values.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/system/monitoring-agents/values.yaml

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The monitoring-agents chart adds kube-state-metrics resource requests and a memory limit. A Helm test verifies these values and confirms that no CPU limit is configured.

Changes

Kube-state-metrics resources

Layer / File(s) Summary
Resource configuration and validation
packages/system/monitoring-agents/values.yaml, packages/system/monitoring-agents/tests/kube_state_metrics_resources_test.yaml
Adds a 50m CPU request, 192Mi memory request, and 2Gi memory limit. Tests verify these values and the absence of a CPU limit.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to c2bc0

The change adds measured memory requests and a limit for kube-state-metrics while preserving the intended absence of a CPU limit; no actionable merge-blocking risk remains after normal checks and review.

Possibly related PRs

Suggested reviewers: ivanhunters

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the primary change: adding a sized memory limit for kube-state-metrics.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kube-state-metrics-resources

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rendered it to check the claim chain: the limit lands on the only container in the pod, so pod-level memory.max is real. Sizing sits inside the series envelope — limit/peak 6.7x vs 6.1x/3.2x for node-exporter/fluent-bit, request/peak 0.63, cpu request 50m matching fluent-bit. The "no CPU limit, following c9ba7a3" claim checks out: that commit touches 19 files and none of them sets limits.cpu. helm unittest green.

Two questions, neither blocking:

  • vmagent in this same package still has resources: {} on both containers, and vpa-vmagent in updateMode: Initial cannot synthesize a limit without a request-to-limit ratio — so it stays in exactly the perpetual-victim state this PR fixes for kube-state-metrics. Follow-up of the series, or deliberately left to the LimitRange?
  • Why 2Gi rather than the 4Gi LimitRange default from #3494? For the one component whose memory scales with object count, an explicit chart limit below the blanket default is the direction the docs call risky. A short "2Gi covers Nx the measured object count" note would close this.

Review asked why the chart pins 2Gi when the default LimitRange limit
proposed in #3494 is 4Gi, and the sizing comment named the measured
peak without saying how much room 2Gi leaves above it.

2Gi is 6.7x the measured 307Mi peak working set. Read as linear in
object count, that is roughly 2100 pods and 170k series against the
322 pods and ~25.6k series the peak was measured on, so the limit
stays a ceiling rather than a bound on a cluster several times larger
than the one behind the numbers.

Comment only. The requests, the limit and the deliberate absence of a
CPU limit are all unchanged.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Both non-blocking but the first one needed a correction to its premise.

Conclusion right, mechanic half wrong. resources: {} doesn't reach the pod as empty. useDefaultResources is unset so it defaults to true, and we set no VM_*DEFAULT_RESOURCE* env on the operator, so operator's own compiled-in defaults land instead: vmagent container comes up with limits 200m/500Mi and requests 50m/200Mi. So a ratio does exist there and vpa rescales against it, visible on one mutated pod - vmagent at requests 182m/125Mi against limits 728m/320000Ki, where 728/182 is exactly 4.0 and 320000Ki/125Mi exactly 2.5, both operator default ratios. The container with no limit is config-reloader, operator gives it requests only (10m/25Mi), and on the same pod it sits at 100m/125Mi requests with no limits at all because there was no ratio to preserve.

Victim state is real anyway. Pod cgroup on the node reads memory.max = max, vmagent child 524288000, config-reloader child max. So the gap in this package is one container and not two, and minimal fix is a limit on config-reloader through configReloaderResources.

Follow-up of the series, not the LimitRange. #3494 is not merged so there is no backstop today, and it carries the side effect its own body names, wide defaulted ceiling against a small request hands vpa a ratio it then preserves. config-reloader already has a 25Mi request from the operator, so defaulted 4Gi is around 163:1, and the 125Mi request seen live becomes a limit around 20Gi. memory.max exists either way but the number stops describing anything.

Note is in values.yaml now: 2Gi is 6.7x the measured 307Mi peak, roughly 2100 pods and 170k series read as linear in object count, against the 322 pods and 25.6k it was measured on.

On the risky direction, I think that sentence is about a different layer. It sits in the Tuning section under the table defining systemNamespaceMemoryLimit, and the hazard it names right after is a defaulted limit landing below a container's own request and being rejected at admission. A chart pinning its own limit cant reach that state, it is set beside its own request and helm-unittest pins the pair. Same page documents chart-set DaemonSet limits as the intended pattern, and c9ba7a3cc already shipped 256Mi on node-exporter and 1Gi on fluent-bit, both under 4Gi.

No measurement forbids 4Gi though, it is one line to raise. Comment states the headroom now so whichever number we keep is chosen against a figure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug size/M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants