# initdb a bouffé deux de nos trois réplicas Postgres. Celui qu'on avait mis au rebut nous a sauvés.

Source: https://oomkilled.com/fr/blog/initdb-ate-two-replicas/
Published: 2026-06-21
Tags: postgres, kubernetes, statefulset, incident, data-recovery

Les pods sont revenus en bonne santé. C'était ça, le pire.

On avait fait un rolling restart de routine sur un StatefulSet Postgres — changement de config, rien d'exotique. `postgres-0` et `postgres-1` ont terminé, ont été reschedulés, sont passés au vert. Les readiness probes étaient OK. Et puis l'application s'est mise à cracher des erreurs qui n'avaient aucun sens : les tables n'existaient pas. Pas « permission denied », pas « connection refused ». *N'existaient pas.*

La base était up. Elle était juste vide. Un cluster flambant neuf, fraîchement initialisé, posé sur les volumes qui, trente minutes plus tôt, contenaient les données de production.

> Une base peut être parfaitement saine et parfaitement vide en même temps. Ce ne sont pas des états mutuellement exclusifs — et ta readiness probe te dira avec le sourire que tout va bien.

## Ce qu'on faisait tourner

Un StatefulSet Postgres à trois nœuds — `postgres-0`, `-1`, `-2` — un primary en streaming et deux réplicas, chacun avec son propre PersistentVolumeClaim (`data-postgres-0`, `-1`, `-2`) adossé à des disques locaux aux nœuds. Le pattern standard : identité réseau stable, stockage stable, rollout ordonné.

Quelques semaines plus tôt, on l'avait scalé de 3 à 2. Le troisième nœud, c'était de la marge surprovisionnée qu'on n'utilisait pas, et quelqu'un — moi — voulait récupérer la capacité. `kubectl scale statefulset postgres --replicas=2`. `postgres-2` a été drainé, le pod a disparu, le compte de réplicas est tombé. Propre. Je n'y ai plus repensé.

Cette décision est la seule raison pour laquelle cette histoire finit bien.

## Comment un restart devient une réinitialisation

On faisait tourner le chart Bitnami `postgresql-ha` — Postgres plus **repmgr** pour la réplication et le failover, avec Pgpool en frontal. La partie qui compte vit dans l'entrypoint du conteneur repmgr, dans une fonction appelée `postgresql_repmgr_initialize`. Elle s'exécute à chaque démarrage de pod. *À chaque fois.*

Au démarrage, le conteneur ne se contente pas de lancer Postgres — il détermine *qui il est*. Il interroge le cluster repmgr pour trouver le primary courant, puis il branche. S'il décide qu'il est le primary et que le data directory a l'air non initialisé, il lance un `initdb` tout neuf. S'il décide qu'il est un standby, il lance `repmgr_clone_primary`, qui tente `pg_rewind` et, quand ça échoue, retombe sur `pg_basebackup --force`. Ce `--force` n'a rien de subtil sur ce qu'il fait :

```
NOTICE: -F/--force provided - deleting existing data directory "/bitnami/postgresql/data"
```

Il supprime le data directory avant de cloner. C'est voulu.

Maintenant, empile les défaillances. On a redémarré le StatefulSet, donc les deux pods restants sont tombés quasiment en même temps. Quand ils sont revenus, repmgr n'a pas pu joindre de primary dans son timeout de connexion — et le chemin de fallback, c'est celui qui met fin à des carrières :

```
Can not find new primary ... There are no nodes with primary role. Assuming the primary role
```

Un nœud qui ne trouve pas de primary *part du principe qu'il en est un*. Donc `postgres-0` s'est bootstrappé comme un primary neuf et vide. `postgres-1` est remonté en standby, a tenté `pg_rewind` — qui échoue d'office sur cette image, parce que la config vit dans `/bitnami/postgresql/conf`, pas dans le data directory, donc rewind ne trouve pas `postgresql.conf` — et est tombé tout droit sur `pg_basebackup --force`. Il a **supprimé son propre data directory intact** et a cloné le primary vide.

Deux de nos trois copies de la base ont été détruites par la propre logique de démarrage de la base. La réplication ne nous a pas sauvés — elle a fait son boulot à la perfection, et a répliqué du *vide*.

## La copie à laquelle personne n'a touché

Pendant que je calculais à quel point on était finis, quelqu'un a lancé `kubectl get pvc` et on a tous fixé la sortie :

```
NAME              STATUS   VOLUME      CAPACITY
data-postgres-0   Bound    pvc-a1b2…   200Gi
data-postgres-1   Bound    pvc-c3d4…   200Gi
data-postgres-2   Bound    pvc-e5f6…   200Gi   # ← toujours là
```

`data-postgres-2` était toujours là. Toujours `Bound`. Intact depuis le jour où on avait scalé vers le bas.

