Kebocoran memori (memory leak) pada Kubelet berdampak langsung terhadap stabilitas node Kubernetes. Saat alokasi memori Kubelet naik secara persisten tanpa terbebaskan oleh Go garbage collector, proses Kubelet berisiko tereliminasi oleh Kernel Out-Of-Memory (OOM) Killer. Kondisi ini membuat status node menjadi NotReady dan memicu pod eviction cascading.

Deteksi dini regresi memori Kubelet memerlukan verifikasi otomatis berbasis stress test pod lifecycle, inspeksi hierarki cgroup v2, serta analisis diferensial (delta) heap profiling menggunakan pprof.

Arsitektur Harness Pengujian Regresi

Harness pengujian regresi dijalankan pada isolated ephemeral node sebelum image sistem operasi didistribusikan ke cluster produksi. Verifikasi deterministik dilakukan melalui tiga tahapan terstruktur:

  1. Baseline Measurement: Mengambil profil memori cgroup dan heap dump Kubelet dalam kondisi node idle (tanpa beban kerja selain sistem daemon).
  2. Lifecycle Stress Ingestion: Menjalankan siklus pembuatan dan penghapusan ribuan pod short-lived guna memicu aktivitas intensif pada goroutine Kubelet, event listener, dan volume mount tracker.
  3. Post-Run Stabilization & Delta Check: Mengistirahatkan node untuk memberi kesempatan Go runtime menjalankan siklus GC, mengambil profil akhir, serta melakukan assertion terhadap ambang batas memori RSS (Resident Set Size).

Akses Heap Profiling Kubelet via API Server

Kubelet mengekspos endpoint debugging /debug/pprof/heap pada port 10250. Akses aman ke endpoint ini dilakukan melalui proxy API Server menggunakan kredensial RBAC administrative.

Untuk mengizinkan eksekusi profil, kubelet configuration harus mengaktifkan flag profil:

# Konfigurasi Kubelet (/var/lib/kubelet/config.yaml)
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
enableProfilingHandler: true
enableDebugFlagsHandler: true

Pengambilan snapshot heap dilakukan dengan perintah curl terotentikasi melalui proxy API Server:

APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
NODE_NAME="worker-node-canary"

# Snapshot baseline
kubectl get --raw "${APISERVER}/api/v1/nodes/${NODE_NAME}/proxy/debug/pprof/heap" > kubelet_baseline.pb.gz

# Menjalankan lifecycle workload...

# Snapshot pasca stress test (setelah waktu stabilisasi GC)
kubectl get --raw "${APISERVER}/api/v1/nodes/${NODE_NAME}/proxy/debug/pprof/heap" > kubelet_post_stress.pb.gz

Parsing Metrik Memori Cgroup v2

Pada Linux dengan cgroup v2 terpadu, proses Kubelet berjalan di bawah cgroup sistem khusus, umumnya beralamat di /sys/fs/cgroup/system.slice/kubelet.service/. Untuk mengukur konsumsi riil memori fisik dan mengabaikan page cache OS, baca file memory.stat dan ambil nilai field anon.

Skrip Bash berikut memverifikasi bahwa delta konsumsi anonymous memory (RSS) Kubelet tidak melampaui batas toleransi yang ditentukan setelah siklus beban kerja selesai:

#!/usr/bin/env bash
set -euo pipefail

CGROUP_PATH="/sys/fs/cgroup/system.slice/kubelet.service"
RSS_THRESHOLD_BYTES=$((50 * 1024 * 1024)) # Toleransi leak: 50MB

get_anon_bytes() {
    awk '$1 == "anon" {print $2}' "${CGROUP_PATH}/memory.stat"
}

echo "[INFO] Membaca baseline anon memory..."
START_ANON=$(get_anon_bytes)

echo "[INFO] Menjalankan stress test: 2000 pod lifecycles..."
# Menjalankan loop pod batch run
for i in {1..20}; do
    kubectl apply -f stress-pod-batch.yaml &>/dev/null
    kubectl delete -f stress-pod-batch.yaml --wait=true &>/dev/null
done

echo "[INFO] Menunggu stabilisasi GC runtime (60 detik)..."
sleep 60

END_ANON=$(get_anon_bytes)
DELTA_ANON=$((END_ANON - START_ANON))

