Skip to content

Commit 2da710c

Browse files
authored
Merge pull request #3933 from alexeldeib/ace/v2root
libct/cg/fs2: use file + anon + swap for usage
2 parents cbf8c67 + 7d2becd commit 2da710c

File tree

3 files changed

+158
-10
lines changed

3 files changed

+158
-10
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
(so that the container init is a child of that process) must now implement
2727
a proper child reaper in case a container does not have its own private PID
2828
namespace, as documented in `container.Signal`. (#3825)
29+
* Sum `anon` and `file` from `memory.stat` for cgroupv2 root usage,
30+
as the root does not have `memory.current` for cgroupv2.
31+
This aligns cgroupv2 root usage more closely with cgroupv1 reporting.
32+
Additionally, report root swap usage as sum of swap and memory usage,
33+
aligned with v1 and existing non-root v2 reporting. (#3933)
2934

3035
### Fixed
3136

libcontainer/cgroups/fs2/memory.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,9 @@ func statMemory(dirPath string, stats *cgroups.Stats) error {
106106
if err != nil {
107107
if errors.Is(err, unix.ENOENT) && dirPath == UnifiedMountpoint {
108108
// The root cgroup does not have memory.{current,max}
109-
// so emulate those using data from /proc/meminfo.
110-
return statsFromMeminfo(stats)
109+
// so emulate those using data from /proc/meminfo and
110+
// /sys/fs/cgroup/memory.stat
111+
return rootStatsFromMeminfo(stats)
111112
}
112113
return err
113114
}
@@ -159,7 +160,7 @@ func getMemoryDataV2(path, name string) (cgroups.MemoryData, error) {
159160
return memoryData, nil
160161
}
161162

162-
func statsFromMeminfo(stats *cgroups.Stats) error {
163+
func rootStatsFromMeminfo(stats *cgroups.Stats) error {
163164
const file = "/proc/meminfo"
164165
f, err := os.Open(file)
165166
if err != nil {
@@ -171,14 +172,10 @@ func statsFromMeminfo(stats *cgroups.Stats) error {
171172
var (
172173
swap_free uint64
173174
swap_total uint64
174-
main_total uint64
175-
main_free uint64
176175
)
177176
mem := map[string]*uint64{
178177
"SwapFree": &swap_free,
179178
"SwapTotal": &swap_total,
180-
"MemTotal": &main_total,
181-
"MemFree": &main_free,
182179
}
183180

184181
found := 0
@@ -211,11 +208,18 @@ func statsFromMeminfo(stats *cgroups.Stats) error {
211208
return &parseError{Path: "", File: file, Err: err}
212209
}
213210

211+
// cgroup v1 `usage_in_bytes` reports memory usage as the sum of
212+
// - rss (NR_ANON_MAPPED)
213+
// - cache (NR_FILE_PAGES)
214+
// cgroup v1 reports SwapUsage values as mem+swap combined
215+
// cgroup v2 reports rss and cache as anon and file.
216+
// sum `anon` + `file` to report the same value as `usage_in_bytes` in v1.
217+
// sum swap usage as combined mem+swap usage for consistency as well.
218+
stats.MemoryStats.Usage.Usage = stats.MemoryStats.Stats["anon"] + stats.MemoryStats.Stats["file"]
219+
stats.MemoryStats.Usage.Limit = math.MaxUint64
214220
stats.MemoryStats.SwapUsage.Usage = (swap_total - swap_free) * 1024
215221
stats.MemoryStats.SwapUsage.Limit = math.MaxUint64
216-
217-
stats.MemoryStats.Usage.Usage = (main_total - main_free) * 1024
218-
stats.MemoryStats.Usage.Limit = math.MaxUint64
222+
stats.MemoryStats.SwapUsage.Usage += stats.MemoryStats.Usage.Usage
219223

220224
return nil
221225
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package fs2
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/opencontainers/runc/libcontainer/cgroups"
10+
)
11+
12+
const exampleMemoryStatData = `anon 790425600
13+
file 6502666240
14+
kernel_stack 7012352
15+
pagetables 8867840
16+
percpu 2445520
17+
sock 40960
18+
shmem 6721536
19+
file_mapped 656187392
20+
file_dirty 1122304
21+
file_writeback 0
22+
swapcached 10
23+
anon_thp 438304768
24+
file_thp 0
25+
shmem_thp 0
26+
inactive_anon 892223488
27+
active_anon 2973696
28+
inactive_file 5307346944
29+
active_file 1179316224
30+
unevictable 31477760
31+
slab_reclaimable 348866240
32+
slab_unreclaimable 10099808
33+
slab 358966048
34+
workingset_refault_anon 0
35+
workingset_refault_file 0
36+
workingset_activate_anon 0
37+
workingset_activate_file 0
38+
workingset_restore_anon 0
39+
workingset_restore_file 0
40+
workingset_nodereclaim 0
41+
pgfault 103216687
42+
pgmajfault 6879
43+
pgrefill 0
44+
pgscan 0
45+
pgsteal 0
46+
pgactivate 1110217
47+
pgdeactivate 292
48+
pglazyfree 267
49+
pglazyfreed 0
50+
thp_fault_alloc 57411
51+
thp_collapse_alloc 443`
52+
53+
func TestStatMemoryPodCgroupNotFound(t *testing.T) {
54+
// We're using a fake cgroupfs.
55+
cgroups.TestMode = true
56+
fakeCgroupDir := t.TempDir()
57+
58+
// only write memory.stat to ensure pod cgroup usage
59+
// still reads memory.current.
60+
statPath := filepath.Join(fakeCgroupDir, "memory.stat")
61+
if err := os.WriteFile(statPath, []byte(exampleMemoryStatData), 0o644); err != nil {
62+
t.Fatal(err)
63+
}
64+
65+
gotStats := cgroups.NewStats()
66+
67+
// use a fake root path to mismatch the file we wrote.
68+
// this triggers the non-root path which should fail to find memory.current.
69+
err := statMemory(fakeCgroupDir, gotStats)
70+
if err == nil {
71+
t.Errorf("expected error when statting memory for cgroupv2 root, but was nil")
72+
}
73+
74+
if !strings.Contains(err.Error(), "memory.current: no such file or directory") {
75+
t.Errorf("expected error to contain 'memory.current: no such file or directory', but was %s", err.Error())
76+
}
77+
}
78+
79+
func TestStatMemoryPodCgroup(t *testing.T) {
80+
// We're using a fake cgroupfs.
81+
cgroups.TestMode = true
82+
fakeCgroupDir := t.TempDir()
83+
84+
statPath := filepath.Join(fakeCgroupDir, "memory.stat")
85+
if err := os.WriteFile(statPath, []byte(exampleMemoryStatData), 0o644); err != nil {
86+
t.Fatal(err)
87+
}
88+
89+
if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.current"), []byte("123456789"), 0o644); err != nil {
90+
t.Fatal(err)
91+
}
92+
93+
if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.max"), []byte("999999999"), 0o644); err != nil {
94+
t.Fatal(err)
95+
}
96+
97+
gotStats := cgroups.NewStats()
98+
99+
// use a fake root path to trigger the pod cgroup lookup.
100+
err := statMemory(fakeCgroupDir, gotStats)
101+
if err != nil {
102+
t.Errorf("expected no error when statting memory for cgroupv2 root, but got %#+v", err)
103+
}
104+
105+
// result should be "memory.current"
106+
var expectedUsageBytes uint64 = 123456789
107+
if gotStats.MemoryStats.Usage.Usage != expectedUsageBytes {
108+
t.Errorf("parsed cgroupv2 memory.stat doesn't match expected result: \ngot %#v\nexpected %#v\n", gotStats.MemoryStats.Usage.Usage, expectedUsageBytes)
109+
}
110+
}
111+
112+
func TestRootStatsFromMeminfo(t *testing.T) {
113+
stats := &cgroups.Stats{
114+
MemoryStats: cgroups.MemoryStats{
115+
Stats: map[string]uint64{
116+
"anon": 790425600,
117+
"file": 6502666240,
118+
},
119+
},
120+
}
121+
122+
if err := rootStatsFromMeminfo(stats); err != nil {
123+
t.Fatal(err)
124+
}
125+
126+
// result is anon + file
127+
var expectedUsageBytes uint64 = 7293091840
128+
if stats.MemoryStats.Usage.Usage != expectedUsageBytes {
129+
t.Errorf("parsed cgroupv2 memory.stat doesn't match expected result: \ngot %d\nexpected %d\n", stats.MemoryStats.Usage.Usage, expectedUsageBytes)
130+
}
131+
132+
// swap is adjusted to mem+swap
133+
if stats.MemoryStats.SwapUsage.Usage < stats.MemoryStats.Usage.Usage {
134+
t.Errorf("swap usage %d should be at least mem usage %d", stats.MemoryStats.SwapUsage.Usage, stats.MemoryStats.Usage.Usage)
135+
}
136+
if stats.MemoryStats.SwapUsage.Limit < stats.MemoryStats.Usage.Limit {
137+
t.Errorf("swap limit %d should be at least mem limit %d", stats.MemoryStats.SwapUsage.Limit, stats.MemoryStats.Usage.Limit)
138+
}
139+
}

0 commit comments

Comments
 (0)