Wat valt onder ephemeral storage en wat niet
De Kubernetes-documentatie definieert local ephemeral storage als opslag op nodeniveau die niet gegarandeerd een pod-restart overleeft. De kubelet biedt het aan voor scratch space, caching, container-image-lagen en de writable layers van draaiende containers. Vanuit een pod gezien dragen drie zaken bij aan deze opslag en tellen mee voor zijn ephemeral-storage-limiet:
- De writable container-laag. Elke draaiende container heeft een read-write filesysteem-laag bovenop de read-only image-lagen. Alles wat de applicatie buiten een gemount volume schrijft komt hier terecht: tijdelijke bestanden in
/tmp(als/tmpgeen aparte emptyDir is), niet-stdout logs in andere paden, bestanden die buildstappen binnen de container achterlaten. - Container-logs. Stdout en stderr van elke container worden door de container-runtime naar bestanden op de node geschreven. De kubelet roteert deze bestanden en telt ze mee voor de ephemeral storage van de pod. De standaard is
containerLogMaxSize: 10Miper bestand metcontainerLogMaxFiles: 5bewaard, dus elke container kan tot ~50 MiB node-disk verbruiken alleen voor logs. emptyDir-volumes (niet-tmpfs). Een emptyDir met de standaard disk-backed medium leeft op het filesysteem van de node en telt mee voor ephemeral storage. Een emptyDir metmedium: Memoryis tmpfs en wordt verrekend als geheugen, niet als opslag.
Een aantal volumetypes valt expliciet niet onder ephemeral-storage-limits, ook al lijken ze ephemeral:
- CSI ephemeral volumes worden beheerd door externe CSI-drivers, niet door de kubelet. De Kubernetes-documentatie zegt het direct: ze zijn "not covered by the storage resource usage limits of a Pod, because that is something that kubelet can only enforce for storage that it manages itself." Zie ephemeral volumes.
- Generic ephemeral volumes zijn dynamisch geprovisionde PVCs die de levenscyclus van de pod delen. Hun capaciteit wordt bepaald door de onderliggende StorageClass en de
resources.requests.storagevan de PVC, niet door deephemeral-storage-limiet van de pod. - PersistentVolumes zijn per definitie niet ephemeral. Voor het levenscyclusverschil zie Kubernetes PersistentVolumes en PersistentVolumeClaims.
Ook de container-image-cache op de node wordt niet aan een individuele pod toegerekend. Die leeft op de imagefs van de node (of op nodefs als er geen aparte image-filesystem is) en wordt opgeruimd door de image garbage collector van de kubelet, niet door per-pod-limieten.
Waarom pods gevict worden onder disk pressure
Twee verschillende mechanismen kunnen een pod om opslagredenen beëindigen. Ze leveren verschillende statussen, oorzaken en oplossingen op.
Container- of pod-limiet overschreden. Wanneer de writable layer plus logs plus emptyDir van een enkele container zijn ephemeral-storage-limiet overschrijdt, of wanneer de optelsom over alle containers in een pod het pod-niveau overschrijdt, vict de kubelet de pod. De pod-status toont Failed met reden Evicted en een bericht dat de overschreden limiet noemt. Dit is de container-level enforcement die alpha werd in Kubernetes 1.7, beta in 1.9, en GA in 1.25.
Node-pressure eviction. Wanneer het filesysteem van de node volloopt ongeacht de limiet van een individuele pod, evict de kubelet pods om disk terug te winnen. De trigger is een van de node-pressure eviction signals:
| Signal | Default hard threshold | Wat hij bewaakt |
|---|---|---|
nodefs.available |
10% |
Vrije ruimte op het primaire filesysteem van de kubelet |
nodefs.inodesFree |
5% |
Vrije inodes op het primaire filesysteem |
imagefs.available |
15% |
Vrije ruimte op de optionele image-filesystem |
memory.available |
100Mi |
Vrij geheugen (los van diskdruk) |
pid.available |
10% |
Beschikbare process-ID's |
Voordat de kubelet pods evict, probeert hij eerst op nodeniveau ruimte vrij te maken: ongebruikte container-images verwijderen (bij imagefs-druk) en dode containers en pods opruimen (bij nodefs-druk). Brengt dat de node niet onder de drempel, dan begint pod-eviction.
De selectievolgorde ligt vast: failed pods eerst, dan pods zonder resource-requests, dan pods die boven hun requests gebruiken, dan pods die onder hun requests blijven. Binnen elke laag worden BestEffort-pods eerst gevict, daarna Burstable, en als laatste Guaranteed. Daarom verandert het zetten van accurate ephemeral-storage-requests op productiepods materieel de volgorde waarin de kubelet slachtoffers kiest.
De twee mechanismen zijn onafhankelijk. Een pod zonder ephemeral-storage-limiet kan nog steeds gevict worden door node-druk. Een pod onder zijn limiet kan alsnog gevict worden als de node als geheel vol zit. Beide worden als evictions gerapporteerd, maar de berichttekst en event-reden verschillen.
Ephemeral-storage requests en limits zetten
Requests en limits gebruiken dezelfde eenheden als geheugen: bytes, met binaire (Mi, Gi) of decimale (M, G) suffixen. Ze staan naast CPU en geheugen in het resources-blok van de container:
apiVersion: v1
kind: Pod
metadata:
name: log-aggregator
namespace: production
spec:
containers:
- name: app
image: registry.internal/app:1.42.0 # versie gepind voor reproduceerbare deploys
resources:
requests:
ephemeral-storage: "2Gi" # scheduler reserveert dit
memory: "256Mi"
limits:
ephemeral-storage: "4Gi" # kubelet vict bij overschrijding
memory: "512Mi"
volumeMounts:
- name: scratch
mountPath: /var/cache/app
volumes:
- name: scratch
emptyDir:
sizeLimit: 1Gi # per-volume cap, vict bij overflow
De scheduler behandelt requests.ephemeral-storage net als elke andere resource-request: hij telt de requests van alle containers in de pod op en plaatst de pod alleen op een node met genoeg allocatable ephemeral storage over. Allocatable storage is de filesysteemcapaciteit van de node minus kube-reserved, system-reserved en de eviction-drempel, volgens de reserve compute resources guide. Op nodes waar de kubelet naar een ander filesysteem schrijft dan het cluster verwacht (een aparte disk voor /var/lib/kubelet), klopt de allocatable-boekhouding niet en gaat de scheduler overcommitten.
Het emptyDir.sizeLimit-veld is een aparte, fijnere cap. De kubelet vict de pod als het volume zijn sizeLimit overschrijdt, ook als de algemene ephemeral-storage-limiet van de pod nog niet is bereikt. Gebruik dit om te voorkomen dat één doorgeslagen emptyDir het hele pod-budget opvreet.
Een LimitRange in de namespace kan een default voor ephemeral-storage invullen op pods die het weglaten, en een ResourceQuota kan het namespace-totaal cappen. Zonder quota en zonder per-pod-limits is ephemeral storage best-effort en sleurt de eerste pod die de node volzet de rest mee.
Per pod ephemeral-storage-gebruik monitoren
kubectl top rapporteert geen ephemeral storage. De data zit in het stats summary endpoint van de kubelet, bereikbaar via kubectl get --raw:
NODE=ip-10-0-1-42.eu-west-1.compute.internal
kubectl get --raw "/api/v1/nodes/${NODE}/proxy/stats/summary" | jq '.pods[] | {
pod: .podRef.name,
namespace: .podRef.namespace,
used_bytes: .ephemeral-storage.usedBytes,
capacity_bytes: .ephemeral-storage.capacityBytes
}'
Het ephemeral-storage.usedBytes per pod bevat de writable layer, container-logs en emptyDir-volumes opgeteld. capacityBytes weerspiegelt de limiet, of de beschikbare capaciteit van de node als er geen limiet staat.
Voor continue zichtbaarheid scrape je deze stats met een sidecar-exporter. De community-onderhouden k8s-ephemeral-storage-metrics exposeert ephemeral storage per pod als Prometheus-metrics, te gebruiken in dezelfde alerting-flow als container_memory_working_set_bytes.
Een detail dat teams verrast: kubelet-meting is periodieke scanning, niet realtime. De kubelet sampelt ephemeral usage elke paar seconden. Een pod die in één burst 2 GiB schrijft kan zijn limiet ruim passeren voordat de volgende scan eviction triggert. Filesystem project quota (als ingeschakeld in de kubelet-config) verkleint deze vertraging, maar vereist een filesysteem dat project quota ondersteunt (XFS of ext4 met project-quota-support) en expliciete kubelet-configuratie.
Veelvoorkomende oorzaken van uitputting van ephemeral storage
Een handvol patronen veroorzaakt het overgrote deel van de disk-pressure-incidenten die ik op Kubernetes-nodes heb gezien.
Onbegrensde container-logs. Een applicatie die met hoge frequentie naar stdout logt, vult de log-bestanden van de kubelet sneller dan rotatie kan opruimen. Met de standaard containerLogMaxSize: 10Mi en containerLogMaxFiles: 5 zit elke container op ~50 MiB op disk, maar een pod met 50 containers (DaemonSet, gestapelde sidecars) komt alleen al uit op 2,5 GiB voor logs. Een verkeerd ingestelde logger die JSON-per-regel schrijft op 1 MB/s produceert 86 GB per dag per container; rotatie draait wel, maar de logmap houdt op elk moment meerdere geroteerde bestanden vast.
Logbestanden die de applicatie zelf direct schrijft. Logs die stdout omzeilen en in /var/log/myapp/ binnen de container belanden, gaan in de writable layer, niet in de kubelet-beheerde logmap. De kubelet roteert ze niet. Zonder logrotate binnen de container of een sidecar die ze ophaalt en truncate, groeit de writable layer monotoon.
emptyDir-volumes als caches gebruikt zonder cleanup. Buildpods, image-processing-services en CI-runners die emptyDir als scratch space gebruiken ruimen zelden op. Het volume wordt alleen gereset als de pod stopt. Langdraaiende pods met churn in emptyDir-content vullen het volume tot zijn sizeLimit (of tot de algemene pod-limiet als er geen sizeLimit staat).
Image pulls op kleine disks. Wanneer imagefs op dezelfde disk staat als nodefs (het typische single-filesystem-geval), eet elke image pull uit dezelfde pool die pods voor logs en writable layers gebruiken. Een node die veel verschillende images doorgewerkt krijgt, stapelt lagen op tot image GC start bij imageGCHighThresholdPercent (85% standaard). Op kleine node-disks racen die drempel en de vrije ruimte voor de pod tegen elkaar.
Crash loops die dump-bestanden produceren. Containers die herhaaldelijk crashen schrijven vaak core dumps, heap dumps of thread dumps naar lokale paden. Elke crash is een paar honderd MB. Een pod in een CrashLoopBackOff met dump-on-exit aan vult de writable layer in een paar uur.
Voorkomen: log-rotatie, image-cleanup, sizeLimit, node-reserveringen
De verdedigingen, in volgorde van hoe vaak ze er echt toe doen:
- Zet
ephemeral-storage-limits op elke productiecontainer. Zonder limit is een podBestEffortvoor ephemeral storage en is hij het eerste dat gevict wordt onder node-druk. - Cap emptyDir-volumes met
sizeLimit. Ook als de pod-limiet hetzelfde dekt, lokaliseertsizeLimitde fout naar het volume dat misdraagt in plaats van de hele pod te evicten zodra een enkele emptyDir doorslaat. - Stel kubelet-log-rotatie af op high-rate loggers.
containerLogMaxSize: 100MimetcontainerLogMaxFiles: 3geeft 300 MiB per container voor rotatie, ruim genoeg voor de meeste workloads.containerLogMaxFilesverlagen naar 2 of 3 helpt op kleine nodes waar 50 MiB maal het aantal containers oploopt. - Reserveer ephemeral storage voor het systeem en de kubelet. Zet
kubeReserved.ephemeral-storageensystemReserved.ephemeral-storagezodat de allocatable-boekhouding van de kubelet weerspiegelt wat er echt beschikbaar is. Zonder dit denkt de scheduler dat de hele disk voor pods is en commit hij over. - Strakke image-GC-drempels op kleine disks.
imageGCHighThresholdPercent: 75enimageGCLowThresholdPercent: 70starten cleanup eerder dan de 85/80-defaults; dat geeft meer marge voordat een image pull faalt. - Stuur applicatie-geschreven logs van de node af. Logs die stdout omzeilen moeten de writable layer verlaten. Een logging-sidecar die het bestand tailt en truncate, of een rolling appender aan applicatiezijde met een kleine backlog, voorkomt dat de writable layer voor altijd blijft groeien.
emptyDir met Memory medium: andere boekhouding
De grootste verrassing met emptyDir is de medium-wissel. Een emptyDir met medium: Memory is een tmpfs-mount, geen disk. Alles wat erin geschreven wordt leeft in RAM, en de bytes tellen mee voor het memory limit van de container, niet voor het ephemeral-storage limit.
volumes:
- name: shared-tmpfs
emptyDir:
medium: Memory
sizeLimit: 256Mi # gecapt, maar tegen geheugen
Sinds Kubernetes 1.22 staat de SizeMemoryBackedVolumes-feature gate standaard aan. Daarmee wordt sizeLimit afgedwongen voor memory-backed emptyDir; daarvoor kon het volume groeien tot wat de node aan tmpfs toestond.
Twee praktische gevolgen:
- Een pod met een 512 MiB memory limit en een 256 MiB tmpfs-emptyDir houdt maar 256 MiB geheugen over voor de applicatie. Volloopt het volume, dan krijg je een OOM-kill, geen disk-pressure-eviction. Voor het verschil tussen OOMKill en node-pressure eviction zie OOMKilled: geheugenfouten in Kubernetes uitgelegd.
- Een tmpfs-emptyDir beschermt niet tegen disk-druk op de node, maar draagt er ook niet aan bij. Als de enige reden dat een workload emptyDir gebruikt het vermijden van disk-I/O is, haalt overstappen naar
medium: Memoryde workload volledig uit de disk-druk-vergelijking.
Het is een geheugen-versus-disk afweging. Geheugen is sneller en gebonden aan RAM. Disk is groter en gebonden aan het filesysteem van de node. Er is geen reden om medium: Memory te kiezen voor algemene scratch space; kies het wanneer de applicatie van tmpfs-latency profiteert of wanneer data van disk houden ertoe doet.
Wat ephemeral storage NIET is
Ephemeral storage is niet hetzelfde als df -h op de node. df -h op een node toont het rootfilesysteem van de node, inclusief systeembestanden, mappen van de container-runtime, en opslag die nog vasthangt aan inmiddels gestopte pods. Het toont niet wat de writable layer van een individuele container verbruikt. Voor cijfers per pod gebruik je het stats summary endpoint van de kubelet.
Ephemeral storage wordt niet beschermd door het memory limit. Geheugendruk en diskdruk staan los van elkaar. Een pod met een ruim memory limit kan alsnog gevict worden omdat hij de disk van de node volzet. Een pod onder diskdruk krijgt geen extra geheugen; de kubelet vict hem, of evict hem eerder als de limiet is gezet.
Stdout en stderr tellen wel mee. Het is verleidelijk om aan te nemen dat logs naar de standaardstreams via een centrale logger gerouteerd worden en de node-disk niet raken. Dat doen ze wel. De container-runtime schrijft ze naar bestanden in /var/log/pods/, de kubelet roteert ze, en ze worden tegen het ephemeral-storage gebruik van de pod opgeteld totdat de pod verwijderd wordt.
CSI ephemeral volumes tellen niet mee. Een pod die een CSI ephemeral volume mount kan daar gigabytes opslaan zonder zijn ephemeral-storage-budget te raken, omdat de kubelet het niet meet. De capaciteit komt van de CSI-driver en de onderliggende opslag. Dat is by design, maar het verrast operators die strakke ephemeral-storage-limits zetten en dan een pod veel meer disk zien gebruiken dan verwacht.
Een emptyDir overleeft geen vervanging van de pod. Pod-restart en container-restart zijn verschillende gebeurtenissen. Container-restart bewaart de emptyDir; pod-vervanging (eviction, deletion, herscheduling naar een andere node) doet dat niet. Voor opslag die pod-vervanging moet overleven gebruik je een PersistentVolumeClaim.
Een limiet zetten betekent niet dat de limiet direct wordt afgedwongen. Omdat de kubelet-meting periodiek is, kan een pod tussen scans door zijn limiet kortstondig overschrijden zonder gevict te worden. Andersom kan een pod net onder zijn limiet alsnog gevict worden door node-druk als het cluster vol zit. Limits sturen gedrag; ze fungeren niet als realtime hard caps zoals memory-cgroups dat doen.