echo "Memori Awal : $((START_ANON / 1024 / 1024)) MB"
echo "Memori Akhir: $((END_ANON / 1024 / 1024)) MB"
echo "Delta Anon  : $((DELTA_ANON / 1024 / 1024)) MB"

if [ "$DELTA_ANON" -gt "$RSS_THRESHOLD_BYTES" ]; then
    echo "[FAILED] Terdeteksi potensi leak: Delta anon memory (${DELTA_ANON} bytes) melebihi batas."
    exit 1
fi

echo "[SUCCESS] Assertion lolos. Delta konsumsi memori dalam batas aman."
exit 0

Analisis Delta Alokasi dengan pprof

Bila assertion cgroup mendeteksi pertumbuhan memori yang abnormal, heap snapshot dianalisis menggunakan go tool pprof dengan flag -base. Opsi ini membandingkan dua snapshot dan menampilkan selisih objek yang dialokasikan namun belum terhapus.

# Menganalisis pertumbuhan alokasi in-use memory
go tool pprof -top -base kubelet_baseline.pb.gz kubelet_post_stress.pb.gz

Contoh output representatif saat terjadi kebocoran memori internal Kubelet:

Showing nodes accounting for 48.20MB, 94.14% of 51.20MB total
      flat  flat%   sum%        cum   cum%
   32.10MB 62.70% 62.70%    32.10MB 62.70%  k8s.io/kubernetes/pkg/kubelet/pleg.(*GenericPLEG).updateCache
   10.50MB 20.51% 83.20%    10.50MB 20.51%  k8s.io/kubernetes/pkg/kubelet/volumemanager/cache.(*actualStateOfWorld).AddPodToVolume
    5.60MB 10.94% 94.14%     5.60MB 10.94%  k8s.io/client-go/tools/cache.(*Reflector).watchHandler

Metrik flat bernilai positif menunjukkan fungsi Go yang mempertahankan alokasi objek baru dalam heap tanpa membebaskannya kembali ke runtime pool.

Pipeline e2e Assertion dalam Go

Program Go berikut digunakan sebagai test runner deterministik pada CI/CD validation stage node image. Runner ini memverifikasi rasio kebocoran secara langsung:

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func getCgroupV2Anon(statPath string) (int64, error) {
	file, err := os.Open(statPath)
	if err != nil {
		return 0, err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) == 2 && fields[0] == "anon" {
			return strconv.ParseInt(fields[1], 10, 64)
		}
	}
	return 0, fmt.Errorf("field anon tidak ditemukan")
}

func main() {
	cgroupStatPath := "/sys/fs/cgroup/system.slice/kubelet.service/memory.stat"
	maxAllowedLeak := int64(30 * 1024 * 1024) // 30 MB

	baseline, err := getCgroupV2Anon(cgroupStatPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Gagal membaca baseline: %v\n", err)
		os.Exit(1)
	}

	// Trigger workload simulasi pod lifecycle (eksekusi e2e runner k8s)
	// executeLifecycleTestRunner()

	postRun, err := getCgroupV2Anon(cgroupStatPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Gagal membaca post-run: %v\n", err)
		os.Exit(1)
	}

	delta := postRun - baseline
	fmt.Printf("Baseline: %d MB | Post: %d MB | Delta: %d MB\n", 
		baseline/1048576, postRun/1048576, delta/1048576)

	if delta > maxAllowedLeak {
		fmt.Printf("Assertion gagal: memory growth %d bytes melebihi ambang batas %d bytes\n", delta, maxAllowedLeak)
		os.Exit(1)
	}

	fmt.Println("Status: PASSED")
}

Isolasi Node Image Sebelum Distribusi Produksi

Sebelum Golden AMI atau OS disk image dideploy ke pool produksi, terapkan langkah isolasi berikut pada pipeline pengujian:

  • Matikan Swap: Pastikan swap memory berstatus disabled atau terisolasi penuh via NodeSwap feature gate agar metrik cgroup v2 tidak terdistorsi oleh swapping halaman memori anonim ke disk.
  • Bypass Eviction Manager: Nonaktifkan konfigurasi evictionHard pada test node agar Kubelet tidak memicu eviksi pod sebelum pola saturasi memori berhasil diobservasi sepenuhnya.
  • Validasi Garbage Collector: Berikan jeda minimum 60-120 detik pasca load ramp-down untuk memastikan Go Runtime telah mengeksekusi sedikitnya dua siklus GC penuh (runtime.GC()) sebelum memori diukur.