Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/system-stats-monitor.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
},
"memory/zswap_compression_efficiency": {
"displayName": "memory/zswap_compression_efficiency"
},
"memory/zswap_pool_percent": {
"displayName": "memory/zswap_pool_percent"
},
"memory/zswap_writeback_pages": {
"displayName": "memory/zswap_writeback_pages"
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions pkg/systemstatsmonitor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ Below metrics are collected from `memory` component:
* `memory_dirty_used`: Dirty pages usage, in Bytes. Memory usage state is reported under the `state` metric label (e.g. `dirty`, `writeback`). `dirty` means the memory is waiting to be written back to disk, and `writeback` means the memory is actively being written back to disk.
* `memory_zswap_bytes_used`: Zswap memory usage, in Bytes. Collected from `/proc/meminfo` (`Zswap`).
* `memory_zswap_compression_efficiency`: Zswap compression efficiency ratio (`Zswapped` / `Zswap`).
* `memory_zswap_pool_percent`: Percentage of the maximum allowed zswap pool currently in use (`Zswap` / (`MemTotal` * `max_pool_percent` / 100) * 100).
* `memory_zswap_writeback_pages`: Cumulative number of pages written back from zswap to backing swap. Collected from `/proc/vmstat` (`zswpwb`) with fallback to `/sys/kernel/debug/zswap/written_back_pages`.

### OS features

Expand Down
26 changes: 26 additions & 0 deletions pkg/systemstatsmonitor/memory_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type memoryCollector struct {
mDirtyUsed *metrics.Int64Metric
mZswapBytesUsed *metrics.Int64Metric
mZswapCompressionEfficiency *metrics.Float64Metric
mZswapPoolPercent *metrics.Float64Metric
mZswapWritebackPages *metrics.Int64Metric

config *ssmtypes.MemoryStatsConfig
}
Expand Down Expand Up @@ -131,6 +133,30 @@ func NewMemoryCollectorOrDie(memoryConfig *ssmtypes.MemoryStatsConfig) *memoryCo
klog.Fatalf("Error initializing metric for %q: %v", metrics.MemoryZswapCompressionEfficiencyID, err)
}
}
if _, ok := memoryConfig.MetricsConfigs[string(metrics.MemoryZswapPoolPercentID)]; ok {
mc.mZswapPoolPercent, err = metrics.NewFloat64Metric(
metrics.MemoryZswapPoolPercentID,
memoryConfig.MetricsConfigs[string(metrics.MemoryZswapPoolPercentID)].DisplayName,
"Percentage of the maximum allowed zswap pool currently in use",
"%",
metrics.LastValue,
[]string{})
if err != nil {
klog.Fatalf("Error initializing metric for %q: %v", metrics.MemoryZswapPoolPercentID, err)
}
}
if _, ok := memoryConfig.MetricsConfigs[string(metrics.MemoryZswapWritebackPagesID)]; ok {
mc.mZswapWritebackPages, err = metrics.NewInt64Metric(
metrics.MemoryZswapWritebackPagesID,
memoryConfig.MetricsConfigs[string(metrics.MemoryZswapWritebackPagesID)].DisplayName,
"Cumulative number of pages written back from zswap to backing swap",
"1",
metrics.Sum,
[]string{})
if err != nil {
klog.Fatalf("Error initializing metric for %q: %v", metrics.MemoryZswapWritebackPagesID, err)
}
}

return &mc
}
51 changes: 51 additions & 0 deletions pkg/systemstatsmonitor/memory_collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ package systemstatsmonitor

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/prometheus/procfs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

