mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 07:39:19 -05:00
Nothing set GOMEMLIMIT, so in a container the heap could grow until the container was OOM-killed while much of it was garbage, for example while large metadata documents were being rewritten. At startup on Linux, read the cgroup v2 or v1 memory limit and set the runtime's soft limit to 80% of it, leaving room for git subprocesses. An explicit GOMEMLIMIT wins.
111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
// 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
|
|
}
|