Skip to content

Volumi

Tre superfici distinte, spesso confuse:

SuperficieMetricheChi le espone
PVC — volumi persistenti montati nei podkubelet_volume_stats_*kubelet
Filesystem effimero — layer scrivibile e emptyDircontainer_fs_*cAdvisor
Filesystem del nodo — /, /var/lib/containersnode_filesystem_*node exporter

Un PVC pieno blocca un’applicazione. Un /var/lib/containers pieno blocca tutti i pod del nodo.

Triagei volumi da guardare per primi
Riempimento PVC · Proiezioni
Inodeil caso che si dimentica
Stato e provisioningPending, 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
)
) > 85

L’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 < 7
and
deriv(kubelet_volume_stats_used_bytes[6h:30m]) > 0

Il secondo predicato elimina i volumi in decrescita, che altrimenti darebbero valori negativi grandi.

T3. PVC Pending

# elenco: ogni serie restituita soddisfa la condizione
kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1

Un 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)
) > 80

T5. 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"}
)
) > 80

Riempimento 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-.*"}
)
) > 80

Spazio residuo assoluto

# GiB
topk(20, kubelet_volume_stats_available_bytes) / 1024^3

La 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:

# byte
kubelet_volume_stats_available_bytes < 5 * 1024^3

Spazio usato

# GiB
topk(20, kubelet_volume_stats_used_bytes) / 1024^3

Capacità provisionata

# GiB
topk(20, kubelet_volume_stats_capacity_bytes) / 1024^3

Efficienza — quanto del provisionato è usato

# byte
kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes

Sotto 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-100
100 * (
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

# MiB
topk(20,
kubelet_volume_stats_used_bytes - kubelet_volume_stats_used_bytes offset 24h
) / 1024^2

MiB cresciuti in un giorno. Utile per capacity planning e per identificare chi ha iniziato a crescere dopo un rilascio.

Crescita settimanale

# GiB
topk(20,
kubelet_volume_stats_used_bytes - kubelet_volume_stats_used_bytes offset 7d
) / 1024^3

Proiezioni

Giorni al riempimento

# giorni
(
kubelet_volume_stats_available_bytes
/
deriv(kubelet_volume_stats_used_bytes[6h:30m])
) / 86400

Giorni 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 scadenza
predict_linear(kubelet_volume_stats_available_bytes[6h], 7 * 24 * 3600) < 0

PVC 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 scadenza
predict_linear(kubelet_volume_stats_available_bytes[7d], 30 * 24 * 3600) < 0

Tasso di crescita in MiB/h

# MiB all'ora
topk(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 inode
topk(20, kubelet_volume_stats_inodes_used)
# numero di inode
kubelet_volume_stats_inodes_free < 100000

Un 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

# byte
kubelet_volume_stats_used_bytes / kubelet_volume_stats_inodes_used

Sotto 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 condizione
sum by (namespace, phase) (kube_persistentvolumeclaim_status_phase == 1)

PVC Pending

# elenco: ogni serie restituita soddisfa la condizione
kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1

Cause, in ordine di frequenza:

CausaVerifica
StorageClass inesistente o non defaultoc get sc
CSI driver degradatooc get csidrivers, oc get co storage
Quota del namespace superataoc describe resourcequota -n $ns
Nessun PV disponibile per binding staticooc get pv
volumeBindingMode: WaitForFirstConsumer e pod non schedulabilenormale: il PVC si lega quando il pod trova un nodo
Terminal window
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 condizione
kube_persistentvolumeclaim_status_phase{phase="Lost"} == 1

Il 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 condizione
sum by (phase) (kube_persistentvolume_status_phase == 1)
# elenco: ogni serie restituita soddisfa la condizione
kube_persistentvolume_status_phase{phase=~"Failed|Pending"} == 1

PV rilasciati e mai riciclati

# elenco: ogni serie restituita soddisfa la condizione
kube_persistentvolume_status_phase{phase="Released"} == 1
# numero di serie
count(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.

Terminal window
oc get pv --sort-by=.status.phase | grep Released
oc 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

# GiB
sum by (storageclass) (
kube_persistentvolumeclaim_resource_requests_storage_bytes
* on(namespace, persistentvolumeclaim) group_left(storageclass)
max by (namespace, persistentvolumeclaim, storageclass) (kube_persistentvolumeclaim_info)
) / 1024^3

GiB 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 serie
count by (storageclass) (kube_persistentvolumeclaim_info)

Access mode

# numero di serie
count 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.

Terminal window
oc get events -A --field-selector reason=FailedAttachVolume --sort-by=.lastTimestamp | tail

Volumi montati per pod

# numero di serie
count 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 secondo
topk(20,
sum by (namespace, pod) (
rate(container_fs_writes_total{namespace!~"kube-.*|openshift-.*"}[5m])
)
)
# operazioni al secondo
topk(20,
sum by (namespace, pod) (
rate(container_fs_reads_total{namespace!~"kube-.*|openshift-.*"}[5m])
)
)
# MiB
topk(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^2

MiB/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

# IOPS
topk(10, sum by (instance, device) (rate(node_disk_reads_completed_total[5m])))
# IOPS
topk(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 coda
100 * rate(node_disk_io_time_seconds_total[5m])

Vicino a 1 = disco saturo.

# percentuale 0-100 di tempo con I/O in coda
100 * 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 secondo
rate(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

# MiB
topk(20,
sum by (namespace, pod) (
container_fs_usage_bytes{
namespace!~"kube-.*|openshift-.*",
container!="", container!="POD"
}
)
) / 1024^2

Per nodo

# GiB
sum by (node) (
container_fs_usage_bytes{container!="", container!="POD"}
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)
) / 1024^3

Container senza limite di ephemeral-storage

# numero di serie
count by (namespace, pod, container) (
kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*"}
)
unless
count 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 condizione
kube_node_status_condition{condition="DiskPressure", status="true"} == 1

Pod evictati

# elenco: ogni serie restituita soddisfa la condizione
kube_pod_status_reason{reason="Evicted"} == 1
# numero di serie
count by (namespace) (kube_pod_status_reason{reason="Evicted"} == 1)
Terminal window
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 ephemeral

StorageClass e CSI

StorageClass disponibili

# sempre 1: metrica portatrice di label (provisioner, reclaim_policy)
kube_storageclass_info

I label provisioner, reclaim_policy, volume_binding_mode.

Terminal window
oc get sc
oc 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

Terminal window
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 condizione
kube_pod_container_status_ready{namespace=~"openshift-.*storage.*|openshift-cluster-csi-drivers"} == 0
# elenco: ogni serie restituita soddisfa la condizione
cluster_operator_conditions{name="storage", condition="Degraded"} == 1
Terminal window
oc get co storage
oc get csidrivers
oc get pods -n openshift-cluster-csi-drivers

Operazioni CSI in errore

# operazioni al secondo
sum by (driver_name, method_name, grpc_status_code) (
rate(csi_sidecar_operations_seconds_count{grpc_status_code!="OK"}[5m])
) > 0

I nomi delle metriche CSI variano con il driver. Verifica cosa esiste:

# numero di serie
count by (__name__) ({__name__=~"csi_.*|storage_operation.*"})

Latenza delle operazioni sui volumi

# secondi
histogram_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

Terminal window
# Verificare che la StorageClass lo permetta
oc get sc $sc -o jsonpath='{.allowVolumeExpansion}{"\n"}'
# Espandere
oc patch pvc $pvc -n $ns -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'
# Seguire lo stato
oc get pvc $pvc -n $ns -o jsonpath='{.status.conditions}' | jq

Molti 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 $ns
kubelet_volume_stats_capacity_bytes{namespace="$ns", persistentvolumeclaim="$pvc"} / 1024^3

Se questa non cresce dopo il patch, il pod va riavviato.


Comandi oc

Terminal window
# PVC di un namespace
oc get pvc -n $ns
oc describe pvc -n $ns $pvc
# Tutti i PVC per dimensione
oc 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 orfani
oc get pv --sort-by=.status.phase
oc get pv -o json | jq -r '.items[] | select(.status.phase=="Released") | .metadata.name'
# Uso reale dentro il pod
oc rsh -n $ns $pod df -h
oc rsh -n $ns $pod df -i
# Le directory più grandi nel volume
oc rsh -n $ns $pod du -xh --max-depth=2 /path/del/mount 2>/dev/null | sort -rh | head -20
# I file più grandi
oc 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 PVC
oc get pods -n $ns -o json | jq -r --arg pvc "$pvc" '
.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName == $pvc) | .metadata.name'
# Disco e inode del nodo
oc debug node/$node -- chroot /host df -hT
oc debug node/$node -- chroot /host df -hi
oc debug node/$node -- chroot /host du -xh --max-depth=1 /var/lib/containers | sort -rh | head
# Errori di attach
oc get events -A --field-selector reason=FailedAttachVolume --sort-by=.lastTimestamp | tail
oc get events -A --field-selector reason=FailedMount --sort-by=.lastTimestamp | tail

Alert

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: storage-health-custom
namespace: openshift-monitoring
labels:
role: alert-rules
spec:
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:

Terminal window
oc -n openshift-monitoring get prometheusrule -o yaml | grep -E "alert: KubePersistentVolume"

Soglie di riferimento

MetricaSanoAttenzioneCritico
Riempimento PVC< 70%70 – 85%> 85%
Spazio residuo assoluto> 20 GiB5 – 20 GiB< 5 GiB
Inode usati< 70%70 – 85%> 85%
Efficienza (usato/provisionato)> 40%20 – 40%< 20% (spreco)
Latenza scrittura del device< 5 ms5 – 20 ms> 20 ms
Giorni al riempimento> 307 – 30< 7

Vedi anche