Lyon, le 24 mars 2025

CloudNativePG ou comment embarquer un éléphant sur un porte-conteneurs !

Beaucoup de choses vous ont été présentées dans les derniers articles de cette série. Bien qu’assez techniques et pratiques, ils ne présentaient pas vraiment de cas concrets, pouvant être rencontrés dans la vraie vie comme on dit. C’est chose faite avec le présent article qui s’attarde sur un problème bien connu des administrateurs PostgreSQL : la saturation de l’espace disque 💥 !

moteur

Cluster Kubernetes

Cette fois-ci, nous passons dans la cour des grands en utilisant un vrai cluster Kubernetes chez un fournisseur de cloud. Bon … presque la cour des grands, comme on se contentera d’utiliser un cluster managé, mais c’est déjà ça. Une fois déployé, nous installons l’opérateur et créons un cluster PostgreSQL avec un primaire et un secondaire à partir de ce fichier.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.0
  instances: 2
  storage:
    size: 20Gi
  walStorage:
    size: 4Gi
  affinity:
    enablePodAntiAffinity: true 
    topologyKey: kubernetes.io/hostname 
    podAntiAffinityType: preferred

Une bonne pratique dans PostgreSQL souvent recommandée mais peu appliquée est d’avoir au moins deux espaces de stockage bien distincts :

  • Un pour les données ;
  • Un pour les journaux de transactions (WAL).

CloudNativePG respecte cette bonne pratique avec la spécification spec.walStorage. Lorsque le cluster est créé, deux Persistant Volumes distincts seront créés automatiquement. Nous ne rentrerons pas dans les détails, mais sachez qu’il est possible de les configurer plus finement, avec par exemple le choix d’une Storage Class.

Au passage, la version 17.0 de PostgreSQL date de quelques mois et il existe même la version 17.4 depuis quelques semaines 💡. Un article sur la montée de version mineure arrive, c’est pour cela que la version 17.0 est toujours utilisée.

Créons ce cluster. Attendons quelques minutes et voilà nos instances prêtes avec pour chacune d’elles deux Persistent Volumes. Les noms des CLAIMS permettent de savoir à quoi ils correspondent.

$ kubectl get persistentvolume -o custom-columns=NAME:.metadata.name,CAPACITY:.spec.capacity.storage,CLAIM:.spec.claimRef.name
NAME                                       CAPACITY   CLAIM
pvc-09fe198a-ba85-440c-94b4-8de3d32af7dc   10Gi       postgresql-1-wal
pvc-22e4f5c6-f9b0-4bf9-b375-49e7be2e528b   20Gi       postgresql-1
pvc-9e8d0c70-c6b7-4941-86d1-7cc53fd67fbd   20Gi       postgresql-2
pvc-d878ce9c-f85e-4e1b-85a6-f40296a4a318   10Gi       postgresql-2-wal

Saturation de l’espace de stockage

Utilisons l’outil pg_bench pour créer suffisamment de données et saturer l’espace de stockage. Le paramètre --scale 2000 permet de générer une trentaine de Go de données.

$ kubectl cnpg pgbench postgresql -- --initialize --scale 2000
job/postgresql-pgbench-876082 created

En regardant les traces du Pod postgresql-1 (le primaire), nous voyons le déclenchement de nombreux CHECKPOINT. Un peu trop d’ailleurs, PostgreSQL nous donne même une astuce :

"message": "checkpoints are occurring too frequently (15 seconds apart)",
"hint": "Consider increasing the configuration parameter \"max_wal_size\"."

Si vous souhaitez en savoir un peu plus, allez jeter un œil à notre article de blog sur les CHECKPOINTS 📰.

Revenons à nos traces PostgreSQL. Un peu plus loin, nous trouvons le message de mauvais augure No space left on device.

"error_severity": "PANIC",
...
"message": "could not write to file \"pg_wal/xlogtemp.537\": No space left on device",
"context": "writing block 2081579 of relation base/16385/16403\nCOPY pgbench_accounts, line 127099000",

C’est à ce moment là que votre outil de surveillance vous remonte l’alerte CRITICAL - Plus de place ! !

Plongeons dans les traces

Ne paniquons pas, regardons ce que nous disent les traces et essayons de comprendre ce qu’il se passe.

La copie de données par pgbench ne peut plus se faire comme l’espace disque est saturé. Le backend associé est arrêté :

"message": "server process (PID 537) was terminated by signal 6: Aborted",
"detail": "Failed process was running: copy pgbench_accounts from stdin with (freeze on)",
"backend_type": "postmaster",

À la suite de quoi, l’instance est arrêtée :

