Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/memlimit/memlimit.go

111 lines
3.3 KiB
Go
Raw Permalink Normal View History

// Package memlimit sets the Go runtime's soft memory limit from the
// container's memory limit.
//
// Without a limit the garbage collector only runs when the heap doubles, so
// a process holding a large live heap, such as a big metadata document
// being rewritten, can grow past a container limit and be OOM-killed while
// most of that memory is garbage. A soft limit just below the container's
// makes the collector work harder as it gets close instead.
package memlimit
import (
"bufio"
"io/fs"
"log/slog"
"os"
"path"
"runtime"
"runtime/debug"
"strconv"
"strings"
)
// ratio is the share of the container limit given to the Go runtime. The
// rest is left for memory the runtime does not account for, such as the git
// processes the proxy runs, which share the container's cgroup.
const ratio = 0.8
// noLimit is the value cgroup v1 reports, rounded to a page, when no limit
// is set. Anything this large is treated as unlimited.
const noLimit = 1 << 62
// Apply sets the runtime memory limit to ratio of the cgroup memory limit,
// unless GOMEMLIMIT is set, which the runtime has applied already, or no
// limit can be found. It only looks for one on Linux.
func Apply(logger *slog.Logger) {
if os.Getenv("GOMEMLIMIT") != "" || runtime.GOOS != "linux" {
return
}
limit, ok := cgroupLimit(os.DirFS("/"))
if !ok {
return
}
set := int64(float64(limit) * ratio)
debug.SetMemoryLimit(set)
logger.Info("set Go memory limit from cgroup", "memory_limit", set, "cgroup_limit", limit)
}
// cgroupLimit returns the memory limit of the cgroup the process is in,
// read from fsys, the root filesystem. It reads cgroup v2's memory.max, or
// v1's memory.limit_in_bytes, first in the process's own cgroup and then at
// the hierarchy root, which is what a container sees as its own.
func cgroupLimit(fsys fs.FS) (int64, bool) {
v2, v1 := ownCgroups(fsys)
candidates := []string{
path.Join("sys/fs/cgroup", v2, "memory.max"),
"sys/fs/cgroup/memory.max",
path.Join("sys/fs/cgroup/memory", v1, "memory.limit_in_bytes"),
"sys/fs/cgroup/memory/memory.limit_in_bytes",
}
for _, name := range candidates {
data, err := fs.ReadFile(fsys, name)
if err != nil {
continue
}
// "max" means no limit at this level; a later candidate may still
// have one.
limit, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
if err == nil && limit > 0 && limit < noLimit {
return limit, true
}
}
return 0, false
}
// ownCgroups returns the process's cgroup v2 path and its cgroup v1 memory
// controller path from /proc/self/cgroup, each "" when not listed.
func ownCgroups(fsys fs.FS) (v2, v1 string) {
f, err := fsys.Open("proc/self/cgroup")
if err != nil {
return "", ""
}
defer func() { _ = f.Close() }()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// Lines are "hierarchy-ID:controller-list:cgroup-path".
id, rest, ok := strings.Cut(scanner.Text(), ":")
if !ok {
continue
}
controllers, cgroup, ok := strings.Cut(rest, ":")
if !ok {
continue
}
switch {
case id == "0" && controllers == "":
v2 = cgroup
case hasController(controllers, "memory"):
v1 = cgroup
}
}
return v2, v1
}
func hasController(list, name string) bool {
for controller := range strings.SplitSeq(list, ",") {
if controller == name {
return true
}
}
return false
}