mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Honor cgroup v1 service memory limits
Resolve the process memory-controller hierarchy before selecting the tightest v1 hard limit, so systemd and nested appliance limits actually inform the existing Go runtime headroom. Contract-Neutral: runtime memory-limit detection only; no API or persistence contract change
This commit is contained in:
+73
-1
@@ -58,9 +58,15 @@ func applyRuntimeMemoryLimit() {
|
||||
}
|
||||
|
||||
func readCgroupMemoryLimit() (int64, bool) {
|
||||
if limit, ok := readCgroupV2MemoryLimit(cgroupV2Root, "/proc/self/cgroup"); ok {
|
||||
const procSelfCgroup = "/proc/self/cgroup"
|
||||
if limit, ok := readCgroupV2MemoryLimit(cgroupV2Root, procSelfCgroup); ok {
|
||||
return limit, true
|
||||
}
|
||||
if limit, ok := readCgroupV1HierarchyMemoryLimit("/sys/fs/cgroup/memory", procSelfCgroup); ok {
|
||||
return limit, true
|
||||
}
|
||||
// Preserve the original best-effort fallback for unusual v1 mounts that
|
||||
// expose the controller limit but not a readable /proc/self/cgroup.
|
||||
return readCgroupV1MemoryLimit("/sys/fs/cgroup/memory/memory.limit_in_bytes")
|
||||
}
|
||||
|
||||
@@ -122,6 +128,72 @@ func cgroupV2PathFrom(content string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// readCgroupV1HierarchyMemoryLimit resolves the process's memory-controller
|
||||
// path and walks its ancestors just as the v2 reader does. Reading only the
|
||||
// controller root misses a tighter systemd MemoryLimit/MemoryMax applied to a
|
||||
// service cgroup on v1 hosts.
|
||||
func readCgroupV1HierarchyMemoryLimit(root, procSelfCgroup string) (int64, bool) {
|
||||
data, err := os.ReadFile(procSelfCgroup)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
rel := cgroupV1MemoryPathFrom(string(data))
|
||||
if rel == "" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
lowest := int64(math.MaxInt64)
|
||||
dir := filepath.Join(root, rel)
|
||||
for {
|
||||
if raw, err := os.ReadFile(filepath.Join(dir, "memory.limit_in_bytes")); err == nil {
|
||||
if v, ok := parseCgroupMemoryValue(string(raw)); ok && v < cgroupV1NoLimit && v < lowest {
|
||||
lowest = v
|
||||
}
|
||||
}
|
||||
if dir == root {
|
||||
break
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
if lowest == math.MaxInt64 {
|
||||
return 0, false
|
||||
}
|
||||
return lowest, true
|
||||
}
|
||||
|
||||
// cgroupV1MemoryPathFrom extracts the path for the v1 memory controller from
|
||||
// /proc/self/cgroup. Controllers may be mounted together, so "memory" can be
|
||||
// one item in a comma-separated controller field.
|
||||
func cgroupV1MemoryPathFrom(content string) string {
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
fields := strings.SplitN(line, ":", 3)
|
||||
if len(fields) != 3 {
|
||||
continue
|
||||
}
|
||||
hasMemory := false
|
||||
for _, controller := range strings.Split(fields[1], ",") {
|
||||
if strings.TrimSpace(controller) == "memory" {
|
||||
hasMemory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasMemory {
|
||||
continue
|
||||
}
|
||||
path := strings.TrimSpace(fields[2])
|
||||
if path == "/" {
|
||||
return "."
|
||||
}
|
||||
return strings.TrimPrefix(path, "/")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func readCgroupV1MemoryLimit(limitFile string) (int64, bool) {
|
||||
raw, err := os.ReadFile(limitFile)
|
||||
if err != nil {
|
||||
|
||||
@@ -118,6 +118,81 @@ func TestReadCgroupV2MemoryLimitNoLimitAnywhere(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCgroupV1MemoryPathFrom(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
content string
|
||||
want string
|
||||
}{
|
||||
{"memory controller", "5:memory:/system.slice/pulse.service\n", "system.slice/pulse.service"},
|
||||
{"combined controllers", "7:cpu,cpuacct,memory:/docker/abc\n", "docker/abc"},
|
||||
{"namespaced root", "5:memory:/\n", "."},
|
||||
{"colon in path", "5:memory:/system.slice/pulse:worker.service\n", "system.slice/pulse:worker.service"},
|
||||
{"other controllers only", "2:cpu,cpuacct:/system.slice/pulse.service\n", ""},
|
||||
{"v2 only", "0::/system.slice/pulse.service\n", ""},
|
||||
{"malformed", "memory:/system.slice/pulse.service\n", ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := cgroupV1MemoryPathFrom(tc.content); got != tc.want {
|
||||
t.Fatalf("cgroupV1MemoryPathFrom(%q) = %q, want %q", tc.content, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCgroupV1HierarchyMemoryLimitAtNamespacedRoot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
proc := filepath.Join(root, "proc-self-cgroup")
|
||||
writeCgroupFixture(t, proc, "5:memory:/\n")
|
||||
writeCgroupFixture(t, filepath.Join(root, "memory.limit_in_bytes"), "536870912\n")
|
||||
|
||||
got, ok := readCgroupV1HierarchyMemoryLimit(root, proc)
|
||||
if !ok || got != 536870912 {
|
||||
t.Fatalf("got (%d, %v), want (536870912, true)", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCgroupV1HierarchyMemoryLimitWalksAncestors(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
proc := filepath.Join(root, "proc-self-cgroup")
|
||||
writeCgroupFixture(t, proc, "5:memory:/system.slice/pulse.service\n")
|
||||
|
||||
// The service limit must win over the unlimited controller root. This is
|
||||
// the layout used by systemd MemoryLimit/MemoryMax on cgroup v1.
|
||||
writeCgroupFixture(t, filepath.Join(root, "system.slice/pulse.service/memory.limit_in_bytes"), "536870912\n")
|
||||
writeCgroupFixture(t, filepath.Join(root, "system.slice/memory.limit_in_bytes"), "1073741824\n")
|
||||
writeCgroupFixture(t, filepath.Join(root, "memory.limit_in_bytes"), strconv.FormatInt(cgroupV1NoLimit, 10)+"\n")
|
||||
|
||||
got, ok := readCgroupV1HierarchyMemoryLimit(root, proc)
|
||||
if !ok || got != 536870912 {
|
||||
t.Fatalf("got (%d, %v), want (536870912, true)", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCgroupV1HierarchyMemoryLimitTakesTightestAncestor(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
proc := filepath.Join(root, "proc-self-cgroup")
|
||||
writeCgroupFixture(t, proc, "8:cpu,memory:/parent/child\n")
|
||||
writeCgroupFixture(t, filepath.Join(root, "parent/child/memory.limit_in_bytes"), "2147483648\n")
|
||||
writeCgroupFixture(t, filepath.Join(root, "parent/memory.limit_in_bytes"), "805306368\n")
|
||||
|
||||
got, ok := readCgroupV1HierarchyMemoryLimit(root, proc)
|
||||
if !ok || got != 805306368 {
|
||||
t.Fatalf("got (%d, %v), want (805306368, true)", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCgroupV1HierarchyMemoryLimitNoMemoryController(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
proc := filepath.Join(root, "proc-self-cgroup")
|
||||
writeCgroupFixture(t, proc, "2:cpu,cpuacct:/system.slice/pulse.service\n")
|
||||
|
||||
if got, ok := readCgroupV1HierarchyMemoryLimit(root, proc); ok {
|
||||
t.Fatalf("expected no memory limit, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCgroupV1MemoryLimit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
limitFile := filepath.Join(dir, "memory.limit_in_bytes")
|
||||
|
||||
Reference in New Issue
Block a user