Conférences  Archives  À propos

Les liens physiques avec pg_upgrade

La création d’un lien sous Unix se réalise avec les commandes ln ou cp. Cette action permet de lier deux fichiers vers la même donnée et de rendre disponible une ressource par l’intermédiaire de l’un ou de l’autre de ces fichiers.

Cependant, les opérations diffèrent selon le type de ce lien. Le plus connu reste le symlink, le lien symbolique. Mais qu’en est-il des autres ? Comment se caractérisent-ils et dans quels contextes ? En vrai, qu’est-ce qu’un inode ? Et PostgreSQL dans tout ça ? Autant de petites questions de curiosité que j’aborde avec vous dans cet article !


Parlons d’abord du lien symbolique

Pour faire simple, on peut comparer le lien symbolique à un raccourci Windows. Il s’agit d’un pointeur vers un fichier ou un répertoire qui permet toutes les opérations simples comme la lecture ou l’écriture.

Par exemple, pour une instance PostgreSQL, il est possible de déporter le répertoire des fichiers WAL sur un autre montage à l’aide d’un lien symbolique. Cette opération nécessite un arrêt du service et la copie des fichiers vers le nouveau montage, comme suit :

# à réaliser avec le compte root
systemctl stop postgresql-12.service

# créer le nouveau répertoire en préservant les permissions
install --owner=postgres --group=postgres --mode=700 -d /u01/pg_wal/12
mv /var/lib/pgsql/12/data/pg_wal/* /u01/pg_wal/12/

# créer le lien vers le nouveau répertoire
rmdir /var/lib/pgsql/12/data/pg_wal
ln --symbolic /u01/pg_wal/12 /var/lib/pgsql/12/data/pg_wal

# et redémarrer l'instance
systemctl start postgresql-12.service

Le lien ainsi obtenu par la commande ln --symbolic se présente comme une fausse copie du répertoire d’origine, permettant aux données d’être consultées à plusieurs endroits sans risque d’être dupliquées. La commande stat nous donne de précieuses informations à son sujet :

# FORMAT="File   = %N\nType   = %F\nOwner  = %U:%G\nAccess = %A\nInode  = %i\n"
# stat --printf="$FORMAT" /var/lib/pgsql/12/data/pg_wal
File   = ‘/var/lib/pgsql/12/data/pg_wal’ -> ‘/u01/pg_wal/12’
Type   = symbolic link
Owner  = root:root
Access = lrwxrwxrwx
Inode  = 33725146

Il apparait que son propriétaire est root, que son accès est ouvert à tous, et que l’inode qui lui est associé vaut 33919915. Qu’en est-il de répertoire contenant les journaux de transactions ?

# stat --printf="$FORMAT" /u01/pg_wal/12
File   = ‘/u01/pg_wal/12’
Type   = directory
Owner  = postgres:postgres
Access = drwx------
Inode  = 33867963

Les deux fichiers sont bien distincts et présentent des différences notables, comme les propriétaires et les droits d’accès. En réalité, Unix propose sept types de fichiers et chacun présente des caractéristiques et comportements que lui sont propres. On retrouve ainsi les fichiers, répertoires, liens symboliques, mais aussi les named pipes, les sockets, les devices ou les doors.

Le dernier attribut que remonte ma commande stat correspond au inumber ou numéro inode. Il s’agit d’un identifiant unique sur le système de fichiers permettant de retrouver toutes les métadonnées du fichier dans une table d’inodes. Nous avions vu à l’instant les droits et le propriétaire, l’inode permet également de stocker les horodatages de création ou de modification ainsi que l’adresse physique des données du fichier sur le disque.

Ainsi, pour chaque fichier sur notre système lui est associé un inode. La représentation suivante permet donc de comprendre la relation entre un lien symbolique et un répertoire.

Relation entre symlink et directory

Les choses sont devenues passionnantes lorsque j’ai découvert la notion de lien physique, ou hardlink, qu’il était possible de créer entre deux fichiers. Ce n’est en rien une nouveauté, car inclu dans les systèmes Unix depuis longtemps mais ça m’a permis de comprendre davantage l’intérêt des fameux inodes.

Puisqu’un exemple concret parle toujours de lui-même, je propose d’étudier la méthode de migration de données d’une version 9.6 vers une version 12 de PostgreSQL avec l’outil pg_upgrade. Ce dernier propose l’option --link pour réduire le temps de migration des données sans copier les fichiers d’une instance à l’autre. Bien sûr, ce n’est pas le comportement par défaut.

La distribution CentOS 7 est utilisée pour la démonstration.
Le paquet postgresql12-server doit être installé au préalable.
# la routine setup permet la création du fichier de service ainsi
# que l'initilisation d'un répertoire de données minimaliste
/usr/pgsql-12/bin/postgresql-12-setup initdb postgresql-12

# durant la migration, l'instance 9.6 doit être arrêtée
systemctl stop postgresql-9.6.service

export PGDATAOLD=/var/lib/pgsql/9.6/data
export PGDATANEW=/var/lib/pgsql/12/data
export PGBINOLD=/usr/pgsql-9.6/bin
export PGBINNEW=/usr/pgsql-12/bin
$PGBINNEW/pg_upgrade --link --verbose

# extrait du déroulement
mappings for database "prod":
public.pgbench_accounts: 16397 to 16391

linking "/var/lib/pgsql/9.6/data/base/16384/16397" to
  "/var/lib/pgsql/12/data/base/16402/16391"
linking "/var/lib/pgsql/9.6/data/base/16384/16397_fsm" to
  "/var/lib/pgsql/12/data/base/16402/16391_fsm"
linking "/var/lib/pgsql/9.6/data/base/16384/16397_vm" to  
  "/var/lib/pgsql/12/data/base/16402/16391_vm"

L’outil pg_upgrade est composé d’une série d’opération de contrôle, de copies de fichiers, d’arrêt/démarrage des instances et d’une remise à zéro des journaux de transactions avec pg_resetwal. Les lignes ci-dessus illustrent le mode --link lors de notre migration, avec la création d’un lien entre les deux versions du fichier de la table pgbench_accounts.

La méthode employée peut être consultée dans les sources de pg_upgrade et repose sur la méthode link. Regardons en détail les métadonnées des fichiers de données de la table pgbench_accounts dans les deux répertoires.

# FORMAT="${FORMAT}Links  = %h\n"
# stat --printf="$FORMAT" /var/lib/pgsql/9.6/data/base/16384/16397
File   = ‘/var/lib/pgsql/9.6/data/base/16384/16397’
Type   = regular file
Owner  = postgres:postgres
Access = -rw-------
Inode  = 101132106
Links  = 2

# stat --printf="$FORMAT" /var/lib/pgsql/12/data/base/16402/16391
File   = ‘/var/lib/pgsql/12/data/base/16402/16391’
Type   = regular file
Owner  = postgres:postgres
Access = -rw-------
Inode  = 101132106
Links  = 2

Au niveau du système, les fichiers sont strictement similaires. Pour dire vrai, il s’agit des mêmes inodes. Les métadonnées sont communes aux deux fichiers et les blocs de données de la table pgbench_accounts résident à la même adresse physique.

À la différence du lien symbolique, ce type de lien rattache un fichier non pas à un autre fichier, mais au numéro d’inode d’un autre fichier. L’attribut Links de ma commande stat correspond au nombre de liens sur l’inode en question. Une représentation de cette relation pourait être la suivante :

Relation hardlink entre deux fichiers

Les avantages sont doubles dans le cas d’une migration majeure :

  • Temps de copie des données quasi-nul : le modèle de données est importé dans la nouvelle instance et les fichiers de données sont liés à ceux de la version précédente ;
  • Économie d’espace disque : la migration n’a pas besoin du double d’espace disque.

En contrepartie :

  • Pas de retour arrière : chacune des deux instances disposent de fichiers internes comme les journaux de transactions ou le fichier de contrôle, rendant incompatibles les fichiers de données à l’une des deux instances dès lors que l’autre a démarré après la migration.

Fin de vie d’une donnée liée

L’une de mes questions à l’issue d’une migration par pg_upgrade a été : « Mais que se passe-t-il si nous ne supprimons pas l’ancien répertoire de données ? ». Puisque la donnée est référencée par la nouvelle instance, toutes les nouveautés y seront stockées et il n’y a pas de risque pour l’espace disque. Vraiment ?

Comme nous le voyions précédemment, les données de la table pgbench_accounts sont accessibles à travers le numéro d’inode 101132106. Les deux liens pointent vers la même adresse physique et la même allocation d’espace disque, ici 13 Mo.

# du -lh 9.6/data/base/16384/16397 12/data/base/16402/16391
13M	9.6/data/base/16384/16397
13M	12/data/base/16402/16391

Il est possible au cours de la vie d’une table de voir son identifiant relfilenode varier au moment de la réécriture du fichier sous un nom différent. Le cas se présente lorsque l’on souhaite déplacer la table dans un autre tablespace, quand il faut la défragmenter avec VACUUM FULL, ou lors de la restauration d’un dump. Ces opérations réalisent toute une reconstruction de la table, avec le déplacement des lignes dans un nouveau fichier.

SELECT pg_relation_filepath('pgbench_accounts');
--  pg_relation_filepath 
-- ----------------------
--  base/16402/16391

VACUUM FULL pgbench_accounts;
CHECKPOINT;

SELECT pg_relation_filepath('pgbench_accounts');
--  pg_relation_filepath 
-- ----------------------
--  base/16402/16435

Ici, la défragmentation reconstruit la table dans un nouveau fichier 16435. L’instruction CHECKPOINT permet d’écrire sur disque la totalité des nouveaux blocs et d’actualiser l’usage des fichiers, supprimant en principe les anciens fichiers de données.

# du -lh 9.6/data/base/16384/16397 12/data/base/16402/16435
0	9.6/data/base/16384/16397
13M	12/data/base/16402/16435

# FORMAT="File  = %n\nInode = %i\n"
# stat --printf="$FORMAT" 9.6/data/base/16384/16397 
File  = 9.6/data/base/16384/16397
Inode = 101132106

# stat --printf="$FORMAT" 12/data/base/16402/16435
File  = 12/data/base/16402/16435
Inode = 34082173

Le nouveau fichier de notre table pour l’instance 12 pèse toujours 13 Mo, le fichier maintenu par le lien physique de la version précédente a été vidé lors de l’opération VACUUM FULL et pèse à présent zéro octet. Mais il n’a pas été supprimé ! Nous nous retrouvons avec un fichier et son inode en trop sur le serveur.

Nouvel inode après un VACUUM FULL

Sur un système de fichiers, le nombre maximal de fichiers que l’on peut créer est défini par la quantité d’inodes disponibles, alors autant faire le ménage dès que possible pour ne pas atteindre cette limite. D’autant plus que la suppression du répertoire 9.6/data est proposée à la fin de la migration par l’outil pg_upgrade et ne présente aucun risque !

Conclusion

Apparu en version 9.0, l’outil pg_upgrade est une petite usine qui simule un import/export des structures d’une instance complète, avec la capacité de copier ou lier les anciens fichiers, de façon bien plus rapide qu’une insertion massive avec l’instruction COPY de pg_restore. C’est une solution de choix lorsque l’on migre d’une version majeure à l’autre sur un même serveur, notamment pour le gain de temps non négligeable que propose l’option --link.

Avec la version 12, l’outil propose une nouvelle option --clone et s’appuie sur la notion de liens « par référence » (ou reflinks), conçus initialement sur les systèmes de fichiers supportant la copie sur écriture. La documentation précise que la copie des fichiers est pratiquement instantanée et n’affecte pas l’ancienne instance.

Peut-être l’occasion de creuser le sujet dans un prochain article ?