Conférences  Archives  À propos

La brêve histoire du fichier backup_label

This article is available in English: A brief history of backup label (2021-12-02)

Je suis resté longtemps ignorant des mécanismes de journalisation et de PITR avec PostgreSQL alors même qu’il s’agit d’un des fonctionnements critiques pour la durabilité des données d’une instance. Mieux comprendre ces concepts m’aurait permis à une époque, d’être plus serein lors de la mise en place de sauvegardes et surtout au moment de leur restauration !

Dans cet article, je vous propose de revenir sur un fichier anecdotique qui a fait parlé de lui pendant plusieurs années : le fichier backup_label. Qui est-il et à quoi sert-il ? Comment a-t-il évolué depuis sa création en version 8.0 de PostgreSQL et qu’adviendra-t-il de lui dans les prochaines années ?


Il était une fois la journalisation

En guise d’introduction pour mieux comprendre cet article, il est bon d’expliquer que chaque opération d’écriture dans PostgreSQL comme un UPDATE ou un INSERT, est écrite une première fois au moment du COMMIT de la transaction dans un groupe de fichiers, que l’on appelle WAL ou journaux de transactions. Ajoutées les unes à la suite des autres, ces modifications représentent un faible coût pour l’activité des disques par rapport aux écritures aléatoires d’autres processus de synchronisation à l’œuvre dans PostgreSQL.

Parmi l’un d’eux, le processus checkpointer s’assure que les nouvelles données en mémoire soient définitivement synchronisées dans les fichiers de données à des moments réguliers que l’on appelle CHECKPOINT. Cette écriture en deux temps sur les disques apporte d’excellentes performances et garantit qu’aucun bloc modifié ne soit perdu lorsqu’une transaction se termine correctement.

Écriture différée sur les disques

Par ce mécanisme de journalisation, les fichiers de données de notre instance sont constamment en retard sur la véritable activité transactionnelle, et ce, jusqu’au prochain CHECKPOINT. En cas d’arrêt brutal du système, les blocs en attente de synchronisation (dirty pages) présents dans la mémoire Shared Buffer sont perdus et les fichiers de données sont dit incohérents car ils mixent des données de transactions anciennes, nouvelles, valides ou invalides.

Dans pareilles situations, il est possible de redémarrer l’instance afin qu’elle rejoue les modifications dans l’ordre des transactions telles qu’elles avaient été écrites dans les WAL. Cette reconstruction des fichiers de données pour retrouver leur état consistant est sobrement appelée la récupération des données ou crash recovery.

En version 8.0 et supérieures, ce mécanisme a permis l’émergence des solutions de restauration dans le temps (Point In Time Recovery) et de réplication par récupération des journaux (Log Shipping) sur une instance secondaire.

Que ce soit à la suite d’un crash ou dans le cadre d’une restauration de sauvegarde, les fichiers de données doivent être cohérents pour assurer le retour du service et l’accès en écriture aux données. Quelle mauvaise surprise n’a-t-on pas lorsqu’une instance PostgreSQL interrompt son démarrage avec le message suivant :

PANIC: could not locate a valid checkpoint record

Il indique que l’instance a détecté une inconsistance dans les fichiers au moment de son démarrage et qu’elle échoue à trouver le point de reprise le plus proche de son état. Sans les journaux, la récupération échoue et s’arrête. À cet instant précis, vos nerfs et votre politique de sauvegarde sont mis à rude épreuve.

Pour le dire encore autrement : en l’absence des journaux de transactions ou de leurs archives, vos plus récentes données sont perdues.

… Et l’outil pg_resetwal ne les récuperera pas pour vous.


Entre en scène le backup_label

Après ce charmant avertissement, on considèrera que l’archivage des journaux de transactions n’est plus une option dans vos plans de sauvegarde. Assurez-vous que ces archives soient stockées sur un espace sécurisé, voire une zone décentralisée pour qu’elles soient accessibles par toutes les instances secondaires lorsque vous devez déclencher votre plan de bascule.

Pour ceux ayant atteint cette partie de l’article, vous ne devriez pas être trop perdus si je vous annonce que le fichier backup_label est un composant d’un plus large concept, à savoir : la sauvegarde.

Le fichier historique de sauvegarde est un simple fichier texte. Il contient le label que vous avez attribué à l’opération pg_basebackup, ainsi que les dates de début, de fin et la liste des segments WAL de la sauvegarde. Si vous avez utilisé le label pour identifier le fichier de sauvegarde associé, alors le fichier historique vous permet de savoir quel fichier de sauvegarde vous devez utiliser pour la restauration.

Source : Réaliser une sauvegarde de base

Prenons une instance classique en cours d’exécution et réalisons une sauvegarde avec l’outil pg_basebackup que nous vante la documentation. Observons son comportement le plus simple avec la génération d’une archive au format tar.

$ pg_basebackup --label=demo --pgdata=backups --format=tar \
    --checkpoint=fast --verbose
pg_basebackup: initiating base backup, waiting for checkpoint to complete
pg_basebackup: checkpoint completed
pg_basebackup: write-ahead log start point: 0/16000028 on timeline 1
pg_basebackup: starting background WAL receiver
pg_basebackup: created temporary replication slot "pg_basebackup_15594"
pg_basebackup: write-ahead log end point: 0/16000100
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: syncing data to disk ...
pg_basebackup: renaming backup_manifest.tmp to backup_manifest
pg_basebackup: base backup completed

Depuis la version 10, l’option --wal-method est définie sur stream par défaut, ce qui indique que tous les journaux de transactions présents et à venir dans le sous-répertoire pg_wal de l’instance seront également sauvegardés dans une archive dédiée, notamment grâce à la création d’un slot de réplication temporaire.

Depuis la version 13, l’outil embarque le fichier manifeste dans la sauvegarde afin de pouvoir contrôler l’intégrité de la copie par la commande pg_verifybackup. Contrôlons le contenu du répertoire de sauvegarde et recherchons le tant attendu backup_label.

$ tree backups/
backups/
├── backup_manifest
├── base.tar
└── pg_wal.tar
$ tar -xf backups/base.tar --to-stdout backup_label
START WAL LOCATION: 0/16000028 (file 000000010000000000000016)
CHECKPOINT LOCATION: 0/16000060
BACKUP METHOD: streamed
BACKUP FROM: master
START TIME: 2021-01-18 15:22:52 CET
LABEL: demo
START TIMELINE: 1

Ce dernier se trouve à la racine de notre archive et joue un rôle très particulier dans le processus de démarrage startup puisqu’il renseigne le point de reprise à partir duquel rejouer les journaux. Dans notre exemple, il s’agit de la position 0/16000060 présente dans le journal 000000010000000000000016. En cas d’absence du backup_label, le processus de démarrage consultera à la place le fichier de contrôle afin de déterminer le plus récent point de reprise sans garantie qu’il soit le bon.


L’heure de gloire

Vous conviendrez que la forme et l’intérêt du fichier backup_label sont anecdotiques (bien qu’essentiels) dans l’architecture de sauvegarde avec PostgreSQL. Il ne s’agit que d’un fichier texte de quelques lignes, requis exclusivement pour assurer certains contrôles lors d’une restauration.

Et pourtant, la petite révolution que provoqua la version 8.0 en janvier 2005 avec l’archivage continu et la restauration PITR suscita naturellement la créativité de l’équipe de développement au cours des années qui suivirent. Le fichier backup_label évolua pour gagner en modularité et en stabilité.

À l’origine, l’outil pg_basebackup n’était pas encore disponible et seul l’appel à la méthode pg_start_backup() permettait de générer le fichier dans lequel se trouvaient les quatres informations suivantes pour accompagner la sauvegarde à chaud :

# backend/access/transam/xlog.c
fprintf(fp, "START WAL LOCATION: %X/%X (file %s)\n",
        startpoint.xlogid, startpoint.xrecoff, xlogfilename);
fprintf(fp, "CHECKPOINT LOCATION: %X/%X\n",
        checkpointloc.xlogid, checkpointloc.xrecoff);
fprintf(fp, "START TIME: %s\n", strfbuf);
fprintf(fp, "LABEL: %s\n", backupidstr);

Les versions majeures se sont enchaînées avec son lot de corrections ou d’améliorations. Parmi les contributions notables, j’ai relevé pour vous :

  • Contribution de Laurenz Albe (commit c979a1fe)

    Publié avec la version 8.4, le code xlog.c se voit enrichir d’une méthode interne pour annuler la sauvegarde en cours. L’exécution de la commande pg_ctl stop en mode fast renomme le fichier en backup_label.old ;

  • Contribution de Dave Kerr (commit 0f04fc67)

    Apparue avec la version mineure 9.0.9, la méthode pg_start_backup() inclut un appel fsync() pour forcer l’écriture sur disque du fichier backup_label. Cette sécurité garantit la consistance d’un instantané matériel ;

  • Contribution de Heikki Linnakangas (commit 41f9ffd9)

    Proposé en version 9.2, ce patch corrige des comportements anormaux de restauration à partir de la nouvelle méthode de sauvegarde par flux. Le fichier backup_label précise la méthode employée entre pg_start_backup ou streamed ;

  • Contribution de Jun Ishizuka et Fujii Masao (commit 8366c780)

    Depuis la version 9.2, la méthode pg_start_backup() peut être exécutée sur une instance secondaire. Le rôle de l’instance d’où provient la sauvegarde est renseignée dans le fichier backup_label ;

  • Contribution de Michael Paquier (commit 6271fceb)

    Ajoutée en version 11, l’information timeline dans le fichier backup_label rejoint les précédentes pour comparer sa valeur avec celles des journaux à rejouer lors d’une récupération de données ;

Vous l’aurez compris, pendant de nombreuses années, la capacité de faire une sauvegarde dite consistante, reposait sur les deux méthodes vues précédemment. La fonction historique pg_start_backup() fut particulièrement touchée par d’incessantes critiques au sujet d’un comportement non souhaité, notamment son mode « exclusif ».

Voyons cela ensemble sur une instance récente en version 13 :

SELECT pg_start_backup('demo');
--  pg_start_backup 
-- -----------------
--  0/1D000028
$ kill -ABRT $(head -1 data/postmaster.pid)
$ cat data/backup_label
START WAL LOCATION: 0/1D000028 (file 00000001000000000000001D)
CHECKPOINT LOCATION: 0/1D000060
BACKUP METHOD: pg_start_backup
BACKUP FROM: master
START TIME: 2021-01-18 16:49:57 CET
LABEL: demo
START TIMELINE: 1

Le signal ABRT interrompt sans préavis le processus postmaster de l’instance et la routine d’arrêt CancelBackup n’est pas appelée pour renommer le fichier en backup_label.old. Avec une activité classique de production, les journaux sont recyclés et archivés à mesure que les transactions s’enchaînent. Au démarrage de l’instance, le fichier backup_label présent dans le répertoire de données est lu par erreur et n’indique plus le bon point de reprise pour la récupération des données.

LOG:  database system was shut down at 2021-01-18 17:08:43 CET
LOG:  invalid checkpoint record
FATAL:  could not locate required checkpoint record
HINT:  If you are restoring from a backup, touch "data/recovery.signal" 
		and add required recovery options.
	If you are not restoring from a backup, try removing the file
		"data/backup_label".
	Be careful: removing "data/backup_label" will result in a corrupt
		cluster if restoring from a backup.
LOG:  startup process (PID 19320) exited with exit code 1
LOG:  aborting startup due to startup process failure
LOG:  database system is shut down

Ce message complet n’est apparu qu’à partir de la version 12 avec un avertissement plus prononcé dans la documentation au sujet du fichier, faisant suite à de longs échanges sur la possibilité de se séparer ou non de cette méthode. Dans l’un d’eux, on peut lire la remarquable intervention de Robert Haas qui revient sur le succès de cette fonctionnalité depuis ses débuts et la confusion fréquente que rencontrent les utilisateurs qui ne comprennent ni la complexité ni les instructions claires de la documentation.

À présent, une note y clarifie les choses.

Ce type de sauvegarde peut seulement être réalisé sur un serveur primaire et ne permet pas des sauvegardes concurrentes. De plus, le fichier backup_label créé sur un serveur primaire peut empêcher le redémarrage de celui-ci en cas de crash. D’un autre côté, la suppression à tord de ce fichier d’une sauvegarde ou d’un serveur secondaire est une erreur fréquente qui peut mener à de sérieuses corruptions de données.

Source : Créer une sauvegarde exclusive de bas niveau


Place à la relève

Cette limitation était connue de longue date et l’équipe de développement proposa une alternative en septembre 2016 avec la sortie de la version 9.6 et l’introduction de la sauvegarde dite « concurrente ». Depuis ce jour, la sauvegarde exclusive est annoncée obsolète par les développeurs et pourrait être supprimée dans les versions à venir.

Le fichier backup_label ne disparaît pas en soi. Ses informations sont toujours requises pour la restauration PITR mais le fichier n’a plus d’état transitoire sur le disque et n’est plus écrit dans le répertoire de l’instance par la méthode pg_start_backup(). En remplacement, l’administrateur ou le script de sauvegarde doit être en capacité d’exécuter la commande pg_stop_backup() dans la même connexion à l’instance pour y récupérer les éléments et reconstruire le fichier au moment de la restauration.

SELECT pg_start_backup(label => 'demo', exclusive => false, fast => true);
--  pg_start_backup 
-- -----------------
--  0/42000028

SELECT labelfile FROM pg_stop_backup(exclusive => false);
--                            labelfile                            
-- ----------------------------------------------------------------
-- START WAL LOCATION: 0/42000028 (file 000000010000000000000042)+
-- CHECKPOINT LOCATION: 0/42000060                               +
-- BACKUP METHOD: streamed                                       +
-- BACKUP FROM: master                                           +
-- START TIME: 2021-01-18 18:17:16 CET                           +
-- LABEL: demo                                                   +
-- START TIMELINE: 1                                             +

Une autre méthode nous permet de retrouver facilement le contenu du fichier, d’autant plus si l’archivage est en place sur l’instance. En effet, à l’annonce de la fin d’une sauvegarde, les éléments précédents sont écrits dans un fichier d’historique .backup au sein des journaux de transactions et un fichier .ready est ajouté dans le répertoire archive_status à destination du processus d’archivage. Une recherche rapide sur le dépôt des archives plus tard, et nous sommes en possession du fichier prêt à l’emploi pour une restauration.

$ find archives -type f -not -size 16M
archives/000000010000000000000016.00000028.backup

$ grep -iv ^stop archives/000000010000000000000016.00000028.backup 
START WAL LOCATION: 0/42000028 (file 000000010000000000000042)
CHECKPOINT LOCATION: 0/42000060
BACKUP METHOD: streamed
BACKUP FROM: master
START TIME: 2021-01-18 18:17:16 CET
LABEL: demo
START TIMELINE: 1

La venue d’une brique complète pour la sauvegarde concurrente a permis l’émergence de nouvelles solutions de sauvegardes, plus performantes et plus modulaires que pg_basebackup. Dans le paysage des outils tiers, vous entendriez peut-être parler de pgBackRest écrit en C, Barman écrit en Python ou pitrery écrit en Bash. En outre, ces outils soulagent l’administrateur de la rédaction de scripts devenus trop complexes et loin d’être immuable dans les années à venir.


Morale de l’histoire

Au fil des versions, le fichier backup_label a enduré de nombreuses tempêtes et rebondissements pour aboutir à une forme plus aboutie de la sauvegarde et de la restauration physique dans PostgreSQL.

Si vous êtes responsable de la maintenance d’instances, particulièrement dans un environnement virtualisé, je ne peux que vous recommander de contrôler vos politiques de sauvegarde et l’outillage associé. Il n’est pas rare de voir des hyperviseurs réaliser des instantanées des machines virtuelles avec des appels de la méthode pg_start_backup() en mode exclusif.

Les outils spécialisés cités plus haut peuvent/doivent être étudiés. S’ils ne correspondent pas très bien à vos besoins, il est toujours possible de bénéficier des mécanismes de la sauvegarde concurrente à l’aide d’un fichier temporaire sous Linux et sa commande mkfifo.

La décision de supprimer définitivement la sauvegarde exclusive n’est actuellement plus débattue et a été retirée du backlog de développement lors du Commitfest de juillet 2020. Lors des derniers échanges, le contributeur David Steele (auteur de pgBackRest notamment) annonçait qu’une sauvegarde exclusive pourrait stocker son fichier backup_label directement en mémoire partagée plutôt que sur le disque et ainsi corriger sa principale faiblesse :

It might be easier/better to just keep the one exclusive slot in shared memory and store the backup label in it. We only allow one exclusive backup now so it wouldn’t be a loss in functionality.

La suite au prochain épisode !