memlimit

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package memlimit derives the Go runtime soft memory limit from the process's own cgroup memory limit, so the garbage collector works harder before the kernel OOM-killer steps in.

A Go process that is not told about its cgroup limit sizes its heap as if the whole host were available; a hard cgroup limit then kills it instead of making the GC collect more aggressively. Calling Apply at startup, before any server or worker starts, sets debug.SetMemoryLimit below the cgroup limit, leaving headroom for memory the Go runtime does not account for.

Usage in an agent's main:

logger := logging.Setup(os.Getenv("LOG_LEVEL"))
if _, err := memlimit.Apply(ctx, logger); err != nil {
    logger.Warn("memlimit: could not apply", "error", err)
}

Which cgroup: the process's own, resolved through /proc/self/cgroup (cgroup v2 "0::<path>", or the v1 "memory" controller line). The tightest limit on the path from that cgroup up to the root wins, so a limit on an ancestor (a systemd unit's MemoryMax, a parent slice) is honoured. Only if /proc/self/cgroup cannot be read does it fall back to the cgroup root itself, which is the container's own cgroup under a private cgroup namespace.

Soft limit: by default 85% of the cgroup limit. Child processes started by the agent live in the same cgroup but are invisible to the Go runtime's accounting, so set a reserve for them: MEMLIMIT_RESERVE_BYTES (or Options.ReserveBytes) makes the soft limit limit-reserve, never below 50% of the limit.

Other rules:

  • "max" (v2) or a "huge" value (v1) at every level means no limit: nothing is set and one warning is logged.
  • If GOMEMLIMIT is already set in the environment it wins and nothing is changed; Apply starts no refresher either.
  • The limit is re-read every Interval (default 60s) until ctx is done; a change (e.g. an operator raising or lowering the container limit) re-applies SetMemoryLimit and logs once per change.

Index

Constants

View Source
const (
	// Ratio is the default fraction of the cgroup limit given to the Go runtime.
	Ratio = 0.85

	// MinFraction is the floor for the soft limit when a reserve is set.
	MinFraction = 0.5

	// ReserveEnv names the environment variable holding the reserve in bytes.
	ReserveEnv = "MEMLIMIT_RESERVE_BYTES"

	// DefaultRoot is where the cgroup filesystem is mounted in a container.
	DefaultRoot = "/sys/fs/cgroup"

	// DefaultSelfCgroup is the file naming the process's own cgroup.
	DefaultSelfCgroup = "/proc/self/cgroup"

	// DefaultInterval is how often the limit is re-read.
	DefaultInterval = 60 * time.Second
)
View Source
const DefaultAnonInterval = 5 * time.Second

DefaultAnonInterval is how often PeakAnon samples.

Variables

View Source
var ErrInvalidConfig = errors.New("memlimit: invalid configuration")

ErrInvalidConfig marks a configuration the caller got wrong: a malformed MEMLIMIT_RESERVE_BYTES or Options.ReserveBytes, or a cgroup limit too small to derive a Go limit from. Test with errors.Is. Callers must treat it as fatal at startup: the process would otherwise run with no soft limit under a hard cgroup cap. Other errors (an unreadable or unparsable cgroup file) are environmental; Apply still starts the refresher for those, which keeps retrying, so they are safe to log and continue.

Functions

func ReadSelfAnon

func ReadSelfAnon(root, selfCgroup string) (int64, error)

ReadSelfAnon returns the anonymous memory in bytes of the process's own cgroup: the "anon" field of cgroup v2 memory.stat, or "total_rss" of cgroup v1 memory.stat. If selfCgroup cannot be read it uses the files directly under root.

func SoftLimit

func SoftLimit(max, reserve int64) int64

SoftLimit returns the Go soft limit for a cgroup limit: max-reserve floored at MinFraction of max when reserve > 0, otherwise Ratio of max.

Types

type AnonStats

type AnonStats struct {
	// CurrentBytes is the most recent anonymous-memory reading of the
	// process's own cgroup (the whole cgroup: the process plus its children).
	CurrentBytes int64
	// MaxBytes is the largest CurrentBytes seen since the sampler started.
	MaxBytes int64
	// Samples is the number of successful readings.
	Samples int64
}

AnonStats is a point-in-time view of a PeakAnon sampler.

type Options