Voilà le truc que beaucoup de gens ne découvrent qu'au moment où il les sauve ou les brûle : **scaler un StatefulSet vers le bas ne supprime pas les PVC.** Kubernetes retire le pod et laisse le volume exactement là où il est. (Depuis la 1.27 il y a `persistentVolumeClaimRetentionPolicy` pour changer ça, et même là, `whenScaled` est par défaut sur `Retain`.) On avait dit à Kubernetes de retirer le troisième réplica. Il a discrètement gardé les données du troisième réplica, sur un disque, sur un nœud, pendant des semaines — et cet entrypoint meurtrier ne s'est jamais exécuté contre lui, parce qu'aucun pod ne l'a jamais remonté. Ni `initdb` ni `pg_basebackup --force` n'ont jamais eu l'occasion d'y toucher.

Le réplica que j'avais supprimé contenait la seule copie intacte de la base.

## La récupération

Comme les volumes étaient locaux aux nœuds, les données n'étaient pas dans une API de block storage abstraite — c'était un répertoire sur une machine précise. Ça a rendu la récupération manuelle, et ça l'a rendue *possible*.

Le chemin :

1. **Trouver le volume.** `kubectl get pv pvc-e5f6…` nous a donné le node affinity et le chemin sur disque — les volumes locaux sont épinglés à un nœud et vivent à un emplacement connu de son filesystem.
2. **Accéder à ce nœud** et confirmer que le PGDATA était intact et dans la bonne version majeure. Il l'était. Les fichiers étaient exactement tels que `postgres-2` les avait laissés.
3. **Le remonter en isolation.** On a monté ce PVC dans un pod de récupération jetable — pas membre du StatefulSet, pas de script d'init, juste un Postgres tout simple pointant sur le data directory récupéré. Il a démarré proprement et toutes les données étaient là.
4. **Restaurer vers l'avant.** `pg_dumpall` depuis l'instance récupérée, restauration dans un `postgres-0` reconstruit comme nouveau primary, puis on a laissé `postgres-1` re-cloner à partir de lui via un base backup neuf. Réplication saine, erreurs applicatives disparues.

Des heures, pas des minutes. Mais les données sont revenues entières.

## Ce que j'en ai vraiment retiré

La version propre de cette histoire, c'est « Kubernetes nous a sauvés en retenant un PVC ». La version honnête, c'est « on a survécu par chance, et la chance n'est pas une architecture ».

- **Un restart peut être une réinitialisation.** Toute logique de démarrage qui choisit le rôle d'un nœud puis lance `initdb` ou `pg_basebackup --force` selon ce qu'elle arrive à joindre en quelques secondes est à un hoquet réseau de t'effacer. Le mode de défaillance que tu veux, c'est « refuser de démarrer et réveiller un humain », jamais « partir du principe que je suis primary et repartir de zéro ».
- **Ne redémarre jamais tous les pods d'un coup sur de la HA façon repmgr.** Les restarts simultanés, c'est exactement ce qui déclenche le fallback « je ne trouve pas de primary, je serai le primary ». Roule un pod à la fois, attends qu'il rejoigne et rattrape son retard, *ensuite* touche au suivant. Un `kubectl rollout restart` sur tout le StatefulSet, c'est le chemin direct vers ce désastre.
- **La réplication n'est pas une sauvegarde.** Elle réplique tes erreurs à la vitesse de la lumière. Un primary vide te donne des réplicas vides.
- **Des sauvegardes que tu n'as jamais restaurées ne sont pas des sauvegardes.** On avait des sauvegardes. On ne s'est pas jeté dessus en premier parce qu'on n'avait jamais répété une restauration et qu'on ne faisait pas confiance au timing. Ce qui nous a sauvés, c'est un accident, pas le plan de récupération qu'on était censés avoir.
- **Sache où vivent physiquement tes données.** Les volumes locaux aux nœuds ont rendu cette récupération manuelle mais faisable. Avec du stockage abstrait, on aurait peut-être eu un chemin plus rapide — ou pas de chemin du tout. Dans tous les cas : savoir exactement où sont les octets, c'est ce qui nous a permis d'aller vite sous pression.
- **`persistentVolumeClaimRetentionPolicy` est désormais une vraie décision.** La rétention qui nous a sauvés est aujourd'hui le défaut, mais c'est configurable. Règle-la exprès. Ne laisse pas « qu'est-ce qui arrive aux données quand je scale vers le bas » être une chose que tu découvres pendant un incident.

On a gardé le troisième PVC par accident et il a ramené la base d'entre les morts. Je préférerais que tu gardes le tien exprès — et que tu testes la restauration avant d'en avoir besoin.

---

*Si tu fais tourner ce chart, le comportement de force-clone-au-restart n'est pas un secret — il est partout sur le issue tracker, et ça vaut le coup de le lire avant qu'il ne te lise : [containers#52213 (standby always full-resyncs on restart)](https://github.com/bitnami/containers/issues/52213), [charts#20998 (restarting node ignores the new primary and assumes the role)](https://github.com/bitnami/charts/issues/20998), [charts#14044 (data loss on Postgres HA)](https://github.com/bitnami/charts/issues/14044).*
