À plusieurs reprises ces dernières années, j’ai été confronté à un problème étonnant. La première fois, il m’a fallu quelque temps, et un peu d’aide pour comprendre ce qu’il se passait.
Ce problème est lié à l’utilisation d’un identifiant de « timeline » avec PostgreSQL.
La notion de « timeline » avec PostgreSQL
Mais qu’est-ce que cette « timeline », et à quel moment est-elle utilisée par PostgreSQL ?
Tout d’abord, rappelons que toute modification de données ou de schémas est durablement enregistrée dans un journal de transactions, avant même d’être effectivement écrite dans la table et les index concernés.
Ce journal de transactions est appelé le WAL, pour « Write-Ahead Log ». L’écriture initiale dans ce journal, un petit fichier de seulement 16MB, permet de valider une transaction rapidement, plus que si on devait s’assurer de la même écriture dans la table et les index.
Cet enregistrement préalable est utile lors d’un crash ou d’un redémarrage inattendu, car l’ensemble des transactions étant présentes dans le journal, il suffit de le rejouer pour retrouver l’instance PostgreSQL dans l’état le plus récent.
Le deuxième moment où le journal de transactions est utile arrive lorsque nous avons besoin de restaurer une sauvegarde de l’instance PostgreSQL. La procédure de sauvegarde des fichiers de l’instance PostgreSQL est couplée avec l’archivage de nos journaux de transactions, et lors de la restauration, l’instance réutilise ces journaux et applique leur contenu pour atteindre une cible : le moment du passé de l’instance que nous souhaitons voir !
La restauration des données consiste à rejouer la ligne du temps depuis le moment de la sauvegarde avec tous les évènements contenus dans les journaux de transactions. Le même principe est appliqué avec le protocole de réplication : une instance secondaire rejoue en continu les transactions de l’instance principale : elle suit donc la « timeline » des transactions.
Lorsque le suivi de cette « timeline » s’interrompt, par exemple, lorsqu’à la fin d’une restauration des données, nous souhaitons pouvoir écrire dans l’instance ainsi restaurée, alors PostgreSQL incrémente l’identifiant de la timeline : les journaux de transactions sont alors nommés en utilisant cette nouvelle valeur, et un fichier contenant l’historique de ces incréments est produit.
L’intérêt de cet incrément est qu’à partir du moment où nous pouvons écrire dans cette instance restaurée, une nouvelle « timeline » diverge de la « timeline » originale, et nous pouvons donc utiliser les mêmes identifiants de transactions, et archiver les journaux de transactions dans le même endroit : il devient nécessaire de distinguer les « timelines » !
Le même problème existe avec les instances répliquées : lors de la promotion d’une instance secondaire en instance primaire, l’identifiant de la nouvelle « timeline » est incrémenté, permettant de faire la distinction avec l’instance originale.
Ainsi, plusieurs instances ayant la même origine peuvent cohabiter et archiver leurs journaux de transactions dans le même dépôt sans que cela ne pose de problème, car chaque instance dispose de son propre identifiant de « timeline ».
Mais quel identifiant ?
Lors d’essais réalisés avant la mise en production d’une instance PostgreSQL, il est fréquent de tester les mécanismes de bascule, permettant à une instance d’être promue afin de prendre le rôle d’instance principal. Ces essais entraînent une succession rapide des incréments d’identifiants de « timeline», et, alors que les administrateurs ont tenté différentes actions, ils m’interpellent à propos des messages suivants :
LOG: fetching timeline history file for timeline 17 from primary server
FATAL: could not receive timeline history file from the primary server:
ERROR: could not open file "pg_xlog/00000011.history": No such file or directory
L’instance PostgreSQL cherche la « timeline » 17, et le message d’erreur évoque l’absence d’un fichier d’historique utilisant l’identifiant de « timeline » 11 !
S’il semble évident qu’il y a un problème ici, la nature du problème ne m’est pas apparue immédiatement, et il a fallu plusieurs paires d’yeux sur ces lignes pour l’identifier : 17, exprimé en décimal, vaut 11, exprimé en hexadécimal :
select x'11'::int = 17::int;
t
Dans notre cas, le fichier d’historique avait en effet été supprimé, et le test réalisé n’a pas pu aboutir, mais là n’est pas l’important aujourd’hui : l’instance PostgreSQL exprime la recherche de l’identifiant de « timeline » en décimal, mais écrit les fichiers WALs et d’historique en hexadécimal.
Pour toutes les valeurs d’identifiant jusqu’à 9, il n’y a pas de problème, les deux représentations sont les mêmes. Mais à partir de 10, les représentations changent !
Si la représentation hexadécimale utilise les caractères de A à F, n’importe quel informaticien comprendra rapidement le problème, et saura interpréter correctement la valeur. Mais l’exemple ci-dessus illustre l’ambigüité des représentations, qui peut amener à une recherche infructueuse, voir à des actions destructrices.
Comme exemple d’actions destructrices, il est possible de supprimer un ensemble de fichiers WALs archivés, et potentiellement utiles pour une restauration de données, parce qu’on aura mal interprété la valeur de l’identifiant de « timeline » : c’est non seulement possible, mais c’est précisément ce qui est arrivé dans le cas à l’origine de ce billet. Les conséquences n’ont pas été dramatiques, car il ne s’agissait que d’un test.
Quel problème est posé par cette ambigüité ?
En réalité, la représentation en hexadécimal de cet identifiant n’est pas fausse, bien au contraire. Mais son utilisation peut amener à des erreurs inattendues.
Quelles sont les utilisations possibles de cet identifiant, et à quel problème s’attendre ?
Paramètre recovery_target_timeline
L’utilisation la plus courante de cet identifiant concerne le
paramètre de configuration recovery_target_timeline
. Ce paramètre
permet d’indiquer la « timeline » à rejoindre lors de la
restauration. Le type de donnée indiqué dans la documentation est
string
, car il est possible d’utiliser une valeur spéciale :
latest
, qui est la valeur par défaut depuis la version 12 de
PostgreSQL. Mais l’identifiant est en réalité un nombre entier, et
PostgreSQL doit donc convertir la valeur reçue, et cela est fait avec
la méthode suivante :
strtoul(newval, NULL, 0);
On peut trouver cet appel de fonction dans le fichier xlogrecovery.c.
La fonction
strtoul()
vient de la bibliothèque de fonction standard (<stdlib.h>
) et permet
de convertir la chaine de caractère newval
en un nombre entier long
(unsigned long
, qui est le type de données réelles de TimelineID
).
Le troisième argument de la fonction désigne la base (décimale,
octale…) du nombre à interpréter : la valeur spéciale 0
autorise la
fonction à déterminer la base en fonction du format de la chaine de
caractère. Donc, un nombre entier sera interprété comme étant exprimé
en base décimale !
Dans notre exemple, la valeur 11, venant du nom du fichier
d’historique, est en fait en base hexadécimale et ne doit pas être
interprétée en base décimale. En fait, cette fonction peut interpréter
correctement un nombre en base hexadécimal en utilisant la notation
0x11
, mais ce détail n’est pas mentionné dans la documentation de
PostgreSQL.
Lors de l’utilisation du paramètre de configuration
recovery_target_timeline
, il est donc important d’utiliser un format
compatible avec la base numérique de la valeur utilisée, selon
l’origine de cette valeur, très souvent le nom du fichier
d’historique, dans lequel l’identifiant est exprimé en hexadécimal.
Commande pg_waldump
L’autre endroit où cet identifiant est utilisé est la commande
pg_waldump
, qui permet d’inspecter le contenu d’un journal de
transaction.
Cette fois, le paramètre -t
est interprété avec la fonction
sscanf(),
et, dans ce cas, cette fonction n’attend qu’un entier en base
décimale : il est donc nécessaire de convertir préalablement le nombre
s’il est lu depuis le nom du fichier de journal de transaction. Notons
toutefois que l’utilisation de ce paramètre est rare, car le plus
souvent, cette commande interprète le nom du journal de transaction
passé en paramètre.
Conclusion
Si cet identifiant de « timeline » apparait représenté avec différentes bases numériques dans les messages de logs de PostgreSQL, il existe quelques cas où une mauvaise utilisation peut amener des erreurs, voir des comportements inattendus.
À minima, il serait nécessaire de documenter dans PostgreSQL le fait
que le paramètre recovery_target_timeline
peut être utilisé dans
différentes bases numériques.
Autre exemple de problème : de nombreux outils de l’écosystème
PostgreSQL utilisent cet identifiant de « timeline » sans
avoir suffisamment tester les possibilités, ce qui peut amener des
problèmes difficilement visibles de prime abord, et nécessiter des
correctifs tels que celui-ci : fix timeline ID conversion in wal
metric from hexadecimal to decimal
Enfin, notons que cet identifiant de « timeline » est affiché en
décimal dans le résultat de la commande pg_controldata
.