mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
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
|
||
|
|
}
|