type Options struct {
	// Root is the cgroup filesystem root. Defaults to DefaultRoot.
	Root string
	// SelfCgroup is the path of the file naming the process's cgroup.
	// Defaults to DefaultSelfCgroup.
	SelfCgroup string
	// Getenv looks up an environment variable. Defaults to os.Getenv.
	Getenv func(string) string
	// SetLimit applies the soft limit. Defaults to debug.SetMemoryLimit.
	SetLimit func(int64) int64
	// ReserveBytes overrides MEMLIMIT_RESERVE_BYTES when > 0.
	ReserveBytes int64
	// Interval is the re-read period; 0 means DefaultInterval, negative
	// disables the refresher.
	Interval time.Duration
	// Tick, if non-nil, replaces the interval timer (tests).
	Tick <-chan time.Time
}

Options is the injection seam. The zero value uses the real cgroup mount, /proc/self/cgroup, the real environment and debug.SetMemoryLimit.

type PeakAnon

type PeakAnon struct {
	// contains filtered or unexported fields
}

PeakAnon samples the anonymous memory of the process's own cgroup and remembers the maximum seen since start. Unlike memory.peak it excludes page cache and reclaimable slab, so it is the number to size a hard memory cap from. The cgroup is resolved exactly as Apply resolves it.

func StartPeakAnon

func StartPeakAnon(ctx context.Context, log *slog.Logger, opts PeakOptions) *PeakAnon

StartPeakAnon takes one sample immediately, then one per interval until ctx is done. It never returns nil. A failing read keeps the previous values and is logged once per distinct error. A nil logger uses slog.Default().

func (*PeakAnon) Current

func (p *PeakAnon) Current() int64

Current is the latest reading in bytes (0 before the first successful one).

func (*PeakAnon) Max

func (p *PeakAnon) Max() int64

Max is the largest reading since start in bytes.

func (*PeakAnon) Stats

func (p *PeakAnon) Stats() AnonStats

Stats returns the current and maximum readings. Safe for concurrent use, e.g. from a health endpoint handler.

type PeakOptions

type PeakOptions struct {
	// Root is the cgroup filesystem root. Defaults to DefaultRoot.
	Root string
	// SelfCgroup is the /proc/self/cgroup-format file. Defaults to DefaultSelfCgroup.
	SelfCgroup string
	// Interval is the sampling period; 0 means DefaultAnonInterval.
	Interval time.Duration
	// Tick, if non-nil, replaces the interval timer (tests).
	Tick <-chan time.Time
}

PeakOptions is the injection seam for StartPeakAnon.

type Result

type Result struct {
	// Applied is true only if debug.SetMemoryLimit was called.
	Applied bool
	// Source is the origin of CgroupBytes, or SourceEnv when GOMEMLIMIT was
	// already set, or SourceNone when no limit was found.
	Source Source
	// CgroupBytes is the detected (tightest) cgroup limit; 0 if none.
	CgroupBytes int64
	// ReserveBytes is the reserve in effect; 0 when the default ratio was used.
	ReserveBytes int64
	// LimitBytes is the Go soft limit that was set; 0 if Applied is false.
	LimitBytes int64
}

Result describes what Apply did at startup.

func Apply

func Apply(ctx context.Context, log *slog.Logger) (Result, error)

Apply reads the cgroup limit, sets the Go soft memory limit and, until ctx is done, keeps it in step with the cgroup. A nil logger uses slog.Default().

An error satisfying errors.Is(err, ErrInvalidConfig) must be treated as fatal by the caller: nothing was applied and no refresher runs. Any other error is environmental; the refresher is running and will retry.

func ApplyWith

func ApplyWith(ctx context.Context, log *slog.Logger, opts Options) (Result, error)

ApplyWith is Apply with an injectable cgroup root, environment and setter.

type Source

type Source string

Source says where the limit came from.

const (
	SourceNone     Source = ""
	SourceCgroupV2 Source = "cgroup-v2"
	SourceCgroupV1 Source = "cgroup-v1"
	SourceEnv      Source = "GOMEMLIMIT"
)

func ReadCgroupLimit

func ReadCgroupLimit(root string) (int64, Source, error)

ReadCgroupLimit reads the limit files directly under root: v2 memory.max, then v1 memory/memory.limit_in_bytes. It is the fallback when /proc/self/cgroup is unreadable. SourceNone with a nil error means the files are absent or report no limit.

func ReadSelfLimit

func ReadSelfLimit(root, selfCgroup string) (int64, Source, error)

ReadSelfLimit returns the tightest memory limit in bytes that applies to the process's own cgroup. selfCgroup is the path of a /proc/self/cgroup-format file; root is the cgroup filesystem mount. SourceNone with a nil error means no limit anywhere on the path. If selfCgroup cannot be read it falls back to the limit files directly under root.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL