Skip to content

memorylimiterprocessor should support periodic cgroup re-read for in-place container resizes #15580

Description

@zolinas

Component(s)

processor/memorylimiter

Is your feature request related to a problem? Please describe.

The memorylimiterprocessor resolves limit_percentage / spike_limit_percentage against the container's cgroup memory limit once at startup (via iruntime.TotalMemory()) and converts them to fixed byte thresholds stored in memUsageChecker. These thresholds are never updated.

When Kubernetes In-Place Pod Vertical Scaling (GA in K8s 1.33, KEP-1287) resizes a container's memory limit, the kubelet updates memory.max in the cgroup filesystem. The memorylimiterprocessor continues using the stale thresholds from before the resize.

Impact

After an upscale (e.g., 896 MiB → 3000 MiB), the memorylimiter refuses data at the old thresholds (~537 MiB soft limit) while the container has gigabytes of unused memory. In my environment, this causes 30–40% of metric data points to be refused unnecessarily.

Reproduction

  1. Deploy an OTel collector with memorylimiterprocessor configured with limit_percentage: 80 and spike_limit_percentage: 20 in a container with 896 MiB memory limit.
  2. Use a VPA or in-place resize mechanism to increase the container's memory limit to 3000 MiB.
  3. Observe the collector startup log:
    memorylimiter: Using percentage memory limiter
      total_memory_mib: 896, limit_percentage: 80, spike_limit_percentage: 20
    memorylimiter: Memory limiter configured
      limit_mib: 716, spike_limit_mib: 179
    
  4. Verify the cgroup was updated: cat /sys/fs/cgroup/memory.max3145728000 (3000 MiB).
  5. The memorylimiter continues refusing data at 537 MiB (soft) / 716 MiB (hard) instead of the correct 1800 / 2400 MiB.

Describe the solution you'd like

Add a refresh_interval configuration field to memorylimiterprocessor that, when set to a non-zero duration, periodically re-reads the cgroup memory limit and recomputes the percentage-based thresholds.

Proposed config

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 20
    refresh_interval: 10s  # NEW: re-read cgroup memory limit every 10s

When refresh_interval is 0 (default), behavior is unchanged — thresholds are resolved once at startup.

Implementation sketch

The pattern already exists in the cgroupruntimeextension (extension/cgroupruntimeextension), which periodically re-reads the cgroup to update GOMEMLIMIT via automemlimit.FromCgroup(). The memorylimiter could follow the same approach:

  1. In MemoryLimiter.Start(), if refresh_interval > 0, spawn a goroutine with a ticker.
  2. On each tick, call GetMemoryFn() (which reads the cgroup via iruntime.TotalMemory()).
  3. If the value changed, recompute memAllocLimit and memSpikeLimit and store them atomically.
  4. The existing CheckMemLimits() loop reads these values on each check — no other changes needed.

The memUsageChecker fields (memAllocLimit, memSpikeLimit) would need to change from plain uint64 to atomic.Uint64 to support concurrent reads from the check loop and writes from the refresh loop.

Describe alternatives you've considered

  1. Restart the collector after resize — works but defeats the purpose of in-place resize (zero-downtime scaling).
  2. Use cgroupruntimeextension with GOMEMLIMIT — we already do this, but GOMEMLIMIT only controls Go's GC target, not the memorylimiter's data-refusal thresholds. The two mechanisms are independent and currently work against each other after a resize.
  3. Custom processor wrapper — this is what we've implemented as a workaround, but it requires reimplementing the memorylimiter logic since the core types are in an internal package.

Additional context

  • The cgroupruntimeextension already has refresh_interval for exactly this use case (tracking in-place resizes). The memorylimiter is the missing piece.
  • Kubernetes 1.33 made InPlacePodVerticalScaling GA, so in-place resizing will become increasingly common.
  • The memorylimiterprocessor and cgroupruntimeextension both read the same cgroup memory.max file but have no integration point. Adding refresh_interval to the memorylimiter would close this gap without requiring cross-component coupling.
  • Related: the iruntime.TotalMemory() function (used by the memorylimiter) and memlimit.FromCgroup() (used by cgroupruntimeextension) both read the same cgroup filesystem path — there's no technical barrier to re-reading it.

Tip

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it. Learn more here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds triageNew item requiring triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions