Les bases de données conteneurisées sont une merveille de productivité jusqu’au jour où vous avez besoin d’une restauration. Là, vous découvrez que vous avez, pendant des mois, envoyé avec soin vers un stockage d’objets une déception gzippée. Elle a un nom de fichier, un checksum, une politique de rétention, et absolument aucune capacité à remettre votre service en marche.
Les « fausses sauvegardes » sont des sauvegardes qui se terminent avec succès mais ne peuvent pas restaurer vers un état de base de données cohérent et fonctionnel dans le RPO/RTO que vous avez promis à l’entreprise (ou à vous-même à 2 h du matin). Dans les conteneurs, les modes de défaillance se multiplient : systèmes de fichiers éphémères, montages de volumes oubliés, WAL/binlogs manquants et snapshots pris au pire moment possible.
À quoi ressemble une fausse sauvegarde (et pourquoi les conteneurs aggravent le problème)
Une fausse sauvegarde est tout ce qui vous donne de la confiance sans vous donner la capacité de récupération. Les coupables habituels :
- Des sauvegardes qui « réussissent » mais dont la restauration échoue (permissions, rôles manquants, extensions absentes, archive corrompue, versions incompatibles).
- Des sauvegardes qui se restaurent mais dont les données sont erronées (snapshot incohérent, dump partiel, binlogs/WAL manquants, surprises liées aux fuseaux horaires/encodages).
- Des sauvegardes qui se restaurent mais prennent 12 heures alors que votre RTO était « moins d’une heure ». La sauvegarde existe ; votre travail n’existe plus.
- Des sauvegardes stockées à l’intérieur du système de fichiers du conteneur, qui disparaît lors des reschedulings, drains de nœud ou rafraîchissements d’image.
- Des sauvegardes qui omettent la seule chose dont vous aviez besoin : permissions d’objet, routines, triggers, utilisateurs, grants, configuration, ou le flux WAL/binlog nécessaire à une restauration point-in-time.
Les conteneurs amplifient ces problèmes parce qu’ils rendent « où vit la donnée » trompeusement ambigu. Vous pouvez exécuter mysqldump ou pg_dump depuis un cron dans le conteneur, voir apparaître un fichier sous /backups, et vous sentir bien. Mais si /backups n’est pas un volume persistant, vous venez de créer une affiche motivationnelle.
Ensuite, les plateformes de conteneurs encouragent la pensée stateless. Super pour les serveurs d’applications. Dangereux pour les bases de données. La plateforme redémarrera volontiers votre pod de base de données pendant que vous prenez un snapshot du système de fichiers, à moins que vous n’ayez fixé des garde-fous. Elle fera aussi tourner les secrets et cassera votre job de sauvegarde tout en signalant le CronJob comme « Completed ».
Votre règle : une sauvegarde n’est « réelle » que si vous pouvez la restaurer, vérifier sa correction, et le faire dans le délai promis, en utilisant une automatisation fiable.
Blague #1 : Une sauvegarde jamais restaurée est comme un parachute acheté en solde et jamais déplié. Devinez comment se passe le premier test.
MySQL vs PostgreSQL : ce que signifie réellement « sauvegarde cohérente »
La cohérence est un contrat, pas un fichier
La cohérence concerne la justesse transactionnelle. MySQL (InnoDB) et PostgreSQL peuvent fournir des sauvegardes cohérentes, mais ils le font différemment et sanctionnent des erreurs différentes.
MySQL : InnoDB, redo logs et binlogs
MySQL comporte deux grands mondes : les sauvegardes logiques (mysqldump, mysqlpump) et les sauvegardes physiques (Percona XtraBackup, MySQL Enterprise Backup). Les sauvegardes logiques sont portables et lisibles ; les sauvegardes physiques sont plus rapides et mieux adaptées aux grosses bases, mais plus sensibles aux incompatibilités de version et de configuration.
Pour la récupération point-in-time (PITR), MySQL s’appuie sur le binary log (binlog). Une sauvegarde complète sans binlogs est souvent une restauration « de la nuit dernière », pas « d’il y a cinq minutes ». Si votre entreprise attend « d’il y a cinq minutes », vous avez besoin que les binlogs soient expédiés et conservés avec un mappage connu vers la sauvegarde.
Patron courant de fausse sauvegarde dans les conteneurs MySQL : vous lancez mysqldump mais oubliez --single-transaction pour InnoDB, ou vous dump depuis une réplica avec un délai de réplication, ou vous n’incluez pas routines/events/triggers, ou vous ne capturez pas les utilisateurs/grants. Cela se restaure, mais l’application plante.
PostgreSQL : MVCC, base backups et WAL
Le monde PostgreSQL est MVCC et write-ahead logging (WAL). Une sauvegarde physique cohérente est une base backup plus les segments WAL nécessaires pour la ramener à un point cohérent (et optionnellement avancer dans le temps). Les sauvegardes logiques (pg_dump) sont cohérentes au niveau de la base de données, mais elles sont plus lentes sur de grands jeux de données et ne capturent pas les objets au niveau du cluster à moins d’ajouter pg_dumpall pour les rôles et globals.
Patron courant de fausse sauvegarde dans les conteneurs PostgreSQL : vous prenez un snapshot du répertoire de données sans vous coordonner avec PostgreSQL, n’incluez pas le WAL, et vous vous retrouvez avec une sauvegarde qui échoue avec « invalid checkpoint record » quand vous tentez de la démarrer. Autre cas : vous utilisez pg_dump mais oubliez rôles, extensions ou hypothèses sur le search_path du schéma.
Conclusion opérationnelle
- Si vous avez besoin de restaurations rapides et de très grandes bases : privilégiez les sauvegardes physiques (XtraBackup /
pg_basebackup) plus les journaux PITR. - Si vous avez besoin de portabilité et que les jeux de données sont plus petits : les sauvegardes logiques conviennent, mais vous devez inclure l’ensemble des objets nécessaires et tester les restaurations.
- Dans les conteneurs : montez correctement les volumes persistants, stockez les logs (WAL/binlog) hors des couches éphémères, et traitez les jobs de sauvegarde comme des systèmes de production.
Une citation opérationnelle qui vieillit bien : « L’espoir n’est pas une stratégie. » — Gene Kranz
Faits et contexte historique intéressants (parce que le passé explique vos pages d’alerte)
- La lignée de PostgreSQL remonte au projet POSTGRES de l’UC Berkeley dans les années 1980 ; WAL et MVCC se sont mûris à mesure que le projet devenait une base axée sur la fiabilité.
- La popularité initiale de MySQL venait de sa vitesse et de sa simplicité dans les stacks web, mais la fiabilité transactionnelle pour des charges sérieuses est arrivée lorsque InnoDB est devenu le moteur par défaut plusieurs années plus tard.
- Le WAL (write-ahead logging) est plus ancien que MySQL et PostgreSQL ; le principe date de la recherche sur les bases de données bien avant que les conteneurs ne rendent tout jetable.
- La réplication MySQL s’appuyait historiquement sur des binlogs basés sur des instructions ; aujourd’hui le logging en ligne (row-based) est courant car il évite des problèmes subtils de relecture non déterministe.
- Le PITR PostgreSQL est devenu courant à mesure que les outils d’archivage WAL ont mûri ; sans rétention WAL, « sauvegarde » signifie « capsule temporelle », pas « récupération ».
- Les snapshots de système de fichiers sont devenus un élément pratique de sauvegarde à mesure que les systèmes copy-on-write et les baies de stockage se sont améliorés ; ils sont puissants mais faciles à mal utiliser sans coordination base de données.
- Percona XtraBackup a gagné en traction car il permettait des sauvegardes physiques à chaud pour InnoDB sans arrêter MySQL, changeant l’économie des sauvegardes de grands jeux de données.
- L’orchestration de conteneurs a normalisé les images immuables et les nœuds éphémères ; le décalage avec les bases de données stateful a créé une décennie de leçons difficiles sur « stateful sur Kubernetes ».
- Les checksums et les « jobs réussis » ont toujours menti par omission : le succès de la sauvegarde est un événement I/O, le succès de la restauration est un événement de correction. Les opérations l’ont appris bruyamment.
Types de sauvegarde adaptés aux conteneurs (logique, physique, snapshots et PITR)
Sauvegardes logiques : portables, plus lentes, faciles à falsifier
MySQL : mysqldump convient si vous l’utilisez correctement et si votre jeu de données n’est pas massif. Vous avez besoin de --single-transaction pour la cohérence InnoDB sans verrouiller les tables, et vous voudrez probablement --routines --triggers --events. Mais les dumps logiques peuvent toujours être faux : incomplets, tronqués silencieusement, ou restaurés dans un schéma qui ne correspond pas à la production.
PostgreSQL : pg_dump génère des dumps cohérents grâce à MVCC, mais c’est par base de données. Les objets au niveau du cluster (rôles, tablespaces, certains réglages) sont séparés. Si vous restaurez dans un cluster propre sans recréer les rôles ou extensions, votre application casse de façon inventive.
Sauvegardes physiques : rapides, strictes opérationnellement
MySQL : les sauvegardes physiques avec XtraBackup (ou équivalent entreprise) sont le choix mature pour les gros jeux de données. Mais vous devez gérer les redo logs, préparer la sauvegarde, et garder la compatibilité de version à l’esprit.
PostgreSQL : pg_basebackup plus l’archivage WAL vous donne des sauvegardes physiques cohérentes et restaurables. C’est simple, mais vous devez garantir l’archivage et la rétention des WAL, et disposer d’une procédure de restauration propre (y compris la configuration de recovery ou des commandes de restauration modernes).
Snapshots : la sauvegarde la plus « rapide » qui aime vous trahir
Les snapshots de volume (LVM, ZFS, Ceph, EBS, snapshots CSI) sont excellents quand vous pouvez garantir la cohérence. Le mot clé est garantir. Un snapshot crash-consistent peut fonctionner, jusqu’au moment où il ne fonctionne plus, et vous ne saurez pas quelle sauvegarde est mauvaise jusqu’au besoin.
Pour faire des snapshots correctement, vous vous coordonnez avec la base de données : flush/verrouillage ou modes de sauvegarde qui assurent la récupérabilité. PostgreSQL peut récupérer d’un crash, mais un snapshot pris en pleine écriture sans la bonne couverture WAL peut toujours échouer. InnoDB de MySQL peut récupérer d’un crash, mais mélanger snapshots avec des logs manquants ou des réglages fsync étranges devient rapidement moche.
PITR : la différence entre « on a perdu une journée » et « on a perdu cinq minutes »
Le PITR n’est pas optionnel si votre RPO est petit. Pour MySQL : les binlogs. Pour PostgreSQL : l’archivage WAL. Dans les conteneurs, le PITR casse quand les répertoires de logs ne sont pas persistés, l’archivage est mal configuré, ou votre politique de rétention supprime les logs avant que vous ne vous en rendiez compte.
Blague #2 : Le PITR, c’est comme une machine à remonter le temps, sauf qu’elle ne va que vers l’arrière—généralement jusqu’au moment exact avant que vous n’ayez exécuté la migration destructive. Pratique.
Tâches pratiques : 12+ commandes pour vérifier que les sauvegardes sont réelles
Ce sont des tâches que vous pouvez lancer aujourd’hui. Chacune inclut : commande, sortie exemple, ce que ça signifie, et la décision à prendre.
Tâche 1 : Confirmer que le répertoire de données de la base est bien sur un montage persistant (hôte du conteneur)
cr0x@server:~$ docker inspect mysql01 --format '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }} ({{ .Type }}){{ "\n" }}{{ end }}'
/var/lib/docker/volumes/mysql01-data/_data -> /var/lib/mysql (volume)
/srv/backups/mysql01 -> /backups (bind)
Signification : /var/lib/mysql est un volume Docker (persistant), et les sauvegardes sont écrites sur un bind mount hôte à /srv/backups/mysql01.
Décision : Si vous ne voyez pas un montage persistant pour le répertoire de données et le répertoire de sauvegarde, arrêtez. Corrigez le stockage avant de débattre des outils.
Tâche 2 : Kubernetes : vérifier que le pod utilise un PVC (et non emptyDir)
cr0x@server:~$ kubectl get pod pg-0 -o jsonpath='{range .spec.volumes[*]}{.name}{"\t"}{.persistentVolumeClaim.claimName}{"\t"}{.emptyDir}{"\n"}{end}'
data pgdata-pg-0
tmp map[]
Signification : Le volume data est soutenu par un PVC. tmp est éphémère (emptyDir).
Décision : Si vos données DB sont sur emptyDir, vous n’avez pas de persistance ; vous avez des vibes.
Tâche 3 : Sauvegarde logique MySQL bien faite (et détection des dumps tronqués)
cr0x@server:~$ docker exec mysql01 sh -lc 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines --triggers --events --set-gtid-purged=OFF --databases appdb | gzip -1 > /backups/appdb.sql.gz && ls -lh /backups/appdb.sql.gz'
-rw-r--r-- 1 root root 412M Dec 31 02:10 /backups/appdb.sql.gz
Signification : Un dump existe et a une taille plausible.
Décision : La taille n’est pas une preuve. Validez ensuite l’intégrité gzip et la présence des objets requis.
Tâche 4 : Valider l’intégrité gzip (détecte uploads partiels et disques pleins)
cr0x@server:~$ gzip -t /srv/backups/mysql01/appdb.sql.gz && echo OK
OK
Signification : Le fichier est un flux gzip valide (non tronqué).
Décision : Si cela échoue, considérez la sauvegarde comme manquante. Examinez l’espace disque, les logs du job et les étapes d’upload.
Tâche 5 : Contrôle rapide à l’intérieur d’un dump MySQL (schéma et données présents ?)
cr0x@server:~$ zcat /srv/backups/mysql01/appdb.sql.gz | head -n 25
-- MySQL dump 10.13 Distrib 8.0.36, for Linux (x86_64)
--
-- Host: localhost Database: appdb
-- ------------------------------------------------------
-- Server version 8.0.36
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
...
Signification : On dirait un vrai dump avec des métadonnées. Confirme aussi la version MySQL au moment de la sauvegarde.
Décision : Si l’en-tête du dump manque ou si le flux est vide, votre job de sauvegarde a écrit une page d’erreur, pas du SQL.
Tâche 6 : MySQL : confirmer que le binlog est activé (PITR en dépend)
cr0x@server:~$ docker exec mysql01 sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES LIKE \"log_bin\"; SHOW VARIABLES LIKE \"binlog_format\";"'
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin | ON |
+---------------+-------+
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+
Signification : Le binlog est activé, et le format est ROW (généralement le plus sûr pour la réplication et le PITR).
Décision : Si log_bin est OFF, acceptez un RPO « uniquement sauvegardes complètes » ou activez-le et mettez en place l’envoi des binlogs.
Tâche 7 : MySQL : vérifier que vous avez bien des binlogs récents (pas seulement activé)
cr0x@server:~$ docker exec mysql01 sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW BINARY LOGS;" | tail -n 5'
| binlog.000231 | 10485760 |
| binlog.000232 | 8912451 |
Signification : Des binlogs existent et tournent.
Décision : S’il n’y a pas de binlogs ou s’ils se sont arrêtés il y a des jours, le PITR n’est pas réel. Corrigez la rétention et l’envoi.
Tâche 8 : PostgreSQL : faire une sauvegarde logique et capturer les globals (rôles) séparément
cr0x@server:~$ kubectl exec -it pg-0 -- bash -lc 'pg_dump -U postgres -Fc -d appdb -f /backups/appdb.dump && pg_dumpall -U postgres --globals-only > /backups/globals.sql && ls -lh /backups/appdb.dump /backups/globals.sql'
-rw-r--r-- 1 root root 2.1G Dec 31 02:12 /backups/appdb.dump
-rw-r--r-- 1 root root 19K Dec 31 02:12 /backups/globals.sql
Signification : Vous avez un dump en format custom plus les objets globaux.
Décision : Si vous ne dumpiez que la base et ignorez les globals, attendez-vous à des échecs de permissions et rôles manquants au moment de la restauration.
Tâche 9 : PostgreSQL : vérifier que l’archivage WAL est activé (le PITR en dépend)
cr0x@server:~$ kubectl exec -it pg-0 -- bash -lc 'psql -U postgres -d postgres -c "SHOW wal_level; SHOW archive_mode; SHOW archive_command;"'
wal_level
-----------
replica
(1 row)
archive_mode
--------------
on
(1 row)
archive_command
-------------------------------------------------
test ! -f /wal-archive/%f && cp %p /wal-archive/%f
(1 row)
Signification : Le WAL est produit au niveau replica (bien pour PITR/réplication). L’archivage est activé.
Décision : Si archive_mode est off, vous n’avez pas de PITR. Acceptez le risque ou corrigez-le avant de revendiquer un RPO.
Tâche 10 : PostgreSQL : confirmer que le WAL est bien archivé (et pas seulement configuré)
cr0x@server:~$ kubectl exec -it pg-0 -- bash -lc 'psql -U postgres -d postgres -c "SELECT now(), last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time FROM pg_stat_archiver;"'
now | last_archived_wal | last_archived_time | failed_count | last_failed_wal | last_failed_time
-------------------------------+-------------------+----------------------------+--------------+-----------------+------------------
2025-12-31 02:14:01.12345+00 | 0000000100000000000000A7 | 2025-12-31 02:13:58+00 | 0 | |
(1 row)
Signification : L’archivage réussit et est récent.
Décision : Si failed_count augmente ou si last_archived_time est ancien, votre flux PITR est cassé. Appelez quelqu’un avant d’en avoir besoin.
Tâche 11 : Tester la restauration d’un dump MySQL dans un conteneur jetable (le seul test qui compte)
cr0x@server:~$ docker run --rm --name mysql-restore -e MYSQL_ROOT_PASSWORD=restorepass -d mysql:8.0
...output...
cr0x@server:~$ sleep 15 && zcat /srv/backups/mysql01/appdb.sql.gz | docker exec -i mysql-restore mysql -uroot -prestorepass
...output...
cr0x@server:~$ docker exec mysql-restore mysql -uroot -prestorepass -e "SHOW DATABASES; USE appdb; SHOW TABLES;" | tail -n 10
Tables_in_appdb
users
orders
order_items
Signification : Le dump s’importe et le schéma existe.
Décision : Si l’import échoue, la sauvegarde est fausse. Si elle s’importe mais que les requêtes applicatives échouent, il vous manque des objets (routines, triggers) ou vous dépendez de réglages spécifiques à l’environnement.
Tâche 12 : Tester la restauration d’un dump PostgreSQL dans une instance jetable
cr0x@server:~$ docker run --rm --name pg-restore -e POSTGRES_PASSWORD=restorepass -d postgres:16
...output...
cr0x@server:~$ sleep 10 && cat /srv/backups/pg/globals.sql | docker exec -i pg-restore psql -U postgres
...output...
cr0x@server:~$ docker exec -i pg-restore createdb -U postgres appdb
...output...
cr0x@server:~$ docker exec -i pg-restore pg_restore -U postgres -d appdb /srv/backups/pg/appdb.dump
pg_restore: connecting to database for restore
pg_restore: creating TABLE "public.users"
pg_restore: creating TABLE "public.orders"
...output...
Signification : Le dump se restaure proprement dans un cluster frais et rejoue la création d’objets.
Décision : Si ça échoue sur les rôles/permissions, votre capture des globals est incomplète. Si ça échoue sur des extensions, vous devez les préinstaller dans l’environnement de restauration.
Tâche 13 : Vérification de sécurité des snapshots : sauvegardez-vous le bon chemin ?
cr0x@server:~$ docker exec mysql01 sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES LIKE \"datadir\";"'
+---------------+----------------+
| Variable_name | Value |
+---------------+----------------+
| datadir | /var/lib/mysql/ |
+---------------+----------------+
Signification : Confirme le répertoire de données réel.
Décision : Si votre snapshot est pris depuis un chemin hôte différent du datadir monté, vous snapshottez la mauvaise chose. Oui, ça arrive.
Tâche 14 : Vérification de capacité basique : les sauvegardes échouent-elles à cause de la pression disque ?
cr0x@server:~$ df -h /srv/backups
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 1.8T 1.7T 62G 97% /srv/backups
Signification : Vous êtes à 97 % d’utilisation. Ce n’est pas un choix de vie.
Décision : Si vous dépassez régulièrement ~85–90 %, attendez-vous à des fichiers partiels, des fsync échoués, et des logs « sauvegarde réussie » qui ne correspondent pas à la réalité. Corrigez la rétention, la compression et la taille du stockage.
Tâche 15 : Confirmer que les jobs de sauvegarde ne sont pas « réussis » alors qu’ils échouent réellement (CronJob Kubernetes)
cr0x@server:~$ kubectl get job -n db -l app=pg-backup -o wide
NAME COMPLETIONS DURATION AGE CONTAINERS IMAGES SELECTOR
pg-backup-28911 1/1 9s 2h backup alpine:3.20 controller-uid=...
cr0x@server:~$ kubectl logs -n db job/pg-backup-28911 | tail -n 20
pg_dump: error: connection to server at "pg" (10.0.2.44), port 5432 failed: FATAL: password authentication failed for user "postgres"
Signification : Le job est terminé du point de vue de Kubernetes, mais la sauvegarde a échoué.
Décision : Traitez les logs comme source de vérité. Assurez-vous que le job renvoie un code de sortie non nul en cas d’échec (pas d’erreur engloutie dans des scripts shell).
Mode opératoire pour diagnostic rapide : trouver rapidement le goulot d’étranglement
Quand les sauvegardes sont lentes, manquantes ou non restaurables, vous n’avez pas le temps pour la philosophie. Vous avez besoin d’une boucle serrée. Voici l’ordre qui attrape le plus de problèmes le plus vite.
Premier : l’artéfact de sauvegarde est-il valide ?
- Vérifier l’intégrité : test gzip, listing tar, ou inspection du format de dump.
- Vérifier la tendance de taille : une chute ou un pic soudain signifie généralement une erreur ou une croissance sauvage des données.
- Vérifier la date du dernier test de restauration réussi : si vous n’en avez pas, vous ne savez rien.
Second : le pipeline inclut-il les logs nécessaires pour le PITR ?
- MySQL : binlog activé, en rotation, expédié, conservé.
- PostgreSQL : archivage WAL activé,
pg_stat_archiversain, rétention couvre la fenêtre désirée.
Troisième : le goulot est-il CPU, disque ou réseau ?
- CPU-bound : niveau de compression trop élevé, overhead chiffrement, dumps mono-thread.
- Disk-bound : PVC lent, disque du nœud saturé, délais de copie de snapshot.
- Network-bound : débit du stockage d’objets, throttling, ou limites d’egress inter-AZ.
Quatrième : prenez-vous des sauvegardes cohérentes ?
- MySQL :
--single-transactionpour les dumps ; sauvegarde physique préparée ; coordination pour les snapshots. - PostgreSQL : base backup + WAL ; ou
pg_dumpplus globals ; coordination pour les snapshots.
Cinquième : la restauration respecte-t-elle le RTO ?
- Testez le temps de restauration bout en bout : téléchargement + déchiffrement + décompression + import + reconstruction d’index + requêtes de vérification.
- Si le RTO est manqué, changez de méthode de sauvegarde, parallélisez la restauration, ou modifiez la promesse métier. Choisissez-en une.
Trois mini-récits d’entreprise venus du royaume du « ça a passé la CI »
Incident : la mauvaise hypothèse (le dump logique « inclut tout », n’est-ce pas ?)
Une société SaaS de taille moyenne exécutait PostgreSQL sur Kubernetes. Leur job de sauvegarde faisait pg_dump -Fc chaque nuit, envoyait le fichier vers un stockage d’objets, et le faisait tourner avec une politique de rétention propre. Dashboards verts. Aucune alerte. Le genre de setup qui fait hocher la tête aux managers.
Puis un incident : une migration de schéma a mal tourné et ils ont dû restaurer « d’hier ». Ils ont monté un nouveau cluster et restauré le dump. Techniquement, ça a marché. La base a démarré. Les tables existaient. L’application ne parvenait toujours pas à authentifier les utilisateurs et générait des erreurs de permissions comme un métronome.
La pièce manquante était ennuyeuse : les rôles et grants n’étaient pas inclus. En production, quelques rôles avaient été créés manuellement lors d’un firefight d’astreinte quelques mois plus tôt. Le dump capturait des objets, pas le modèle d’identité et de privilèges au niveau du cluster. Leur « sauvegarde » ne savait pas qui était autorisé à faire quoi.
La correction fut immédiate : ajouter pg_dumpall --globals-only au pipeline, imposer la création des rôles via migrations/IaC, et ajouter un test de restauration qui exécute un flux minimal de connexion applicative contre la base restaurée. Ils ont cessé de supposer que « sauvegarde de base de données » équivalait à « récupération applicative ».
Optimisation qui s’est retournée contre eux (compression et malice versus temps)
Une plateforme e‑commerce a migré ses sauvegardes MySQL de physiques à logiques pour « simplifier ». La base faisait plusieurs centaines de Go. Les dumps étaient fortement compressés pour réduire le coût du stockage d’objets. Ils exécutaient aussi les sauvegardes pendant les heures de travail car « les conteneurs scalent ». Le job de sauvegarde avait une belle valeur nice et tout semblait civilisé.
Ce qui s’est passé est classique : le CPU a grimpé à cause de la compression élevée, provoquant une latence des requêtes. Ensuite la sauvegarde a duré plus longtemps, chevauchant le trafic de pointe. Le chevauchement prolongé a généré plus de contention, rendant le dump encore plus long. La base n’était pas arrêtée ; elle était pire—lente, imprévisible et en colère.
Quand ils ont finalement testé le temps de restauration, ce fut brutal : le téléchargement et la décompression du dump prenaient une éternité, puis l’import prenait une éternité, puis la reconstruction des index encore. L’équipe avait optimisé le mauvais indicateur (coût de stockage) et ignoré celui qui compte pendant un incident (temps de restauration).
Ils sont passés aux sauvegardes physiques pour MySQL, ont réduit la compression à un niveau raisonnable, et ont implémenté l’envoi des binlogs pour le PITR. Le coût de stockage a augmenté, mais le chemin de restauration est passé de « on vous recontacte demain » à quelque chose que vous pouvez annoncer dans un statut.
Pratique ennuyeuse mais correcte qui a sauvé la mise (exercices de restauration et verrouillage de version)
Une équipe de services financiers exécutait PostgreSQL avec archivage WAL et backups de base hebdomadaires. Rien de fancy : configuration cohérente, rétention stricte, et un exercice mensuel de restauration dans un environnement isolé. Ils épinglaient les versions mineures de Postgres pour les environnements de restauration et tenaient une petite matrice « sauvegarde prise sur la version X, restaurée sur la version X/Y » qu’ils testaient réellement.
Un trimestre, un incident de stockage a corrompu un volume primaire et a mis la base sérieusement hors service. La réplication ne les a pas sauvés ; la réplique avait déjà rejoué la corruption. Le seul chemin était la restauration.
Le runbook d’astreinte était douloureusement explicite. Récupérer la base backup, récupérer les WAL dans l’archive, restaurer dans un PV frais, rejouer jusqu’à une heure cible, exécuter des requêtes de vérification, basculer le trafic. Ils l’ont suivi étape par étape avec l’énergie calme de gens qui ont répété la chose ennuyeuse.
Ils ont restauré dans la fenêtre promise. Pas d’héroïsme. Pas d’archéologie Slack. Le post-mortem fut court parce que le système a fait ce pour quoi il était conçu. C’est la norme à copier.
Erreurs courantes : symptôme → cause racine → correction
1) « Le job de sauvegarde dit succès, mais le fichier est minuscule »
Symptôme : les sauvegardes tombent soudainement de centaines de Mo/Go à quelques Ko/Mo.
Cause racine : le job a écrit un message d’erreur dans le flux de sortie, l’authentification a échoué, ou le disque s’est rempli en cours d’écriture ; la pipeline a quand même renvoyé 0 à cause d’un scripting shell négligent.
Correction : activer set -euo pipefail dans les scripts ; valider l’intégrité des artefacts (test gzip) ; alerter sur les anomalies de taille ; échouer le job sur patterns stderr ou codes de sortie non nuls.
2) « Restauration Postgres échoue : invalid checkpoint / WAL manquant »
Symptôme : le répertoire de données restauré ne démarre pas, se plaint de checkpoint invalide ou WAL manquant.
Cause racine : snapshot crash-inconsistent, ou base backup sans segments WAL correspondants.
Correction : utiliser pg_basebackup avec archivage WAL ; assurer la rétention WAL ; si vous utilisez des snapshots, coordonner avec PostgreSQL et inclure les WAL nécessaires pour atteindre la cohérence.
3) « Restauration MySQL fonctionne, mais routines/events manquent »
Symptôme : erreurs applicatives après restauration ; procédures stockées ou événements programmés absents.
Cause racine : mysqldump sans --routines --events --triggers.
Correction : ajouter ces flags ; ajouter des requêtes de vérification de restauration qui contrôlent la présence de ces objets.
4) « Les sauvegardes existent, mais vous ne pouvez pas faire de PITR »
Symptôme : vous ne pouvez restaurer qu’au moment de la sauvegarde, pas à un instant précis.
Cause racine : binlogs/WAL non activés, non archivés, ou non retenus assez longtemps ; le chemin des logs est éphémère dans le conteneur.
Correction : activer et expédier binlogs/WAL vers un stockage durable ; surveiller la santé de l’archivage ; aligner la rétention sur le RPO métier.
5) « Kubernetes reprogrammé le pod et vous perdez les sauvegardes »
Symptôme : les sauvegardes disparaissent après une maintenance de nœud ou un redémarrage du pod.
Cause racine : les sauvegardes étaient écrites à l’intérieur du système de fichiers du conteneur ou dans emptyDir.
Correction : écrire les sauvegardes sur un PVC ou streamer directement vers un stockage d’objets ; vérifier les montages via les manifests et l’inspection runtime.
6) « La restauration est trop lente pour tenir le RTO »
Symptôme : la restauration prend des heures/jours ; la réponse incident est capturée par la vitesse d’import.
Cause racine : dumps logiques pour des jeux de données très volumineux, sur-compression, chemin de restauration mono-thread, classe de stockage lente.
Correction : passer aux sauvegardes physiques ; ajuster la compression ; tester le temps de restauration ; envisager des replicas en lecture pour un basculement plus rapide ; utiliser des tiers de stockage PVC plus rapides pour les environnements de récupération.
7) « Sauvegardes depuis les réplicas restaurent des données périmées »
Symptôme : la restauration est cohérente mais il manque des écritures récentes.
Cause racine : vous avez sauvegardé depuis un réplica en retard sans vérifier le lag, ou la réplication était cassée.
Correction : imposer des vérifications de lag avant sauvegarde ; alerter sur la santé de la réplication ; pour le PITR, vous appuyer sur les logs avec un ordonnancement et une rétention connus.
8) « Tout se restaure, mais l’application échoue »
Symptôme : la base est up ; l’application renvoie des erreurs d’auth, extensions manquantes, problèmes d’encodage, bizarreries de fuseau horaire.
Cause racine : vous avez sauvegardé les données mais pas le contrat d’environnement : rôles, extensions, collations, hypothèses de configuration.
Correction : coder le bootstrap DB (rôles/extensions) en tant que code ; ajouter des smoke tests qui exécutent de vraies requêtes applicatives ; stocker l’image/environnement de restauration connu avec les sauvegardes.
Listes de contrôle / plan étape par étape (ennuyeux volontairement)
Étape 1 : Décidez vos cibles de récupération (RPO/RTO) et rendez-les exécutables
- Choisissez un RPO (perte de données maximale acceptable) et un RTO (temps d’indisponibilité maximal acceptable).
- Translatez-les en implémentation : fréquence de sauvegarde complète + rétention des logs PITR + budget temporel du workflow de restauration.
- Si vous ne pouvez pas vous permettre la complexité du PITR, admettez que le RPO est « dernière sauvegarde ». N’improvisez pas plus tard.
Étape 2 : Choisissez la bonne méthode de sauvegarde selon la base et la taille
- MySQL petit/moyen :
mysqldumpavec les flags corrects + envoi des binlogs si PITR nécessaire. - MySQL grand : sauvegardes physiques (XtraBackup/entreprise) + envoi des binlogs.
- PostgreSQL petit/moyen :
pg_dump -Fc+pg_dumpall --globals-only. - PostgreSQL grand :
pg_basebackup+ archivage WAL, plus tests de restauration périodiques.
Étape 3 : Rendre explicite le placement du stockage dans les manifests de conteneurs
- Répertoire de données sur PVC/volume avec une classe de performance connue.
- Répertoire de staging des sauvegardes sur PVC (ou stream direct vers un stockage d’objets).
- WAL/binlog sur stockage durable si un archivage local est utilisé.
Étape 4 : Intégrer la vérification dans la pipeline
- Contrôles d’intégrité d’artefact : validation gzip/tar, introspection du format de dump.
- Capture de métadonnées : version DB, hash du schéma, timestamp de la sauvegarde, version de l’outil.
- Automatisation des tests de restauration : restaurer dans un environnement jetable et exécuter des requêtes de vérification.
Étape 5 : Surveiller le système de sauvegarde comme la production
- Alerter sur sauvegardes manquantes, anomalies de taille, et archivage WAL/binlog obsolète.
- Suivre les tendances du temps de restauration (téléchargement + décryptage + décompression + application + vérification).
- Faire des échecs de sauvegarde un événement visible si vous revendiquez un RPO/RTO. Les échecs silencieux sont la façon dont les fausses sauvegardes se reproduisent.
Étape 6 : S’exercer à restaurer (et contrôler la dérive de version)
- Exercices de restauration mensuels, minimum. Hebdomadaires si la base est critique pour le business.
- Épingler les versions des outils de restauration (mysqldump/mysql client, pg_restore, etc.).
- Conserver une image d’environnement de restauration connue pour chaque version majeure de DB que vous exécutez.
FAQ
1) Qu’est-ce qui rend exactement une sauvegarde « fausse » ?
Tout ce qui ne peut pas être restauré vers un état cohérent et fonctionnel dans la fenêtre temporelle requise. « Le fichier existe » et « le job a réussi » ne comptent pas.
2) Les sauvegardes logiques (mysqldump/pg_dump) sont-elles intrinsèquement dangereuses ?
Non. Elles sont simplement faciles à mal configurer et lentes à grande échelle. Les sauvegardes logiques conviennent si vous les testez en restauration et si votre RTO peut tolérer le temps d’import.
3) Dans MySQL, --single-transaction est-il toujours requis ?
Pour des dumps InnoDB cohérents sans verrouiller les tables, oui. Si vous avez des tables non transactionnelles (par ex. MyISAM), la cohérence devient plus compliquée et vous devriez reconsidérer le moteur de stockage ou la méthode de sauvegarde.
4) Dans PostgreSQL, pourquoi ai-je besoin de pg_dumpall --globals-only ?
pg_dump s’exécute par base de données. Les rôles et autres objets globaux vivent au niveau du cluster. Sans eux, les restaurations échouent fréquemment avec des problèmes de permissions.
5) Puis-je me fier uniquement aux snapshots de volumes comme seule sauvegarde ?
Seulement si vous pouvez garantir la cohérence et avez testé les restaurations depuis les snapshots. Les snapshots sont excellents comme composant, pas comme religion. La plupart des équipes combinent snapshots (rapides) et logs PITR (récupération granulaire).
6) Quel est le schéma de sauvegarde le plus simple et sûr pour les conteneurs ?
Streamer la sauvegarde directement vers un stockage durable (stockage d’objets ou serveur de sauvegarde) et garder le staging local minimal. N’écrivez pas d’artefacts critiques dans des couches éphémères du conteneur.
7) Comment savoir si le PITR fonctionne vraiment ?
MySQL : confirmer que le binlog est activé, que des binlogs sont présents et récents, et que vous pouvez rejouer jusqu’à un timestamp cible lors d’un test de restauration. PostgreSQL : pg_stat_archiver montre un archivage récent réussi et vous pouvez restaurer jusqu’à un moment cible en utilisant les WAL archivés.
8) Dois‑je sauvegarder depuis le primaire ou une réplique ?
Les sauvegardes depuis une réplique réduisent la charge sur le primaire, mais vous devez imposer des vérifications de santé de la réplication et des seuils de lag. Sinon vous obtenez des sauvegardes propres mais à la mauvaise horloge.
9) Pourquoi les restaurations échouent-elles seulement dans des environnements proches de la production ?
Parce que la production contient les parties désordonnées : rôles, grants, extensions et comportements applicatifs qui testent vos hypothèses. Vos tests de restauration doivent inclure un petit test applicatif, pas seulement « la base démarre ».
10) Quel est le test de restauration minimum à automatiser ?
Lancer une instance DB jetable, restaurer la dernière sauvegarde, exécuter quelques requêtes représentatives (y compris des chemins critiques d’auth), et enregistrer le temps total. Si ça échoue, alerter fortement.
Conclusion : prochaines étapes qui réduisent vraiment le risque
Si vous ne faites rien d’autre, faites ces trois choses cette semaine :
- Tester la restauration de la dernière sauvegarde de MySQL et/ou PostgreSQL dans un environnement jetable. Chronométrez-la. Enregistrez-la.
- Vérifier les prérequis PITR : binlogs MySQL ou archivage WAL PostgreSQL, avec une rétention correspondant à votre RPO.
- Rendre explicite le stockage : confirmer que les données, sauvegardes et logs résident sur des volumes persistants ou un stockage distant durable, pas sur des éléments éphémères de conteneur.
Puis passez à l’étape adulte : intégrez ces contrôles dans l’automatisation et la supervision, pour ne plus dépendre de la mémoire, de l’espoir, et de cette personne qui « connaît les sauvegardes ». L’objectif n’est pas un fichier de sauvegarde. L’objectif est une restauration pratiquée.