"message": "shutting down because \"restart_after_crash\" is off",
"backend_type": "postmaster",

Le paramètre restart_after_crash étant à off, l’instance PostgreSQL ne va pas redémarrer automatiquement. Cela laisse la possibilité à l’opérateur de gérer convenablement l’instance. Ce paramètre est fixé par l’opérateur, il n’est pas possible de le changer. Il fait partie des Fixed Parameters imposés par CloudNativePG.

L’instance-manager donne également quelques indications :

{
  "level": "info",
  "ts": "2025-03-11T07:44:45.138680326Z",
  "msg": "Checking for free disk space for WALs after PostgreSQL finished",
  "logger": "instance-manager",
  "logging_pod": "postgresql-1"
}
{
  "level": "info",
  "ts": "2025-03-11T07:44:45.157419701Z",
  "msg": "Detected low-disk space condition",
  "logger": "instance-manager",
  "logging_pod": "postgresql-1"
}

L’opérateur va tenter de redémarrer lui-même l’instance. Avant de faire cela, il vérifie s’il reste de l’espace disque pour les journaux de transactions.

{
  "level": "info",
  "ts": "2025-03-11T07:44:45.202130923Z",
  "msg": "Checking for free disk space for WALs before starting PostgreSQL",
  "logger": "instance-manager",
  "logging_pod": "postgresql-1"
}

Comme ce n’est pas le cas, l’instance ne sera pas redémarrée.

{
  "level": "info",
  "ts": "2025-03-11T07:44:45.222583176Z",
  "msg": "Detected low-disk space condition, avoid starting the instance",
  "logger": "instance-manager",
  "logging_pod": "postgresql-1"
}

Nous nous retrouvons donc avec une instance primaire arrêtée car son espace disque dédié aux journaux de transactions est saturé 😨. L’instance secondaire attend quant à elle toujours un journal de transactions "message": "waiting for WAL to become available at 2/5283F7F3".

Notez donc qu’en cas de saturation du volume de stockage, l’opérateur ne déclenche pas de bascule sur le secondaire. Avant toute chose, il faut résoudre ce problème d’espace disque.

Agrandissement du volume

Pour revenir à une situation normale, nous devons faire deux choses. La première est d’agrandir l’espace défini dans le PVC associé au volume des journaux de transactions de l’instance postgresql-1. La seconde est de faire correspondre le paramètre spec.walStorage.size de la définition du cluster PostgreSQL à cette nouvelle taille.

Modifier la taille d’un PVC à chaud n’est pas forcément possible ! Il est nécessaire que la Storage Class utilisée le permette via la fonctionnalité de Volume Expansion. Par défaut, la Storage Class utilisée est sbs-default. C’est une classe de stockage fournie par le CSI du fournisseur cloud. En regardant ses informations, nous voyons qu’elle implémente bien cette capacité :

$ kubectl describe sc sbs-default | grep AllowVolumeExpansion
AllowVolumeExpansion:  True

Comprenez donc qu’il s’agit d’un critère essentiel à prendre en compte lors du choix du CSI ⚠️.

La modification du paramètre spec.resources.requests.storage du PVC postgresql-1-wal est donc possible à chaud. 1 Go de plus devrait suffire. Il ne reste plus qu’à modifier la valeur de spec.walStorage.size à 5 Go et de réappliquer la définition de notre cluster.

Le Pod postgresql-1 va redémarrer et, contrairement à tout à l’heure, la vérification de l’espace disque ne bloquera plus. L’instance-manager va être démarré suivi par tout le processus de redémarrage de l’instance PostgreSQL jusqu’au fameux message de trace "database system is ready to accept connection". Durant tout le procédé, l’instance postgresql-1 a gardé son rôle de primaire, et l’instance postgresql-2 son rôle de secondaire. Nous nous retrouvons alors dans la situation initiale.

Conclusion

Si vous recevez une alerte CRITICAL sur un volume dédié aux WALs, vous savez maintenant de quoi il en retourne. Vous serez très probablement confrontés à la saturation de l’un ou l’autre de vos Persistant Volumes. Préparez-vous à cela et ayez en tête les différentes étapes à suivre pour résoudre ce problème.

Avant de déployer des instances dans Kubernetes, prenez le temps de bien choisir le CSI que vous utiliserez notamment sur les fonctionnalités qu’il possède. La fonctionnalité de Volume Expansion est un must have !

Des questions, des commentaires ? Écrivez-nous !


DALIBO

DALIBO est le spécialiste français de PostgreSQL®. Nous proposons du support, de la formation et du conseil depuis 2005.