Skip to content

Commit f755c80

Browse files
committed
libct/cg/stats: support misc for cgroup v2
Signed-off-by: Mikko Ylinen <[email protected]>
1 parent ee45b9b commit f755c80

File tree

4 files changed

+170
-1
lines changed

4 files changed

+170
-1
lines changed

libcontainer/cgroups/fs2/fs2.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ func (m *Manager) GetStats() (*cgroups.Stats, error) {
133133
if err := fscommon.RdmaGetStats(m.dirPath, st); err != nil && !os.IsNotExist(err) {
134134
errs = append(errs, err)
135135
}
136+
// misc (since kernel 5.13)
137+
if err := statMisc(m.dirPath, st); err != nil && !os.IsNotExist(err) {
138+
errs = append(errs, err)
139+
}
136140
if len(errs) > 0 && !m.config.Rootless {
137141
return st, fmt.Errorf("error while statting cgroup v2: %+v", errs)
138142
}

libcontainer/cgroups/fs2/misc.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package fs2
2+
3+
import (
4+
"bufio"
5+
"os"
6+
"strings"
7+
8+
"github.com/opencontainers/runc/libcontainer/cgroups"
9+
"github.com/opencontainers/runc/libcontainer/cgroups/fscommon"
10+
)
11+
12+
func statMisc(dirPath string, stats *cgroups.Stats) error {
13+
for _, file := range []string{"current", "events"} {
14+
fd, err := cgroups.OpenFile(dirPath, "misc."+file, os.O_RDONLY)
15+
if err != nil {
16+
return err
17+
}
18+
19+
s := bufio.NewScanner(fd)
20+
for s.Scan() {
21+
key, value, err := fscommon.ParseKeyValue(s.Text())
22+
if err != nil {
23+
fd.Close()
24+
return err
25+
}
26+
27+
key = strings.TrimSuffix(key, ".max")
28+
29+
if _, ok := stats.MiscStats[key]; !ok {
30+
stats.MiscStats[key] = cgroups.MiscStats{}
31+
}
32+
33+
tmp := stats.MiscStats[key]
34+
35+
switch file {
36+
case "current":
37+
tmp.Usage = value
38+
case "events":
39+
tmp.Events = value
40+
}
41+
42+
stats.MiscStats[key] = tmp
43+
}
44+
fd.Close()
45+
46+
if err := s.Err(); err != nil {
47+
return err
48+
}
49+
}
50+
51+
return nil
52+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 exampleMiscCurrentData = `res_a 123
13+
res_b 456
14+
res_c 42`
15+
16+
const exampleMiscEventsData = `res_a.max 1
17+
res_b.max 2
18+
res_c.max 3`
19+
20+
func TestStatMiscPodCgroupEmpty(t *testing.T) {
21+
// We're using a fake cgroupfs.
22+
cgroups.TestMode = true
23+
fakeCgroupDir := t.TempDir()
24+
25+
// create empty misc.current and misc.events files to test the common case
26+
// where no misc resource keys are available
27+
for _, file := range []string{"misc.current", "misc.events"} {
28+
if _, err := os.Create(filepath.Join(fakeCgroupDir, file)); err != nil {
29+
t.Fatal(err)
30+
}
31+
}
32+
33+
gotStats := cgroups.NewStats()
34+
35+
err := statMisc(fakeCgroupDir, gotStats)
36+
if err != nil {
37+
t.Errorf("expected no error when statting empty misc.current/misc.events for cgroupv2, but got %#v", err)
38+
}
39+
40+
if len(gotStats.MiscStats) != 0 {
41+
t.Errorf("parsed cgroupv2 misc.* returns unexpected resources: got %#v but expected nothing", gotStats.MiscStats)
42+
}
43+
}
44+
45+
func TestStatMiscPodCgroupNotFound(t *testing.T) {
46+
// We're using a fake cgroupfs.
47+
cgroups.TestMode = true
48+
fakeCgroupDir := t.TempDir()
49+
50+
// only write misc.current to ensure pod cgroup usage
51+
// still reads misc.events.
52+
statPath := filepath.Join(fakeCgroupDir, "misc.current")
53+
if err := os.WriteFile(statPath, []byte(exampleMiscCurrentData), 0o644); err != nil {
54+
t.Fatal(err)
55+
}
56+
57+
gotStats := cgroups.NewStats()
58+
59+
// use a fake root path to mismatch the file we wrote.
60+
// this triggers the non-root path which should fail to find misc.events.
61+
err := statMisc(fakeCgroupDir, gotStats)
62+
if err == nil {
63+
t.Errorf("expected error when statting misc.current for cgroupv2 root, but was nil")
64+
}
65+
66+
if !strings.Contains(err.Error(), "misc.events: no such file or directory") {
67+
t.Errorf("expected error to contain 'misc.events: no such file or directory', but was %s", err.Error())
68+
}
69+
}
70+
71+
func TestStatMiscPodCgroup(t *testing.T) {
72+
// We're using a fake cgroupfs.
73+
cgroups.TestMode = true
74+
fakeCgroupDir := t.TempDir()
75+
76+
currentPath := filepath.Join(fakeCgroupDir, "misc.current")
77+
if err := os.WriteFile(currentPath, []byte(exampleMiscCurrentData), 0o644); err != nil {
78+
t.Fatal(err)
79+
}
80+
81+
eventsPath := filepath.Join(fakeCgroupDir, "misc.events")
82+
if err := os.WriteFile(eventsPath, []byte(exampleMiscEventsData), 0o644); err != nil {
83+
t.Fatal(err)
84+
}
85+
86+
gotStats := cgroups.NewStats()
87+
88+
// use a fake root path to trigger the pod cgroup lookup.
89+
err := statMisc(fakeCgroupDir, gotStats)
90+
if err != nil {
91+
t.Errorf("expected no error when statting misc for cgroupv2 root, but got %#+v", err)
92+
}
93+
94+
// make sure all res_* from exampleMisc*Data are returned
95+
if len(gotStats.MiscStats) != 3 {
96+
t.Errorf("parsed cgroupv2 misc doesn't return all expected resources: \ngot %#v\nexpected %#v\n", len(gotStats.MiscStats), 3)
97+
}
98+
99+
var expectedUsageBytes uint64 = 42
100+
if gotStats.MiscStats["res_c"].Usage != expectedUsageBytes {
101+
t.Errorf("parsed cgroupv2 misc.current for res_c doesn't match expected result: \ngot %#v\nexpected %#v\n", gotStats.MiscStats["res_c"].Usage, expectedUsageBytes)
102+
}
103+
}

libcontainer/cgroups/stats.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ type RdmaStats struct {
170170
RdmaCurrent []RdmaEntry `json:"rdma_current,omitempty"`
171171
}
172172

173+
type MiscStats struct {
174+
// current resource usage for a key in misc
175+
Usage uint64 `json:"usage,omitempty"`
176+
// number of times the resource usage was about to go over the max boundary
177+
Events uint64 `json:"events,omitempty"`
178+
}
179+
173180
type Stats struct {
174181
CpuStats CpuStats `json:"cpu_stats,omitempty"`
175182
CPUSetStats CPUSetStats `json:"cpuset_stats,omitempty"`
@@ -179,10 +186,13 @@ type Stats struct {
179186
// the map is in the format "size of hugepage: stats of the hugepage"
180187
HugetlbStats map[string]HugetlbStats `json:"hugetlb_stats,omitempty"`
181188
RdmaStats RdmaStats `json:"rdma_stats,omitempty"`
189+
// the map is in the format "misc resource name: stats of the key"
190+
MiscStats map[string]MiscStats `json:"misc_stats,omitempty"`
182191
}
183192

184193
func NewStats() *Stats {
185194
memoryStats := MemoryStats{Stats: make(map[string]uint64)}
186195
hugetlbStats := make(map[string]HugetlbStats)
187-
return &Stats{MemoryStats: memoryStats, HugetlbStats: hugetlbStats}
196+
miscStats := make(map[string]MiscStats)
197+
return &Stats{MemoryStats: memoryStats, HugetlbStats: hugetlbStats, MiscStats: miscStats}
188198
}

0 commit comments

Comments
 (0)