diff --git a/libcontainer/cgroups/fs2/io.go b/libcontainer/cgroups/fs2/io.go index bd6f2088f31..77174f437fe 100644 --- a/libcontainer/cgroups/fs2/io.go +++ b/libcontainer/cgroups/fs2/io.go @@ -8,6 +8,8 @@ import ( "strconv" "strings" + "github.com/sirupsen/logrus" + "github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/cgroups/fscommon" "github.com/opencontainers/runc/libcontainer/configs" @@ -88,22 +90,22 @@ func readCgroup2MapFile(dirPath string, name string) (map[string][]string, error } func statIo(dirPath string, stats *cgroups.Stats) error { - // more details on the io.stat file format: https://www.kernel.org/doc/Documentation/cgroup-v2.txt - var ioServiceBytesRecursive []cgroups.BlkioStatEntry values, err := readCgroup2MapFile(dirPath, "io.stat") if err != nil { return err } + // more details on the io.stat file format: https://www.kernel.org/doc/Documentation/cgroup-v2.txt + var parsedStats cgroups.BlkioStats for k, v := range values { d := strings.Split(k, ":") if len(d) != 2 { continue } - major, err := strconv.ParseUint(d[0], 10, 0) + major, err := strconv.ParseUint(d[0], 10, 64) if err != nil { return err } - minor, err := strconv.ParseUint(d[1], 10, 0) + minor, err := strconv.ParseUint(d[1], 10, 64) if err != nil { return err } @@ -115,15 +117,32 @@ func statIo(dirPath string, stats *cgroups.Stats) error { } op := d[0] - // Accommodate the cgroup v1 naming + // Map to the cgroupv1 naming and layout (in separate tables). + var targetTable *[]cgroups.BlkioStatEntry switch op { + // Equivalent to cgroupv1's blkio.io_service_bytes. case "rbytes": op = "Read" + targetTable = &parsedStats.IoServiceBytesRecursive case "wbytes": op = "Write" + targetTable = &parsedStats.IoServiceBytesRecursive + // Equivalent to cgroupv1's blkio.io_serviced. + case "rios": + op = "Read" + targetTable = &parsedStats.IoServicedRecursive + case "wios": + op = "Write" + targetTable = &parsedStats.IoServicedRecursive + default: + // Skip over entries we cannot map to cgroupv1 stats for now. + // In the future we should expand the stats struct to include + // them. + logrus.Debugf("cgroupv2 io stats: skipping over unmappable %s entry", item) + continue } - value, err := strconv.ParseUint(d[1], 10, 0) + value, err := strconv.ParseUint(d[1], 10, 64) if err != nil { return err } @@ -134,9 +153,9 @@ func statIo(dirPath string, stats *cgroups.Stats) error { Minor: minor, Value: value, } - ioServiceBytesRecursive = append(ioServiceBytesRecursive, entry) + *targetTable = append(*targetTable, entry) } } - stats.BlkioStats = cgroups.BlkioStats{IoServiceBytesRecursive: ioServiceBytesRecursive} + stats.BlkioStats = parsedStats return nil } diff --git a/libcontainer/cgroups/fs2/io_test.go b/libcontainer/cgroups/fs2/io_test.go new file mode 100644 index 00000000000..c6cb7a63a72 --- /dev/null +++ b/libcontainer/cgroups/fs2/io_test.go @@ -0,0 +1,87 @@ +package fs2 + +import ( + "io/ioutil" + "os" + "path/filepath" + "reflect" + "sort" + "testing" + + "github.com/opencontainers/runc/libcontainer/cgroups" + "github.com/opencontainers/runc/libcontainer/cgroups/fscommon" +) + +const exampleIoStatData = `254:1 rbytes=6901432320 wbytes=14245535744 rios=263278 wios=248603 dbytes=0 dios=0 +254:0 rbytes=2702336 wbytes=0 rios=97 wios=0 dbytes=0 dios=0 +259:0 rbytes=6911345664 wbytes=14245536256 rios=264538 wios=244914 dbytes=530485248 dios=2` + +var exampleIoStatsParsed = cgroups.BlkioStats{ + IoServiceBytesRecursive: []cgroups.BlkioStatEntry{ + {Major: 254, Minor: 1, Value: 6901432320, Op: "Read"}, + {Major: 254, Minor: 1, Value: 14245535744, Op: "Write"}, + {Major: 254, Minor: 0, Value: 2702336, Op: "Read"}, + {Major: 254, Minor: 0, Value: 0, Op: "Write"}, + {Major: 259, Minor: 0, Value: 6911345664, Op: "Read"}, + {Major: 259, Minor: 0, Value: 14245536256, Op: "Write"}, + }, + IoServicedRecursive: []cgroups.BlkioStatEntry{ + {Major: 254, Minor: 1, Value: 263278, Op: "Read"}, + {Major: 254, Minor: 1, Value: 248603, Op: "Write"}, + {Major: 254, Minor: 0, Value: 97, Op: "Read"}, + {Major: 254, Minor: 0, Value: 0, Op: "Write"}, + {Major: 259, Minor: 0, Value: 264538, Op: "Read"}, + {Major: 259, Minor: 0, Value: 244914, Op: "Write"}, + }, +} + +func lessBlkioStatEntry(a, b cgroups.BlkioStatEntry) bool { + if a.Major != b.Major { + return a.Major < b.Major + } + if a.Minor != b.Minor { + return a.Minor < b.Minor + } + if a.Op != b.Op { + return a.Op < b.Op + } + return a.Value < b.Value +} + +func sortBlkioStats(stats *cgroups.BlkioStats) { + for _, table := range []*[]cgroups.BlkioStatEntry{ + &stats.IoServicedRecursive, + &stats.IoServiceBytesRecursive, + } { + sort.SliceStable(*table, func(i, j int) bool { return lessBlkioStatEntry((*table)[i], (*table)[j]) }) + } +} + +func TestStatIo(t *testing.T) { + // We're using a fake cgroupfs. + fscommon.TestMode = true + + fakeCgroupDir, err := ioutil.TempDir("", "runc-stat-io-test.*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(fakeCgroupDir) + statPath := filepath.Join(fakeCgroupDir, "io.stat") + + if err := ioutil.WriteFile(statPath, []byte(exampleIoStatData), 0644); err != nil { + t.Fatal(err) + } + + var gotStats cgroups.Stats + if err := statIo(fakeCgroupDir, &gotStats); err != nil { + t.Error(err) + } + + // Sort the output since statIo uses a map internally. + sortBlkioStats(&gotStats.BlkioStats) + sortBlkioStats(&exampleIoStatsParsed) + + if !reflect.DeepEqual(gotStats.BlkioStats, exampleIoStatsParsed) { + t.Errorf("parsed cgroupv2 io.stat doesn't match expected result: \ngot %#v\nexpected %#v\n", gotStats.BlkioStats, exampleIoStatsParsed) + } +}