ssmtypes "k8s.io/node-problem-detector/pkg/systemstatsmonitor/types"
)
Expand All @@ -34,6 +38,12 @@ const (
},
"memory/zswap_compression_efficiency": {
"displayName": "memory/zswap_compression_efficiency"
},
"memory/zswap_pool_percent": {
"displayName": "memory/zswap_pool_percent"
},
"memory/zswap_writeback_pages": {
"displayName": "memory/zswap_writeback_pages"
}
}
}
Expand All @@ -48,6 +58,8 @@ func TestMemoryCollector(t *testing.T) {
// Ensure zswap metrics are nil when not in config
assert.Nil(t, mc.mZswapBytesUsed)
assert.Nil(t, mc.mZswapCompressionEfficiency)
assert.Nil(t, mc.mZswapPoolPercent)
assert.Nil(t, mc.mZswapWritebackPages)
}

func TestMemoryCollectorZswap(t *testing.T) {
Expand All @@ -61,6 +73,45 @@ func TestMemoryCollectorZswap(t *testing.T) {
// Ensure zswap metrics are initialized when in config
assert.NotNil(t, mc.mZswapBytesUsed)
assert.NotNil(t, mc.mZswapCompressionEfficiency)
assert.NotNil(t, mc.mZswapPoolPercent)
assert.NotNil(t, mc.mZswapWritebackPages)

mc.collect()
}

func TestCalculateZswapPoolPercent(t *testing.T) {
tmpDir := t.TempDir()
maxPoolPath := filepath.Join(tmpDir, "max_pool_percent")
require.NoError(t, os.WriteFile(maxPoolPath, []byte("20\n"), 0o600))

memTotal := uint64(1000000) // 1,000,000 kB -> max pool (20%) = 200,000 kB
zswap := uint64(50000) // 50,000 kB -> 25% of max pool
meminfo := &procfs.Meminfo{
MemTotal: &memTotal,
Zswap: &zswap,
}

assert.InDelta(t, 25.0, calculateZswapPoolPercent(meminfo, maxPoolPath), 1e-6)
assert.Equal(t, 0.0, calculateZswapPoolPercent(nil, maxPoolPath))
assert.Equal(t, 0.0, calculateZswapPoolPercent(meminfo, filepath.Join(tmpDir, "nonexistent")))
}

func TestReadZswapWritebackPages(t *testing.T) {
tmpDir := t.TempDir()
vmstatPath := filepath.Join(tmpDir, "vmstat")
debugPath := filepath.Join(tmpDir, "written_back_pages")

require.NoError(t, os.WriteFile(vmstatPath, []byte("pswpin 10\nzswpin 100\nzswpout 200\nzswpwb 42\n"), 0o600))
require.NoError(t, os.WriteFile(debugPath, []byte("99\n"), 0o600))

// Prefers /proc/vmstat zswpwb when available
assert.Equal(t, int64(42), readZswapWritebackPages(vmstatPath, debugPath))

// Falls back to debugfs when /proc/vmstat lacks zswpwb
vmstatNoZswap := filepath.Join(tmpDir, "vmstat_no_zswap")
require.NoError(t, os.WriteFile(vmstatNoZswap, []byte("pswpin 10\n"), 0o600))
assert.Equal(t, int64(99), readZswapWritebackPages(vmstatNoZswap, debugPath))

// Returns 0 when neither is available
assert.Equal(t, int64(0), readZswapWritebackPages(vmstatNoZswap, filepath.Join(tmpDir, "missing")))
}
67 changes: 67 additions & 0 deletions pkg/systemstatsmonitor/memory_collector_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,20 @@ limitations under the License.
package systemstatsmonitor

import (
"os"
"strconv"
"strings"

"github.com/prometheus/procfs"
"k8s.io/klog/v2"
)

var (
zswapMaxPoolPercentPath = "/sys/module/zswap/parameters/max_pool_percent"
procVMStatPath = "/proc/vmstat"
zswapWrittenBackPagesDebug = "/sys/kernel/debug/zswap/written_back_pages"
)

func (mc *memoryCollector) collect() {
if mc == nil {
return
Expand Down Expand Up @@ -121,6 +131,10 @@ func (mc *memoryCollector) collect() {
}
}

mc.collectZswap(&meminfo)
}

func (mc *memoryCollector) collectZswap(meminfo *procfs.Meminfo) {
if mc.mZswapBytesUsed != nil {
var bytesUsed int64
if meminfo.Zswap != nil && *meminfo.Zswap > 0 {
Expand All @@ -140,4 +154,57 @@ func (mc *memoryCollector) collect() {
klog.Errorf("Failed to record zswap compression efficiency: %v", err)
}
}

if mc.mZswapPoolPercent != nil {
poolPercent := calculateZswapPoolPercent(meminfo, zswapMaxPoolPercentPath)
if err := mc.mZswapPoolPercent.Record(map[string]string{}, poolPercent); err != nil {
klog.Errorf("Failed to record zswap pool percent: %v", err)
}
}

if mc.mZswapWritebackPages != nil {
writebackPages := readZswapWritebackPages(procVMStatPath, zswapWrittenBackPagesDebug)
if err := mc.mZswapWritebackPages.Record(map[string]string{}, writebackPages); err != nil {
klog.Errorf("Failed to record zswap writeback pages: %v", err)
}
}
}

func calculateZswapPoolPercent(meminfo *procfs.Meminfo, maxPoolPctPath string) float64 {
if meminfo == nil || meminfo.Zswap == nil || *meminfo.Zswap == 0 || meminfo.MemTotal == nil || *meminfo.MemTotal == 0 {
return 0
}
data, err := os.ReadFile(maxPoolPctPath)
if err != nil {
return 0
}
maxPoolPct, err := strconv.ParseFloat(strings.TrimSpace(string(data)), 64)
if err != nil || maxPoolPct <= 0 {
return 0
}
maxPoolKB := float64(*meminfo.MemTotal) * (maxPoolPct / 100.0)
if maxPoolKB <= 0 {
return 0
}
return (float64(*meminfo.Zswap) / maxPoolKB) * 100.0
}

func readZswapWritebackPages(vmstatPath, debugfsPath string) int64 {
if data, err := os.ReadFile(vmstatPath); err == nil {
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Fields(line)
if len(fields) == 2 && fields[0] == "zswpwb" {
if val, err := strconv.ParseInt(fields[1], 10, 64); err == nil && val >= 0 {
return val
}
break
}
}
}
if data, err := os.ReadFile(debugfsPath); err == nil {
if val, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil && val >= 0 {
return val
}
}
return 0
}
2 changes: 2 additions & 0 deletions pkg/util/metrics/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const (
MemoryPercentUsedID MetricID = "memory/percent_used"
MemoryZswapBytesUsedID MetricID = "memory/zswap_bytes_used"
MemoryZswapCompressionEfficiencyID MetricID = "memory/zswap_compression_efficiency"
MemoryZswapPoolPercentID MetricID = "memory/zswap_pool_percent"
MemoryZswapWritebackPagesID MetricID = "memory/zswap_writeback_pages"
OSFeatureID MetricID = "system/os_feature"
SystemProcessesTotal MetricID = "system/processes_total"
SystemProcsRunning MetricID = "system/procs_running"
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/metriconly/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ var _ = ginkgo.Describe("NPD should export Prometheus metrics.", func() {
assertMetricExist(gotMetrics, "memory_percent_used", map[string]string{}, false)
assertMetricExist(gotMetrics, "memory_zswap_bytes_used", map[string]string{}, false)
assertMetricExist(gotMetrics, "memory_zswap_compression_efficiency", map[string]string{}, false)
assertMetricExist(gotMetrics, "memory_zswap_pool_percent", map[string]string{}, false)
assertMetricExist(gotMetrics, "memory_zswap_writeback_pages", map[string]string{}, false)
assertMetricExist(gotMetrics, "host_uptime", map[string]string{}, false)
assertMetricExist(gotMetrics, "system_os_feature", map[string]string{}, false)
})
Expand Down
Loading