diff --git a/libcontainer/cgroups/fs/memory.go b/libcontainer/cgroups/fs/memory.go index f81ed050..39809ef0 100644 --- a/libcontainer/cgroups/fs/memory.go +++ b/libcontainer/cgroups/fs/memory.go @@ -5,7 +5,9 @@ package fs import ( "bufio" "fmt" + "math" "os" + "path" "path/filepath" "strconv" "strings" @@ -16,8 +18,16 @@ import ( ) const ( - cgroupMemorySwapLimit = "memory.memsw.limit_in_bytes" - cgroupMemoryLimit = "memory.limit_in_bytes" + numaNodeSymbol = "N" + numaStatColumnSeparator = " " + numaStatKeyValueSeparator = "=" + numaStatMaxColumns = math.MaxUint8 + 1 + numaStatValueIndex = 1 + numaStatTypeIndex = 0 + numaStatColumnSliceLength = 2 + cgroupMemorySwapLimit = "memory.memsw.limit_in_bytes" + cgroupMemoryLimit = "memory.limit_in_bytes" + cgroupMemoryPagesByNuma = "memory.numa_stat" ) type MemoryGroup struct { @@ -209,6 +219,13 @@ func (s *MemoryGroup) GetStats(path string, stats *cgroups.Stats) error { if value == 1 { stats.MemoryStats.UseHierarchy = true } + + pagesByNUMA, err := getPageUsageByNUMA(path) + if err != nil { + return err + } + stats.MemoryStats.PageUsageByNUMA = pagesByNUMA + return nil } @@ -269,3 +286,77 @@ func getMemoryData(path, name string) (cgroups.MemoryData, error) { return memoryData, nil } + +func getPageUsageByNUMA(cgroupPath string) (cgroups.PageUsageByNUMA, error) { + stats := cgroups.PageUsageByNUMA{} + + file, err := os.Open(path.Join(cgroupPath, cgroupMemoryPagesByNuma)) + if err != nil { + return stats, err + } + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + var statsType string + statsByType := cgroups.PageStats{Nodes: map[uint8]uint64{}} + columns := strings.SplitN(scanner.Text(), numaStatColumnSeparator, numaStatMaxColumns) + + for _, column := range columns { + pagesByNode := strings.SplitN(column, numaStatKeyValueSeparator, numaStatColumnSliceLength) + + if strings.HasPrefix(pagesByNode[numaStatTypeIndex], numaNodeSymbol) { + nodeID, err := strconv.ParseUint(pagesByNode[numaStatTypeIndex][1:], 10, 8) + if err != nil { + return cgroups.PageUsageByNUMA{}, err + } + + statsByType.Nodes[uint8(nodeID)], err = strconv.ParseUint(pagesByNode[numaStatValueIndex], 0, 64) + if err != nil { + return cgroups.PageUsageByNUMA{}, err + } + } else { + statsByType.Total, err = strconv.ParseUint(pagesByNode[numaStatValueIndex], 0, 64) + if err != nil { + return cgroups.PageUsageByNUMA{}, err + } + + statsType = pagesByNode[numaStatTypeIndex] + } + + err := addNUMAStatsByType(&stats, statsByType, statsType) + if err != nil { + return cgroups.PageUsageByNUMA{}, err + } + } + } + err = scanner.Err() + if err != nil { + return cgroups.PageUsageByNUMA{}, err + } + + return stats, nil +} + +func addNUMAStatsByType(stats *cgroups.PageUsageByNUMA, byTypeStats cgroups.PageStats, statsType string) error { + switch statsType { + case "total": + stats.Total = byTypeStats + case "file": + stats.File = byTypeStats + case "anon": + stats.Anon = byTypeStats + case "unevictable": + stats.Unevictable = byTypeStats + case "hierarchical_total": + stats.Hierarchical.Total = byTypeStats + case "hierarchical_file": + stats.Hierarchical.File = byTypeStats + case "hierarchical_anon": + stats.Hierarchical.Anon = byTypeStats + case "hierarchical_unevictable": + stats.Hierarchical.Unevictable = byTypeStats + default: + return fmt.Errorf("unsupported NUMA page type found: %s", statsType) + } + return nil +} diff --git a/libcontainer/cgroups/fs/memory_test.go b/libcontainer/cgroups/fs/memory_test.go index 62de5637..c84b4fe3 100644 --- a/libcontainer/cgroups/fs/memory_test.go +++ b/libcontainer/cgroups/fs/memory_test.go @@ -18,6 +18,18 @@ rss 1024` memoryFailcnt = "100\n" memoryLimitContents = "8192\n" memoryUseHierarchyContents = "1\n" + memoryNUMAStatContents = `total=44611 N0=32631 N1=7501 N2=1982 N3=2497 +file=44428 N0=32614 N1=7335 N2=1982 N3=2497 +anon=183 N0=17 N1=166 N2=0 N3=0 +unevictable=0 N0=0 N1=0 N2=0 N3=0 +hierarchical_total=768133 N0=509113 N1=138887 N2=20464 N3=99669 +hierarchical_file=722017 N0=496516 N1=119997 N2=20181 N3=85323 +hierarchical_anon=46096 N0=12597 N1=18890 N2=283 N3=14326 +hierarchical_unevictable=20 N0=0 N1=0 N2=0 N3=20` + memoryNUMAStatNoHierarchyContents = `total=44611 N0=32631 N1=7501 N2=1982 N3=2497 +file=44428 N0=32614 N1=7335 N2=1982 N3=2497 +anon=183 N0=17 N1=166 N2=0 N3=0 +unevictable=0 N0=0 N1=0 N2=0 N3=0` ) func TestMemorySetMemory(t *testing.T) { @@ -276,6 +288,7 @@ func TestMemoryStats(t *testing.T) { "memory.kmem.failcnt": memoryFailcnt, "memory.kmem.limit_in_bytes": memoryLimitContents, "memory.use_hierarchy": memoryUseHierarchyContents, + "memory.numa_stat": memoryNUMAStatContents, }) memory := &MemoryGroup{} @@ -284,7 +297,21 @@ func TestMemoryStats(t *testing.T) { if err != nil { t.Fatal(err) } - expectedStats := cgroups.MemoryStats{Cache: 512, Usage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, SwapUsage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, KernelUsage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, Stats: map[string]uint64{"cache": 512, "rss": 1024}, UseHierarchy: true} + expectedStats := cgroups.MemoryStats{Cache: 512, Usage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, SwapUsage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, KernelUsage: cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}, Stats: map[string]uint64{"cache": 512, "rss": 1024}, UseHierarchy: true, + PageUsageByNUMA: cgroups.PageUsageByNUMA{ + PageUsageByNUMAInner: cgroups.PageUsageByNUMAInner{ + Total: cgroups.PageStats{Total: 44611, Nodes: map[uint8]uint64{0: 32631, 1: 7501, 2: 1982, 3: 2497}}, + File: cgroups.PageStats{Total: 44428, Nodes: map[uint8]uint64{0: 32614, 1: 7335, 2: 1982, 3: 2497}}, + Anon: cgroups.PageStats{Total: 183, Nodes: map[uint8]uint64{0: 17, 1: 166, 2: 0, 3: 0}}, + Unevictable: cgroups.PageStats{Total: 0, Nodes: map[uint8]uint64{0: 0, 1: 0, 2: 0, 3: 0}}, + }, + Hierarchical: cgroups.PageUsageByNUMAInner{ + Total: cgroups.PageStats{Total: 768133, Nodes: map[uint8]uint64{0: 509113, 1: 138887, 2: 20464, 3: 99669}}, + File: cgroups.PageStats{Total: 722017, Nodes: map[uint8]uint64{0: 496516, 1: 119997, 2: 20181, 3: 85323}}, + Anon: cgroups.PageStats{Total: 46096, Nodes: map[uint8]uint64{0: 12597, 1: 18890, 2: 283, 3: 14326}}, + Unevictable: cgroups.PageStats{Total: 20, Nodes: map[uint8]uint64{0: 0, 1: 0, 2: 0, 3: 20}}, + }, + }} expectMemoryStatEquals(t, expectedStats, actualStats.MemoryStats) } @@ -454,3 +481,26 @@ func TestMemorySetOomControl(t *testing.T) { t.Fatalf("Got the wrong value, set memory.oom_control failed.") } } + +func TestNoHierarchicalNumaStat(t *testing.T) { + helper := NewCgroupTestUtil("memory", t) + defer helper.cleanup() + helper.writeFileContents(map[string]string{ + "memory.numa_stat": memoryNUMAStatNoHierarchyContents, + }) + + actualStats, err := getPageUsageByNUMA(helper.CgroupPath) + if err != nil { + t.Fatal(err) + } + pageUsageByNUMA := cgroups.PageUsageByNUMA{ + PageUsageByNUMAInner: cgroups.PageUsageByNUMAInner{ + Total: cgroups.PageStats{Total: 44611, Nodes: map[uint8]uint64{0: 32631, 1: 7501, 2: 1982, 3: 2497}}, + File: cgroups.PageStats{Total: 44428, Nodes: map[uint8]uint64{0: 32614, 1: 7335, 2: 1982, 3: 2497}}, + Anon: cgroups.PageStats{Total: 183, Nodes: map[uint8]uint64{0: 17, 1: 166, 2: 0, 3: 0}}, + Unevictable: cgroups.PageStats{Total: 0, Nodes: map[uint8]uint64{0: 0, 1: 0, 2: 0, 3: 0}}, + }, + Hierarchical: cgroups.PageUsageByNUMAInner{}, + } + expectPageUsageByNUMAEquals(t, pageUsageByNUMA, actualStats) +} diff --git a/libcontainer/cgroups/fs/stats_util_test.go b/libcontainer/cgroups/fs/stats_util_test.go index c5a8d185..cdce03cf 100644 --- a/libcontainer/cgroups/fs/stats_util_test.go +++ b/libcontainer/cgroups/fs/stats_util_test.go @@ -4,11 +4,10 @@ package fs import ( "fmt" + "reflect" "testing" "github.com/opencontainers/runc/libcontainer/cgroups" - - "github.com/sirupsen/logrus" ) func blkioStatEntryEquals(expected, actual []cgroups.BlkioStatEntry) error { @@ -26,57 +25,47 @@ func blkioStatEntryEquals(expected, actual []cgroups.BlkioStatEntry) error { func expectBlkioStatsEquals(t *testing.T, expected, actual cgroups.BlkioStats) { if err := blkioStatEntryEquals(expected.IoServiceBytesRecursive, actual.IoServiceBytesRecursive); err != nil { - logrus.Printf("blkio IoServiceBytesRecursive do not match - %s\n", err) - t.Fail() + t.Errorf("blkio IoServiceBytesRecursive do not match - %s\n", err) } if err := blkioStatEntryEquals(expected.IoServicedRecursive, actual.IoServicedRecursive); err != nil { - logrus.Printf("blkio IoServicedRecursive do not match - %s\n", err) - t.Fail() + t.Errorf("blkio IoServicedRecursive do not match - %s\n", err) } if err := blkioStatEntryEquals(expected.IoQueuedRecursive, actual.IoQueuedRecursive); err != nil { - logrus.Printf("blkio IoQueuedRecursive do not match - %s\n", err) - t.Fail() + t.Errorf("blkio IoQueuedRecursive do not match - %s\n", err) } if err := blkioStatEntryEquals(expected.SectorsRecursive, actual.SectorsRecursive); err != nil { - logrus.Printf("blkio SectorsRecursive do not match - %s\n", err) - t.Fail() + t.Errorf("blkio SectorsRecursive do not match - %s\n", err) } if err := blkioStatEntryEquals(expected.IoServiceTimeRecursive, actual.IoServiceTimeRecursive); err != nil { - logrus.Printf("blkio IoServiceTimeRecursive do not match - %s\n", err) - t.Fail() + t.Errorf("blkio IoServiceTimeRecursive do not match - %s\n", err) } if err := blkioStatEntryEquals(expected.IoWaitTimeRecursive, actual.IoWaitTimeRecursive); err != nil { - logrus.Printf("blkio IoWaitTimeRecursive do not match - %s\n", err) - t.Fail() + t.Errorf("blkio IoWaitTimeRecursive do not match - %s\n", err) } if err := blkioStatEntryEquals(expected.IoMergedRecursive, actual.IoMergedRecursive); err != nil { - logrus.Printf("blkio IoMergedRecursive do not match - %v vs %v\n", expected.IoMergedRecursive, actual.IoMergedRecursive) - t.Fail() + t.Errorf("blkio IoMergedRecursive do not match - %v vs %v\n", expected.IoMergedRecursive, actual.IoMergedRecursive) } if err := blkioStatEntryEquals(expected.IoTimeRecursive, actual.IoTimeRecursive); err != nil { - logrus.Printf("blkio IoTimeRecursive do not match - %s\n", err) - t.Fail() + t.Errorf("blkio IoTimeRecursive do not match - %s\n", err) } } func expectThrottlingDataEquals(t *testing.T, expected, actual cgroups.ThrottlingData) { if expected != actual { - logrus.Printf("Expected throttling data %v but found %v\n", expected, actual) - t.Fail() + t.Errorf("Expected throttling data %v but found %v\n", expected, actual) } } func expectHugetlbStatEquals(t *testing.T, expected, actual cgroups.HugetlbStats) { if expected != actual { - logrus.Printf("Expected hugetlb stats %v but found %v\n", expected, actual) - t.Fail() + t.Errorf("Expected hugetlb stats %v but found %v\n", expected, actual) } } @@ -84,40 +73,61 @@ func expectMemoryStatEquals(t *testing.T, expected, actual cgroups.MemoryStats) expectMemoryDataEquals(t, expected.Usage, actual.Usage) expectMemoryDataEquals(t, expected.SwapUsage, actual.SwapUsage) expectMemoryDataEquals(t, expected.KernelUsage, actual.KernelUsage) + expectPageUsageByNUMAEquals(t, expected.PageUsageByNUMA, actual.PageUsageByNUMA) if expected.UseHierarchy != actual.UseHierarchy { - logrus.Printf("Expected memory use hierarchy %v, but found %v\n", expected.UseHierarchy, actual.UseHierarchy) - t.Fail() + t.Errorf("Expected memory use hierarchy %v, but found %v\n", expected.UseHierarchy, actual.UseHierarchy) } for key, expValue := range expected.Stats { actValue, ok := actual.Stats[key] if !ok { - logrus.Printf("Expected memory stat key %s not found\n", key) - t.Fail() + t.Errorf("Expected memory stat key %s not found\n", key) } if expValue != actValue { - logrus.Printf("Expected memory stat value %d but found %d\n", expValue, actValue) - t.Fail() + t.Errorf("Expected memory stat value %d but found %d\n", expValue, actValue) } } } func expectMemoryDataEquals(t *testing.T, expected, actual cgroups.MemoryData) { if expected.Usage != actual.Usage { - logrus.Printf("Expected memory usage %d but found %d\n", expected.Usage, actual.Usage) - t.Fail() + t.Errorf("Expected memory usage %d but found %d\n", expected.Usage, actual.Usage) } if expected.MaxUsage != actual.MaxUsage { - logrus.Printf("Expected memory max usage %d but found %d\n", expected.MaxUsage, actual.MaxUsage) - t.Fail() + t.Errorf("Expected memory max usage %d but found %d\n", expected.MaxUsage, actual.MaxUsage) } if expected.Failcnt != actual.Failcnt { - logrus.Printf("Expected memory failcnt %d but found %d\n", expected.Failcnt, actual.Failcnt) - t.Fail() + t.Errorf("Expected memory failcnt %d but found %d\n", expected.Failcnt, actual.Failcnt) } if expected.Limit != actual.Limit { - logrus.Printf("Expected memory limit %d but found %d\n", expected.Limit, actual.Limit) - t.Fail() + t.Errorf("Expected memory limit %d but found %d\n", expected.Limit, actual.Limit) + } +} + +func expectPageUsageByNUMAEquals(t *testing.T, expected, actual cgroups.PageUsageByNUMA) { + if !reflect.DeepEqual(expected.Total, actual.Total) { + t.Errorf("Expected total page usage by NUMA %#v but found %#v", expected.Total, actual.Total) + } + if !reflect.DeepEqual(expected.File, actual.File) { + t.Errorf("Expected file page usage by NUMA %#v but found %#v", expected.File, actual.File) + } + if !reflect.DeepEqual(expected.Anon, actual.Anon) { + t.Errorf("Expected anon page usage by NUMA %#v but found %#v", expected.Anon, actual.Anon) + } + if !reflect.DeepEqual(expected.Unevictable, actual.Unevictable) { + t.Errorf("Expected unevictable page usage by NUMA %#v but found %#v", expected.Unevictable, actual.Unevictable) + } + if !reflect.DeepEqual(expected.Hierarchical.Total, actual.Hierarchical.Total) { + t.Errorf("Expected hierarchical total page usage by NUMA %#v but found %#v", expected.Hierarchical.Total, actual.Hierarchical.Total) + } + if !reflect.DeepEqual(expected.Hierarchical.File, actual.Hierarchical.File) { + t.Errorf("Expected hierarchical file page usage by NUMA %#v but found %#v", expected.Hierarchical.File, actual.Hierarchical.File) + } + if !reflect.DeepEqual(expected.Hierarchical.Anon, actual.Hierarchical.Anon) { + t.Errorf("Expected hierarchical anon page usage by NUMA %#v but found %#v", expected.Hierarchical.Anon, actual.Hierarchical.Anon) + } + if !reflect.DeepEqual(expected.Hierarchical.Unevictable, actual.Hierarchical.Unevictable) { + t.Errorf("Expected hierarchical total page usage by NUMA %#v but found %#v", expected.Hierarchical.Unevictable, actual.Hierarchical.Unevictable) } } diff --git a/libcontainer/cgroups/stats.go b/libcontainer/cgroups/stats.go index 8eeedc55..eaf8f234 100644 --- a/libcontainer/cgroups/stats.go +++ b/libcontainer/cgroups/stats.go @@ -51,12 +51,33 @@ type MemoryStats struct { KernelUsage MemoryData `json:"kernel_usage,omitempty"` // usage of kernel TCP memory KernelTCPUsage MemoryData `json:"kernel_tcp_usage,omitempty"` + // usage of memory pages by NUMA node + // see chapter 5.6 of memory controller documentation + PageUsageByNUMA PageUsageByNUMA `json:"page_usage_by_numa,omitempty"` // if true, memory usage is accounted for throughout a hierarchy of cgroups. UseHierarchy bool `json:"use_hierarchy"` Stats map[string]uint64 `json:"stats,omitempty"` } +type PageUsageByNUMA struct { + // Embedding is used as types can't be recursive. + PageUsageByNUMAInner + Hierarchical PageUsageByNUMAInner `json:"hierarchical,omitempty"` +} + +type PageUsageByNUMAInner struct { + Total PageStats `json:"total,omitempty"` + File PageStats `json:"file,omitempty"` + Anon PageStats `json:"anon,omitempty"` + Unevictable PageStats `json:"unevictable,omitempty"` +} + +type PageStats struct { + Total uint64 `json:"total,omitempty"` + Nodes map[uint8]uint64 `json:"nodes,omitempty"` +} + type PidsStats struct { // number of pids in the cgroup Current uint64 `json:"current,omitempty"`