Volumi
Tre superfici distinte, spesso confuse:
| Superficie | Metriche | Chi le espone |
|---|---|---|
| PVC — volumi persistenti montati nei pod | kubelet_volume_stats_* | kubelet |
Filesystem effimero — layer scrivibile e emptyDir | container_fs_* | cAdvisor |
Filesystem del nodo — /, /var/lib/containers | node_filesystem_* | node exporter |
Un PVC pieno blocca un’applicazione. Un /var/lib/containers pieno blocca tutti i pod del nodo.
| Triage | i volumi da guardare per primi |
| Riempimento PVC · Proiezioni | |
| Inode | il caso che si dimentica |
| Stato e provisioning | Pending, Failed, Released |
| IOPS e latenza | |
| Filesystem effimero | |
| StorageClass e CSI | |
Comandi oc · Alert |
Triage
T1. PVC oltre l’85%
# percentuale (0-100)100 * ( 1 - ( kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes )) > 85L’output porta già namespace e persistentvolumeclaim.
T2. PVC che si riempiranno entro 7 giorni
# giorni( kubelet_volume_stats_available_bytes / deriv(kubelet_volume_stats_used_bytes[6h:30m])) / 86400 < 7andderiv(kubelet_volume_stats_used_bytes[6h:30m]) > 0Il secondo predicato elimina i volumi in decrescita, che altrimenti darebbero valori negativi grandi.
T3. PVC Pending
# elenco: ogni serie restituita soddisfa la condizionekube_persistentvolumeclaim_status_phase{phase="Pending"} == 1Un PVC Pending blocca l’avvio del pod che lo usa.
T4. Inode quasi esauriti
# percentuale (0-100)100 * ( 1 - (kubelet_volume_stats_inodes_free / kubelet_volume_stats_inodes)) > 80T5. Filesystem dei nodi
# percentuale (0-100)100 * ( 1 - ( node_filesystem_avail_bytes{fstype!~"tmpfs|devtmpfs|overlay|squashfs"} / node_filesystem_size_bytes{fstype!~"tmpfs|devtmpfs|overlay|squashfs"} )) > 80Riempimento PVC
Top 20 per percentuale
# percentuale (0-100)topk(20, 100 * ( 1 - (kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes) ))Solo i namespace applicativi
# percentuale (0-100)100 * ( 1 - ( kubelet_volume_stats_available_bytes{namespace!~"kube-.*|openshift-.*"} / kubelet_volume_stats_capacity_bytes{namespace!~"kube-.*|openshift-.*"} )) > 80Spazio residuo assoluto
# GiBtopk(20, kubelet_volume_stats_available_bytes) / 1024^3La percentuale inganna sui volumi grandi: il 5% libero su 2 TiB sono 100 GiB, il 5% su 10 GiB sono 500 MiB. Guarda sempre entrambe.
Solo quelli sotto una soglia assoluta:
# bytekubelet_volume_stats_available_bytes < 5 * 1024^3Spazio usato
# GiBtopk(20, kubelet_volume_stats_used_bytes) / 1024^3Capacità provisionata
# GiBtopk(20, kubelet_volume_stats_capacity_bytes) / 1024^3Efficienza — quanto del provisionato è usato
# bytekubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytesSotto 0.20 il volume è molto più grande del necessario. Su storage a pagamento è un costo diretto e ricorrente, ed è la voce che si taglia più facilmente in una revisione.
Aggregato per namespace:
# percentuale 0-100100 * ( sum by (namespace) (kubelet_volume_stats_used_bytes{namespace!~"kube-.*|openshift-.*"}) / sum by (namespace) (kubelet_volume_stats_capacity_bytes{namespace!~"kube-.*|openshift-.*"}))Crescita nelle ultime 24 ore
# MiBtopk(20, kubelet_volume_stats_used_bytes - kubelet_volume_stats_used_bytes offset 24h) / 1024^2MiB cresciuti in un giorno. Utile per capacity planning e per identificare chi ha iniziato a crescere dopo un rilascio.
Crescita settimanale
# GiBtopk(20, kubelet_volume_stats_used_bytes - kubelet_volume_stats_used_bytes offset 7d) / 1024^3Proiezioni
Giorni al riempimento
# giorni( kubelet_volume_stats_available_bytes / deriv(kubelet_volume_stats_used_bytes[6h:30m])) / 86400Giorni residui al ritmo delle ultime 6 ore. Valori negativi = il volume si sta svuotando.
Versione con predict_linear
# byte previsti a fine finestra: negativo = esaurimento prima della scadenzapredict_linear(kubelet_volume_stats_available_bytes[6h], 7 * 24 * 3600) < 0PVC che si riempiranno entro 7 giorni. predict_linear è più robusto della divisione per deriv perché gestisce meglio le serie rumorose, ed è la forma da usare negli alert.
Su finestra più lunga, per il capacity planning:
# byte previsti a fine finestra: negativo = esaurimento prima della scadenzapredict_linear(kubelet_volume_stats_available_bytes[7d], 30 * 24 * 3600) < 0Tasso di crescita in MiB/h
# MiB all'oratopk(20, deriv(kubelet_volume_stats_used_bytes[6h:30m]) * 3600 / 1024^2)Inode
# percentuale (0-100)100 * ( 1 - (kubelet_volume_stats_inodes_free / kubelet_volume_stats_inodes))# numero di inodetopk(20, kubelet_volume_stats_inodes_used)# numero di inodekubelet_volume_stats_inodes_free < 100000Un volume con inode esauriti si comporta come pieno pur mostrando spazio libero: df -h rassicura e ogni scrittura fallisce con No space left on device. Tipico di applicazioni che creano moltissimi file piccoli — cache su filesystem, sessioni, code su file, upload temporanei mai puliti.
Dimensione media dei file
# bytekubelet_volume_stats_used_bytes / kubelet_volume_stats_inodes_usedSotto i 4 KiB medi il volume è pieno di file minuscoli e gli inode si esauriranno prima dello spazio. È il segnale predittivo: consente di intervenire prima che accada.
Stato e provisioning
PVC per fase
# elenco: ogni serie restituita soddisfa la condizionesum by (namespace, phase) (kube_persistentvolumeclaim_status_phase == 1)PVC Pending
# elenco: ogni serie restituita soddisfa la condizionekube_persistentvolumeclaim_status_phase{phase="Pending"} == 1Cause, in ordine di frequenza:
| Causa | Verifica |
|---|---|
| StorageClass inesistente o non default | oc get sc |
| CSI driver degradato | oc get csidrivers, oc get co storage |
| Quota del namespace superata | oc describe resourcequota -n $ns |
| Nessun PV disponibile per binding statico | oc get pv |
volumeBindingMode: WaitForFirstConsumer e pod non schedulabile | normale: il PVC si lega quando il pod trova un nodo |
oc describe pvc -n $ns $pvc | sed -n '/Events:/,$p'L’ultimo caso è quello che confonde: il PVC resta Pending di proposito finché lo scheduler non decide su quale nodo va il pod. Non è un errore.
PVC Lost
# elenco: ogni serie restituita soddisfa la condizionekube_persistentvolumeclaim_status_phase{phase="Lost"} == 1Il PV a cui il PVC era legato non esiste più. È una situazione da cui non si esce senza intervento manuale: i dati potrebbero essere ancora sullo storage, ma il legame è rotto.
PV per fase
# elenco: ogni serie restituita soddisfa la condizionesum by (phase) (kube_persistentvolume_status_phase == 1)# elenco: ogni serie restituita soddisfa la condizionekube_persistentvolume_status_phase{phase=~"Failed|Pending"} == 1PV rilasciati e mai riciclati
# elenco: ogni serie restituita soddisfa la condizionekube_persistentvolume_status_phase{phase="Released"} == 1# numero di seriecount(kube_persistentvolume_status_phase{phase="Released"} == 1)Con reclaimPolicy: Retain i PV restano Released dopo l’eliminazione del PVC: occupano spazio sullo storage backend e non sono riutilizzabili. Su cluster di lunga vita se ne accumulano decine, ed è spazio pagato e sprecato.
oc get pv --sort-by=.status.phase | grep Releasedoc get pv -o json | jq -r '.items[] | select(.status.phase=="Released") | "\(.metadata.name)\t\(.spec.capacity.storage)\t\(.spec.claimRef.namespace)/\(.spec.claimRef.name)"'Capacità richiesta per StorageClass
# GiBsum by (storageclass) ( kube_persistentvolumeclaim_resource_requests_storage_bytes * on(namespace, persistentvolumeclaim) group_left(storageclass) max by (namespace, persistentvolumeclaim, storageclass) (kube_persistentvolumeclaim_info)) / 1024^3GiB richiesti per StorageClass. Da confrontare con la capacità reale del backend (datastore VMware, pool NFS): Kubernetes non la conosce e continuerà a fare provisioning finché il backend non rifiuta — e a quel punto i PVC restano Pending senza un messaggio chiaro.
Numero di PVC per StorageClass
# numero di seriecount by (storageclass) (kube_persistentvolumeclaim_info)Access mode
# numero di seriecount by (access_mode) (kube_persistentvolume_info)ReadWriteOnce non può essere montato da pod su nodi diversi: è la causa di pod bloccati in ContainerCreating con Multi-Attach error durante un rollout, quando il vecchio pod non ha ancora rilasciato il volume.
oc get events -A --field-selector reason=FailedAttachVolume --sort-by=.lastTimestamp | tailVolumi montati per pod
# numero di seriecount by (namespace, pod) (kubelet_volume_stats_capacity_bytes)IOPS e latenza
Il kubelet non espone IOPS per PVC: kubelet_volume_stats_* sono solo metriche di capacità. Per l’I/O bisogna scendere di livello, in due modi complementari.
Lato container — include tutto l’I/O del pod
# operazioni al secondotopk(20, sum by (namespace, pod) ( rate(container_fs_writes_total{namespace!~"kube-.*|openshift-.*"}[5m]) ))# operazioni al secondotopk(20, sum by (namespace, pod) ( rate(container_fs_reads_total{namespace!~"kube-.*|openshift-.*"}[5m]) ))# MiBtopk(20, sum by (namespace, pod) ( rate(container_fs_reads_bytes_total{namespace!~"kube-.*|openshift-.*"}[5m]) + rate(container_fs_writes_bytes_total{namespace!~"kube-.*|openshift-.*"}[5m]) )) / 1024^2MiB/s combinati. Identifica i pod che stressano lo storage del nodo: export, dump, indicizzazione, compattazione di database.
Attenzione: container_fs_* misura tutto l’I/O del container, non solo quello verso il PVC. Non è possibile separarli da queste metriche.
Lato nodo — il device fisico
# IOPStopk(10, sum by (instance, device) (rate(node_disk_reads_completed_total[5m])))# IOPStopk(10, sum by (instance, device) (rate(node_disk_writes_completed_total[5m])))# secondi per secondo (frazione di tempo)rate(node_disk_read_time_seconds_total[5m])/rate(node_disk_reads_completed_total[5m])# secondi per secondo (frazione di tempo)rate(node_disk_write_time_seconds_total[5m])/rate(node_disk_writes_completed_total[5m])Secondi per operazione. Su storage enterprise ci si aspetta sotto 0.005. Sopra 0.020 sostenuto il backend è in affanno e tutto ne risente.
Saturazione del device
# percentuale 0-100 di tempo con I/O in coda100 * rate(node_disk_io_time_seconds_total[5m])Vicino a 1 = disco saturo.
# percentuale 0-100 di tempo con I/O in coda100 * rate(node_disk_io_time_weighted_seconds_total[5m])Coda media ponderata: sopra 1 ci sono richieste che aspettano.
Dimensione media dell’I/O
# byte al secondorate(node_disk_written_bytes_total[5m])/rate(node_disk_writes_completed_total[5m])Scritture molto piccole (< 8 KiB) ad alta frequenza saturano gli IOPS del backend molto prima della banda. È il profilo tipico di un database o di un’applicazione che fa fsync a ogni transazione.
Filesystem effimero
Il layer scrivibile del container e gli emptyDir non hanno limite di default: un pod che riempie /tmp riempie il disco del nodo, e il kubelet inizia a evictare pod su quel nodo — anche quelli innocenti.
Uso per pod
# MiBtopk(20, sum by (namespace, pod) ( container_fs_usage_bytes{ namespace!~"kube-.*|openshift-.*", container!="", container!="POD" } )) / 1024^2Per nodo
# GiBsum by (node) ( container_fs_usage_bytes{container!="", container!="POD"} * on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)) / 1024^3Container senza limite di ephemeral-storage
# numero di seriecount by (namespace, pod, container) ( kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*"})unlesscount by (namespace, pod, container) ( kube_pod_container_resource_limits{resource="ephemeral_storage"})Impostare limits.ephemeral-storage fa sì che il kubelet evicti solo il pod colpevole invece di mettere in pressione l’intero nodo. Su cluster multi-tenant dovrebbe essere obbligatorio, imposto via LimitRange.
Nodi in DiskPressure
# elenco: ogni serie restituita soddisfa la condizionekube_node_status_condition{condition="DiskPressure", status="true"} == 1Pod evictati
# elenco: ogni serie restituita soddisfa la condizionekube_pod_status_reason{reason="Evicted"} == 1# numero di seriecount by (namespace) (kube_pod_status_reason{reason="Evicted"} == 1)oc get pods -A --field-selector=status.phase=Failed \ -o custom-columns=NS:.metadata.namespace,POD:.metadata.name,NODE:.spec.nodeName,MSG:.status.message \ | grep -i ephemeralStorageClass e CSI
StorageClass disponibili
# sempre 1: metrica portatrice di label (provisioner, reclaim_policy)kube_storageclass_infoI label provisioner, reclaim_policy, volume_binding_mode.
oc get scoc get sc -o custom-columns=NAME:.metadata.name,PROVISIONER:.provisioner,RECLAIM:.reclaimPolicy,BINDING:.volumeBindingMode,EXPANSION:.allowVolumeExpansion,DEFAULT:'.metadata.annotations.storageclass\.kubernetes\.io/is-default-class'allowVolumeExpansion: false significa che non si può ingrandire un PVC pieno senza migrare i dati: è la cosa da verificare prima che un volume si riempia, non dopo.
Nessuna StorageClass di default
oc get sc -o json | jq -r '.items[] | select(.metadata.annotations."storageclass.kubernetes.io/is-default-class"=="true") | .metadata.name'Output vuoto = ogni PVC senza storageClassName esplicito resterà Pending per sempre. Output con più di una riga = comportamento indefinito.
Stato dei CSI driver
# elenco: ogni serie restituita soddisfa la condizionekube_pod_container_status_ready{namespace=~"openshift-.*storage.*|openshift-cluster-csi-drivers"} == 0# elenco: ogni serie restituita soddisfa la condizionecluster_operator_conditions{name="storage", condition="Degraded"} == 1oc get co storageoc get csidriversoc get pods -n openshift-cluster-csi-driversOperazioni CSI in errore
# operazioni al secondosum by (driver_name, method_name, grpc_status_code) ( rate(csi_sidecar_operations_seconds_count{grpc_status_code!="OK"}[5m])) > 0I nomi delle metriche CSI variano con il driver. Verifica cosa esiste:
# numero di seriecount by (__name__) ({__name__=~"csi_.*|storage_operation.*"})Latenza delle operazioni sui volumi
# secondihistogram_quantile(0.99, sum by (le, operation_name) ( rate(storage_operation_duration_seconds_bucket[5m]) ))volume_mount e volume_attach lenti spiegano i pod bloccati in ContainerCreating durante i rollout.
Espansione di un PVC
# Verificare che la StorageClass lo permettaoc get sc $sc -o jsonpath='{.allowVolumeExpansion}{"\n"}'
# Espandereoc patch pvc $pvc -n $ns -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'
# Seguire lo statooc get pvc $pvc -n $ns -o jsonpath='{.status.conditions}' | jqMolti driver richiedono il riavvio del pod per completare l’espansione del filesystem: la capacità del PV cresce subito, quella vista dal container no.
# GiB · sostituisci $nskubelet_volume_stats_capacity_bytes{namespace="$ns", persistentvolumeclaim="$pvc"} / 1024^3Se questa non cresce dopo il patch, il pod va riavviato.
Comandi oc
# PVC di un namespaceoc get pvc -n $nsoc describe pvc -n $ns $pvc
# Tutti i PVC per dimensioneoc get pvc -A -o json | jq -r '.items[] | "\(.metadata.namespace)\t\(.metadata.name)\t\(.spec.resources.requests.storage)\t\(.status.phase)"' | sort -k3 -hr
# PV orfanioc get pv --sort-by=.status.phaseoc get pv -o json | jq -r '.items[] | select(.status.phase=="Released") | .metadata.name'
# Uso reale dentro il podoc rsh -n $ns $pod df -hoc rsh -n $ns $pod df -i
# Le directory più grandi nel volumeoc rsh -n $ns $pod du -xh --max-depth=2 /path/del/mount 2>/dev/null | sort -rh | head -20
# I file più grandioc rsh -n $ns $pod find /path/del/mount -type f -size +100M -exec ls -lh {} \; 2>/dev/null | head -20
# Contare i file (se gli inode sono il problema)oc rsh -n $ns $pod find /path/del/mount -type f 2>/dev/null | wc -l
# Quale pod usa un PVCoc get pods -n $ns -o json | jq -r --arg pvc "$pvc" ' .items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName == $pvc) | .metadata.name'
# Disco e inode del nodooc debug node/$node -- chroot /host df -hToc debug node/$node -- chroot /host df -hioc debug node/$node -- chroot /host du -xh --max-depth=1 /var/lib/containers | sort -rh | head
# Errori di attachoc get events -A --field-selector reason=FailedAttachVolume --sort-by=.lastTimestamp | tailoc get events -A --field-selector reason=FailedMount --sort-by=.lastTimestamp | tailAlert
apiVersion: monitoring.coreos.com/v1kind: PrometheusRulemetadata: name: storage-health-custom namespace: openshift-monitoring labels: role: alert-rulesspec: groups: - name: storage.pvc rules: - alert: PVCNearlyFull expr: | ( 1 - kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes ) > 0.85 for: 30m labels: severity: warning annotations: summary: "PVC {{ $labels.persistentvolumeclaim }} ({{ $labels.namespace }}) al {{ $value | humanizePercentage }}" description: "Verificare se la StorageClass permette l'espansione prima che si riempia del tutto."
- alert: PVCCritical expr: | ( 1 - kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes ) > 0.95 for: 10m labels: severity: critical annotations: summary: "PVC {{ $labels.persistentvolumeclaim }} ({{ $labels.namespace }}) al {{ $value | humanizePercentage }}"
- alert: PVCWillFillIn7Days expr: | predict_linear(kubelet_volume_stats_available_bytes[6h], 7 * 24 * 3600) < 0 and kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes < 0.30 for: 1h labels: severity: warning annotations: summary: "PVC {{ $labels.persistentvolumeclaim }} ({{ $labels.namespace }}) pieno entro 7 giorni" description: "Proiezione lineare sulle ultime 6 ore. Pianificare l'espansione o la pulizia."
- alert: PVCInodesNearlyExhausted expr: | kubelet_volume_stats_inodes_free / kubelet_volume_stats_inodes < 0.15 for: 30m labels: severity: warning annotations: summary: "PVC {{ $labels.persistentvolumeclaim }}: inode al {{ $value | humanizePercentage }} liberi" description: "Il volume si comporterà come pieno pur mostrando spazio disponibile."
- name: storage.provisioning rules: - alert: PVCPending expr: kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1 for: 15m labels: severity: warning annotations: summary: "PVC {{ $labels.persistentvolumeclaim }} ({{ $labels.namespace }}) Pending da 15 minuti" description: "Il pod che lo usa non parte. Verificare StorageClass, quota e stato del CSI driver."
- alert: PVCLost expr: kube_persistentvolumeclaim_status_phase{phase="Lost"} == 1 labels: severity: critical annotations: summary: "PVC {{ $labels.persistentvolumeclaim }} in stato Lost" description: "Il PV associato non esiste più. Richiede intervento manuale."
- alert: PVReleasedAccumulating expr: count(kube_persistentvolume_status_phase{phase="Released"} == 1) > 20 for: 24h labels: severity: info annotations: summary: "{{ $value }} PV in stato Released" description: "Spazio occupato sul backend e non riutilizzabile. Valutare la pulizia."
- name: storage.performance rules: - alert: StorageOperationSlow expr: | histogram_quantile(0.99, sum by (le, operation_name) (rate(storage_operation_duration_seconds_bucket[5m])) ) > 30 for: 15m labels: severity: warning annotations: summary: "Operazione storage {{ $labels.operation_name }}: p99 a {{ $value | printf \"%.0f\" }}s" description: "Mount e attach lenti bloccano i pod in ContainerCreating durante i rollout."KubePersistentVolumeFillingUp e KubePersistentVolumeInodesFillingUp esistono già nativamente. Verifica prima di duplicare:
oc -n openshift-monitoring get prometheusrule -o yaml | grep -E "alert: KubePersistentVolume"Soglie di riferimento
| Metrica | Sano | Attenzione | Critico |
|---|---|---|---|
| Riempimento PVC | < 70% | 70 – 85% | > 85% |
| Spazio residuo assoluto | > 20 GiB | 5 – 20 GiB | < 5 GiB |
| Inode usati | < 70% | 70 – 85% | > 85% |
| Efficienza (usato/provisionato) | > 40% | 20 – 40% | < 20% (spreco) |
| Latenza scrittura del device | < 5 ms | 5 – 20 ms | > 20 ms |
| Giorni al riempimento | > 30 | 7 – 30 | < 7 |
Vedi anche
- Nodo — filesystem dei nodi e DiskPressure
- Pod / Container — filesystem effimero
- Namespace — storage per namespace
- Control plane — disco di etcd