Linux Page Cache Thrashing v Kontajneroch: Keď Voľná Pamäť Nie Je Voľná
Na papieri sme mali pamate dost a aj tak sme rozmlatili page cache. “Kontajner má 2GB memory limit a používa len 500MB. Prečo je pomalý?” Pretože zvyšných 1.5GB je page cache, a vaše file I/O vytláča kód vašej aplikácie z pamäte.
Testované na: Linux 5.15, cgroups v2, Kubernetes 1.28, Go aplikácia so spracovaním súborov
Pochopenie Page Cache v Kontajneroch
Čo Je Page Cache
Linux Memory Model:
┌─────────────────────────────────────────────────────────────┐
│ Container Memory Limit │
├─────────────────────────────────────────────────────────────┤
│ Application Memory (anonymous pages) │
│ - Heap alokácie │
│ - Stack │
│ - mmap'd pamäť │
├─────────────────────────────────────────────────────────────┤
│ Page Cache (file-backed pages) │
│ - Cached čítania súborov │
│ - Memory-mapped súbory │
│ - Executable code pages (z bináriek) │
└─────────────────────────────────────────────────────────────┘
Oboje sa počíta proti cgroup memory limitu!
Problém
Scenár: Kontajner s 2GB limitom spracovávajúci súbory
1. Aplikácia štartuje: 500MB anonymous memory
2. Prečítaj 100MB súbor: 500MB app + 100MB page cache = 600MB
3. Prečítaj 1GB súborov: 500MB app + 1000MB cache = 1500MB
4. Prečítaj ďalšie súbory: Cache sa naplní na 1500MB
Teraz na limite: 500MB app + 1500MB cache = 2000MB
5. Prečítaj ďalší súbor:
- Kernel potrebuje niečo evictovať
- Application code pages sú "inactive"
- Code pages evictované pre miesto pre file cache!
6. Aplikácia spustí kód z evictovanej page:
- Major page fault
- Čítanie z disku pre reload kódu
- Výkon sa prepadne
File I/O vytlačilo tvoj vlastný executable!
Demonštrácia
Test Program
// page_cache_demo.go
package main
import (
"fmt"
"io"
"os"
"runtime"
"syscall"
"time"
)
func main() {
// Vynúť GC a vypísať memory stats
printMemStats("Start")
// Simuluj spracovanie súborov
for i := 0; i < 100; i++ {
processLargeFile(fmt.Sprintf("/data/file_%d.bin", i))
if i%10 == 0 {
printMemStats(fmt.Sprintf("Po %d súboroch", i))
measureCodePageFaults()
}
}
}
func processLargeFile(path string) {
f, _ := os.Open(path)
defer f.Close()
io.Copy(io.Discard, f) // Prečítaj celý súbor
}
func printMemStats(label string) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// Prečítaj cgroup memory stats
current, _ := os.ReadFile("/sys/fs/cgroup/memory.current")
limit, _ := os.ReadFile("/sys/fs/cgroup/memory.max")
fmt.Printf("[%s] Heap: %dMB, Cgroup: %s/%s\n",
label, m.HeapAlloc/1024/1024,
string(current[:len(current)-1]),
string(limit[:len(limit)-1]))
}
func measureCodePageFaults() {
var rusage syscall.Rusage
syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
fmt.Printf(" Major faults: %d, Minor faults: %d\n",
rusage.Majflt, rusage.Minflt)
}
Výsledky
# Spusti v kontajneri s 512MB limitom
docker run --memory=512m -v $(pwd)/data:/data page-cache-demo
[Start] Heap: 2MB, Cgroup: 15728640/536870912
Major faults: 0, Minor faults: 1542
[Po 10 súboroch] Heap: 2MB, Cgroup: 524288000/536870912 # Blízko limitu!
Major faults: 0, Minor faults: 2341
[Po 20 súboroch] Heap: 2MB, Cgroup: 536870912/536870912 # Na limite!
Major faults: 145, Minor faults: 8923 # Thrashing začína
[Po 30 súboroch] Heap: 2MB, Cgroup: 536870912/536870912
Major faults: 892, Minor faults: 15234 # Severe thrashing
# App pamäť je len 2MB ale kontajner je na 512MB limite
# Major faults = code pages sa znovu načítavajú z disku
Riešenia
1. Direct I/O (Obídenie Page Cache)
// Použi O_DIRECT pre čítanie veľkých súborov
import "golang.org/x/sys/unix"
func processWithDirectIO(path string) error {
fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECT, 0)
if err != nil {
return err
}
defer unix.Close(fd)
// Buffer musí byť zarovnaný pre O_DIRECT
bufSize := 4096 * 256 // 1MB zarovnané
buf := make([]byte, bufSize+4096)
alignedBuf := buf[4096-int(uintptr(unsafe.Pointer(&buf[0]))%4096):][:bufSize]
for {
n, err := unix.Read(fd, alignedBuf)
if n == 0 || err != nil {
break
}
// Spracuj dáta...
}
return nil
}
2. Povedz Kernelu Nech Dropne Cache
import "golang.org/x/sys/unix"
func processAndDropCache(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// Spracuj súbor
io.Copy(io.Discard, f)
// Povedz kernelu že toto už nebudeme potrebovať
fd := int(f.Fd())
fi, _ := f.Stat()
unix.Fadvise(fd, 0, fi.Size(), unix.FADV_DONTNEED)
return nil
}
3. Streaming s Malými Buffermi
// Nečítaj celý súbor naraz
func processStreaming(path string, chunkSize int) error {
f, _ := os.Open(path)
defer f.Close()
buf := make([]byte, chunkSize) // 64KB chunky
for {
n, err := f.Read(buf)
if n == 0 || err != nil {
break
}
processChunk(buf[:n])
// Periodicky povedz kernelu
if shouldDropCache() {
fd := int(f.Fd())
pos, _ := f.Seek(0, io.SeekCurrent)
unix.Fadvise(fd, 0, pos, unix.FADV_DONTNEED)
}
}
return nil
}
4. Kubernetes Memory Konfigurácia
# Daj rezervu pre page cache
apiVersion: v1
kind: Pod
spec:
containers:
- name: file-processor
resources:
requests:
memory: "512Mi"
limits:
memory: "1Gi" # 2x request pre page cache rezervu
env:
# Povedz appke jej skutočný memory budget
- name: APP_MEMORY_LIMIT
value: "512Mi" # App by mala použiť len polovicu
---
# Alebo použi memory.high pre soft limiting (cgroups v2)
apiVersion: v1
kind: Pod
metadata:
annotations:
# Soft limit na 512Mi, hard limit na 1Gi
# Kernel bude reclaim cache agresívnejšie
memory.high: "512Mi"
5. Memory.high pre Soft Limity
# cgroups v2: memory.high spustí reclaim pred dosiahnutím limitu
# Skontroluj či cgroups v2
cat /proc/mounts | grep cgroup2
# Nastav soft limit
echo 512M > /sys/fs/cgroup/memory.high
echo 1G > /sys/fs/cgroup/memory.max
# Správanie:
# - Pod memory.high: Normálna operácia
# - Nad memory.high: Agresívny page cache reclaim
# - Na memory.max: OOM killer ak potrebné
# Application code pages menej pravdepodobne evictované
# Page cache evictovaný prvý keď nad memory.high
Monitoring
Detekcia Page Cache Thrashingu
# Vnútri kontajnera
cat /sys/fs/cgroup/memory.stat
# Kľúčové metriky:
# file: bajty page cache
# anon: bajty anonymous memory
# pgmajfault: major page faulty (čítania z disku pre evictované pages)
# pgscan: pages skenované pre reclaim
# pgsteal: pages reclaimed
# Vysoké pgmajfault + vysoké pgscan = thrashing
Prometheus Metriky
# cadvisor exponuje tieto
container_memory_rss # Anonymous memory
container_memory_cache # Page cache
container_memory_mapped_file # mmap'd súbory
container_memory_working_set_bytes # "Skutočné" použitie
# Alert na cache tlak
- alert: ContainerPageCacheThrashing
expr: |
rate(container_memory_pgmajfault_total[5m]) > 100
for: 5m
annotations:
summary: "Kontajner {{ $labels.container }} thrashuje"
Pochopenie Memory Metrík
# Čo zvyčajne vidíš
container_memory_usage_bytes # Zahŕňa page cache
# Čo záleží pre OOM
container_memory_working_set_bytes # Anon + aktívny cache
# Page cache špecificky
container_memory_cache
# Tvoja skutočná aplikačná pamäť
container_memory_rss - container_memory_mapped_file
# Indikátor thrashingu
rate(container_memory_pgmajfault_total[5m])
Bežné Vzory
Problém Log Rotácie
Problém: Logovanie do súborov plní page cache
┌─────────────────────────────────────────────────────────────┐
│ App píše logy → Page cache rastie → Evictuje app kód │
│ │
│ Riešenie: Loguj na stdout, nechaj container runtime │
│ Alebo použi O_DIRECT pre log súbory │
└─────────────────────────────────────────────────────────────┘
Spracovanie Temp Súborov
// Zlé: Vytvorí obrovský temp súbor, naplní cache
func processBad(data []byte) {
tmp, _ := os.CreateTemp("", "process-*")
tmp.Write(data)
tmp.Seek(0, 0)
// Spracuj zo súboru...
// Page cache teraz drží celý temp súbor
}
// Dobré: Použi pamäť priamo alebo dropni cache
func processGood(data []byte) {
tmp, _ := os.CreateTemp("", "process-*")
tmp.Write(data)
tmp.Sync()
unix.Fadvise(int(tmp.Fd()), 0, int64(len(data)), unix.FADV_DONTNEED)
// Alebo len spracuj v pamäti ak sa zmestí
}
Databáza v Kontajneri
# PostgreSQL page cache management
apiVersion: v1
kind: Pod
spec:
containers:
- name: postgres
resources:
limits:
memory: "4Gi" # Celkový limit
env:
# PostgreSQL shared_buffers (managed cache)
- name: POSTGRES_SHARED_BUFFERS
value: "1GB" # Len 25% limitu
# Necháva 3GB pre OS page cache + connections
Checklist
## Page Cache Management
### Detekcia
- [ ] Monitoruj container_memory_cache
- [ ] Alert na pgmajfault rate
- [ ] Kontroluj memory.stat pre cache vs anon pomer
### Prevencia
- [ ] Použi FADV_DONTNEED pre čítanie veľkých súborov
- [ ] Zváž O_DIRECT pre sekvenčné I/O
- [ ] Streamuj súbory namiesto čítania celých
- [ ] Daj 2x memory rezervu pre file-heavy workloady
### Konfigurácia
- [ ] Nastav memory.high (cgroups v2) pre soft limiting
- [ ] Konfiguruj app memory budget separátne od limitu
- [ ] Loguj na stdout namiesto súborov
### Architektúra
- [ ] Presun spracovanie súborov do dedikovaných podov
- [ ] Použi externé object storage namiesto lokálnych súborov
- [ ] Zváž tmpfs pre temp súbory (počíta sa k pamäti tak či tak)
Záver
Page cache nie je “voľná” pamäť v kontajneroch:
- File I/O plní page cache počítaný proti tvojmu limitu
- App code pages môžu byť evictované spôsobujúc major faulty
- Použi FADV_DONTNEED na uvoľnenie cache po čítaní
- Daj 2x memory rezervu pre file-heavy workloady
Monitoruj pgmajfault - keď vyskočí, thrashuje.
Súvisiace články
- JVM Native Memory v Kubernetes - Kontajner pamäť
- K8s CPU Throttling - Kontajner performance
Súvisiace články
Kubernetes OOM Killer: Prečo Kontajner Zomiera pri 50% Pamäte
Kontajner má 4GB memory limit ale OOM kill pri 2GB used. Kernel buffers, page cache a cgroup accounting triky spôsobujú skoré OOMKills. Tu je celý obraz.
JVM Native Memory v Kubernetes: Prečo Pod Dostane OOMKilled s 50% Heap
Heap je 50% plný ale pod dostane OOMKilled. Ukážem ako sledovať native memory (Metaspace, threads, NIO) a zabrániť container memory problémom.
Kubernetes p99 špičky bez OOM: Diagnostika cgroup v2 memory.high cez PSI
Použite PSI a cgroup v2 memory.high na vysvetlenie p99 špičiek bez OOMKill. Kubernetes runbook s príkazmi, diffs, bezpečnými mitigáciami a alertmi.
Python GIL a Kubernetes CPU Limity: Pasca Threadingu
Vaša Python appka má 4 thready ale K8s dáva 1 CPU. GIL + CFS kvóta = brutálny throttling. Ukážem prečo a ako správne nastaviť workery.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.