fix: Docker container memory now subtracts inactive_file on cgroup v2 systems. Fixes container memory reporting to match 'docker stats' output by excluding reclaimable filesystem cache. Related to #435

This commit is contained in:
rcourtman
2025-12-30 12:31:57 +00:00
parent f855625f65
commit c22dac5d8d
2 changed files with 32 additions and 5 deletions

View File

@@ -324,7 +324,7 @@ func TestCalculateMemoryUsage(t *testing.T) {
wantPercent: 25.0,
},
{
name: "cache larger than usage falls back to raw usage",
name: "cache larger than usage keeps raw usage",
stats: containertypes.StatsResponse{
MemoryStats: containertypes.MemoryStats{
Usage: 1000000,
@@ -332,10 +332,23 @@ func TestCalculateMemoryUsage(t *testing.T) {
Stats: map[string]uint64{"cache": 2000000}, // more than usage
},
},
wantUsage: 1000000, // falls back to raw usage
wantUsage: 1000000, // keeps raw usage when cache > usage
wantLimit: 4000000,
wantPercent: 25.0,
},
{
name: "cgroup v2 uses inactive_file when no cache",
stats: containertypes.StatsResponse{
MemoryStats: containertypes.MemoryStats{
Usage: 1000000,
Limit: 4000000,
Stats: map[string]uint64{"inactive_file": 300000}, // cgroup v2 style
},
},
wantUsage: 700000, // 1000000 - 300000
wantLimit: 4000000,
wantPercent: 17.5,
},
{
name: "zero limit returns zero percent",
stats: containertypes.StatsResponse{

View File

@@ -715,11 +715,25 @@ func calculateCPUPercent(stats containertypes.StatsResponse, hostCPUs int) float
func calculateMemoryUsage(stats containertypes.StatsResponse) (usage int64, limit int64, percent float64) {
usage = int64(stats.MemoryStats.Usage)
// Subtract reclaimable cache from usage to match `docker stats` behavior.
// Docker subtracts cache/file to show "actual" memory usage rather than
// memory.current which includes reclaimable filesystem cache.
//
// cgroup v1: "cache" stat contains the reclaimable cache
// cgroup v2: "cache" doesn't exist, use "inactive_file" (preferred) or "file"
var cacheBytes uint64
if cache, ok := stats.MemoryStats.Stats["cache"]; ok {
usage -= int64(cache)
// cgroup v1
cacheBytes = cache
} else if inactiveFile, ok := stats.MemoryStats.Stats["inactive_file"]; ok {
// cgroup v2: inactive_file is the reclaimable portion of file cache
// This matches what docker CLI does internally
cacheBytes = inactiveFile
}
if usage < 0 {
usage = int64(stats.MemoryStats.Usage)
if cacheBytes > 0 && int64(cacheBytes) < usage {
usage -= int64(cacheBytes)
}
limit = int64(stats.MemoryStats.Limit)