Dba2 Handout
Dba2 Handout
Dba2 Handout
PostgreSQL Avancé
DALIBO
L'expertise PostgreSQL
23.09
Table des matières
iii
DALIBO Formations
2/ Configuration de PostgreSQL 47
2.1 Au menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2.2 Paramètres en lecture seule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.3 Fichiers de configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.4 postgresql.conf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
2.4.1 Surcharge des paramètres de postgresql.conf . . . . . . . . . . . . . . . . . . 52
2.4.2 Survol de postgresql.conf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.5 pg_hba.conf et pg_ident.conf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.6 Tablespaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
2.6.1 Tablespaces : mise en place . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
2.6.2 Tablespaces : configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.7 Gestion des connexions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2.7.1 TCP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2.7.2 SSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
2.8 Statistiques sur l’activité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
2.8.1 Statistiques d’activité collectées . . . . . . . . . . . . . . . . . . . . . . . . . 68
2.8.2 Vues système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
2.9 Statistiques sur les données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
2.10 Optimiseur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
2.10.1 Optimisation par les coûts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
2.10.2 Paramètres supplémentaires de l’optimiseur (1) . . . . . . . . . . . . . . . . . 79
2.10.3 Paramètres supplémentaires de l’optimiseur (2) . . . . . . . . . . . . . . . . . 80
2.10.4 Débogage de l’optimiseur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
2.11 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
2.11.1 Questions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
2.12 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
2.13 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
2.13.1 Tablespace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
2.13.2 Statistiques d’activités, tables et vues système . . . . . . . . . . . . . . . . . . 87
2.13.3 Statistiques sur les données . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
2.14 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
2.14.1 Tablespace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
2.14.2 Statistiques d’activités, tables et vues système . . . . . . . . . . . . . . . . . . 91
2.14.3 Statistiques sur les données . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
iv PostgreSQL Avancé
DALIBO Formations
PostgreSQL Avancé v
DALIBO Formations
vi PostgreSQL Avancé
DALIBO Formations
PostgreSQL Avancé ix
DALIBO Formations
x PostgreSQL Avancé
DALIBO Formations
Nos formations PostgreSQL sont issues de nombreuses années d’études, d’expérience de terrain et
de passion pour les logiciels libres. Pour Dalibo, l’utilisation de PostgreSQL n’est pas une marque
d’opportunisme commercial, mais l’expression d’un engagement de longue date. Le choix de l’Open
Source est aussi le choix de l’implication dans la communauté du logiciel.
Au‑delà du contenu technique en lui‑même, notre intention est de transmettre les valeurs qui animent
et unissent les développeurs de PostgreSQL depuis toujours : partage, ouverture, transparence, créati‑
vité, dynamisme… Le but premier de nos formations est de vous aider à mieux exploiter toute la puis‑
sance de PostgreSQL mais nous espérons également qu’elles vous inciteront à devenir un membre
actif de la communauté en partageant à votre tour le savoir‑faire que vous aurez acquis avec nous.
Nous mettons un point d’honneur à maintenir nos manuels à jour, avec des informations précises et
des exemples détaillés. Toutefois malgré nos efforts et nos multiples relectures, il est probable que ce
document contienne des oublis, des coquilles, des imprécisions ou des erreurs. Si vous constatez un
souci, n’hésitez pas à le signaler via l’adresse formation@dalibo.com1 !
À propos de DALIBO
Remerciements
Ce manuel de formation est une aventure collective qui se transmet au sein de notre société depuis
des années. Nous remercions chaleureusement ici toutes les personnes qui ont contribué directement
ou indirectement à cet ouvrage, notamment :
Jean‑Paul Argudo, Alexandre Anriot, Carole Arnaud, Alexandre Baron, David Bidoc, Sharon Bonan,
Franck Boudehen, Arnaud Bruniquel, Pierrick Chovelon, Damien Clochard, Christophe Courtois, Marc
Cousin, Gilles Darold, Jehan‑Guillaume de Rorthais, Ronan Dunklau, Vik Fearing, Stefan Fercot, Pierre
Giraud, Nicolas Gollet, Dimitri Fontaine, Florent Jardin, Virginie Jourdan, Luc Lamarle, Denis Laxalde,
Guillaume Lelarge, Alain Lesage, Benoit Lobréau, Jean‑Louis Louër, Thibaut Madelaine, Adrien Nayrat,
Alexandre Pereira, Flavie Perette, Robin Portigliatti, Thomas Reiss, Maël Rimbault, Julien Rouhaud,
Stéphane Schildknecht, Julien Tachoires, Nicolas Thauvin, Be Hai Tran, Christophe Truffier, Cédric
Villemain, Thibaud Walkowiak, Frédéric Yhuel.
1
mailto:formation@dalibo.com
PostgreSQL Avancé 1
DALIBO Formations
Cette formation est sous licence CC‑BY‑NC‑SA2 . Vous êtes libre de la redistribuer et/ou modifier aux
conditions suivantes :
– Paternité
– Pas d’utilisation commerciale
– Partage des conditions initiales à l’identique
Vous n’avez pas le droit d’utiliser cette création à des fins commerciales.
Si vous modifiez, transformez ou adaptez cette création, vous n’avez le droit de distribuer la création
qui en résulte que sous un contrat identique à celui‑ci.
Vous devez citer le nom de l’auteur original de la manière indiquée par l’auteur de l’œuvre ou le ti‑
tulaire des droits qui vous confère cette autorisation (mais pas d’une manière qui suggérerait qu’ils
vous soutiennent ou approuvent votre utilisation de l’œuvre). À chaque réutilisation ou distribution
de cette création, vous devez faire apparaître clairement au public les conditions contractuelles de
sa mise à disposition. La meilleure manière de les indiquer est un lien vers cette page web. Chacune
de ces conditions peut être levée si vous obtenez l’autorisation du titulaire des droits sur cette œuvre.
Rien dans ce contrat ne diminue ou ne restreint le droit moral de l’auteur ou des auteurs.
Le texte complet de la licence est disponible sur http://creativecommons.org/licenses/by‑nc‑sa/2.0
/fr/legalcode
Cela inclut les diapositives, les manuels eux‑mêmes et les travaux pratiques.
Cette formation peut également contenir quelques images dont la redistribution est soumise à des
licences différentes qui sont alors précisées.
Marques déposées
PostgreSQL® Postgres® et le logo Slonik sont des marques déposées3 par PostgreSQL Community As‑
sociation of Canada.
Sur ce document
2
http://creativecommons.org/licenses/by‑nc‑sa/2.0/fr/legalcode
3
https://www.postgresql.org/about/policies/trademarks/
2 PostgreSQL Avancé
DALIBO Formations
EPUB https://dali.bo/dba2_epub
HTML https://dali.bo/dba2_html
Slides https://dali.bo/dba2_slides
Vous trouverez en ligne les différentes versions complètes de ce document. La version imprimée ne
contient pas les travaux pratiques. Ils sont présents dans la version numérique (PDF ou HTML).
PostgreSQL Avancé 3
1/ Architecture & fichiers de PostgreSQL
5
DALIBO Formations
1.1 AU MENU
Le présent module vise à donner un premier aperçu global du fonctionnement interne de Post‑
greSQL.
Après quelques rappels sur l’installation, nous verrons essentiellement les processus et les fichiers
utilisés.
6 PostgreSQL Avancé
DALIBO Formations
® – Plusieurs possibilités
– paquets Linux précompilés
– outils externes d’installation
– code source
– Chacun ses avantages et inconvénients
– Dalibo recommande fortement les paquets précompilés
Nous recommandons très fortement l’utilisation des paquets Linux précompilés. Dans certains cas,
il ne sera pas possible de faire autrement que de passer par des outils externes, comme l’installeur
d’EntrepriseDB sous Windows.
Debian et Red Hat fournissent des paquets précompilés adaptés à leur distribution. Dalibo recom‑
mande d’installer les paquets de la communauté, ces derniers étant bien plus à jour que ceux des
distributions.
L’installation d’un paquet provoque la création d’un utilisateur système nommé postgres et
l’installation des binaires. Suivant les distributions, l’emplacement des binaires change. Habituelle‑
ment, tout est placé dans /usr/pgsql-<version majeure> pour les distributions Red Hat et
dans /usr/lib/postgresql/<version majeure> pour les distributions Debian.
PostgreSQL Avancé 7
DALIBO Formations
Dans le cas d’une distribution Debian, une instance est immédiatement créée dans /var/lib/postgresql/<vers
majeure>/main. Elle est ensuite démarrée.
Dans le cas d’une distribution Red Hat, aucune instance n’est créée automatiquement. Il faudra uti‑
liser un script (dont le nom dépend de la version de la distribution) pour créer l’instance, puis nous
pourrons utiliser le script de démarrage pour lancer le serveur.
L’annexe ci‑dessous décrit l’installation de PostgreSQL sans configuration particulière pour suivre le
reste de la formation.
8 PostgreSQL Avancé
DALIBO Formations
® – PostgreSQL est :
– multi‑processus (et non multi‑thread)
– à mémoire partagée
– client‑serveur
PostgreSQL Avancé 9
DALIBO Formations
Nous constatons que plusieurs processus sont présents dès le démarrage de PostgreSQL. Nous allons
les détailler.
NB : sur Debian, le postmaster est nommé postgres comme ses processus fils ; sous Windows les noms
des processus sont par défaut moins verbeux.
Le postmaster est responsable de la supervision des autres processus, ainsi que de la prise en
compte des connexions entrantes.
Le background writer et le checkpointer s’occupent d’effectuer les écritures en arrière plan,
évitant ainsi aux sessions des utilisateurs de le faire.
Le walwriter écrit le journal de transactions de façon anticipée, afin de limiter le travail de
l’opération COMMIT.
L’autovacuum launcher pilote les opérations d’« autovacuum ».
Avant la version 15, le stats collector collecte les statistiques d’activité du serveur. À partir de
la version 15, ces informations sont conservées en mémoire jusqu’à l’arrêt du serveur où elles sont
stockées sur disque jusqu’au prochain démarrage.
Le logical replication launcher est un processus dédié à la réplication logique, activé par
défaut à partir de la version 10.
Des processus supplémentaires peuvent apparaître, comme un walsender dans le cas où la base
est le serveur primaire du cluster de réplication, un logger si PostgreSQL doit gérer lui‑même les
fichiers de traces (par défaut sous Red Hat, mais pas sous Debian), ou un archiver si l’instance est
paramétrée pour générer des archives de ses journaux de transactions.
Ces différents processus seront étudiées en détail dans d’autres modules de formation.
10 PostgreSQL Avancé
DALIBO Formations
Aucun de ces processus ne traite de requête pour le compte des utilisateurs. Ce sont des processus
d’arrière‑plan effectuant des tâches de maintenance.
Il existe aussi les background workers (processus d’arrière‑plan), lancés par PostgreSQL, mais aussi par
des extensions tierces. Par exemple, la parallélisation des requêtes se base sur la création temporaire
de background workers épaulant le processus principal de la requête. La réplication logique utilise
des background workers à plus longue durée de vie. De nombreuses extensions en utilisent pour des
raisons très diverses. Le paramètre max_worker_processes régule le nombre de ces workers. Ne
descendez pas en‑dessous du défaut (8). Il faudra même parfois monter plus haut.
PostgreSQL Avancé 11
DALIBO Formations
Pour chaque nouvelle session à l’instance, le processus postmaster crée un processus fils qui
s’occupe de gérer cette session.
Ce processus reçoit les ordres SQL, les interprète, exécute les requêtes, trie les données, et enfin
retourne les résultats. À partir de la version 9.6, dans certains cas, il peut demander le lancement
d’autres processus pour l’aider dans l’exécution d’une requête en lecture seule (parallélisme).
Il y a un processus dédié à chaque connexion cliente, et ce processus est détruit à fin de cette
connexion.
Le dialogue entre le client et ce processus respecte un protocole réseau bien défini. Le client n’a jamais
accès aux données par un autre moyen que par ce protocole.
Le nombre maximum de connexions à l’instance simultanées, actives ou non, est limité par le para‑
mètre max_connections. Le défaut est 100. Afin de permettre à l’administrateur de se connecter à
l’instance si cette limite était atteinte, superuser_reserved_connections sont réservées aux
superutilisateurs de l’instance.
Une prise en compte de la modification de ces deux paramètres impose un redémarrage complet
de l’instance, puisqu’ils ont un impact sur la taille de la mémoire partagée entre les processus Post‑
greSQL.
La valeur 100 pour max_connections est généralement suffisante. Il peut être intéressant de la
diminuer pour monter work_mem et autoriser plus de mémoire de tri. Il est possible de l’augmenter
pour qu’un plus grand nombre d’utilisateurs puisse se connecter en même temps.
12 PostgreSQL Avancé
DALIBO Formations
Il s’agit aussi d’arbitrer entre le nombre de requêtes à exécuter à un instant t, le nombre de CPU dis‑
ponibles, la complexité des requêtes, et le nombre de processus que peut gérer l’OS.
Cela est encore compliqué par le parallélisme et la limitation de la bande passante des disques.
Intercaler un « pooler » entre les clients et l’instance peut se justifier dans certains cas :
PostgreSQL Avancé 13
DALIBO Formations
La gestion de la mémoire dans PostgreSQL mérite un module de formation à lui tout seul.
Pour le moment, bornons‑nous à la séparer en deux parties : la mémoire partagée et celle attribuée à
chacun des nombreux processus.
La mémoire partagée stocke principalement le cache des données de PostgreSQL (shared buffers, pa‑
ramètre shared_buffers), et d’autres zones plus petites : cache des journaux de transactions, don‑
nées de sessions, les verrous, etc.
La mémoire propre à chaque processus sert notamment aux tris en mémoire (définie en premier lieu
par le paramètre work_mem), au cache de tables temporaires, etc.
14 PostgreSQL Avancé
DALIBO Formations
1.6 FICHIERS
PostgreSQL Avancé 15
DALIBO Formations
® postgres$ ls $PGDATA
base pg_ident.conf pg_stat pg_xact
current_logfiles pg_logical pg_stat_tmp postgresql.auto.conf
global pg_multixact pg_subtrans postgresql.conf
log pg_notify pg_tblspc postmaster.opts
pg_commit_ts pg_replslot pg_twophase postmaster.pid
pg_dynshmem pg_serial PG_VERSION
pg_hba.conf pg_snapshots pg_wal
Le répertoire de données est souvent appelé PGDATA, du nom de la variable d’environnement que
l’on peut faire pointer vers lui pour simplifier l’utilisation de nombreux utilitaires PostgreSQL. Il est
possible aussi de le connaître, une fois connecté à une base de l’instance, en interrogeant le paramètre
data_directory.
SHOW data_directory;
data_directory
---------------------------
/var/lib/pgsql/15/data
Ce répertoire ne doit être utilisé que par une seule instance (processus) à la fois !
Á PostgreSQL vérifie au démarrage qu’aucune autre instance du même serveur n’utilise
les fichiers indiqués, mais cette protection n’est pas absolue, notamment avec des accès
depuis des systèmes différents.
Faites donc bien attention de ne lancer PostgreSQL qu’une seule fois sur un répertoire
de données.
Il est recommandé de ne jamais créer ce répertoire PGDATA à la racine d’un point de montage,
quel que soit le système d’exploitation et le système de fichiers utilisé. Si un point de montage
est dédié à l’utilisation de PostgreSQL, positionnez‑le toujours dans un sous‑répertoire, voire deux
niveaux en dessous du point de montage. (par exemple <point de montage>/<version
majeure>/<nom instance>).
Voir à ce propos le chapitre Use of Secondary File Systems dans la documentation officielle : https:
//www.postgresql.org/docs/current/creating‑cluster.html.
Vous pouvez trouver une description de tous les fichiers et répertoires dans la documentation offi‑
cielle1 .
1
https://www.postgresql.org/docs/current/static/storage‑file‑layout.html
16 PostgreSQL Avancé
DALIBO Formations
® – postgresql.conf
– postgresql.auto.conf
– pg_hba.conf
– pg_ident.conf
Les fichiers de configuration sont de simples fichiers textes. Habituellement, ce sont les suivants.
postgresql.conf contient une liste de paramètres, sous la forme paramètre=valeur. Tous
les paramètres sont modifiables (et présents) dans ce fichier. Selon la configuration, il peut inclure
d’autres fichiers, mais ce n’est pas le cas par défaut.
postgresql.auto.conf stocke les paramètres de configuration fixés en utilisant la commande
ALTER SYSTEM. Il surcharge donc postgresql.conf. Il est déconseillé de le modifier à la main.
pg_hba.conf contient les règles d’authentification à la base selon leur identité, la base, la prove‑
nance, etc.
pg_ident.conf est plus rarement utilisé. Il complète pg_hba.conf, par exemple pour rappro‑
cher des utilisateurs système ou propres à PostgreSQL.
Leur utilisation est décrite dans notre première formation2 .
PG_VERSION est un fichier. Il contient en texte lisible la version majeure devant être utilisée pour
accéder au répertoire (par exemple 15). On trouve ces fichiers PG_VERSION à de nombreux endroits
de l’arborescence de PostgreSQL, par exemple dans chaque répertoire de base du répertoire PG-
DATA/base/ ou à la racine de chaque tablespace.
2
https://dali.bo/f_html
PostgreSQL Avancé 17
DALIBO Formations
~$ cat /var/lib/postgresql/15/data/postmaster.pid
7771
/var/lib/postgresql/15/data
1503584802
5432
/tmp
localhost
5432001 54919263
ready
$ ps -HFC postgres
UID PID SZ RSS PSR STIME TIME CMD
pos 7771 0 42486 16536 3 16:26 00:00 /usr/local/pgsql/bin/postgres
-D /var/lib/postgresql/15/data
pos 7773 0 42486 4656 0 16:26 00:00 postgres: checkpointer
pos 7774 0 42486 5044 1 16:26 00:00 postgres: background writer
pos 7775 0 42486 8224 1 16:26 00:00 postgres: walwriter
pos 7776 0 42850 5640 1 16:26 00:00 postgres: autovacuum launcher
pos 7777 0 42559 3684 0 16:26 00:00 postgres: logical replication launcher
Le processus père de cette instance PostgreSQL a comme PID le 7771. Ce processus a bien ré‑
clamé une sémaphore d’identifiant 54919263. Cette sémaphore correspond à des segments de
mémoire partagée pour un total de 56 octets. Le répertoire de données se trouve bien dans
/var/lib/postgresql/15/data.
Le fichier postmaster.pid est supprimé lors de l’arrêt de PostgreSQL. Cependant, ce n’est pas le
cas après un arrêt brutal. Dans ce genre de cas, PostgreSQL détecte le fichier et indique qu’il va malgré
tout essayer de se lancer s’il ne trouve pas de processus en cours d’exécution avec ce PID. Un fichier
supplémentaire peut être créé ailleurs grâce au paramètre external_pid_file, c’est notamment
le défaut sous Debian :
external_pid_file = '/var/run/postgresql/15-main.pid'
$ cat $PGDATA/postmaster.opts
/usr/local/pgsql/bin/postgres "-D" "/var/lib/postgresql/15/data"
18 PostgreSQL Avancé
DALIBO Formations
base/ contient les fichiers de données (tables, index, vues matérialisées, séquences). Il contient un
sous‑répertoire par base, le nom du répertoire étant l’OID de la base dans pg_database. Dans ces
répertoires, nous trouvons un ou plusieurs fichiers par objet à stocker. Ils sont nommés ainsi :
– Le nom de base du fichier correspond à l’attribut relfilenode de l’objet stocké, dans la table
pg_class (une table, un index…). Il peut changer dans la vie de l’objet (par exemple lors d’un
VACUUM FULL, un TRUNCATE…)
– Si le nom est suffixé par un « . » suivi d’un chiffre, il s’agit d’un fichier d’extension de l’objet : un
objet est découpé en fichiers de 1 Go maximum.
– Si le nom est suffixé par _fsm, il s’agit du fichier stockant la Free Space Map (liste des blocs
réutilisables).
– Si le nom est suffixé par _vm, il s’agit du fichier stockant la Visibility Map (liste des blocs intégra‑
lement visibles, et donc ne nécessitant pas de traitement par VACUUM).
Un fichier base/1247/14356.1 est donc le second segment de l’objet ayant comme relfile-
node 14356 dans le catalogue pg_class, dans la base d’OID 1247 dans la table pg_database.
Savoir identifier cette correspondance ne sert que dans des cas de récupération de base très endom‑
magée. Vous n’aurez jamais, durant une exploitation normale, besoin d’obtenir cette correspondance.
Si, par exemple, vous avez besoin de connaître la taille de la table test dans une base, il vous suffit
d’exécuter la fonction pg_table_size(). En voici un exemple complet :
CREATE TABLE test (id integer);
INSERT INTO test SELECT generate_series(1, 5000000);
SELECT pg_table_size('test');
pg_table_size
---------------
181305344
Depuis la ligne de commande, il existe un utilitaire nommé oid2name, dont le but est de faire la
liaison entre le nom de fichier et le nom de l’objet PostgreSQL. Il a besoin de se connecter à la base :
$ pwd
/var/lib/pgsql/15/data/base/16388
PostgreSQL Avancé 19
DALIBO Formations
Le répertoire base peut aussi contenir un répertoire pgsql_tmp. Ce répertoire contient des
fichiers temporaires utilisés pour stocker les résultats d’un tri ou d’un hachage. À partir de la
version 12, il est possible de connaître facilement le contenu de ce répertoire en utilisant la fonction
pg_ls_tmpdir(), ce qui peut permettre de suivre leur consommation.
alors nous pourrons suivre les fichiers temporaires depuis une autre session :
Le répertoire global/ contient notamment les objets globaux à toute une instance, comme la table
des bases de données, celle des rôles ou celle des tablespaces ainsi que leurs index.
20 PostgreSQL Avancé
DALIBO Formations
Le répertoire pg_wal contient les journaux de transactions. Ces journaux garantissent la durabilité
des données dans la base, en traçant toute modification devant être effectuée AVANT de l’effectuer
réellement en base.
Les fichiers contenus dans pg_wal ne doivent jamais être effacés manuellement. Ces
Á fichiers sont cruciaux au bon fonctionnement de la base. PostgreSQL gère leur création
et suppression. S’ils sont toujours présents, c’est que PostgreSQL en a besoin.
Par défaut, les fichiers des journaux font tous 16 Mo. Ils ont des noms sur 24 caractères, comme par
exemple :
$ ls -l
total 2359320
...
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007C
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007D
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007E
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007F
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 000000020000014300000000
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 000000020000014300000001
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 000000020000014300000002
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 000000020000014300000003
...
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001430000001D
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001430000001E
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001430000001F
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 000000020000014300000020
-rw------- 1 postgres postgres 33554432 Mar 26 16:25 000000020000014300000021
PostgreSQL Avancé 21
DALIBO Formations
La première partie d’un nom de fichier (ici 00000002) correspond à la timeline (« ligne de temps »),
qui ne s’incrémente que lors d’une restauration de sauvegarde ou une bascule entre serveurs primaire
et secondaire. La deuxième partie (ici 00000142 puis 00000143) correspond au numéro de jour‑
nal à proprement parler, soit un ensemble de fichiers représentant 4 Go. La dernière partie corres‑
pond au numéro du segment au sein de ce journal. Selon la taille du segment fixée à l’initialisation, il
peut aller de 00000000 à 000000FF (256 segments de 16 Mo, configuration par défaut, soit 4 Go),
à 00000FFF (4096 segments de 1 Mo), ou à 0000007F (128 segments de 32 Mo, exemple ci‑dessus),
etc. Une fois ce maximum atteint, le numéro de journal au centre est incrémenté et les numéros de
segments reprennent à zéro.
L’ordre d’écriture des journaux est numérique (en hexadécimal), et leur archivage doit suivre cet ordre.
Il ne faut pas se fier à la date des fichiers pour le tri : pour des raisons de performances, PostgreSQL
recycle généralement les fichiers en les renommant. Dans l’exemple ci‑dessus, le dernier journal écrit
est 000000020000014300000020 et non 000000020000014300000024. À partir de la ver‑
sion 12, ce mécanisme peut toutefois être désactivé en passant wal_recycle à off (ce qui a un
intérêt sur certains systèmes de fichiers comme ZFS).
Dans le cadre d’un archivage PITR et/ou d’une réplication par log shipping, le sous‑répertoire
pg_wal/archive_status indique l’état des journaux dans le contexte de l’archivage. Les fichiers
.ready indiquent les journaux restant à archiver (normalement peu nombreux), les .done ceux
déjà archivés.
À partir de la version 12, il est possible de connaître facilement le contenu de ce répertoire en utilisant
la fonction pg_ls_archive_statusdir() :
Le répertoire pg_xact contient l’état de toutes les transactions passées ou présentes sur la base
(validées, annulées, en sous‑transaction ou en cours), comme nous le détaillerons dans le module
« Mécanique du moteur transactionnel ».
22 PostgreSQL Avancé
DALIBO Formations
Les fichiers contenus dans le répertoire pg_xact ne doivent jamais être effacés. Ils
Á sont cruciaux au bon fonctionnement de la base.
– dans les noms de fonctions et d’outils, xlog a été remplacé par wal (par exemple
pg_switch_xlog est devenue pg_switch_wal) ;
– toujours dans les fonctions, location a été remplacé par lsn.
® – pg_logical/
– pg_repslot/
PostgreSQL Avancé 23
DALIBO Formations
® – pg_tblspc/ : tablespaces
– si vraiment nécessaires
– liens symboliques ou points de jonction
– totalement optionnels
Dans PGDATA, le sous‑répertoire pg_tblspc contient les tablespaces, c’est‑à‑dire des espaces de
stockage.
Sous Linux, ce sont des liens symboliques vers un simple répertoire extérieur à PGDATA. Chaque lien
symbolique a comme nom l’OID du tablespace (table système pg_tablespace). PostgreSQL y crée
un répertoire lié aux versions de PostgreSQL et du catalogue, et y place les fichiers de données.
postgres=# \db+
Liste des tablespaces
Nom | Propriétaire | Emplacement | … | Taille |…
------------+--------------+-----------------------+---+---------+-
froid | postgres | /HDD/tbl/froid | | 3576 kB |
pg_default | postgres | | | 6536 MB |
pg_global | postgres | | | 587 kB |
sudo ls -R /HDD/tbl/froid
/HDD/tbl/froid:
PG_15_202209061
/HDD/tbl/froid/PG_15_202209061:
5
/HDD/tbl/froid/PG_15_202209061/5:
142532 142532_fsm 142532_vm
Sous Windows, les liens sont à proprement parler des Reparse Points (ou Junction Points) :
postgres=# \db
Liste des tablespaces
Nom | Propriétaire | Emplacement
------------+--------------+-------------
pg_default | postgres |
pg_global | postgres |
tbl1 | postgres | T:\TBL1
24 PostgreSQL Avancé
DALIBO Formations
Par défaut, pg_tblspc/ est vide. N’existent alors que les tablespaces pg_global (sous‑répertoire
global/ des objets globaux à l’instance) et pg_default (soit base/).
Statistiques d’activité :
® – collecteur (< v15) + extensions
– pg_stat_tmp/ : temporaires
– pg_stat/ : définitif
pg_stat_tmp est, jusqu’en version 15, le répertoire par défaut de stockage des statistiques d’activité
de PostgreSQL, comme les entrées‑sorties ou les opérations de modifications sur les tables. Ces fi‑
chiers pouvant générer une grande quantité d’entrées‑sorties, l’emplacement du répertoire peut être
modifié avec le paramètre stats_temp_directory. Par exemple, Debian place ce paramètre par
défaut en tmpfs :
-- jusque v14
SHOW stats_temp_directory;
stats_temp_directory
-----------------------------------------
/var/run/postgresql/14-main.pg_stat_tmp
® – pg_dynshmem/
– pg_notify/
PostgreSQL Avancé 25
DALIBO Formations
pg_dynshmem est utilisé par les extensions utilisant de la mémoire partagée dynamique.
pg_notify est utilisé par le mécanisme de gestion de notification de PostgreSQL (LISTEN et NO-
TIFY) qui permet de passer des messages de notification entre sessions.
Le paramétrage des journaux est très fin. Leur configuration est le sujet est évoquée dans notre pre‑
mière formation3 .
Si logging_collector est activé, c’est‑à‑dire que PostgreSQL collecte lui‑même ses traces,
l’emplacement de ces journaux se paramètre grâce aux paramètres log_directory, le répertoire
où les stocker, et log_filename, le nom de fichier à utiliser, ce nom pouvant utiliser des échappe‑
ments comme %d pour le jour de la date, par exemple. Les droits attribués au fichier sont précisés
par le paramètre log_file_mode.
Un exemple pour log_filename avec date et heure serait :
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
La liste des échappements pour le paramètre log_filename est disponible dans la page de manuel
de la fonction strftime sur la plupart des plateformes de type UNIX.
3
https://dali.bo/h1_html
26 PostgreSQL Avancé
DALIBO Formations
1.7 CONCLUSION
– Une bonne compréhension de cette architecture est la clé d’une bonne adminis‑
tration.
1.7.1 Questions
PostgreSQL Avancé 27
DALIBO Formations
1.8 QUIZ
https://dali.bo/m1_quiz
®
28 PostgreSQL Avancé
DALIBO Formations
L’installation est détaillée ici pour Rocky Linux 8 (similaire à Red Hat 8), Red Hat/CentOS 7, et De‑
bian/Ubuntu.
Elle ne dure que quelques minutes.
ATTENTION : Red Hat et CentOS 6 et 7, comme Rocky 8, fournissent par défaut des
Á versions de PostgreSQL qui ne sont plus supportées. Ne jamais installer les packages
postgresql, postgresql-client et postgresql-server !
L’utilisation des dépôts du PGDG est donc obligatoire.
Installation de PostgreSQL 15 :
# dnf install -y postgresql15-server postgresql15-contrib
PostgreSQL Avancé 29
DALIBO Formations
Objet Chemin
Binaires /usr/pgsql-15/bin
Répertoire de l’utilisateur postgres /var/lib/pgsql
PGDATA par défaut /var/lib/pgsql/15/data
Fichiers de configuration dans PGDATA/
Traces dans PGDATA/log
Configuration :
Modifier postgresql.conf est facultatif pour un premier essai.
Démarrage/arrêt de l’instance, rechargement de configuration :
# systemctl start postgresql-15
# systemctl stop postgresql-15
# systemctl reload postgresql-15
30 PostgreSQL Avancé
DALIBO Formations
Si plusieurs instances d’une même version majeure (forcément de la même version mineure) doivent
cohabiter sur le même serveur, il faudra les installer dans des PGDATA différents.
– Ne pas utiliser de tiret dans le nom d’une instance (problèmes potentiels avec systemd).
– Respecter les normes et conventions de l’OS : placer les instances dans un sous‑répertoire de
/var/lib/pgsqsl/15/ (ou l’équivalent pour d’autres versions majeures).
– Création du fichier service de la deuxième instance :
# cp /lib/systemd/system/postgresql-15.service \
/etc/systemd/system/postgresql-15-secondaire.service
Fondamentalement, le principe reste le même qu’en version 8. Il faudra utiliser yum plutôt que dnf.
Il n’y a pas besoin de désactiver de module AppStream. Le JIT (Just In Time compilation), nécessite un
paquet séparé, qui lui‑même nécessite des paquets du dépôt EPEL :
# yum install epel-release
# yum install postgresql15-llvmjit
PostgreSQL Avancé 31
DALIBO Formations
Installation de PostgreSQL 15 :
La méthode la plus propre consiste à modifier la configuration par défaut avant l’installation :
# apt update
# apt install postgresql-common
Objet Chemin
Binaires /usr/lib/postgresql/15/bin/
Répertoire de l’utilisateur postgres /var/lib/postgresql
PGDATA de l’instance par défaut /var/lib/postgresql/15/main
Fichiers de configuration dans
/etc/postgresql/15/main/
Traces dans /var/log/postgresql/
Configuration
Modifier postgresql.conf est facultatif pour un premier essai.
Démarrage/arrêt de l’instance, rechargement de configuration :
Debian fournit ses propres outils :
# pg_ctlcluster 15 main [start|stop|reload|status]
32 PostgreSQL Avancé
DALIBO Formations
– démarrage :
# pg_ctlcluster 15 secondaire start
Par défaut, l’instance n’est accessible que par l’utilisateur système postgres, qui n’a pas de mot de
passe. Un détour par sudo est nécessaire :
PostgreSQL Avancé 33
DALIBO Formations
La connexion en tant qu’utilisateur postgres (ou tout autre) n’est alors plus sécurisée :
dalibo:~$ psql -U postgres
psql (15.1)
Saisissez « help » pour l'aide.
postgres=#
– dans pg_hba.conf, mise en place d’une authentification par mot de passe (md5 par
défaut) pour les accès à localhost :
# IPv4 local connections:
host all all 127.0.0.1/32 md5
# IPv6 local connections:
host all all ::1/128 md5
(une authentification scram-sha-256 est plus conseillée mais elle impose que pass-
word_encryption soit à cette valeur dans postgresql.conf avant de définir les mots
de passe).
– pour se connecter sans taper le mot de passe, un fichier .pgpass dans le répertoire personnel
doit contenir les informations sur cette connexion :
localhost:5432:*:postgres:motdepassetrèslong
34 PostgreSQL Avancé
DALIBO Formations
– pour n’avoir à taper que psql, on peut définir ces variables d’environnement dans la session
voire dans ~/.bashrc :
export PGUSER=postgres
export PGDATABASE=postgres
export PGHOST=localhost
Rappels :
PostgreSQL Avancé 35
DALIBO Formations
1.10.1 Processus
Dans un autre terminal lister de nouveau les processus du serveur PostgreSQL. Qu’observe‑t‑on ?
Se connecter à la base de données b0 et créer une table t1 avec une colonne id de type inte-
ger.
Dans un autre terminal lister de nouveau les processus du serveur PostgreSQL. Qu’observe‑t‑on ?
Se connecter 15 fois à l’instance PostgreSQL sans fermer les sessions, par exemple en lançant
plusieurs fois :
psql -c 'SELECT pg_sleep(1000)' &
36 PostgreSQL Avancé
DALIBO Formations
1.10.2 Fichiers
À quelle base est lié chaque répertoire présent dans le répertoire base ? (Voir l’oid de la base
dans pg_database ou l’utilitaire en ligne de commande oid2name)
Créer une nouvelle base de données nommée b1. Qu’observe‑t‑on dans le répertoire base ?
Se connecter à la base de données b1. Créer une table t1 avec une colonne id de type integer.
Récupérer le chemin vers le fichier correspondant à la table t1 (il existe une fonction
pg_relation_filepath).
Insérer une ligne dans la table t1. Quelle taille fait le fichier de la table t1 ?
Insérer 500 lignes dans la table t1 avec generate_series. Quelle taille fait le fichier de la
table t1 ?
PostgreSQL Avancé 37
DALIBO Formations
1.11.1 Processus
Si ce n’est pas déjà fait, démarrer l’instance PostgreSQL.
$ psql postgres
psql (15.0)
Type "help" for help.
postgres=#
Dans un autre terminal lister de nouveau les processus du serveur PostgreSQL. Qu’observe‑t‑on ?
$ ps -o pid,cmd fx
PID CMD
28746 -bash
28779 \_ psql
27886 -bash
28781 \_ ps -o pid,cmd fx
27814 /usr/pgsql-15/bin/postmaster -D /var/lib/pgsql/15/data/
27815 \_ postgres: logger
27816 \_ postgres: checkpointer
27817 \_ postgres: background writer
27819 \_ postgres: walwriter
27820 \_ postgres: autovacuum launcher
27821 \_ postgres: logical replication launcher
28780 \_ postgres: postgres postgres [local] idle
Il y a un nouveau processus (ici PID 28780) qui va gérer l’exécution des requêtes du client psql.
38 PostgreSQL Avancé
DALIBO Formations
Se connecter à la base de données b0 et créer une table t1 avec une colonne id de type inte-
ger.
Création de la table :
CREATE TABLE t1 (id integer);
INSERT 0 10000000
Dans un autre terminal lister de nouveau les processus du serveur PostgreSQL. Qu’observe‑t‑on ?
$ ps -o pid,cmd fx
PID CMD
28746 -bash
28779 \_ psql
27886 -bash
28781 \_ ps -o pid,cmd fx
27814 /usr/pgsql-15/bin/postmaster -D /var/lib/pgsql/15/data/
27815 \_ postgres: logger
27816 \_ postgres: checkpointer
27817 \_ postgres: background writer
27819 \_ postgres: walwriter
27820 \_ postgres: autovacuum launcher
27821 \_ postgres: logical replication launcher
28780 \_ postgres: postgres postgres [local] INSERT
Le processus serveur exécute l’INSERT, ce qui se voit au niveau du nom du processus. Seul est affiché
le dernier ordre SQL (ie le mot INSERT et non pas la requête complète).
PostgreSQL Avancé 39
DALIBO Formations
Pour cela, il faut ouvrir le fichier de configuration postgresql.conf et modifier la valeur du para‑
mètre max_connections à 15.
Alternativement :
ALTER SYSTEM SET max_connections TO 15 ;
max_connections
-----------------
15
Se connecter 15 fois à l’instance PostgreSQL sans fermer les sessions, par exemple en lançant
plusieurs fois :
psql -c 'SELECT pg_sleep(1000)' &
$ psql postgres
psql: FATAL: sorry, too many clients already
Il est impossible de se connecter une fois que le nombre de connexions a atteint sa limite configurée
avec max_connections. Il faut donc attendre que les utilisateurs se déconnectent pour accéder de
nouveau au serveur.
40 PostgreSQL Avancé
DALIBO Formations
Toutefois si l’instance est démarrée et qu’il est encore possible de s’y connecter, le plus propre est
ceci :
ALTER SYSTEM RESET max_connections ;
Puis redémarrer PostgreSQL : toutes les connexions en cours vont être coupées.
# systemctl restart postgresql-15
FATAL: terminating connection due to administrator command
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
[...]
FATAL: terminating connection due to administrator command
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost
Il est à présent possible de se reconnecter. Vérifier que cela a été pris en compte :
postgres=# SHOW max_connections ;
max_connections
-----------------
100
1.11.2 Fichiers
Aller dans le répertoire des données de l’instance PostgreSQL. Lister les fichiers.
$ cd $PGDATA
PostgreSQL Avancé 41
DALIBO Formations
$ ls -al
total 68
drwx------. 7 postgres postgres 59 Nov 4 09:55 base
-rw-------. 1 postgres postgres 30 Nov 4 10:38 current_logfiles
drwx------. 2 postgres postgres 4096 Nov 4 10:38 global
drwx------. 2 postgres postgres 58 Nov 4 07:58 log
drwx------. 2 postgres postgres 6 Nov 3 14:11 pg_commit_ts
drwx------. 2 postgres postgres 6 Nov 3 14:11 pg_dynshmem
-rw-------. 1 postgres postgres 4658 Nov 4 09:50 pg_hba.conf
-rw-------. 1 postgres postgres 1685 Nov 3 14:16 pg_ident.conf
drwx------. 4 postgres postgres 68 Nov 4 10:38 pg_logical
drwx------. 4 postgres postgres 36 Nov 3 14:11 pg_multixact
drwx------. 2 postgres postgres 6 Nov 3 14:11 pg_notify
drwx------. 2 postgres postgres 6 Nov 3 14:11 pg_replslot
drwx------. 2 postgres postgres 6 Nov 3 14:11 pg_serial
drwx------. 2 postgres postgres 6 Nov 3 14:11 pg_snapshots
drwx------. 2 postgres postgres 6 Nov 4 10:38 pg_stat
drwx------. 2 postgres postgres 35 Nov 4 10:38 pg_stat_tmp
drwx------. 2 postgres postgres 18 Nov 3 14:11 pg_subtrans
drwx------. 2 postgres postgres 6 Nov 3 14:11 pg_tblspc
drwx------. 2 postgres postgres 6 Nov 3 14:11 pg_twophase
-rw-------. 1 postgres postgres 3 Nov 3 14:11 PG_VERSION
drwx------. 3 postgres postgres 92 Nov 4 09:55 pg_wal
drwx------. 2 postgres postgres 18 Nov 3 14:11 pg_xact
-rw-------. 1 postgres postgres 88 Nov 3 14:11 postgresql.auto.conf
-rw-------. 1 postgres postgres 29475 Nov 4 09:36 postgresql.conf
-rw-------. 1 postgres postgres 58 Nov 4 10:38 postmaster.opts
-rw-------. 1 postgres postgres 104 Nov 4 10:38 postmaster.pid
$ cd base
$ ls -al
total 60
drwx------ 8 postgres postgres 78 Nov 4 16:21 .
drwx------ 20 postgres postgres 4096 Nov 4 15:33 ..
drwx------. 2 postgres postgres 8192 Nov 4 10:38 1
drwx------. 2 postgres postgres 8192 Nov 4 09:50 16404
drwx------. 2 postgres postgres 8192 Nov 3 14:11 4
drwx------. 2 postgres postgres 8192 Nov 4 10:39 5
drwx------ 2 postgres postgres 6 Nov 3 15:58 pgsql_tmp
À quelle base est lié chaque répertoire présent dans le répertoire base ? (Voir l’oid de la base
dans pg_database ou l’utilitaire en ligne de commande oid2name)
Chaque répertoire correspond à une base de données. Le numéro indiqué est un identifiant système
(OID). Il existe deux moyens pour récupérer cette information :
– directement dans le catalogue système pg_database :
$ psql postgres
psql (15.0)
Type "help" for help.
42 PostgreSQL Avancé
DALIBO Formations
oid | datname
-------+-----------
1 | template1
16404 | b0
4 | template0
5 | postgres
Donc ici, le répertoire 1 correspond à la base template1, et le répertoire 5 à la base postgres (ces
nombres peuvent changer suivant l’installation).
Créer une nouvelle base de données nommée b1. Qu’observe‑t‑on dans le répertoire base ?
$ createdb b1
$ ls -al
total 60
drwx------ 8 postgres postgres 78 Nov 4 16:21 .
drwx------ 20 postgres postgres 4096 Nov 4 15:33 ..
drwx------. 2 postgres postgres 8192 Nov 4 10:38 1
drwx------. 2 postgres postgres 8192 Nov 4 09:50 16404
drwx------. 2 postgres postgres 8192 Nov 4 09:55 16405
drwx------. 2 postgres postgres 8192 Nov 3 14:11 4
drwx------. 2 postgres postgres 8192 Nov 4 10:39 5
drwx------ 2 postgres postgres 6 Nov 3 15:58 pgsql_tmp
Un nouveau sous‑répertoire est apparu, nommé 16405. Il correspond bien à la base b1 d’après
oid2name.
Se connecter à la base de données b1. Créer une table t1 avec une colonne id de type integer.
$ psql b1
psql (15.0)
Type "help" for help.
CREATE TABLE
Récupérer le chemin vers le fichier correspondant à la table t1 (il existe une fonction
pg_relation_filepath).
PostgreSQL Avancé 43
DALIBO Formations
chemin
-----------------------------------------
/var/lib/pgsql/15/data/base/16405/16406
$ ls -l /var/lib/pgsql/15/data/base/16393/16398
-rw-------. 1 postgres postgres 0 Nov 4 10:42
↪ /var/lib/pgsql/15/data/base/16405/16406
La table vient d’être créée. Aucune donnée n’a encore été ajoutée. Les métadonnées se trouvent dans
d’autres tables (des catalogues systèmes). Donc il est logique que le fichier soit vide.
Insérer une ligne dans la table t1. Quelle taille fait le fichier de la table t1 ?
$ ls -l /var/lib/pgsql/15/data/base/16393/16398
-rw-------. 1 postgres postgres 8192 Nov 4 12:40
↪ /var/lib/pgsql/15/data/base/16405/16406
Il fait à présent 8 ko. En fait, PostgreSQL travaille par blocs de 8 ko. Si une ligne ne peut être placée
dans un espace libre d’un bloc existant, un bloc entier est ajouté à la table.
Vous pouvez consulter le fichier avec la commande hexdump -x <nom du fichier> (faites un
CHECKPOINT avant pour être sûr qu’il soit écrit sur le disque).
Insérer 500 lignes dans la table t1 avec generate_series. Quelle taille fait le fichier de la
table t1 ?
$ ls -l /var/lib/pgsql/15/data/base/16393/16398
-rw-------. 1 postgres postgres 24576 Nov 4 12:41
↪ /var/lib/pgsql/15/data/base/16405/16406
44 PostgreSQL Avancé
DALIBO Formations
Nous avons enregistré 501 entiers dans la table. Un entier de type int4 prend 4 octets. Donc nous
avons 2004 octets de données utilisateurs. Et pourtant, nous arrivons à un fichier de 24 ko.
En fait, PostgreSQL enregistre aussi dans chaque bloc des informations systèmes en plus des données
utilisateurs. Chaque bloc contient un en‑tête, des pointeurs, et l’ensemble des lignes du bloc. Chaque
ligne contient les colonnes utilisateurs mais aussi des colonnes système. La requête suivante permet
d’en savoir plus sur les colonnes présentes dans la table :
b1=# SELECT CASE WHEN attnum<0 THEN 'systeme' ELSE 'utilisateur' END AS type,
attname, attnum, typname, typlen,
sum(typlen) OVER (PARTITION BY attnum<0) AS longueur_tot
FROM pg_attribute a
JOIN pg_type t ON t.oid=a.atttypid
WHERE attrelid ='t1'::regclass
ORDER BY attnum;
L’en‑tête de chaque ligne pèse 26 octets dans le cas général (avant PostgreSQL 12, un éventuel champ
oid pouvait ajouter 4 octets). Dans notre cas très particulier avec une seule petite colonne, c’est très
défavorable mais ce n’est généralement pas le cas.
Avec 501 lignes de 26+4 octets, nous obtenons 15 ko. Chaque bloc possède quelques informations
de maintenance : nous dépassons alors 16 ko, ce qui explique pourquoi nous en sommes à 24 ko (3
blocs).
PostgreSQL Avancé 45
2/ Configuration de PostgreSQL
47
DALIBO Formations
2.1 AU MENU
48 PostgreSQL Avancé
DALIBO Formations
Ces paramètres sont en lecture seule, mais peuvent être consultés par la commande SHOW, ou en
interrogeant la vue pg_settings. Il est possible aussi d’obtenir l’information via la commande
pg_controldata.
– block_size est la taille d’un bloc de données de la base, par défaut 8192 octets ;
– wal_block_size est la taille d’un bloc de journal, par défaut 8192 octets ;
– segment_size est la taille maximum d’un fichier de données, par défaut 1 Go ;
– wal_segment_size est la taille d’un fichier de journal de transactions (WAL), par défaut
16 Mo.
Ces paramètres sont tous fixés à la compilation, sauf wal_segment_size à partir de la version
11 : initdb accepte alors l’option --wal-segsize et l’on peut monter la taille des journaux de
transactions à 1 Go. Cela n’a d’intérêt que pour des instances générant énormément de journaux.
Recompiler avec une taille de bloc de 32 ko s’est déjà vu sur de très grosses installations (comme le
rapporte par exemple Christophe Pettus (San Francisco, 2023)1 ) avec un shared_buffers énorme,
mais cette configuration est très peu testée, nous la déconseillons dans le cas général.
Un moteur compilé avec des options non standard ne pourra pas ouvrir des fichiers
Á n’ayant pas les mêmes valeurs pour ces options.
Des tailles non standard vous exposent à rencontrer des problèmes avec des outils
Á s’attendant à des blocs de 8 ko. (Remontez alors le bug.)
1
https://thebuild.com/blog/2023/02/08/xtreme‑postgresql/
PostgreSQL Avancé 49
DALIBO Formations
® – postgresql.conf
– postgresql.auto.conf
– pg_hba.conf
– pg_ident.conf
50 PostgreSQL Avancé
DALIBO Formations
2.4 POSTGRESQL.CONF
Dans le doute, il est possible de consulter la valeur du paramètre config_file, ici dans la configu‑
ration par défaut sur Rocky Linux :
# SHOW config_file;
config_file
---------------------------------------------
/var/lib/postgresql/15/data/postgresql.conf
clé = valeur
Les commentaires commencent par « # » (croisillon) et les chaînes de caractères doivent être enca‑
drées de « ’ » (single quote). Par exemple :
data_directory = '/var/lib/postgresql/15/main'
listen_addresses = 'localhost'
port = 5432
shared_buffers = 128MB
PostgreSQL Avancé 51
DALIBO Formations
® – pg_ctl
– Inclusion externe : include, include_if_exists
– Surcharge :
– ALTER SYSTEM SET … ( renseigne postgresql.auto.conf )
– paramètres de pg_ctl
– ALTER DATABASE | ROLE … SET paramètre = …
– SET / SET LOCAL
– Consulter :
– SHOW
– pg_settings
– pg_file_settings
En effet, si des options sont passées en arguments à pg_ctl, elles seront prises en compte en priorité
par rapport à celles du fichier de configuration.
Nous pouvons aussi inclure d’autres fichiers dans le fichier postgresql.conf grâce à l’une de ces
directives :
include = 'nom_fichier'
include_if_exists = 'nom_fichier'
include_dir = 'répertoire' # contient des fichiers .conf
Le ou les fichiers indiqués sont alors inclus à l’endroit où la directive est positionnée. Avec include,
si le fichier n’existe pas, une erreur FATAL est levée ; au contraire la directive include_if_exists
permet de ne pas s’arrêter si le fichier n’existe pas. Ces directives permettent notamment des ajus‑
tements de configuration propres à plusieurs machines d’un ensemble primaire/secondaires
dont le postgresql.conf de base est identique, ou de gérer la configuration hors de post-
gresql.conf.
Si des paramètres sont répétés dans postgresql.conf, éventuellement suite à des inclusions,
la dernière occurrence écrase les précédentes. Si un paramètre est absent, la valeur par défaut
s’applique.
Le fichier postgresql.auto.conf contient le résultat des commandes de ce type :
ALTER SYSTEM SET paramètre = valeur ;
qui sont principalement utilisés par les administrateurs et les outils n’ayant pas accès au système de
fichiers.
Il est possible de surcharger les options modifiables à chaud par utilisateur, par base, et par combi‑
naison « utilisateur+base », avec par exemple :
52 PostgreSQL Avancé
DALIBO Formations
Ces surcharges sont visibles dans la table pg_db_role_setting ou via la commande \drds de
psql.
Ensuite, un utilisateur peut changer à volonté les valeurs de beaucoup de paramètres dans sa ses‑
sion :
SET parametre = valeur ;
ou une transaction :
SET LOCAL parametre = valeur ;
La meilleure source d’information sur les valeurs actives est la vue pg_settings :
SELECT name,source,context,setting,boot_val,reset_val
FROM pg_settings
WHERE name IN ('client_min_messages', 'log_checkpoints', 'wal_segment_size');
Nous constatons par exemple que, dans la session ayant effectué la requête, la valeur du paramètre
client_min_messages a été modifiée à la valeur debug. Nous pouvons aussi voir le contexte
dans lequel le paramètre est modifiable : le client_min_messages est modifiable par l’utilisateur
dans sa session. Le log_checkpoints seulement par sighup, c’est‑à‑dire par un pg_ctl re-
load, et le wal_segment_size n’est pas modifiable après l’initialisation de l’instance.
De nombreuses autres colonnes sont disponibles dans pg_settings, comme une description dé‑
taillée du paramètre, l’unité de la valeur, ou le fichier et la ligne d’où provient le paramètre. Le champ
pending_restart indique si un paramètre a été modifié mais attend encore un redémarrage pour
être appliqué.
Il existe aussi une vue pg_file_settings, qui indique la configuration présente dans les fichiers
de configuration (mais pas forcément active !). Elle peut être utile lorsque la configuration est répartie
dans plusieurs fichiers. Par exemple, suite à un ALTER SYSTEM, les paramètres sont ajoutés dans
PostgreSQL Avancé 53
DALIBO Formations
SELECT pg_reload_conf() ;
pg_reload_conf
----------------
t
-[ RECORD 1 ]-------------------------------------------------
sourcefile | /var/lib/postgresql/15/data/postgresql.conf
sourceline | 64
seqno | 2
name | max_connections
setting | 100
applied | f
error |
-[ RECORD 2 ]-------------------------------------------------
sourcefile | /var/lib/postgresql/15/data/postgresql.auto.conf
sourceline | 4
seqno | 17
name | max_connections
setting | 200
applied | f
error | setting could not be applied
-[ RECORD 3 ]-------------------------------------------------
sourcefile | /var/lib/postgresql/15/data/postgresql.auto.conf
sourceline | 3
seqno | 16
name | work_mem
setting | 16MB
applied | t
error |
54 PostgreSQL Avancé
DALIBO Formations
® – Emplacement de fichiers
– Connections & authentification
– Ressources (hors journaux de transactions)
– Journaux de transactions
– Réplication
– Optimisation de requête
– Traces
– Statistiques d’activité
– Autovacuum
– Paramétrage client par défaut
– Verrous
– Compatibilité
postgresql.conf contient environ 300 paramètres. Il est séparé en plusieurs sections, dont les
plus importantes figurent ci‑dessous. Il n’est pas question de les détailler toutes.
La plupart des paramètres ne sont jamais modifiés. Les défauts sont souvent satisfaisants pour une
petite installation. Les plus importants sont supposés acquis (au besoin, voir la formation DBA12 ).
Les principales sections sont :
Connections and authentication
S’y trouveront les classiques listen_addresses, port, max_connections, pass-
word_encryption, ainsi que les paramétrages TCP (keepalive) et SSL.
Resource usage (except WAL)
Cette partie fixe des limites à certaines consommations de ressources.
Sont normalement déjà bien connus shared_buffers, work_mem et maintenance_work_mem
(qui seront couverts extensivement plus loin).
On rencontre ici aussi le paramétrage du VACUUM (pas directement de l’autovacuum !), du background
writer, du parallélisme dans les requêtes.
Write‑Ahead Log
Les journaux de transaction sont gérés ici. Cette partie sera également détaillée dans un autre mo‑
dule.
Depuis la version 10, tout est prévu pour faciliter la mise en place d’une réplication sans modification
de cette partie sur le primaire (notamment wal_level).
2
https://dali.bo/dba1_html
PostgreSQL Avancé 55
DALIBO Formations
Dans la partie Archiving, l’archivage des journaux peut être activé pour une sauvegarde PITR ou une
réplication par log shipping.
Depuis la version 12, tous les paramètres de restauration (qui peuvent servir à la réplication) figurent
aussi dans les sections Archive Recovery et Recovery Target. Auparavant, ils figuraient dans un fichier
recovery.conf séparé.
Replication
Cette partie fournit le nécessaire pour alimenter un secondaire en réplication par streaming, physique
ou logique.
Ici encore, depuis la version 12, l’essentiel du paramétrage nécessaire à un secondaire physique ou
logique est intégré dans ce fichier.
Query tuning
Les paramètres qui peuvent influencer l’optimiseur sont à définir dans cette partie, notamment
seq_page_cost et random_page_cost en fonction des disques, et éventuellement le parallé‑
lisme, le niveau de finesse des statistiques, le JIT…
Reporting and logging
Si le paramétrage par défaut des traces ne convient pas, le modifier ici. Il faudra généralement aug‑
menter leur verbosité. Quelques paramètres log_* figurent dans d’autres sections.
Autovacuum
L’autovacuum fonctionne généralement convenablement, et des ajustements se font généralement
table par table. Il arrive cependant que certains paramètres doivent être modifiés globalement.
Client connection defaults
Cette partie un peu fourre‑tout définit le paramétrage au niveau d’un client : langue, fuseau horaire,
extensions à précharger, tablespaces par défaut…
Lock management
Les paramètres de cette section sont rarement modifiés.
56 PostgreSQL Avancé
DALIBO Formations
® – Authentification multiple :
– utilisateur / base / source de connexion
– Fichiers :
– pg_hba.conf (Host Based Authentication)
– pg_ident.conf : si mécanisme externe d’authentification
– paramètres : hba_file et ident_file
L’authentification est paramétrée au moyen du fichier pg_hba.conf. Dans ce fichier, pour une
tentative de connexion à une base donnée, pour un utilisateur donné, pour un transport (IP, IPV6,
Socket Unix, SSL ou non), et pour une source donnée, ce fichier permet de spécifier le mécanisme
d’authentification attendu.
Si le mécanisme d’authentification s’appuie sur un système externe (LDAP, Kerberos, Radius…), des
tables de correspondances entre utilisateur de la base et utilisateur demandant la connexion peuvent
être spécifiées dans pg_ident.conf.
Ces noms de fichiers ne sont que les noms par défaut. Ils peuvent tout à fait être remplacés
en spécifiant de nouvelles valeurs de hba_file et ident_file dans postgresql.conf
(les installations Red Hat et Debian utilisent là aussi des emplacements différents, comme pour
postgresql.conf).
Leur utilisation est décrite dans notre première formation3 .
3
https://dali.bo/f_html
PostgreSQL Avancé 57
DALIBO Formations
2.6 TABLESPACES
Par défaut, PostgreSQL se charge du placement des objets sur le disque, dans son répertoire des
données, mais il est possible de créer des répertoires de stockage supplémentaires, nommés tables‑
paces.
Un tablespace, vu de PostgreSQL, est un espace de stockage des objets (tables et index principale‑
ment). Son rôle est purement physique, il n’a pas à être utilisé pour une séparation logique des tables
(c’est le rôle des bases et des schémas), encore moins pour gérer des droits.
Pour le système d’exploitation, il s’agit juste d’un répertoire, déclaré ainsi :
CREATE TABLESPACE ssd LOCATION '/var/lib/postgresql/tbl_ssd';
Ce répertoire doit impérativement être placé hors de PGDATA. Certains outils poseraient problème
sinon.
Si ce conseil n’est pas suivi, PostgreSQL crée le tablespace mais renvoie un avertissement :
WARNING: tablespace location should not be inside the data directory
CREATE TABLESPACE
Attention, pour des raisons de sécurité et de fiabilité, le répertoire choisi ne doit pas
Á être à la racine d’un point de montage. (Cela vaut aussi pour les répertoires PGDATA ou
pg_wal). Positionnez toujours les données dans un sous‑répertoire, voire deux niveaux
en‑dessous du point de montage.
58 PostgreSQL Avancé
DALIBO Formations
Il est aussi déconseillé de mettre le numéro de version de PostgreSQL dans le chemin du tablespace.
PostgreSQL le gère à l’intérieur du tablespace, et en tient notamment compte dans les migrations avec
pg_upgrade.
L’idée est de séparer les objets suivant leur utilisation. Les cas d’utilisation des tablespaces dans Post‑
greSQL sont :
Sans un réel besoin, il n’y a pas besoin de créer des tablespaces, et de complexifier
b l’administration.
Il n’existe pas de notion de tablespace en lecture seule, ni de tablespace transportable entre deux
bases ou deux instances.
4
https://doc.postgresql.fr/current/creating‑cluster.html#CREATING‑CLUSTER‑MOUNT‑POINTS
5
https://bugzilla.redhat.com/show_bug.cgi?id=1247477#c1
PostgreSQL Avancé 59
DALIBO Formations
Le répertoire du tablespace doit exister et les accès ouverts et restreints à l’utilisateur système sous
lequel tourne l’instance (en général postgres sous Linux, Network Service sous Windows) :
# mkdir /SSD/tbl/chaud
# chown postgres:postgres /SSD/tbl/chaud
# chmod 700 /SSD/tbl/chaud
60 PostgreSQL Avancé
DALIBO Formations
– Les index existants ne « suivent » pas automatiquement une table déplacée, il faut
les déplacer séparément.
– Par défaut, les nouveaux index ne sont pas créés automatiquement dans le même
tablespace que la table, mais en fonction de default_tablespace.
Les tablespaces des tables sont visibles dans la vue système pg_tables, dans \d+ sous psql, et
dans pg_indexes pour les index :
SELECT schemaname, indexname, tablespace
FROM pg_indexes
WHERE tablename = 'ma_table';
® – default_tablespace
– temp_tablespaces
– Droits à ouvrir :
– Performances :
– seq_page_cost, random_page_cost
– effective_io_concurrency, maintenance_io_concurrency
Données :
Le paramètre default_tablespace permet d’utiliser un autre tablespace que celui par défaut
dans PGDATA. En plus du postgresql.conf, il peut être défini au niveau rôle, base, ou le temps
d’une session :
PostgreSQL Avancé 61
DALIBO Formations
Tri :
Les opérations de tri et les tables temporaires peuvent être déplacées vers un ou plusieurs tablespaces
dédiés grâce au paramètre temp_tablespaces. Le premier intérêt est de dédier aux tris une parti‑
tion rapide (SSD, disque local…). Un autre est de ne plus risquer de saturer la partition du PGDATA en
cas de fichiers temporaires énormes dans base/pgsql_tmp/.
Ne jamais utiliser de ramdisk (comme tmpfs) pour des tablespaces de tri : la mémoire
¾ de la machine ne doit servir qu’aux applications et outils, au cache de l’OS, et aux tris
en RAM. Favorisez ces derniers en jouant sur work_mem.
En cas de redémarrage, ce tablespace ne serait d’ailleurs plus utilisable. Un ramdisk est
encore plus dangereux pour les tablespaces de données, bien sûr.
Si plusieurs tablespaces de tri sont paramétrés, chaque transaction en choisira un de façon aléatoire
à la création d’un objet temporaire, puis utilisera alternativement chaque tablespace. Un gros tri sera
donc étalé sur plusieurs de ces tablespaces. afin de répartir la charge.
Paramètres de performances :
Dans le cas de disques de performances différentes, il faut adapter les paramètres concernés aux ca‑
ractéristiques du tablespace si la valeur par défaut ne convient pas. Ce sont des paramètres classiques
qui ne seront pas décrits en détail ici :
– seq_page_cost (coût d’accès à un bloc pendant un parcours) ;
– random_page_cost (coût d’accès à un bloc isolé) ;
– effective_io_concurrency (nombre d’I/O simultanées) et maintenance_io_concurrency
(idem, pour une opération de maintenance).
Notamment : effective_io_concurrency a pour but d’indiquer le nombre d’opérations
disques possibles en même temps pour un client (prefetch). Seuls les parcours Bitmap Scan sont
impactés par ce paramètre. Selon la documentation6 , pour un système disque utilisant un RAID
matériel, il faut le configurer en fonction du nombre de disques utiles dans le RAID (n s’il s’agit d’un
RAID 1, n‑1 s’il s’agit d’un RAID 5 ou 6, n/2 s’il s’agit d’un RAID 10). Avec du SSD, il est possible de
monter à plusieurs centaines, étant donné la rapidité de ce type de disque. À l’inverse, il faut tenir
compte du nombre de requêtes simultanées qui utiliseront ce nœud. Le défaut est seulement de 1,
et la valeur maximale est 1000. Attention, à partir de la version 13, le principe reste le même, mais
la valeur exacte de ce paramètre doit être 2 à 5 fois plus élevée qu’auparavant, selon la formule des
notes de version7 .
6
https://docs.postgresql.fr/current/runtime‑config‑resource.html#GUC‑EFFECTIVE‑IO‑CONCURRENCY
7
https://docs.postgresql.fr/13/release.html
62 PostgreSQL Avancé
DALIBO Formations
PostgreSQL Avancé 63
DALIBO Formations
Le processus postmaster est en écoute sur les différentes sockets déclarées dans la configuration.
Cette déclaration se fait au moyen des paramètres suivants :
– port : le port TCP. Il sera aussi utilisé dans le nom du fichier socket Unix (par exemple :
/tmp/.s.PGSQL.5432 ou /var/run/postgresql/.s.PGSQL.5432 selon les distri‑
butions) ;
– listen_adresses : la liste des adresses IP du serveur auxquelles s’attacher ;
– unix_socket_directories : le répertoire où sera stocké la socket Unix ;
– unix_socket_group : le groupe (système) autorisé à accéder à la socket Unix ;
– unix_socket_permissions : les droits d’accès à la socket Unix.
Les connexions par socket Unix ne sont possibles sous Windows qu’à partir de la version 13.
2.7.1 TCP
Il faut bien faire la distinction entre session TCP et session de PostgreSQL. Si une session TCP sert
de support à une requête particulièrement longue, laquelle ne renvoie pas de données pendant plu‑
64 PostgreSQL Avancé
DALIBO Formations
sieurs minutes, alors le firewall peut considérer la session inactive, même si le statut du backend dans
pg_stat_activity est active.
Il est possible de préciser les propriétés keepalive des sockets TCP, pour peu que le système
d’exploitation les gère. Le keepalive est un mécanisme de maintien et de vérification des
sessions TCP, par l’envoi régulier de messages de vérification sur une session TCP inactive.
tcp_keepalives_idle est le temps en secondes d’inactivité d’une session TCP avant l’envoi
d’un message de keepalive. tcp_keepalives_interval est le temps entre un keepalive et le
suivant, en cas de non‑réponse. tcp_keepalives_count est le nombre maximum de paquets
sans réponse accepté avant que la session ne soit déclarée comme morte.
Les valeurs par défaut (0) reviennent à utiliser les valeurs par défaut du système d’exploitation.
Le mécanisme de keepalive a deux intérêts :
– il permet de détecter les clients déconnectés même si ceux‑ci ne notifient pas la déconnexion
(plantage du système d’exploitation, fermeture de la session par un firewall…) ;
– il permet de maintenir une session active au travers de firewalls, qui seraient fermées sinon :
la plupart des firewalls ferment une session inactive après 5 minutes, alors que la norme TCP
prévoit plusieurs jours.
Un autre cas peut survenir. Parfois, un client lance une requête. Cette requête met du temps à
s’exécuter et le client quitte la session avant de récupérer les résultats. Dans ce cas, le serveur
continue à exécuter la requête et ne se rendra compte de l’absence du client qu’au moment de
renvoyer les premiers résultats. Depuis la version 14, il est possible d’autoriser la vérification de la
connexion pendant l’exécution d’une requête. Il faut pour cela définir la durée d’intervalle entre
deux vérifications avec le paramètre client_connection_check_interval. Par défaut, cette
option est désactivée et sa valeur est de 0.
2.7.2 SSL
® – Paramètres SSL
– ssl, ssl_ciphers, ssl_renegotiation_limit
Il existe des options pour activer SSL et le paramétrer. ssl vaut on ou off, ssl_ciphers est la liste
des algorithmes de chiffrement autorisés, et ssl_renegotiation_limit le volume maximum
de données échangées sur une session avant renégociation entre le client et le serveur. Le paramé‑
trage SSL impose aussi la présence d’un certificat. Pour plus de détails, consultez la documentation
officielle8 .
8
https://docs.postgresql.fr/current/ssl‑tcp.html
PostgreSQL Avancé 65
DALIBO Formations
Les différents processus de PostgreSQL collectent des statistiques d’activité qui ont pour but de me‑
surer l’activité de la base. Notamment :
Il ne faut pas confondre les statistiques d’activité avec celles sur les données (taille des tables, des
enregistrements, fréquences des valeurs…), qui sont à destination de l’optimiseur de requête !
Chaque session collecte des statistiques, dès qu’elle effectue une opération. Avant la version 15, ces
informations, si elles sont transitoires, comme la requête en cours, sont directement stockées dans
la mémoire partagée de PostgreSQL. Si elles doivent être agrégées et stockées, elles sont remontées
au processus responsable de cette tâche, le Stats Collector. À partir de la version 15, ce collecteur
disparaît. Toutes les informations sont enregistrées en mémoire pendant toute la durée d’exécution
du service. Quand PostgreSQL est arrêté, il enregistre sur disque les statistiques qui se trouvaient en
mémoire. Au redémarrage, il peut ainsi retrouver les statistiques sur disque.
Voici les paramètres concernés par cette collecte d’informations.
66 PostgreSQL Avancé
DALIBO Formations
track_activities (on par défaut) précise si les processus doivent mettre à jour leur activité dans
pg_stat_activity.
track_counts (on par défaut) indique que les processus doivent collecter des informations sur
leur activité. Il est vital pour le déclenchement de l’autovacuum.
track_io_timing (off par défaut) précise si les processus doivent collecter des informations
de chronométrage sur les lectures et écritures, pour compléter les champs blk_read_time et
blk_write_time des vues pg_stat_database et pg_stat_statements, ainsi que les
plans d’exécutions appelés avec EXPLAIN (ANALYZE,BUFFERS) et les traces de l’autovacuum
(pour un VACUUM comme un ANALYZE). Avant de l’activer sur une machine peu performante,
vérifiez l’impact avec l’outil pg_test_timing (il doit montrer des durées de chronométrage
essentiellement sous la microseconde).
track_functions indique si les processus doivent aussi collecter des informations sur l’exécution
des routines stockées. Les valeurs sont none (par défaut), pl pour ne tracer que les routines en lan‑
gages procéduraux, all pour tracer aussi les routines en C et en SQL.
Ce répertoire existe toujours en version 15, notamment si vous utilisez le module pg_stat_statements.
Cependant, en dehors de ce module, rien d’autre ne l’utilise. Quant au paramètre stats_temp_directory,
il a disparu.
PostgreSQL Avancé 67
DALIBO Formations
® – Supervision / métrologie
– Diagnostiquer
– Vues système :
– pg_stat_user_*
– pg_statio_user_*
– pg_stat_activity : requêtes
– pg_stat_bgwriter
– pg_locks
PostgreSQL propose de nombreuses vues, accessibles en SQL, pour obtenir des informations sur son
fonctionnement interne. Il est possible d’avoir des informations sur le fonctionnement des bases, des
processus d’arrière‑plan, des tables, les requêtes en cours…
Pour les statistiques aux objets, le système fournit à chaque fois trois vues différentes :
– Une pour tous les objets du type. Elle contient all dans le nom, pg_statio_all_tables par
exemple ;
– Une pour uniquement les objets systèmes. Elle contient sys dans le nom, pg_statio_sys_tables
par exemple ;
– Une pour uniquement les objets non‑systèmes. Elle contient user dans le nom, pg_statio_user_tables
par exemple.
Les accès logiques aux objets (tables, index et routines) figurent dans les vues pg_stat_xxx_tables,
pg_stat_xxx_indexes et pg_stat_user_functions.
68 PostgreSQL Avancé
DALIBO Formations
Les accès physiques aux objets sont visibles dans les vues pg_statio_xxx_tables, pg_statio_xxx_indexes
et pg_statio_xxx_sequences.
Des statistiques globales par base sont aussi disponibles, dans pg_stat_database : le nombre de
transactions validées et annulées, quelques statistiques sur les sessions, et quelques statistiques sur
les accès physiques et en cache, ainsi que sur les opérations logiques.
pg_stat_bgwriter stocke les statistiques d’écriture des buffers des Background Writer, Check‑
pointer et des sessions elles‑mêmes.
pg_stat_activity est une des vues les plus utilisées et est souvent le point de départ d’une re‑
cherche : elle donne des informations sur les processus en cours sur l’instance, que ce soit des proces‑
sus en tâche de fond ou des processus backends associés aux clients : numéro de processus, adresse
et port, date de début d’ordre, de transaction, de session, requête en cours, état, ordre SQL et nom de
l’application si elle l’a renseigné. (Noter qu’avant la version 10, cette vue n’affichait que les processus
backend ; à partir de la version 10 apparaissent des workers, le checkpointer, le walwriter… ; à partir
de la version 14 apparaît le processus d’archivage).
=# SELECT datname, pid, usename, application_name, backend_start, state,
↪ backend_type, query
FROM pg_stat_activity \gx
-[ RECORD 1 ]----+-------------------------------------------------------------
datname | ¤
pid | 26378
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.236776+02
state | ¤
backend_type | autovacuum launcher
query |
-[ RECORD 2 ]----+-------------------------------------------------------------
datname | ¤
pid | 26380
usename | postgres
application_name |
backend_start | 2019-10-24 18:25:28.238157+02
state | ¤
backend_type | logical replication launcher
query |
-[ RECORD 3 ]----+-------------------------------------------------------------
datname | pgbench
pid | 22324
usename | test_performance
application_name | pgbench
backend_start | 2019-10-28 10:26:51.167611+01
state | active
backend_type | client backend
query | UPDATE pgbench_accounts SET abalance = abalance + -3810 WHERE…
-[ RECORD 4 ]----+-------------------------------------------------------------
datname | postgres
pid | 22429
usename | postgres
application_name | psql
backend_start | 2019-10-28 10:27:09.599426+01
PostgreSQL Avancé 69
DALIBO Formations
state | active
backend_type | client backend
query | select datname, pid, usename, application_name, backend_start…
-[ RECORD 5 ]----+-------------------------------------------------------------
datname | pgbench
pid | 22325
usename | test_performance
application_name | pgbench
backend_start | 2019-10-28 10:26:51.172585+01
state | active
backend_type | client backend
query | UPDATE pgbench_accounts SET abalance = abalance + 4360 WHERE…
-[ RECORD 6 ]----+-------------------------------------------------------------
datname | pgbench
pid | 22326
usename | test_performance
application_name | pgbench
backend_start | 2019-10-28 10:26:51.178514+01
state | active
backend_type | client backend
query | UPDATE pgbench_accounts SET abalance = abalance + 2865 WHERE…
-[ RECORD 7 ]----+-------------------------------------------------------------
datname | ¤
pid | 26376
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.235574+02
state | ¤
backend_type | background writer
query |
-[ RECORD 8 ]----+-------------------------------------------------------------
datname | ¤
pid | 26375
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.235064+02
state | ¤
backend_type | checkpointer
query |
-[ RECORD 9 ]----+-------------------------------------------------------------
datname | ¤
pid | 26377
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.236239+02
state | ¤
backend_type | walwriter
query |
Cette vue fournit aussi des informations sur ce que chaque session attend. Pour les détails sur
wait_event_type (type d’événement en attente) et wait_event (nom de l’événement en
attente), voir le tableau des événements d’attente9 .
# SELECT datname, pid, wait_event_type, wait_event, query FROM pg_stat_activity
9
https://docs.postgresql.fr/current/monitoring‑stats.html#wait‑event‑table
70 PostgreSQL Avancé
DALIBO Formations
-[ RECORD 1 ]---+--------------------------------------------------------------
datname | pgbench
pid | 1590
state | idle in transaction
wait_event_type | Client
wait_event | ClientRead
query | UPDATE pgbench_accounts SET abalance = abalance + 1438 WHERE…
-[ RECORD 2 ]---+--------------------------------------------------------------
datname | pgbench
pid | 1591
state | idle
wait_event_type | Client
wait_event | ClientRead
query | END;
-[ RECORD 3 ]---+--------------------------------------------------------------
datname | pgbench
pid | 1593
state | idle in transaction
wait_event_type | Client
wait_event | ClientRead
query | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES…
-[ RECORD 4 ]---+--------------------------------------------------------------
datname | postgres
pid | 1018
state | idle in transaction
wait_event_type | Client
wait_event | ClientRead
query | delete from t1 ;
-[ RECORD 5 ]---+--------------------------------------------------------------
datname | postgres
pid | 1457
state | active
wait_event_type | Lock
wait_event | transactionid
query | delete from t1 ;
PostgreSQL Avancé 71
DALIBO Formations
72 PostgreSQL Avancé
DALIBO Formations
Afin de calculer les plans d’exécution des requêtes au mieux, le moteur a besoin de statistiques sur
les données qu’il va interroger. Il est très important pour lui de pouvoir estimer la sélectivité d’une
clause WHERE, l’augmentation ou la diminution du nombre d’enregistrements entraînée par une join‑
ture, tout cela afin de déterminer le coût approximatif d’une requête, et donc de choisir un bon plan
d’exécution.
Il ne faut pas les confondre avec les statistiques d’activité, vues précédemment !
Les statistiques sont collectées dans la table pg_statistic. La vue pg_stats affiche le contenu
de cette table système de façon plus accessible.
Les statistiques sont collectées sur :
Le recueil des statistiques s’effectue quand on lance un ordre ANALYZE sur une table, ou que
l’autovacuum le lance de son propre chef.
Les statistiques sont calculées sur un échantillon égal à 300 fois le paramètre STATISTICS de la co‑
lonne (ou, s’il n’est pas précisé, du paramètre default_statistics_target, 100 par défaut).
La vue pg_stats affiche les statistiques collectées :
\d pg_stats
View "pg_catalog.pg_stats"
Column | Type | Collation | Nullable | Default
PostgreSQL Avancé 73
DALIBO Formations
------------------------+----------+-----------+----------+---------
schemaname | name | | |
tablename | name | | |
attname | name | | |
inherited | boolean | | |
null_frac | real | | |
avg_width | integer | | |
n_distinct | real | | |
most_common_vals | anyarray | | |
most_common_freqs | real[] | | |
histogram_bounds | anyarray | | |
correlation | real | | |
most_common_elems | anyarray | | |
most_common_elem_freqs | real[] | | |
elem_count_histogram | real[] | | |
– inherited : la statistique concerne‑t‑elle un objet utilisant l’héritage (table parente, dont hé‑
ritent plusieurs tables) ;
– null_frac : fraction d’enregistrements dont la colonne vaut NULL ;
– avg_width : taille moyenne de cet attribut dans l’échantillon collecté ;
– n_distinct : si positif, nombre de valeurs distinctes, si négatif, fraction de valeurs distinctes
pour cette colonne dans la table. Il est possible de forcer le nombre de valeurs distinctes, s’il est
constaté que la collecte des statistiques n’y arrive pas : ALTER TABLE xxx ALTER COLUMN
yyy SET (n_distinct = -0.5) ; ANALYZE xxx; par exemple indique à l’optimiseur
que chaque valeur apparaît statistiquement deux fois ;
– most_common_vals et most_common_freqs : les valeurs les plus fréquentes de la table,
et leur fréquence. Le nombre de valeurs collectées est au maximum celui indiqué par le para‑
mètre STATISTICS de la colonne, ou à défaut par default_statistics_target. Le dé‑
faut de 100 échantillons sur 30 000 lignes peut être modifié par ALTER TABLE matable AL-
TER COLUMN macolonne SET statistics 300 ; (avec une évolution proportionnelle
du nombre de lignes consultées) sachant que le temps de planification augmente exponentiel‑
lement et qu’il vaut mieux ne pas dépasser la valeur 1000 ;
– histogram_bounds : les limites d’histogramme sur la colonne. Les histogrammes per‑
mettent d’évaluer la sélectivité d’un filtre par rapport à sa valeur précise. Ils permettent par
exemple à l’optimiseur de déterminer que 4,3 % des enregistrements d’une colonne noms com‑
mencent par un A, ou 0,2 % par AL. Le principe est de regrouper les enregistrements triés dans
des groupes de tailles approximativement identiques, et de stocker les limites de ces groupes
(on ignore les most_common_vals, pour lesquelles il y a déjà une mesure plus précise). Le
nombre d’histogram_bounds est calculé de la même façon que les most_common_vals
;
– correlation : le facteur de corrélation statistique entre l’ordre physique et l’ordre logique
des enregistrements de la colonne. Il vaudra par exemple 1 si les enregistrements sont physi‑
quement stockés dans l’ordre croissant, -1 si ils sont dans l’ordre décroissant, ou 0 si ils sont
totalement aléatoirement répartis. Ceci sert à affiner le coût d’accès aux enregistrements ;
– most_common_elems et most_common_elems_freqs : les valeurs les plus fréquentes si
la colonne est un tableau (NULL dans les autres cas), et leur fréquence. Le nombre de valeurs
collectées est au maximum celui indiqué par le paramètre STATISTICS de la colonne, ou à
défaut par default_statistics_target ;
74 PostgreSQL Avancé
DALIBO Formations
– elem_count_histogram : les limites d’histogramme sur la colonne si elle est de type ta‑
bleau.
Parfois, il est intéressant de calculer des statistiques sur un ensemble de colonnes ou d’expressions.
Dans ce cas, il faut créer un objet statistique en indiquant les colonnes et/ou expressions à traiter et
le type de statistiques à calculer (voir la documentation de CREATE STATISTICS).
PostgreSQL Avancé 75
DALIBO Formations
2.10 OPTIMISEUR
Cet ordre décrit le résultat souhaité. Nous ne précisons pas au moteur comment accéder aux tables
path et file (par index ou parcours complet par exemple), ni comment effectuer la jointure (Post‑
greSQL dispose de plusieurs méthodes). C’est à l’optimiseur de prendre la décision, en fonction des
informations qu’il possède.
Les informations les plus importantes pour lui, dans le contexte de cette requête, seront :
– quelle fraction de la table path est ramenée par le critère path LIKE '/usr/%’ ?
– y a‑t‑il un index utilisable sur cette colonne ?
– y a‑t‑il des index utilisables sur file.pathid, sur path.pathid ?
– quelles sont les tailles des deux tables ?
La stratégie la plus efficace ne sera donc pas la même suivant les informations retournées par toutes
ces questions.
Par exemple, il pourrait être intéressant de charger les deux tables séquentiellement, suppri‑
mer les enregistrements de path ne correspondant pas à la clause LIKE, trier les deux jeux
d’enregistrements et fusionner les deux jeux de données triés (cette technique est un merge join).
Cependant, si les tables sont assez volumineuses, et que le LIKE est très discriminant (il ramène peu
d’enregistrements de la table path), la stratégie d’accès sera totalement différente : nous pourrions
préférer récupérer les quelques enregistrements de path correspondant au LIKE par un index, puis
pour chacun de ces enregistrements, aller chercher les informations correspondantes dans la table
file (c’est un nested loop).
76 PostgreSQL Avancé
DALIBO Formations
Afin de choisir un bon plan, le moteur essaie des plans d’exécution. Il estime, pour chacun de ces plans,
le coût associé. Afin d’évaluer correctement ces coûts, il utilise plusieurs informations :
– Les statistiques sur les données, qui lui permettent d’estimer le nombre d’enregistrements ra‑
menés par chaque étape du plan et le nombre d’opérations de lecture à effectuer pour chaque
étape de ce plan ;
– Des informations de paramétrage lui permettant d’associer un coût arbitraire à chacune des
opérations à effectuer. Ces informations sont les suivantes :
– seq_page_cost (1 par défaut) : coût de la lecture d’une page disque de façon séquen‑
tielle (au sein d’un parcours séquentiel de table par exemple) ;
– random_page_cost (4 par défaut) : coût de la lecture d’une page disque de façon aléa‑
toire (lors d’un accès à une page d’index par exemple) ;
– cpu_tuple_cost (0,01 par défaut) : coût de traitement par le processeur d’un enregis‑
trement de table ;
– cpu_index_tuple_cost (0,005 par défaut) : coût de traitement par le processeur d’un
enregistrement d’index ;
– cpu_operator_cost (0,0025 par défaut) : coût de traitement par le processeur de
l’exécution d’un opérateur.
Ce sont les coûts relatifs de ces différentes opérations qui sont importants : l’accès à une page de
façon aléatoire est par défaut 4 fois plus coûteux que de façon séquentielle, du fait du déplacement
des têtes de lecture sur un disque dur. Ceci prend déjà en considération un potentiel effet du cache.
Sur une base fortement en cache, il est donc possible d’être tenté d’abaisser le random_page_cost
à 3, voire 2,5, ou des valeurs encore bien moindres dans le cas de bases totalement en mémoire.
Le cas des disques SSD est particulièrement intéressant. Ces derniers n’ont pas à proprement parler
de tête de lecture. De ce fait, comme les paramètres seq_page_cost et random_page_cost sont
principalement là pour différencier un accès direct et un accès après déplacement de la tête de lecture,
PostgreSQL Avancé 77
DALIBO Formations
la différence de configuration entre ces deux paramètres n’a pas lieu d’être si les index sont placés sur
des disques SSD. Dans ce cas, une configuration très basse et pratiquement identique (voire identique)
de ces deux paramètres est intéressante.
effective_io_concurrency a pour but d’indiquer le nombre d’opérations disques possibles en
même temps pour un client (prefetch). Seuls les parcours Bitmap Scan sont impactés par ce paramètre.
Selon la documentation10 , pour un système disque utilisant un RAID matériel, il faut le configurer en
fonction du nombre de disques utiles dans le RAID (n s’il s’agit d’un RAID 1, n‑1 s’il s’agit d’un RAID 5 ou
6, n/2 s’il s’agit d’un RAID 10). Avec du SSD, il est possible de monter à plusieurs centaines, étant donné
la rapidité de ce type de disque. À l’inverse, il faut tenir compte du nombre de requêtes simultanées
qui utiliseront ce nœud. Le défaut est seulement de 1, et la valeur maximale est 1000. Attention, à
partir de la version 13, le principe reste le même, mais la valeur exacte de ce paramètre doit être 2 à 5
fois plus élevée qu’auparavant, selon la formule des notes de version11 .
Toujours à partir de la version 13, un nouveau paramètre apparaît : maintenance_io_concurrency.
Il a le même sens que effective_io_concurrency, mais pour les opérations de main‑
tenance, non les requêtes. Celles‑ci peuvent ainsi se voir accorder plus de ressources qu’une
simple requête. Le défaut est de 10, et il faut penser à le monter aussi si nous adaptons effec-
tive_io_concurrency.
seq_page_cost, random_page_cost, effective_io_concurrency et mainte-
nance_io_concurrency peuvent être paramétrés par tablespace, afin de refléter les carac‑
téristiques de disques différents.
La mise en place du parallélisme dans une requête représente un coût : il faut mettre en place une
mémoire partagée, lancer des processus… Ce coût est pris en compte par le planificateur à l’aide du
paramètre parallel_setup_cost. Par ailleurs, le transfert d’enregistrement entre un worker et le
processus principal a également un coût représenté par le paramètre parallel_tuple_cost.
Ainsi une lecture complète d’une grosse table peut être moins coûteuse sans parallélisation du fait que
le nombre de lignes retournées par les workers est très important. En revanche, en filtrant les résultats,
le nombre de lignes retournées peut être moins important, la répartition du filtrage entre différents
processeurs devient « rentable » et le planificateur peut être amené à choisir un plan comprenant la
parallélisation.
Certaines autres informations permettent de nuancer les valeurs précédentes. effective_cache_size
est la taille totale du cache. Il permet à PostgreSQL de modéliser plus finement le coût réel d’une
opération disque, en prenant en compte la probabilité que cette information se trouve dans le cache
du système d’exploitation ou dans celui de l’instance, et soit donc moins coûteuse à accéder.
Le parcours de l’espace des solutions est un parcours exhaustif. Sa complexité est principalement liée
au nombre de jointures de la requête et est de type exponentiel. Par exemple, planifier de façon ex‑
haustive une requête à une jointure dure 200 microsecondes environ, contre 7 secondes pour 12 join‑
tures. Une autre stratégie, l’optimiseur génétique, est donc utilisée pour éviter le parcours exhaustif
quand le nombre de jointure devient trop élevé.
Pour plus de détails, voir l’article sur les coûts de planification12 issu de la base de connaissance Da‑
10
https://docs.postgresql.fr/current/runtime‑config‑resource.html#GUC‑EFFECTIVE‑IO‑CONCURRENCY
11
https://docs.postgresql.fr/13/release.html
12
https://support.dalibo.com/kb/cout_planification
78 PostgreSQL Avancé
DALIBO Formations
libo.
® – Partitionnement
– constraint_exclusion
– enable_partition_pruning
– Réordonne les tables
– from_collapse_limit & join_collapse_limit (défaut : 8)
– Requêtes préparées
– plan_cache_mode
– Curseurs
– cursor_tuple_fraction
– Mutualiser les entrées‑sorties
– synchronize_seqscans
PostgreSQL Avancé 79
DALIBO Formations
qualité du plan d’exécution généré, mais permet qu’il soit généré dans un temps raisonnable. Il est
fréquent de monter les valeurs à 10 ou un peu au‑delà si de longues requêtes impliquent beaucoup
de tables.
Pour les requêtes préparées, l’optimiseur génère des plans personnalisés pour les cinq premières exé‑
cutions d’une requête préparée, puis il bascule sur un plan générique dès que celui‑ci devient plus
intéressant que la moyenne des plans personnalisés précédents. Ceci décrit le mode auto en place
depuis de nombreuses versions. Depuis la version 12, il est possible de modifier ce comportement
grâce au paramètre de configuration plan_cache_mode :
Lors de l’utilisation de curseurs, le moteur n’a aucun moyen de connaître le nombre d’enregistrements
que souhaite récupérer réellement l’utilisateur : peut‑être seulement les premiers enregistre‑
ments. Si c’est le cas, le plan d’exécution optimal ne sera plus le même. Le paramètre cur-
sor_tuple_fraction, par défaut à 0,1, permet d’indiquer à l’optimiseur la fraction du nombre
d’enregistrements qu’un curseur souhaitera vraisemblablement récupérer, et lui permettra donc de
choisir un plan en conséquence. Si vous utilisez des curseurs, il vaut mieux indiquer explicitement le
nombre d’enregistrements dans les requêtes avec LIMIT, et passer cursor_tuple_fraction à
1,0.
Quand plusieurs requêtes souhaitent accéder séquentiellement à la même table, les processus se
rattachent à ceux déjà en cours de parcours, afin de profiter des entrées‑sorties que ces processus
effectuent, le but étant que le système se comporte comme si un seul parcours de la table était en
cours, et réduise donc fortement la charge disque. Le seul problème de ce mécanisme est que les
processus se rattachant ne parcourent pas la table dans son ordre physique : elles commencent leur
parcours de la table à l’endroit où se trouve le processus auquel elles se rattachent, puis rebouclent
sur le début de la table. Les résultats n’arrivent donc pas forcément toujours dans le même ordre, ce
qui n’est normalement pas un problème (on est censé utiliser ORDER BY dans ce cas). Mais il est
toujours possible de désactiver ce mécanisme en passant synchronize_seqscans à off.
® – GEQO :
– un optimiseur génétique
– état initial, puis mutations aléatoires
– rapide, mais non optimal
– paramètres : geqo & geqo_threshold (12 tables)
80 PostgreSQL Avancé
DALIBO Formations
PostgreSQL, pour les requêtes trop complexes, bascule vers un optimiseur appelé GEQO (GEnetic
Query Optimizer). Comme tout algorithme génétique, il fonctionne par introduction de mutations
aléatoires sur un état initial donné. Il permet de planifier rapidement une requête complexe, et de
fournir un plan d’exécution acceptable.
Le code source de PostgreSQL décrit le principe13 , résumé aussi dans ce schéma :
Ce mécanisme est configuré par des paramètres dont le nom commence par « geqo ». Exceptés ceux
évoqués ci‑dessous, il est déconseillé de modifier les paramètres sans une bonne connaissance des
algorithmes génétiques.
Malgré l’introduction de ces mutations aléatoires, le moteur arrive tout de même à conserver un fonc‑
tionnement déterministe. Tant que le paramètre geqo_seed ainsi que les autres paramètres contrô‑
lant GEQO restent inchangés, le plan obtenu pour une requête donnée restera inchangé. Il est donc
possible de faire varier la valeur de geqo_seed pour chercher d’autres plans (voir la documentation
officielle14 ).
Ces paramètres dissuadent le moteur d’utiliser un type de nœud d’exécution (en augmentant énormé‑
ment son coût). Ils permettent de vérifier ou d’invalider une erreur de l’optimiseur. Par exemple :
-- création de la table de test
CREATE TABLE test2(a integer, b integer);
PostgreSQL Avancé 81
DALIBO Formations
Figure 2/ .1: Principe d’un algorithme génétique (schéma de la documentation officielle, licence
PostgreSQL)
82 PostgreSQL Avancé
DALIBO Formations
SET max_parallel_workers_per_gather TO 0;
QUERY PLAN
------------------------------------------------------------
Seq Scan on test2 (cost=0.00..8463.00 rows=500000 width=8)
(actual time=0.031..63.194 rows=500000 loops=1)
Filter: (a < 3)
Planning Time: 0.411 ms
Execution Time: 86.824 ms
Le moteur a choisi un parcours séquentiel de table. Si l’on veut vérifier qu’un parcours par l’index sur
la colonne a n’est pas plus rentable :
-- désactivation des parcours SeqScan, IndexOnlyScan et BitmapScan
SET enable_seqscan TO off;
SET enable_indexonlyscan TO off;
SET enable_bitmapscan TO off;
-- création de l'index
CREATE INDEX ON test2(a);
QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using test2_a_idx on test2 (cost=0.42..16462.42 rows=500000 width=8)
(actual time=0.183..90.926 rows=500000 loops=1)
Index Cond: (a < 3)
Planning Time: 0.517 ms
Execution Time: 111.609 ms
Non seulement le plan est plus coûteux, mais il est aussi (et surtout) plus lent.
Attention aux effets du cache : le parcours par index est ici relativement performant à la deuxième exé‑
cution parce que les données ont été trouvées dans le cache disque. La requête, sinon, aurait été bien
plus lente. La requête initiale est donc non seulement plus rapide, mais aussi plus sûre : son temps
d’exécution restera prévisible même en cas d’erreur d’estimation sur le nombre d’enregistrements.
Si nous supprimons l’index, nous constatons que le sequential scan n’a pas été désactivé. Il a juste été
rendu très coûteux par ces options de débogage :
-- suppression de l'index
DROP INDEX test2_a_idx;
QUERY PLAN
------------------------------------------------------------------------------
Seq Scan on test2 (cost=10000000000.00..10000008463.00 rows=500000 width=8)
(actual time=0.044..60.126 rows=500000 loops=1)
Filter: (a < 3)
PostgreSQL Avancé 83
DALIBO Formations
Le « très coûteux » est un coût majoré de 10 milliards pour l’exécution d’un nœud interdit.
Voici la liste des options de désactivation :
– enable_bitmapscan ;
– enable_gathermerge ;
– enable_hashagg ;
– enable_hashjoin ;
– enable_incremental_sort ;
– enable_indexonlyscan ;
– enable_indexscan ;
– enable_material ;
– enable_mergejoin ;
– enable_nestloop ;
– enable_parallel_append ;
– enable_parallel_hash ;
– enable_partition_pruning ;
– enable_partitionwise_aggregate ;
– enable_partitionwise_join ;
– enable_seqscan ;
– enable_sort ;
– enable_tidscan.
84 PostgreSQL Avancé
DALIBO Formations
2.11 CONCLUSION
® – Nombreuses fonctionnalités
– donc nombreux paramètres
2.11.1 Questions
PostgreSQL Avancé 85
DALIBO Formations
2.12 QUIZ
https://dali.bo/m2_quiz
®
86 PostgreSQL Avancé
DALIBO Formations
2.13.1 Tablespace
Se connecter à la base de données b1. Créer une table t_dans_ts1 avec une colonne id de
type integer dans le tablespace ts1.
Écrire une requête SQL qui affiche le nom et l’identifiant de toutes les bases de données, avec le
nom du propriétaire et l’encodage. (Utiliser les table pg_database et pg_roles).
PostgreSQL Avancé 87
DALIBO Formations
Comparer la requête avec celle qui est exécutée lorsque l’on tape la commande \l dans la
console (penser à \set ECHO_HIDDEN).
– dans un autre terminal, ouvrir une session psql sur la base b0, qu’on n’utilisera plus ;
– se connecter à la base b1 depuis une autre session ;
– la vue pg_stat_activity affiche les sessions connectées. Qu’y trouve‑t‑on ?
Rechercher la ligne ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
Rechercher la ligne ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
Rechercher la ligne ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
88 PostgreSQL Avancé
DALIBO Formations
Rechercher les lignes ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
Rechercher les lignes ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
PostgreSQL Avancé 89
DALIBO Formations
2.14.1 Tablespace
postgres=# \db
Se connecter à la base de données b1. Créer une table t_dans_ts1 avec une colonne id de
type integer dans le tablespace ts1.
chemin
--------------------------------------------------------------------
/var/lib/pgsql/15/data/pg_tblspc/16394/PG_15_202107181/16393/16395
Le fichier n’a pas été créé dans un sous‑répertoire du répertoire base, mais dans le tablespace indiqué
par la commande CREATE TABLE. /opt/ts1 n’apparaît pas ici : il y a un lien symbolique dans le
chemin.
$ ls -l $PGDATA/pg_tblspc/
total 0
lrwxrwxrwx 1 postgres postgres 8 Apr 16 16:26 16394 -> /opt/ts1
$ cd /opt/ts1/PG_15_202107181/
90 PostgreSQL Avancé
DALIBO Formations
$ ls -lR
.:
total 0
drwx------ 2 postgres postgres 18 Apr 16 16:26 16393
./16393:
total 0
-rw------- 1 postgres postgres 0 Apr 16 16:26 16395
Il est à noter que ce fichier se trouve réellement dans un sous‑répertoire de /opt/ts1 mais que
PostgreSQL le retrouve à partir de pg_tblspc grâce à un lien symbolique.
Supprimer le tablespace ts1. Qu’observe‑t‑on ?
La suppression échoue tant que le tablespace est utilisé. Il faut déplacer la table dans le tablespace
par défaut :
b1=# DROP TABLESPACE ts1 ;
ERROR: tablespace "ts1" is not empty
b1=# \x
Expanded display is on.
b1=# SELECT * FROM pg_stat_user_tables WHERE relname = 't3';
-[ RECORD 1 ]-----+-------
relid | 24594
schemaname | public
relname | t3
seq_scan | 0
seq_tup_read | 0
idx_scan |
idx_tup_fetch |
n_tup_ins | 1000
PostgreSQL Avancé 91
DALIBO Formations
n_tup_upd | 0
n_tup_del | 0
n_tup_hot_upd | 0
n_live_tup | 1000
n_dead_tup | 0
last_vacuum |
last_autovacuum |
last_analyze |
last_autoanalyze |
vacuum_count | 0
autovacuum_count | 0
analyze_count | 0
autoanalyze_count | 0
Les statistiques indiquent bien que 1000 lignes ont été insérées.
Écrire une requête SQL qui affiche le nom et l’identifiant de toutes les bases de données, avec le
nom du propriétaire et l’encodage. (Utiliser les table pg_database et pg_roles).
Une jointure est possible avec la table pg_roles pour déterminer le propriétaire des bases :
SELECT db.datname, r.rolname, db.encoding
FROM pg_database db, pg_roles r
WHERE db.datdba = r.oid ;
Comparer la requête avec celle qui est exécutée lorsque l’on tape la commande \l dans la
console (penser à \set ECHO_HIDDEN).
92 PostgreSQL Avancé
DALIBO Formations
Taper la commande \l. La requête envoyée par psql au serveur est affichée juste avant le résultat :
\l
********* QUERY **********
SELECT d.datname as "Name",
pg_catalog.pg_get_userbyid(d.datdba) as "Owner",
pg_catalog.pg_encoding_to_char(d.encoding) as "Encoding",
d.datcollate as "Collate",
d.datctype as "Ctype",
pg_catalog.array_to_string(d.datacl, E'\n') AS "Access privileges"
FROM pg_catalog.pg_database d
ORDER BY 1;
**************************
– dans un autre terminal, ouvrir une session psql sur la base b0, qu’on n’utilisera plus ;
– se connecter à la base b1 depuis une autre session ;
– la vue pg_stat_activity affiche les sessions connectées. Qu’y trouve‑t‑on ?
# terminal 1
$ psql b0
# terminal 2
$ psql b1
PostgreSQL Avancé 93
DALIBO Formations
La session dans b1 est idle, c’est‑à‑dire en attente. La seule session active (au moment où elle tour‑
nait) est celle qui exécute la requête. Les autres lignes correspondent à des processus système.
Remarque : Ce n’est qu’à partir de la version 10 de PostgreSQL que la vue pg_stat_activity liste
les processus d’arrière‑plan (checkpointer, background writer….). Les connexions clientes peuvent
s’obtenir en filtrant sur la colonne backend_type le contenu client backend.
SELECT datname, count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY datname
HAVING count(*)>0;
Se connecter à la base de données b1 et créer une table t4 avec une colonne id de type entier.
NB : ceci n’est à faire qu’à titre d’exercice ! En production, c’est une très mauvaise idée.
Rechercher la ligne ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
94 PostgreSQL Avancé
DALIBO Formations
QUERY PLAN
------------------------------------------------------------------------
Gather (cost=1000.00..11866.15 rows=5642 width=4)
Workers Planned: 2
-> Parallel Seq Scan on t4 (cost=0.00..10301.95 rows=2351 width=4)
Filter: (id = 100000)
Rechercher la ligne ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
QUERY PLAN
--------------------------------------------------------------------
Gather (cost=1000.00..10633.43 rows=1 width=4)
Workers Planned: 2
-> Parallel Seq Scan on t4 (cost=0.00..9633.33 rows=1 width=4)
Filter: (id = 100000)
Les statistiques sont beaucoup plus précises. PostgreSQL sait maintenant qu’il ne va récupérer qu’une
seule ligne, sur le million de lignes dans la table. C’est le cas typique où un index serait intéressant.
Ajouter un index sur la colonne id de la table t4.
Rechercher la ligne ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
QUERY PLAN
-------------------------------------------------------------------------
Index Only Scan using t4_id_idx on t4 (cost=0.42..8.44 rows=1 width=4)
Index Cond: (id = 100000)
Après création de l’index, nous constatons que PostgreSQL choisit un autre plan qui permet d’utiliser
cet index.
Modifier le contenu de la table t4 avec UPDATE t4 SET id = 100000;
PostgreSQL Avancé 95
DALIBO Formations
Rechercher les lignes ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
QUERY PLAN
---------------------------------------------------------
Index Only Scan using t4_id_idx on t4
(cost=0.43..8.45 rows=1 width=4)
(actual time=0.040..265.573 rows=1000000 loops=1)
Index Cond: (id = 100000)
Heap Fetches: 1000001
Planning time: 0.066 ms
Execution time: 303.026 ms
Là, un parcours séquentiel serait plus performant. Mais comme PostgreSQL n’a plus de statistiques à
jour, il se trompe de plan et utilise toujours l’index.
Rechercher les lignes ayant comme valeur 100000 dans la colonne id et afficher le plan
d’exécution.
QUERY PLAN
----------------------------------------------------------
Seq Scan on t4
(cost=0.00..21350.00 rows=1000000 width=4)
(actual time=75.185..186.019 rows=1000000 loops=1)
Filter: (id = 100000)
Planning time: 0.122 ms
Execution time: 223.357 ms
Avec des statistiques à jour et malgré la présence de l’index, PostgreSQL va utiliser un parcours sé‑
quentiel qui, au final, sera plus performant.
Si l’autovacuum avait été activé, les modifications massives dans la table auraient provoqué assez
rapidement la mise à jour des statistiques.
96 PostgreSQL Avancé
3/ Mémoire et journalisation dans PostgreSQL
97
DALIBO Formations
3.1 AU MENU
98 PostgreSQL Avancé
DALIBO Formations
® – Implémentation
– shared_memory_type
– Zone de mémoire partagée :
– shared_buffers : cache disque des fichiers de données
– wal_buffers : cache disque des journaux de transactions
– données de session : max_connections &
track_activity_query_size
– verrous : max_connections & max_locks_per_transaction
– etc
– Récupérer sa taille (v15+)
SHOW shared_memory_size ;
SHOW shared_memory_size_in_huge_pages ;
La zone de mémoire partagée est allouée statiquement au démarrage de l’instance. Depuis la version
12, le type de mémoire partagée est configuré avec le paramètre shared_memory_type. Sous Li‑
nux, il s’agit par défaut de mmap, sachant qu’une très petite partie utilise toujours sysv (System V). Il
est possible de basculer uniquement en sysv mais ceci n’est pas recommandé et nécessite généra‑
lement un paramétrage du noyau Linux. Sous Windows, le type est windows. Avant la version 12, ce
paramètre n’existe pas.
La zone de mémoire partagée est calculée en fonction du dimensionnement des différentes zones,
principalement :
– shared_buffers : le cache des fichiers de données ;
– wal_buffers : le cache des journaux de transaction ;
– les données de sessions :
– max_connections (défaut : 100)
– et track_activity_query_size (défaut : 1024) ;
– les verrous :
– max_connections à nouveau
– et max_locks_per_transaction (défaut : 64).
Toute modification des paramètres régissant la mémoire partagée imposent un redémarrage de
l’instance.
À partir de la version 15, le paramètre shared_memory_size permet de connaître la taille com‑
plète de mémoire partagée allouée. Dans le cas de l’utilisation de Huge Pages, il est possible d’utiliser
PostgreSQL Avancé 99
DALIBO Formations
Nous verrons en détail l’utilité de certaines de ces zones dans les chapitres suivants.
® – work_mem
– × hash_mem_multiplier (v 13)
– maintenance_work_mem
– autovacuum_work_mem
– temp_buffers
– Pas de limite stricte à la consommation mémoire d’une session !
– ni total
– Augmenter prudemment & superviser
Les processus de PostgreSQL ont accès à la mémoire partagée, définie principalement par sha-
red_buffers, mais ils ont aussi leur mémoire propre. Cette mémoire n’est utilisable que par le
processus l’ayant allouée.
Le paramètre le plus important est work_mem, qui définit la taille de la mémoire de travail d’un pro‑
cessus lors d’une requête, principalement lors d’opérations de tri : ORDER BY, certaines jointures,
déduplication… Autre paramètre capital, maintenance_work_mem est la mémoire pour les opé‑
rations de maintenance lourdes : VACUUM, CREATE INDEX, ajouts de clé étrangère…
maintenance_work_mem peut être monté à 256 Mo à 1 Go sur les machines récentes, car il
concerne des opérations lourdes rarement exécutées plusieurs fois simultanément. Monter au‑delà
est rare, mais peut avoir un intérêt dans les créations de très gros index.
Paramétrage de work_mem :
Si work_mem est trop bas, beaucoup d’opérations de tri, y compris nombre de jointures, ne
s’effectueront pas en RAM. Par exemple, si une jointure par hachage impose d’utiliser 100 Mo en
mémoire, mais que work_mem vaut 10 Mo, PostgreSQL écrira des dizaines de Mo sur disque à chaque
appel de la jointure. Si, par contre, le paramètre work_mem vaut 60 Mo, aucune écriture n’aura lieu
sur disque, ce qui accélérera forcément la requête.
Trop de fichiers temporaires peuvent ralentir les opérations, voire saturer le disque. Un work_mem
trop bas peut aussi contraindre le planificateur à choisir des plans d’exécution moins optimaux.
Par contre, si work_mem est trop haut, et que trop de requêtes le consomment simul‑
¾ tanément, le danger est de saturer la RAM. Il n’existe en effet pas de limite à la consom‑
mation des sessions de PostgreSQL, ni globalement ni par session !
Or l’overcommit n’est pas paramétré sous Linux par défaut : la première conséquence
de la saturation est l’assèchement du cache système (complémentaire de celui de Post‑
greSQL), et la dégradation des performances. Puis le système va se mettre à swapper,
avec à la clé un ralentissement général et durable. Enfin le noyau, à court de mémoire,
peut être amené à tuer un processus de PostgreSQL. Cela mène à l’arrêt de l’instance,
ou plus fréquemment à son redémarrage brutal avec coupure de toutes les connexions
et requêtes en cours.
On obtient alors, sur un serveur dédié avec 16 Go de RAM et 200 connexions autorisées :
work_mem = 80MB
Soit, pour une machine dédiée avec 16 Go de RAM, donc 4 Go de shared buffers, et 200 connections :
work_mem = 240MB
Dans l’idéal, si l’on a le temps pour une étude, on montera work_mem jusqu’à voir disparaître
l’essentiel des fichiers temporaires dans les traces, tout en restant loin de saturer la RAM lors des pics
de charge.
1
https://dali.bo/j1_html#configuration‑du‑oom
2
https://thebuild.com/blog/2023/03/13/everything‑you‑know‑about‑setting‑work_mem‑is‑wrong/
En pratique, le défaut de 4 Mo est très conservateur, souvent insuffisant. Généralement, la valeur varie
entre 10 et 100 Mo. Au‑delà de 100 Mo, il y a souvent un problème ailleurs : des tris sur de trop gros
volumes de données, une mémoire insuffisante, un manque d’index (utilisés pour les tris), etc. Des
valeurs vraiment grandes ne sont valables que sur des systèmes d’infocentre.
Augmenter globalement la valeur du work_mem peut parfois mener à une consommation excessive
de mémoire. Il est possible de ne la modifier que le temps d’une session pour les besoins d’une re‑
quête ou d’un traitement particulier :
SET work_mem TO '30MB' ;
hash_mem_multiplier :
À partir de PostgreSQL 13, un paramètre multiplicateur peut s’appliquer à certaines opérations parti‑
culières (le hachage, lors de jointures ou agrégations). Nommé hash_mem_multiplier, il vaut 1
par défaut en versions 13 et 14, et 2 à partir de la 15. hash_mem_multiplier permet de donner
plus de RAM à ces opérations sans augmenter globalement work_mem.
Tables temporaires
Les tables temporaires (et leurs index) sont locales à chaque session, et disparaîtront avec elle. Elles
sont tout de même écrites sur disque dans le répertoire de la base.
Le cache dédié à ces tables pour minimiser les accès est séparé des shared buffers, parce qu’il est
propre à la session. Sa taille dépend du paramètre temp_buffers. La valeur par défaut (8 Mo) peut
être insuffisante dans certains cas pour éviter les accès aux fichiers de la table. Elle doit être augmen‑
tée avant la création de la table temporaire.
PostgreSQL dispose de son propre mécanisme de cache. Toute donnée lue l’est de ce cache. Si la don‑
née n’est pas dans le cache, le processus devant effectuer cette lecture l’y recopie avant d’y accéder
dans le cache.
L’unité de travail du cache est le bloc (de 8 ko par défaut) de données. C’est‑à‑dire qu’un processus
charge toujours un bloc dans son entier quand il veut lire un enregistrement. Chaque bloc du cache
correspond donc exactement à un bloc d’un fichier d’un objet. Cette information est d’ailleurs, bien
sûr, stockée en en‑tête du bloc de cache.
Tous les processus accèdent à ce cache unique. C’est la zone la plus importante, par la taille, de la
mémoire partagée. Toute modification de données est tracée dans le journal de transaction, puis mo‑
difiée dans ce cache. Elle n’est donc pas écrite sur le disque par le processus effectuant la modification,
sauf en dernière extrémité (voir Synchronisation en arrière plan.
Tout accès à un bloc nécessite la prise de verrous. Un pin lock, qui est un simple compteur, indique
qu’un processus se sert du buffer, et qu’il n’est donc pas réutilisable. C’est un verrou potentiellement
de longue durée. Il existe de nombreux autres verrous, de plus courte durée, pour obtenir le droit de
modifier le contenu d’un buffer, d’un enregistrement dans un buffer, le droit de recycler un buffer…
mais tous ces verrous n’apparaissent pas dans la table pg_locks, car ils sont soit de très courte
durée, soit partagés (comme le spin lock). Il est donc très rare qu’ils soient sources de contention,
mais le diagnostic d’une contention à ce niveau est difficile.
Les lectures et écritures de PostgreSQL passent toutefois toujours par le cache du système. Les deux
caches risquent donc de stocker les mêmes informations. Les algorithmes d’éviction sont différents
entre le système et PostgreSQL, PostgreSQL disposant de davantage d’informations sur l’utilisation
des données, et le type d’accès qui y est fait. La redondance est donc habituellement limitée.
Dimensionner correctement ce cache est important pour de nombreuses raisons.
Un cache trop petit :
– ralentit l’accès aux données, car des données importantes risquent de ne plus s’y trouver ;
– force l’écriture de données sur le disque, ralentissant les sessions qui auraient pu effectuer uni‑
quement des opérations en mémoire ;
– limite le regroupement d’écritures, dans le cas où un bloc viendrait à être modifié plusieurs fois.
– limite l’efficacité du cache système en augmentant la redondance de données entre les deux
caches ;
– peut ralentir PostgreSQL, car la gestion des shared_buffers a un coût de traitement ;
– réduit la mémoire disponible pour d’autres opérations (tris en mémoire notamment).
Ce paramétrage du cache est malgré tout moins critique que sur de nombreux autres SGBD : le cache
système limite la plupart du temps l’impact d’un mauvais paramétrage de shared_buffers, et il
est donc préférable de sous‑dimensionner shared_buffers que de le sur‑dimensionner.
Le défaut de 128 Mo n’est donc pas adapté à un serveur sur une machine récente.
Suivant les cas, une valeur inférieure ou supérieure à 25 % sera encore meilleure pour les perfor‑
mances, mais il faudra tester avec votre charge (en lecture, en écriture, et avec le bon nombre de
clients).
Modifier shared_buffers impose de redémarrer l’instance.
Un cache supplémentaire est disponible pour PostgreSQL : celui du système d’exploitation. Il est donc
intéressant de préciser à PostgreSQL la taille approximative du cache, ou du moins de la part du cache
qu’occupera PostgreSQL. Le paramètre effective_cache_size n’a pas besoin d’être très précis,
mais il permet une meilleure estimation des coûts par le moteur. Il est paramétré habituellement aux
alentours des 2/3 de la taille de la mémoire vive du système d’exploitation, pour un serveur dédié.
Par exemple pour une machine avec 32 Go de RAM, on peut paramétrer en première intention dans
postgresql.conf :
shared_buffers = '8GB'
effective_cache_size = '21GB'
® – Buffer pin
– Buffer dirty/clean
– Compteur d’utilisation
– Clocksweep
Les principales notions à connaître pour comprendre le mécanisme de gestion du cache de Post‑
greSQL sont :
Buffer pin
Chaque processus voulant accéder à un buffer (un bloc du cache) doit d’abord en forcer le maintien
en cache (to pin signifie épingler). Chaque processus accédant à un buffer incrémente ce compteur, et
le décrémente quand il a fini. Un buffer dont le pin est différent de 0 est donc utilisé et ne peut être
recyclé.
Buffer dirty/clean
Un buffer est dirty (« sale ») si son contenu dans le cache ne correspond pas à son contenu sur disque :
il a été modifié dans le cache, ce qui a généralement été journalisé, mais le fichier de données n’est
plus à jour.
Au contraire, un buffer non modifié (clean) peut être supprimé du cache immédiatement pour faire
de la place sans être réécrit sur le disque, ce qui est le moins coûteux.
Compteur d’utilisation
Cette technique vise à garder dans le cache les blocs les plus utilisés.
À chaque fois qu’un processus a fini de se servir d’un buffer (quand il enlève son pin), ce compteur
est incrémenté (à hauteur de 5 dans l’implémentation actuelle). Il est décrémenté par le clocksweep
évoqué plus bas.
Seul un buffer dont le compteur est à zéro peut voir son contenu remplacé par un nouveau bloc.
Clocksweep (ou algorithme de balayage)
Un processus ayant besoin de charger un bloc de données dans le cache doit trouver un buffer dis‑
ponible. Soit il y a encore des buffers vides (cela arrive principalement au démarrage d’une instance),
soit il faut libérer un buffer.
L’algorithme clocksweep parcourt la liste des buffers de façon cyclique à la recherche d’un buffer un‑
pinned dont le compteur d’utilisation est à zéro. Tout buffer visité voit son compteur décrémenté de
1. Le système effectue autant de passes que nécessaire sur tous les blocs jusqu’à trouver un buffer à
0. Ce clocksweep est effectué par chaque processus, au moment où ce dernier a besoin d’un nouveau
buffer.
Une table peut être plus grosse que les shared buffers. Sa lecture intégrale (lors d’un parcours complet
ou d’une opération de maintenance) ne doit pas mener à l’éviction de tous les blocs du cache.
PostgreSQL utilise donc plutôt un ring buffer quand la taille de la relation dépasse 1/4 de sha-
red_buffers. Un ring buffer est une zone de mémoire gérée à l’écart des autres blocs du cache.
Pour un parcours complet d’une table, cette zone est de 256 ko (taille choisie pour tenir dans un
cache L2). Si un bloc y est modifié (UPDATE…), il est traité hors du ring buffer comme un bloc sale
normal. Pour un VACUUM, la même technique est utilisée, mais les écritures se font dans le ring buffer.
Pour les écritures en masse (notamment COPY ou CREATE TABLE AS SELECT), une technique
similaire utilise un ring buffer de 16 Mo.
Le site The Internals of PostgreSQL4 et un README5 dans le code de PostgreSQL entrent plus en détail
sur tous ces sujets tout en restant lisibles.
4
https://www.interdb.jp/pg/pgsql08.html
5
https://github.com/postgres/postgres/blob/master/src/backend/storage/buffer/README
2 extensions en « contrib » :
® – pg_buffercache
– pg_prewarm
Deux extensions sont livrées dans les contribs de PostgreSQL qui impactent le cache.
pg_buffercache permet de consulter le contenu du cache (à utiliser de manière très ponctuelle).
La requête suivante indique les objets non système de la base en cours, présents dans le cache et s’ils
sont dirty ou pas :
pgbench=# CREATE EXTENSION pg_buffercache ;
pgbench=# SELECT
relname,
isdirty,
count(bufferid) AS blocs,
pg_size_pretty(count(bufferid) * current_setting ('block_size')::int) AS taille
FROM pg_buffercache b
INNER JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE relname NOT LIKE 'pg\_%'
GROUP BY
relname,
isdirty
ORDER BY 1, 2 ;
L’extension pg_prewarm permet de précharger un objet dans le cache de PostgreSQL (s’il y tient,
bien sûr) :
=# CREATE EXTENSION pg_prewarm ;
=# SELECT pg_prewarm ('nom_table_ou_index', 'buffer') ;
Il permet même de recharger dès le démarrage le contenu du cache lors d’un arrêt (voir la documen‑
tation6 ).
6
https://docs.postgresql.fr/current/pgprewarm.html
Afin de limiter les attentes des sessions interactives, PostgreSQL dispose de deux processus, le Back‑
ground Writer et le Checkpointer, tous deux essayant d’effectuer de façon asynchrone les écritures
des buffers sur le disque. Le but est que les temps de traitement ressentis par les utilisateurs soient
les plus courts possibles, et que les écritures soient lissées sur de plus grandes plages de temps (pour
ne pas saturer les disques).
Le Background Writer anticipe les besoins de buffers des sessions. À intervalle régulier, il se réveille et
synchronise un nombre de buffers proportionnel à l’activité sur l’intervalle précédent, dans ceux qui
seront examinés par les sessions pour les prochaines allocations. Quatre paramètres régissent son
comportement :
Le checkpointer est responsable d’un autre mécanisme : il synchronise tous les blocs modifiés lors
des checkpoints. Son rôle est d’effectuer cette synchronisation, en évitant de saturer les disques en
lissant la charge (voir plus loin).
Lors d’écritures intenses, il est possible que ces deux mécanismes soient débordés. Les processus ba‑
ckend peuvent alors écrire eux‑mêmes dans les fichiers de données (après les journaux de transaction,
bien sûr). Cette situation est évidemment à éviter, ce qui implique généralement de rendre le bgwriter
plus agressif.
3.5 JOURNALISATION
La journalisation, sous PostgreSQL, permet de garantir l’intégrité des fichiers, et la durabilité des opé‑
rations :
– L’intégrité : quoi qu’il arrive, exceptée la perte des disques de stockage bien sûr, la base reste
cohérente. Un arrêt d’urgence ne corrompra pas la base.
– Toute donnée validée (COMMIT) est écrite. Un arrêt d’urgence ne va pas la faire disparaître.
Pour cela, le mécanisme est relativement simple : toute modification affectant un fichier sera d’abord
écrite dans le journal. Les modifications affectant les vrais fichiers de données ne sont écrites qu’en
mémoire, dans les shared buffers. Elles seront écrites de façon asynchrone, soit par un processus re‑
cherchant un buffer libre, soit par le Background Writer, soit par le Checkpointer.
Les écritures dans le journal, bien que synchrones, sont relativement performantes, car elles sont
séquentielles (moins de déplacement de têtes pour les disques).
Essentiellement :
® – pg_wal/ : journaux de transactions
– sous‑répertoire archive_status
– nom : timeline, journal, segment
– ex : 00000002 00000142 000000FF
– pg_xact/ : état des transactions
– Ces fichiers sont vitaux !
Rappelons que les journaux de transaction sont des fichiers de 16 Mo par défaut, stockés dans PG-
DATA/pg_wal (pg_xlog avant la version 10), dont les noms comportent le numéro de timeline, un
numéro de journal de 4 Go et un numéro de segment, en hexadécimal.
$ ls -l
total 2359320
...
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007C
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007D
...
-rw------- 1 postgres postgres 33554432 Mar 26 16:25 000000020000014300000023
-rw------- 1 postgres postgres 33554432 Mar 26 16:25 000000020000014300000024
drwx------ 2 postgres postgres 16384 Mar 26 16:28 archive_status
3.5.2 Checkpoint
® – « Point de reprise »
– À partir d’où rejouer les journaux ?
– Données écrites au moins au niveau du checkpoint
– il peut durer
– Processus checkpointer
PostgreSQL trace les modifications de données dans les journaux WAL. Ceux‑ci sont générés au fur et
à mesure des écritures.
Si le système ou l’instance sont arrêtés brutalement, il faut que PostgreSQL puisse appliquer le
contenu des journaux non traités sur les fichiers de données. Il a donc besoin de savoir à partir d’où
rejouer ces données. Ce point est ce qu’on appelle un checkpoint, ou « point de reprise ».
Les principes sont les suivants :
Toute entrée dans les journaux est idempotente, c’est‑à‑dire qu’elle peut être appliquée plusieurs
fois, sans que le résultat final ne soit changé. C’est nécessaire, au cas où la récupération serait inter‑
rompue, ou si un fichier sur lequel la reprise est effectuée était plus récent que l’entrée qu’on souhaite
appliquer.
Tout fichier de journal antérieur à l’avant‑dernier point de reprise valide (ou au dernier à partir de
la version 11) peut être supprimé ou recyclé, car il n’est plus nécessaire à la récupération.
PostgreSQL a besoin des fichiers de données qui contiennent toutes les données jusqu’au point
de reprise. Ils peuvent être plus récents et contenir des informations supplémentaires, ce n’est pas
un problème.
C’est le processus checkpointer qui est responsable de l’écriture des buffers devant être synchro‑
nisés durant un checkpoint.
Une fois le checkpoint terminé, les journaux sont à priori inutiles. Ils peuvent être effacés pour re‑
descendre en‑dessous de la quantité définie par max_wal_size. Ils sont généralement « recyclés »,
c’est‑à‑dire renommés, et prêt à être réécris.
Cependant, les journaux peuvent encore être retenus dans pg_wal/ si l’archivage a été activé et que
certains n’ont pas été sauvegardés, ou si l’on garde des journaux pour des serveurs secondaires.
À cause de cela, le volume de l’ensemble des fichiers WAL peut largement dépasser la
Á taille fixée par max_wal_size. Ce n’est pas une valeur plafond !
Il existe un paramètre min_wal_size (défaut : 80 Mo) qui fixe la quantité minimale de journaux
à tout moment, même sans activité en écriture. Ils seront donc vides et prêts à être remplis en cas
d’écriture imprévue. Bien sûr, s’il y a des grosses écritures, PostgreSQL créera au besoin des journaux
supplémentaires, jusque max_wal_size, voire au‑delà. Mais il lui faudra les créer et les remplir in‑
tégralement de zéros avant utilisation.
Après un gros pic d’activité suivi d’un checkpoint et d’une période calme, la quantité de journaux va
très progressivement redescendre de max_wal_size à min_wal_size.
Des checkpoints espacés ont aussi pour effet de réduire la quantité totale de journaux écrits. En effet,
par défaut, un bloc modifié est intégralement écrit dans les journaux la première fois après un check‑
point. Par contre, un écart plus grand entre checkpoints peut allonger la restauration après un arrêt
brutal, car il y aura plus de journaux à rejouer.
En pratique, une petite instance se contentera du paramétrage de base ; une plus grosse montera
max_wal_size à quelques Go.
Pour min_wal_size, rien n’interdit de prendre une valeur élevée pour mieux absorber les montées
d’activité brusques.
Enfin, le checkpoint comprend un sync sur disque final. Toujours pour éviter des à‑coups d’écriture,
PostgreSQL demande au système d’exploitation de forcer un vidage du cache quand check-
point_flush_after a déjà été écrit (par défaut 256 ko). (Avant PostgreSQL 9.6, ceci se
paramétrait au niveau de Linux en abaissant les valeurs des sysctl vm.dirty_*.)
Quand le checkpoint démarre, il vise à lisser le débit en écriture, et donc le calcule à partir d’une
fraction de la durée d’exécution des précédents checkpoints. Cette fraction est fixée par check-
point_completion_target, et vaut 0,5 par défaut jusqu’en version 13 incluse, et 0,9 depuis la
version 14. PostgreSQL prévoit donc une durée de checkpoint de 150 secondes au départ, mais cette
valeur pourra évoluer ensuite suivant la durée réelle des checkpoints précédents. La valeur préconisée
pour checkpoint_completion_target est 0,9 (et pas plus) car elle permet de lisser davantage
les écritures dues aux checkpoints dans le temps.
Il est possible de suivre le déroulé des checkpoints dans les traces si log_checkpoints est à
on. De plus, si deux checkpoints sont rapprochés d’un intervalle de temps inférieur à check-
point_warning (défaut : 30 secondes), un message d’avertissement sera tracé. Une répétition
fréquente indique que max_wal_size est bien trop petit.
Enfin, répétons que max_wal_size n’est pas une limite en dur de la taille de pg_wal/.
La journalisation s’effectue par écriture dans les journaux de transactions. Toutefois, afin de ne pas
effectuer des écritures synchrones pour chaque opération dans les fichiers de journaux, les écritures
sont préparées dans des tampons (buffers) en mémoire. Les processus écrivent donc leur travail de
journalisation dans des buffers, ou WAL buffers. Ceux‑ci sont vidés quand une session demande vali‑
dation de son travail (COMMIT), qu’il n’y a plus de buffer disponible, ou que le walwriter se réveille
(wal_writer_delay).
Écrire un ou plusieurs blocs séquentiels de façon synchrone sur un disque a le même coût à peu de
chose près. Ce mécanisme permet donc de réduire fortement les demandes d’écriture synchrone sur
le journal, et augmente donc les performances.
Afin d’éviter qu’un processus n’ait tous les buffers à écrire à l’appel de COMMIT, et que cette opération
ne dure trop longtemps, un processus d’arrière‑plan appelé walwriter écrit à intervalle régulier tous
les buffers à synchroniser.
Ce mécanisme est géré par ces paramètres, rarement modifiés :
– wal_buffers : taille des WAL buffers, soit par défaut 1/32e de shared_buffers avec un
maximum de 16 Mo (la taille d’un segment), des valeurs supérieures (par exemple 128 Mo7 ) pou‑
vant être intéressantes pour les très grosses charges ;
– wal_writer_delay (défaut : 200 ms) : intervalle auquel le walwriter se réveille pour écrire
les buffers non synchronisés ;
– wal_writer_flush_after (défaut : 1 Mo) : au‑delà de cette valeur, les journaux écrits sont
synchronisés sur disque pour éviter l’accumulation dans le cache de l’OS.
Pour la fiabilité, on ne touchera pas à ceux‑ci :
– wal_sync_method : appel système à utiliser pour demander l’écriture synchrone (sauf très
rare exception, PostgreSQL détecte tout seul le bon appel système à utiliser) ;
7
https://thebuild.com/blog/2023/02/08/xtreme‑postgresql/
– full_page_writes : doit‑on réécrire une image complète d’une page suite à sa première
modification après un checkpoint ? Sauf cas très particulier, comme un système de fichiers Copy
On Write comme ZFS ou btrfs, ce paramètre doit rester à on pour éviter des corruptions de don‑
nées (et il est alors conseillé d’espacer les checkpoints pour réduire la volumétrie des journaux) ;
– fsync : doit‑on réellement effectuer les écritures synchrones ? Le défaut est on et il est très
fortement conseillé de le laisser ainsi en production. Avec off, les performances en écri‑
tures sont certes très accélérées, mais en cas d’arrêt d’urgence de l’instance, les données seront
totalement corrompues ! Ce peut être intéressant pendant le chargement initial d’une nouvelle
instance par exemple, sans oublier de revenir à on après ce chargement initial. (D’autres para‑
mètres et techniques existent pour accélérer les écritures et sans corrompre votre instance, si
vous êtes prêt à perdre certaines données non critiques : synchronous_commit à off, les
tables unlogged…)
® – wal_compression
– compression
– un peu de CPU
wal_compression compresse les blocs complets enregistrés dans les journaux de transactions,
réduisant le volume des WAL et la charge en écriture sur les disques.
Le rejeu des WAL est aussi plus rapide, ce qui accélère la réplication et la reprise après un crash. Le
prix est une augmentation de la consommation en CPU.
® – synchronous_commit
– perte potentielle de données validées
– commit_delay / commit_siblings
– Par session
Le coût d’un fsync est parfois rédhibitoire. Avec certains sacrifices, il est parfois possible d’améliorer
les performances sur ce point.
8
https://docs.postgresql.fr/current/wal‑configuration.html
® – Sauvegarde PITR
– Réplication physique
– par log shipping
– par streaming
Le système de journalisation de PostgreSQL étant très fiable, des fonctionnalités très intéressantes
ont été bâties dessus.
® – Repartir à partir :
– d’une vieille sauvegarde
– les journaux archivés
– Sauvegarde à chaud
– Sauvegarde en continu
– Paramètres
– wal_level, archive_mode
– archive_command ou archive_library
Les journaux permettent de rejouer, suite à un arrêt brutal de la base, toutes les modifications depuis
le dernier checkpoint. Les journaux devenus obsolète depuis le dernier checkpoint (l’avant‑dernier
avant la version 11) sont à terme recyclés ou supprimés, car ils ne sont plus nécessaires à la réparation
de la base.
Le but de l’archivage est de stocker ces journaux, afin de pouvoir rejouer leur contenu, non plus depuis
le dernier checkpoint, mais depuis une sauvegarde. Le mécanisme d’archivage permet de repartir
d’une sauvegarde binaire de la base (c’est‑à‑dire des fichiers, pas un pg_dump), et de réappliquer le
contenu des journaux archivés.
Il suffit de rejouer tous les journaux depuis le checkpoint précédent la sauvegarde jusqu’à la fin de la
sauvegarde, ou même à un point précis dans le temps. L’application de ces journaux permet de rendre
à nouveau cohérents les fichiers de données, même si ils ont été sauvegardés en cours de modifica‑
tion.
Ce mécanisme permet aussi de fournir une sauvegarde continue de la base, alors même que celle‑ci
travaille.
Tout ceci est vu dans le module Point In Time Recovery9 .
Même si l’archivage n’est pas en place, il faut connaître les principaux paramètres impliqués :
wal_level :
Il vaut replica par défaut depuis la version 10. Les journaux contiennent les informations néces‑
saires pour une sauvegarde PITR ou une réplication vers une instance secondaire.
Si l’on descend à minimal (défaut jusqu’en version 9.6 incluse), les journaux ne contiennent plus que
ce qui est nécessaire à une reprise après arrêt brutal sur le serveur en cours. Ce peut être intéressant
pour réduire, parfois énormément, le volume des journaux générés, si l’on a bien une sauvegarde non
PITR par ailleurs.
Le niveau logical est destiné à la réplication logique10 .
(Avant la version 9.6 existaient les niveaux intermédiaires archive et hot_standby, respective‑
ment pour l’archivage et pour un serveur secondaire en lecture seule. Ils sont toujours acceptés, et
assimilés à replica.)
archive_mode & archive_command/archive_library :
Il faut qu’archive_command soit à on pour activer l’archivage. Les journaux sont alors copiés grâce
à une commande shell à fournir dans archive_command ou grâce à une bibliothèque partagée
indiquée dans archive_library (version 15 ou postérieure). En général on y indiquera ce qu’exige
un outil de sauvegarde dédié (par exemple pgBackRest ou barman) dans sa documentation.
3.6.2 Réplication
La restauration d’une sauvegarde peut se faire en continu sur un autre serveur, qui peut même être
actif (bien que forcément en lecture seule). Les journaux peuvent être :
– envoyés régulièrement vers le secondaire, qui les rejouera : c’est le principe de la réplication par
log shipping ;
9
https://dali.bo/i2_html
10
https://dali.bo/w5_html
– envoyés par fragments vers cet autre serveur : c’est la réplication par streaming.
Ces thèmes ne seront pas développés ici. Signalons juste que la réplication par log shipping implique
un archivage actif sur le primaire, et l’utilisation de restore_command (et d’autres pour affiner)
sur le secondaire. Le streaming permet de se passer d’archivage, même si coupler streaming et sau‑
vegarde PITR est une bonne idée. Sur un PostgreSQL récent, le primaire a par défaut le nécessaire
activé pour se voir doté d’un secondaire : wal_level est à replica ; max_wal_senders per‑
met d’ouvrir des processus dédiés à la réplication ; et l’on peut garder des journaux en paramétrant
wal_keep_size (ou wal_keep_segments avant la version 13) pour limiter les risques de décro‑
chage du secondaire.
Une configuration supplémentaire doit se faire sur le serveur secondaire, indiquant comment récupé‑
rer les fichiers de l’archive, et comment se connecter au primaire pour récupérer des journaux. Elle a
lieu dans les fichiers recovery.conf (jusqu’à la version 11 comprise), ou (à partir de la version 12)
postgresql.conf dans les sections évoquées plus haut, ou postgresql.auto.conf.
3.7 CONCLUSION
Mémoire et journalisation :
® – complexe
– critique
– mais fiable
– et le socle de nombreuses fonctionnalités évoluées
3.7.1 Questions
3.8 QUIZ
https://dali.bo/m3_quiz
®
pgbench est un outil de test livré avec PostgreSQL. Son but est de faciliter la mise en place de bench‑
marks simples et rapides. Par défaut, il installe une base assez simple, génère une activité plus ou
moins intense et calcule le nombre de transactions par seconde et la latence. C’est ce qui sera fait ici
dans cette introduction. On peut aussi lui fournir ses propres scripts.
La documentation complète est sur https://docs.postgresql.fr/current/pgbench.html. L’auteur
principal, Fabien Coelho, a fait une présentation complète, en français, à la PG Session #9 de 201711 .
3.9.1 Installation
L’outil est installé avec les paquets habituels de PostgreSQL, client ou serveur suivant la distribu‑
tion.
Dans le cas des paquets RPM du PGDG, l’outil n’est pas dans le PATH par défaut ; il faudra donc fournir
le chemin complet :
/usr/pgsql-15/bin/pgbench
Il est préférable de créer un rôle non privilégié dédié, qui possédera la base de données :
CREATE ROLE pgbench LOGIN PASSWORD 'unmotdepassebienc0mplexe';
CREATE DATABASE pgbench OWNER pgbench ;
--scale permet de faire varier proportionnellement la taille de la base. À 100, la base pèsera 1,5 Go,
avec 10 millions de lignes dans la table principale pgbench_accounts :
pgbench@pgbench=# \d+
Liste des relations
Schéma | Nom | Type | Propriétaire | Taille | Description
--------+------------------+-------+--------------+---------+-------------
public | pg_buffercache | vue | postgres | 0 bytes |
public | pgbench_accounts | table | pgbench | 1281 MB |
public | pgbench_branches | table | pgbench | 40 kB |
public | pgbench_history | table | pgbench | 0 bytes |
public | pgbench_tellers | table | pgbench | 80 kB |
Pour simuler une activité de 20 clients simultanés, répartis sur 4 processeurs, pendant 100 se‑
condes :
11
https://youtu.be/aTwh_CgRaE0
NB : ne pas utiliser -d pour indiquer la base, qui signifie --debug pour pgbench, qui noiera alors
l’affichage avec ses requêtes :
UPDATE pgbench_accounts SET abalance = abalance + -3455 WHERE aid = 3789437;
SELECT abalance FROM pgbench_accounts WHERE aid = 3789437;
UPDATE pgbench_tellers SET tbalance = tbalance + -3455 WHERE tid = 134;
UPDATE pgbench_branches SET bbalance = bbalance + -3455 WHERE bid = 78;
INSERT INTO pgbench_history (tid, bid, aid, delta, mtime)
VALUES (134, 78, 3789437, -3455, CURRENT_TIMESTAMP);
Des tests rigoureux doivent durer bien sûr beaucoup plus longtemps que 100 s, par
Á exemple pour tenir compte des effets de cache, des checkpoints périodiques, etc.
Dans un second terminal, activer la trace des fichiers temporaires ainsi que l’affichage du niveau
LOG pour le client (il est possible de le faire sur la session uniquement).
Activer le chronométrage dans la session (\timing on). Lire les données de la table t2 en
triant par la colonne id Qu’observe‑t‑on ?
Configurer la valeur du paramètre work_mem à 100MB (il est possible de le faire sur la session
uniquement).
Lire de nouveau les données de la table t2 en triant par la colonne id. Qu’observe‑t‑on ?
Activer l’affichage de la durée des requêtes. Lire les données de la table t2, en notant la durée
d’exécution de la requête. Que contient le cache de PostgreSQL ?
Lire de nouveau les données de la table t2. Que contient le cache de PostgreSQL ?
Se connecter à la base de données b1 et extraire de nouveau toutes les données de la table t2.
Que contient le cache de PostgreSQL ?
3.10.4 Journaux
Se connecter à la base de données b0 et créer une table t2 avec une colonne id de type inte-
ger.
$ psql b0
List of functions
-[ RECORD 1 ]-------+---------------------
Schema | pg_catalog
Name | pg_relation_filepath
Result data type | text
Argument data types | regclass
Type | func
relname | pg_stat_reset_single_table_counters
---------+-------------------------------------
t2 |
-[ RECORD 1 ]---+-------
relid | 24576
schemaname | public
relname | t2
heap_blks_read | 3
heap_blks_hit | 0
idx_blks_read |
idx_blks_hit |
toast_blks_read |
toast_blks_hit |
tidx_blks_read |
tidx_blks_hit |
-[ RECORD 1 ]---+-------
relid | 24576
schemaname | public
relname | t2
heap_blks_read | 3
heap_blks_hit | 3
…
Les 3 blocs sont maintenant lus à partir du cache de PostgreSQL (colonne heap_blks_hit).
Lire de nouveau les données de la table t2 et consulter ses statistiques. Qu’observe‑t‑on ?
b0=# SELECT * FROM t2;
[...]
b0=# SELECT * FROM pg_statio_user_tables WHERE relname = 't2';
-[ RECORD 1 ]---+-------
relid | 24576
schemaname | public
relname | t2
heap_blks_read | 3
heap_blks_hit | 6
…
Quelle que soit la session, le cache étant partagé, tout le monde profite des données en cache.
Le nom du fichier dépend de l’installation et du moment. Pour suivre tout ce qui se passe dans le
fichier de traces, utiliser tail -f :
$ tail -f /var/lib/pgsql/15/data/log/postgresql-Tue.log
Dans un second terminal, activer la trace des fichiers temporaires ainsi que l’affichage du niveau
LOG pour le client (il est possible de le faire sur la session uniquement).
Dans la session :
postgres=# SET client_min_messages TO log;
SET
postgres=# SET log_temp_files TO 0;
SET
INSERT 0 1000000
Activer le chronométrage dans la session (\timing on). Lire les données de la table t2 en
triant par la colonne id Qu’observe‑t‑on ?
b0=# \timing on
b0=# SELECT * FROM t2 ORDER BY id;
PostgreSQL a dû créer un fichier temporaire pour stocker le résultat temporaire du tri. Ce fichier
s’appelle base/pgsql_tmp/pgsql_tmp1197.0. Il est spécifique à la session et sera détruit dès
qu’il ne sera plus utile. Il fait 14 Mo.
Écrire un fichier de tri sur disque prend évidemment un certain temps, c’est généralement à éviter si
le tri peut se faire en mémoire.
Configurer la valeur du paramètre work_mem à 100MB (il est possible de le faire sur la session
uniquement).
Lire de nouveau les données de la table t2 en triant par la colonne id. Qu’observe‑t‑on ?
id
---------
1
1
2
2
[...]
Time: 240.565 ms
Il n’y a plus de fichier temporaire généré. La durée d’exécution est bien moindre.
relfilenode | count
-------------+-------
| 16181
1249 | 57
1259 | 26
2659 | 15
[...]
Les valeurs exactes peuvent varier. La colonne relfilenode correspond à l’identifiant système de
la table. La deuxième colonne indique le nombre de blocs. Il y a ici 16 181 blocs non utilisés pour
l’instant dans le cache (126 Mo), ce qui est logique vu que PostgreSQL vient de redémarrer. Il y a
quelques blocs utilisés par des tables systèmes, mais aucune table utilisateur (on les repère par leur
OID supérieur à 16384).
Activer l’affichage de la durée des requêtes. Lire les données de la table t2, en notant la durée
d’exécution de la requête. Que contient le cache de PostgreSQL ?
b1=# \timing on
Timing is on.
id
---------
1
2
3
4
5
[...]
Time: 277.800 ms
relfilenode | count
-------------+-------
| 16220
16410 | 32
1249 | 29
1259 | 9
2659 | 8
[...]
Time: 30.694 ms
32 blocs ont été alloués pour la lecture de la table t2 (filenode 16410). Cela représente 256 ko alors
que la table fait 35 Mo :
b1=# SELECT pg_size_pretty(pg_table_size('t2'));
pg_size_pretty
----------------
35 MB
(1 row)
Time: 1.913 ms
Un simple SELECT * ne suffit donc pas à maintenir la table dans le cache. Par contre, ce deuxième
accès était déjà beaucoup rapide, ce qui suggère que le système d’exploitation, lui, a probablement
gardé les fichiers de la table dans son propre cache.
Lire de nouveau les données de la table t2. Que contient le cache de PostgreSQL ?
id
---------
[...]
Time: 184.529 ms
relfilenode | count
-------------+-------
| 16039
1249 | 85
16410 | 64
1259 | 39
2659 | 22
[...]
Il y en en a un peu plus dans le cache (en fait, 2 fois 32 ko). Plus vous exécuterez la requête, et plus
le nombre de blocs présents en cache augmentera. Sur le long terme, les 4425 blocs de la table t2
peuvent se retrouver dans le cache.
Pour cela, il faut ouvrir le fichier de configuration postgresql.conf et modifier la valeur du para‑
mètre shared_buffers à un quart de la mémoire. Par exemple :
shared_buffers = 2GB
Se connecter à la base de données b1 et extraire de nouveau toutes les données de la table t2.
Que contient le cache de PostgreSQL ?
b1=# \timing on
b1=# SELECT * FROM t2;
id
---------
1
[...]
Time: 340.444 ms
relfilenode | count
-------------+--------
| 257581
16410 | 4425
1249 | 29
[...]
PostgreSQL se retrouve avec toute la table directement dans son cache, et ce dès la première exécu‑
tion.
PostgreSQL est optimisé principalement pour du multi‑utilisateurs. Dans ce cadre, il faut pouvoir exé‑
cuter plusieurs requêtes en même temps et donc chaque requête ne peut pas monopoliser tout le
cache. De ce fait, chaque requête ne peut prendre qu’une partie réduite du cache. Mais plus le cache
est gros, plus la partie octroyée est grosse.
Modifier le contenu de la table t2, par exemple avec :
UPDATE t2 SET id = 0 WHERE id < 1000 ;
b1=# SELECT
relname,
isdirty,
count(bufferid) AS blocs,
pg_size_pretty(count(bufferid) * current_setting ('block_size')::int) AS taille
FROM pg_buffercache b
INNER JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE relname NOT LIKE 'pg\_%'
GROUP BY
relname,
isdirty
ORDER BY 1, 2 ;
15 blocs ont été modifiés (isdirty est à true), le reste n’a pas bougé.
b1=# CHECKPOINT;
CHECKPOINT
b1=# SELECT
relname,
isdirty,
count(bufferid) AS blocs,
pg_size_pretty(count(bufferid) * current_setting ('block_size')::int) AS taille
FROM pg_buffercache b
INNER JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE relname NOT LIKE 'pg\_%'
GROUP BY
relname,
isdirty
ORDER BY 1, 2 ;
Les blocs dirty ont tous été écrits sur le disque et sont devenus « propres ».
3.11.4 Journaux
Insérer 10 millions de lignes dans la table t2 avec generate_series. Que se passe‑t‑il au
niveau du répertoire pg_wal ?
$ ls -al $PGDATA/pg_wal
total 131076
$ ls -al $PGDATA/pg_wal
total 638984
drwx------ 3 postgres postgres 4096 Apr 16 17:55 .
drwx------ 20 postgres postgres 4096 Apr 16 17:48 ..
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000033
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000034
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000035
…
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000054
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000055
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000056
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000057
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000058
Des journaux de transactions sont écrits lors des écritures dans la base. Leur nombre varie avec
l’activité récente.
Exécuter un checkpoint. Que se passe‑t‑il au niveau du répertoire pg_wal ?
b1=# CHECKPOINT;
CHECKPOINT
$ ls -al $PGDATA/pg_wal
total 131076
total 638984
drwx------ 3 postgres postgres 4096 Apr 16 17:56 .
drwx------ 20 postgres postgres 4096 Apr 16 17:48 ..
-rw------- 1 postgres postgres 16777216 Apr 16 17:56 000000010000000000000059
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 00000001000000000000005A
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 00000001000000000000005B
…
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000079
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 00000001000000000000007A
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 00000001000000000000007B
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 00000001000000000000007C
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 00000001000000000000007D
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 00000001000000000000007E
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 00000001000000000000007F
drwx------ 2 postgres postgres 6 Apr 16 15:01 archive_status
Le nombre de journaux n’a pas forcément décru, mais le dernier journal d’avant le checkpoint est à
présent le plus ancien (selon l’ordre des noms des journaux).
Ici, il n’y a ni PITR ni archivage. Les anciens journaux sont donc totalement inutiles et sont donc re‑
cyclés : renommés, il sont prêts à être remplis à nouveau. Noter que leur date de création n’a pas été
mise à jour !
139
DALIBO Formations
4.1 INTRODUCTION
PostgreSQL s’appuie sur un modèle de gestion de transactions appelé MVCC. Nous allons expliquer
cet acronyme, puis étudier en profondeur son implémentation dans le moteur.
Cette technologie a en effet un impact sur le fonctionnement et l’administration de PostgreSQL.
4.2 AU MENU
® – Présentation de MVCC
– Niveaux d’isolation
– Implémentation de MVCC de PostgreSQL
– Les verrous
– Le mécanisme TOAST
MVCC est un sigle signifiant MultiVersion Concurrency Control, ou « contrôle de concurrence multi‑
version ».
Le principe est de faciliter l’accès concurrent de plusieurs utilisateurs (sessions) à la base en disposant
en permanence de plusieurs versions différentes d’un même enregistrement. Chaque session peut
travailler simultanément sur la version qui s’applique à son contexte (on parle d’« instantané » ou de
snapshot).
Par exemple, une transaction modifiant un enregistrement va créer une nouvelle version de cet en‑
registrement. Mais celui‑ci ne devra pas être visible des autres transactions tant que le travail de mo‑
dification n’est pas validé en base. Les autres transactions verront donc une ancienne version de cet
enregistrement. La dénomination technique est « lecture cohérente » (consistent read en anglais).
Précisons que la granularité des modifications est bien l’enregistrement (ou ligne) d’une table. Modi‑
fier un champ (colonne) revient à modifier la ligne. Deux transactions ne peuvent pas modifier deux
champs différents d’un même enregistrement sans entrer en conflit, et les verrous portent toujours
sur des lignes entières.
Avant d’expliquer en détail MVCC, voyons l’autre solution de gestion de la concurrence qui s’offre à
nous, afin de comprendre le problème que MVCC essaye de résoudre.
Une table contient une liste d’enregistrements.
– Une transaction voulant consulter un enregistrement doit le verrouiller (pour s’assurer qu’il
n’est pas modifié) de façon partagée, le consulter, puis le déverrouiller.
– Une transaction voulant modifier un enregistrement doit le verrouiller de façon exclusive (per‑
sonne d’autre ne doit pouvoir le modifier ou le consulter), le modifier, puis le déverrouiller.
Cette solution a l’avantage de la simplicité : il suffit d’un gestionnaire de verrous pour gérer l’accès
concurrent aux données. Elle a aussi l’avantage de la performance, dans le cas où les attentes de ver‑
rous sont peu nombreuses, la pénalité de verrouillage à payer étant peu coûteuse.
Elle a par contre des inconvénients :
– Les verrous sont en mémoire. Leur nombre est donc probablement limité. Que se passe‑t‑il si
une transaction doit verrouiller 10 millions d’enregistrements ? Des mécanismes de promotion
de verrou sont implémentés. Les verrous lignes deviennent des verrous bloc, puis des verrous
table. Le nombre de verrous est limité, et une promotion de verrou peut avoir des consé‑
quences dramatiques ;
– Un processus devant lire un enregistrement devra attendre la fin de la modification de celui‑ci.
Ceci entraîne rapidement de gros problèmes de contention. Les écrivains bloquent les lec‑
teurs, et les lecteurs bloquent les écrivains. Évidemment, les écrivains se bloquent entre eux,
mais cela est normal (il n’est pas possible que deux transactions modifient le même enregistre‑
ment simultanément, chacune sans conscience de ce qu’a effectué l’autre) ;
– Un ordre SQL (surtout s’il dure longtemps) n’a aucune garantie de voir des données cohérentes
du début à la fin de son exécution : si, par exemple, durant un SELECT long, un écrivain modifie
à la fois des données déjà lues par le SELECT, et des données qu’il va lire, le SELECT n’aura
pas une vue cohérente de la table. Il pourrait y avoir un total faux sur une table comptable par
exemple, le SELECT ayant vu seulement une partie des données validées par une nouvelle tran‑
saction ;
– Comment annuler une transaction ? Il faut un moyen de défaire ce qu’une transaction a effectué,
au cas où elle ne se terminerait pas par une validation mais par une annulation.
C’est l’implémentation d’Oracle, par exemple. Un enregistrement, quand il doit être modifié, est re‑
copié précédemment dans le tablespace d’UNDO. La nouvelle version de l’enregistrement est ensuite
écrite par‑dessus. Ceci implémente le MVCC (les anciennes versions de l’enregistrement sont toujours
disponibles), et présente plusieurs avantages :
– Les enregistrements ne sont pas dupliqués dans la table. Celle‑ci ne grandit donc pas suite à
une mise à jour (si la nouvelle version n’est pas plus grande que la version précédente) ;
– Les enregistrements gardent la même adresse physique dans la table. Les index correspondant
à des données non modifiées de l’enregistrement n’ont donc pas à être modifiés eux‑mêmes,
les index permettant justement de trouver l’adresse physique d’un enregistrement par rapport
à une valeur.
– La gestion de l’undo est très complexe : comment décider ce qui peut être purgé ? Il arrive que la
purge soit trop agressive, et que des transactions n’aient plus accès aux vieux enregistrements
(erreur SNAPSHOT TOO OLD sous Oracle, par exemple) ;
– La lecture cohérente est complexe à mettre en œuvre : il faut, pour tout enregistrement
modifié, disposer des informations permettant de retrouver l’image avant modification de
l’enregistrement (et la bonne image, il pourrait y en avoir plusieurs). Il faut ensuite pouvoir le
reconstituer en mémoire ;
– Il est difficile de dimensionner correctement le fichier d’undo. Il arrive d’ailleurs qu’il soit trop
petit, déclenchant l’annulation d’une grosse transaction. Il est aussi potentiellement une source
de contention entre les sessions ;
– L’annulation (ROLLBACK) est très lente : il faut, pour toutes les modifications d’une transaction,
défaire le travail, donc restaurer les images contenues dans l’undo, les réappliquer aux tables
(ce qui génère de nouvelles écritures). Le temps d’annulation peut être supérieur au temps de
traitement initial devant être annulé.
Dans une table PostgreSQL, un enregistrement peut être stocké dans plusieurs versions. Une mo‑
dification d’un enregistrement entraîne l’écriture d’une nouvelle version de celui‑ci. Une ancienne
version ne peut être recyclée que lorsqu’aucune transaction ne peut plus en avoir besoin, c’est‑à‑
dire qu’aucune transaction n’a un instantané de la base plus ancien que l’opération de modification
de cet enregistrement, et que cette version est donc invisible pour tout le monde. Chaque version
d’enregistrement contient bien sûr des informations permettant de déterminer s’il est visible ou non
dans un contexte donné.
Les avantages de cette implémentation stockant plusieurs versions dans la table principale sont mul‑
tiples :
– La lecture cohérente est très simple à mettre en œuvre : à chaque session de lire la version qui
l’intéresse. La visibilité d’une version d’enregistrement est simple à déterminer ;
– Il n’y a pas d’undo. C’est un aspect de moins à gérer dans l’administration de la base ;
– Il n’y a pas de contention possible sur l’undo ;
– Il n’y a pas de recopie dans l’undo avant la mise à jour d’un enregistrement. La mise à jour est
donc moins coûteuse ;
– L’annulation d’une transaction est instantanée : les anciens enregistrements sont toujours dis‑
ponibles.
Chaque transaction, en plus d’être atomique, s’exécute séparément des autres. Le niveau de sépara‑
tion demandé est un compromis entre le besoin applicatif (pouvoir ignorer sans risque ce que font
les autres transactions) et les contraintes imposées au niveau de PostgreSQL (performances, risque
d’échec d’une transaction). Quatre niveaux sont définis, ils ne sont pas tous implémentés par Post‑
greSQL.
Ce niveau d’isolation n’est disponible que pour les SGBD non‑MVCC. Il est très dangereux : il est pos‑
sible de lire des données invalides, ou temporaires, puisque tous les enregistrements de la table sont
lus, quels que soient leurs états. Il est utilisé dans certains cas où les performances sont cruciales, au
détriment de la justesse des données.
Sous PostgreSQL, ce mode n’est pas disponible. Une transaction qui demande le niveau d’isolation
READ UNCOMMITTED s’exécute en fait en READ COMMITTED.
Ce mode est le mode par défaut, et est suffisant dans de nombreux contextes. PostgreSQL étant MVCC,
les écrivains et les lecteurs ne se bloquent pas mutuellement, et chaque ordre s’exécute sur un instan‑
tané de la base (ce n’est pas un prérequis de READ COMMITTED dans la norme SQL). Il n’y a plus de
lectures d’enregistrements non valides (dirty reads). Il est toutefois possible d’avoir deux problèmes
majeurs d’isolation dans ce mode :
– Les lectures non‑répétables (non‑repeatable reads) : une transaction peut ne pas voir les mêmes
enregistrements d’une requête sur l’autre, si d’autres transactions ont validé des modifications
entre temps ;
– Les lectures fantômes (phantom reads) : des enregistrements peuvent ne plus satisfaire une
clause WHERE entre deux requêtes d’une même transaction.
Ce mode, comme son nom l’indique, permet de ne plus avoir de lectures non‑répétables. Deux ordres
SQL consécutifs dans la même transaction retourneront les mêmes enregistrements, dans la même
version. Ceci est possible car la transaction voit une image de la base figée. L’image est figée non au
démarrage de la transaction, mais à la première commande non TCL (Transaction Control Language)
de la transaction, donc généralement au premier SELECT ou à la première modification.
Cette image sera utilisée pendant toute la durée de la transaction. En lecture seule, ces transactions
ne peuvent pas échouer. Elles sont entre autres utilisées pour réaliser des exports des données : c’est
ce que fait pg_dump.
Dans le standard, ce niveau d’isolation souffre toujours des lectures fantômes, c’est‑à‑dire de lecture
d’enregistrements différents pour une même clause WHERE entre deux exécutions de requêtes. Ce‑
pendant, PostgreSQL est plus strict et ne permet pas ces lectures fantômes en REPEATABLE READ.
Autrement dit, un même SELECT renverra toujours le même résultat.
En écriture, par contre (ou SELECT FOR UPDATE, FOR SHARE), si une autre transaction a modifié
les enregistrements ciblés entre temps, une transaction en REPEATABLE READ va échouer avec
l’erreur suivante :
ERROR: could not serialize access due to concurrent update
Dans ce mode, toutes les transactions déclarées comme telles s’exécutent comme si elles étaient
seules sur la base, et comme si elles se déroulaient les unes à la suite des autres. Dès que cette garantie
ne peut plus être apportée, PostgreSQL annule celle qui entraînera le moins de perte de données.
Le niveau SERIALIZABLE est utile quand le résultat d’une transaction peut être influencé par une
transaction tournant en parallèle, par exemple quand des valeurs de lignes dépendent de valeurs
d’autres lignes : mouvements de stocks, mouvements financiers… avec calculs de stocks. Autrement
dit, si une transaction lit des lignes, elle a la garantie que leurs valeurs ne seront pas modifiées jusqu’à
son COMMIT, y compris par les transactions qu’elle ne voit pas — ou bien elle tombera en erreur.
Au niveau SERIALIZABLE (comme en REPEATABLE READ), il est donc essentiel de pouvoir re‑
jouer une transaction en cas d’échec. Par contre, nous simplifions énormément tous les autres points
du développement. Il n’y a plus besoin de SELECT FOR UPDATE, solution courante mais très gê‑
nante pour les transactions concurrentes. Les triggers peuvent être utilisés sans soucis pour valider
des opérations.
Ce mode doit être mis en place globalement, car toute transaction non sérialisable peut en théorie
s’exécuter n’importe quand, ce qui rend inopérant le mode sérialisable sur les autres.
La sérialisation utilise le « verrouillage de prédicats ». Ces verrous sont visibles dans la vue pg_locks
sous le nom SIReadLock, et ne gênent pas les opérations habituelles, du moins tant que la sérialisa‑
tion est respectée. Un enregistrement qui « apparaît » ultérieurement suite à une mise à jour réalisée
par une transaction concurrente déclenchera aussi une erreur de sérialisation.
Le wiki PostgreSQL1 , et la documentation officielle2 donnent des exemples, et ajoutent quelques
conseils pour l’utilisation de transactions sérialisables. Afin de tenter de réduire les verrous et le
nombre d’échecs :
– faire des transactions les plus courtes possibles (si possible uniquement ce qui a trait à
l’intégrité) ;
– limiter le nombre de connexions actives ;
– utiliser les transactions en mode READ ONLY dès que possible, voire en SERIALIZABLE
READ ONLY DEFERRABLE (au risque d’un délai au démarrage) ;
– augmenter certains paramètres liés aux verrous, c’est‑à‑dire augmenter la mémoire dédiée ; car
si elle manque, des verrous de niveau ligne pourraient être regroupés en verrous plus larges et
plus gênants ;
– éviter les parcours de tables (Seq Scan), et donc privilégier les accès par index.
1
https://wiki.postgresql.org/wiki/SSI/fr
2
https://docs.postgresql.fr/current/transaction‑iso.html#XACT‑SERIALIZABLE
® – 1 bloc = 8 ko
– ctid = (bloc, item dans le bloc)
Figure 4/ .1: Répartition des lignes au sein d’un bloc (schéma de la documentation officielle, licence
PostgreSQL)
Le bloc (ou page) est l’unité de base de transfert pour les I/O, le cache mémoire… Il fait généralement
8 ko (ce qui ne peut être modifié qu’en recompilant). Les lignes y sont stockées avec des informa‑
tions d’administration telles que décrites dans le schéma ci‑dessus. Une ligne ne fait jamais partie
que d’un seul bloc (si cela ne suffit pas, un mécanisme que nous verrons plus tard, nommé TOAST, se
déclenche).
Nous distinguons dans ce bloc :
– un entête de page avec diverses informations, notamment la somme de contrôle (si activée) ;
– des identificateurs de 4 octets, pointant vers les emplacements des lignes au sein du bloc ;
– les lignes, stockées à rebours depuis la fin du bloc ;
– un espace spécial, vide pour les tables ordinaires, mais utilisé par les blocs d’index.
Le ctid identifie une ligne, en combinant le numéro du bloc (à partir de 0) et l’identificateur dans le
bloc (à partir de 1). Comme la plupart des champs administratifs liés à une ligne, il suffit de l’inclure
dans un SELECT pour l’afficher. L’exemple suivant affiche les premiers et derniers éléments des deux
blocs d’une table et vérifie qu’il n’y a pas de troisième bloc :
# CREATE TABLE deuxblocs AS SELECT i, i AS j FROM generate_series(1, 452) i;
SELECT 452
ctid | i | j
---------+-----+-----
(0,1) | 1 | 1
(0,226) | 226 | 226
(1,1) | 227 | 227
(1,226) | 452 | 452
Un ctid ne doit jamais servir à désigner une ligne de manière pérenne et ne doit pas
Á être utilisé dans des requêtes ! Il peut changer n’importe quand, notamment en cas
d’UPDATE ou de VACUUM FULL !
3
https://docs.postgresql.fr/current/storage‑page‑layout.html
4
https://docs.postgresql.fr/current/pageinspect.html#id‑1.11.7.33.5
Table initiale :
Ici, les deux enregistrements ont été créés par la transaction 100. Il s’agit peut‑être, par exemple, de
la transaction ayant importé tous les soldes à l’initialisation de la base.
® BEGIN;
UPDATE soldes SET solde = solde - 200 WHERE nom = 'M. Durand';
Nous décidons d’enregistrer un virement de 200 € du compte de M. Durand vers celui de Mme Mar‑
tin. Ceci doit être effectué dans une seule transaction : l’opération doit être atomique, sans quoi de
l’argent pourrait apparaître ou disparaître de la table.
Nous allons donc tout d’abord démarrer une transaction (ordre SQL BEGIN). PostgreSQL fournit
donc à notre session un nouveau numéro de transaction (150 dans notre exemple). Puis nous effec‑
tuerons :
UPDATE soldes SET solde = solde - 200 WHERE nom = 'M. Durand';
® UPDATE soldes SET solde = solde + 200 WHERE nom = 'Mme Martin';
Dans le cas le plus simple, 150 ayant été validée, une transaction 160 ne verra pas les premières ver‑
sions : xmax valant 150, ces enregistrements ne sont pas visibles. Elle verra les secondes versions,
puisque xmin = 150, et pas de xmax.
– La suppression d’un enregistrement s’effectue simplement par l’écriture d’un xmax dans la ver‑
sion courante ;
– Il n’y a rien à écrire dans les tables pour annuler une transaction. Il suffit de marquer la transac‑
tion comme étant annulée dans la CLOG.
4.7 CLOG
La CLOG est stockée dans une série de fichiers de 256 ko, stockés dans le répertoire pg_xact/ de
PGDATA (répertoire racine de l’instance PostgreSQL).
Chaque transaction est créée dans ce fichier dès son démarrage et est encodée sur deux bits
puisqu’une transaction peut avoir quatre états :
Nous avons donc un million d’états de transactions par fichier de 256 ko.
Annuler une transaction (ROLLBACK) est quasiment instantané sous PostgreSQL : il suffit d’écrire
TRANSACTION_STATUS_ABORTED dans l’entrée de CLOG correspondant à la transaction.
Toute modification dans la CLOG, comme toute modification d’un fichier de données (table, index,
séquence, vue matérialisée), est bien sûr enregistrée tout d’abord dans les journaux de transactions
(dans le répertoire pg_wal/).
® – Avantages :
– avantages classiques de MVCC (concurrence d’accès)
– implémentation simple et performante
– peu de sources de contention
– verrouillage simple d’enregistrement
– ROLLBACK instantané
– données conservées aussi longtemps que nécessaire
– Les lecteurs ne bloquent pas les écrivains, ni les écrivains les lecteurs ;
– Le code gérant les instantanés est simple, ce qui est excellent pour la fiabilité, la maintenabilité
et les performances ;
– Les différentes sessions ne se gênent pas pour l’accès à une ressource commune (l’undo) ;
– Un enregistrement est facilement identifiable comme étant verrouillé en écriture : il suffit qu’il
ait une version ayant un xmax correspondant à une transaction en cours ;
– L’annulation est instantanée : il suffit d’écrire le nouvel état de la transaction annulée dans la
CLOG. Pas besoin de restaurer les valeurs précédentes, elles sont toujours là ;
– Les anciennes versions restent en ligne aussi longtemps que nécessaire. Elles ne pourront être
effacées de la base qu’une fois qu’aucune transaction ne les considérera comme visibles.
(Précisons toutefois que ceci est une vision un peu simplifiée pour les cas courants. La signification
du xmax est parfois altérée par des bits positionnés dans des champs systèmes inaccessibles par
l’utilisateur. Cela arrive, par exemple, quand des transactions insèrent des lignes portant une clé étran‑
gère, pour verrouiller la ligne pointée par cette clé, laquelle ne doit pas disparaître pendant la durée
de cette transaction.)
Comme toute solution complexe, l’implémentation MVCC de PostgreSQL est un compromis. Les avan‑
tages cités précédemment sont obtenus au prix de concessions.
4.9.0.1 VACUUM
Il faut nettoyer les tables de leurs enregistrements morts. C’est le travail de la commande VACUUM. Il a
un avantage sur la technique de l’undo : le nettoyage n’est pas effectué par un client faisant des mises
à jour (et créant donc des enregistrements morts), et le ressenti est donc meilleur.
VACUUM peut se lancer à la main, mais dans le cas général on s’en remet à l’autovacuum, un démon
qui lance les VACUUM (et bien plus) en arrière‑plan quand il le juge nécessaire. Tout cela sera traité en
détail par la suite.
4.9.0.2 Bloat
Les tables sont forcément plus volumineuses que dans l’implémentation par undo, pour deux rai‑
sons :
– les informations de visibilité y sont stockées, il y a donc un surcoût d’une douzaine d’octets par
enregistrement ;
– il y a toujours des enregistrements morts dans une table, une sorte de fond de roulement, qui se
stabilise quand l’application est en régime stationnaire.
4.9.0.3 Visibilité
Les index n’ont pas d’information de visibilité. Il est donc nécessaire d’aller vérifier dans la table
associée que l’enregistrement trouvé dans l’index est bien visible. Cela a un impact sur le temps
d’exécution de requêtes comme SELECT count(*) sur une table : dans le cas le plus défavorable,
il est nécessaire d’aller visiter tous les enregistrements pour s’assurer qu’ils sont bien visibles. La
visibility map permet de limiter cette vérification aux données les plus récentes.
Un VACUUM ne s’occupe pas de l’espace libéré par des colonnes supprimées (fragmentation verticale).
Un VACUUM FULL est nécessaire pour reconstruire la table.
Le numéro de transaction stocké dans les tables de PostgreSQL est sur 32 bits, même si PostgreSQL
utilise en interne 64 bits. Il y aura donc dépassement de ce compteur au bout de 4 milliards de tran‑
sactions. Sur les machines actuelles, cela peut être atteint relativement rapidement.
En fait, ce compteur est cyclique, et toute transaction considère que les 2 milliards de transactions
supérieures à la sienne sont dans le futur, et les 2 milliards inférieures dans le passé. Le risque de
bouclage est donc plus proche des 2 milliards. Si nous bouclions, de nombreux enregistrements de‑
viendraient invisibles, car validés par des transactions futures. Heureusement PostgreSQL l’empêche.
Au fil des versions, la protection est devenue plus efficace.
La parade consiste à « geler » les lignes avec des identifiants de transaction suffisamment anciens.
C’est le rôle de l’opération appelée VACUUM FREEZE. Ce dernier peut être déclenché manuellement,
mais il fait aussi partie des tâches de maintenance habituellement gérées par le démon autovacuum,
en bonne partie en même temps que les VACUUM habituels. Un VACUUM FREEZE n’est pas bloquant,
mais les verrous sont parfois plus gênants que lors d’un VACUUM simple.
Si cela ne suffit pas, le moteur déclenche automatiquement un VACUUM FREEZE quand les tables
sont trop âgées, et ce, même si autovacuum est désactivé.
Quand le stock de transactions disponibles descend en dessous de 40 millions (10 millions avant la
version 14), des messages d’avertissements apparaissent dans les traces.
Dans le pire des cas, après bien des messages d’avertissements, le moteur refuse toute nouvelle tran‑
saction dès que le stock de transactions disponibles se réduit à 3 millions (1 million avant la version
14 ; valeurs codées en dur).
Il faudra alors lancer un VACUUM FREEZE manuellement. Ceci ne peut plus arriver qu’exceptionnellement
(par exemple si une transaction préparée a été oubliée depuis 2 milliards de transactions et qu’aucune
supervision ne l’a détectée).
VACUUM FREEZE sera développé dans le module VACUUM et autovacuum5 . La documentation offi‑
cielle6 contient aussi un paragraphe sur ce sujet.
5
https://dali.bo/m5_html
6
https://docs.postgresql.fr/current/maintenance.html
– Heap‑Only Tuples (HOT) s’agit de pouvoir stocker, sous condition, plusieurs versions du même
enregistrement dans le même bloc. Ceci permet au fur et à mesure des mises à jour de suppri‑
mer automatiquement les anciennes versions, sans besoin de VACUUM. Cela permet aussi de
ne pas toucher aux index, qui pointent donc grâce à cela sur plusieurs versions du même enre‑
gistrement. Les conditions sont les suivantes :
– Le bloc contient assez de place pour la nouvelle version (les enregistrements ne sont pas
chaînés entre plusieurs blocs). Afin que cette première condition ait plus de chance d’être
vérifiée, il peut être utile de baisser la valeur du paramètre fillfactor pour une table
donnée (cf documentation officielle7 ) ;
– Aucune colonne indexée n’a été modifiée par l’opération.
– Chaque table possède une Free Space Map avec une liste des espaces libres de chaque table.
Elle est stockée dans les fichiers *_fsm associés à chaque table.
– La Visibility Map permet de savoir si l’ensemble des enregistrements d’un bloc est visible. En cas
de doute, ou d’enregistrement non visible, le bloc n’est pas marqué comme totalement visible.
Cela permet à la phase 1 du traitement de VACUUM de ne plus parcourir toute la table, mais
uniquement les enregistrements pour lesquels la Visibility Map est à faux (des données sont
potentiellement obsolètes dans le bloc). À l’inverse, les parcours d’index seuls utilisent cette
Visibility Map pour savoir s’il faut aller voir les informations de visibilité dans la table. VACUUM
repositionne la Visibility Map à vrai après nettoyage d’un bloc, si tous les enregistrements sont
visibles pour toutes les sessions. Enfin, depuis la 9.6, elle repère aussi les bloc entièrement gelés
pour accélérer les VACUUM FREEZE.
Toutes ces optimisations visent le même but : rendre VACUUM le moins pénalisant possible, et simpli‑
fier la maintenance.
7
https://docs.postgresql.fr/current/sql‑createtable.html#SQL‑CREATETABLE‑STORAGE‑PARAMETERS
Le gestionnaire de verrous de PostgreSQL est capable de gérer des verrous sur des tables, sur des en‑
registrements, sur des ressources virtuelles. De nombreux types de verrous sont disponibles, chacun
entrant en conflit avec d’autres.
Chaque opération doit tout d’abord prendre un verrou sur les objets à manipuler. Si le verrou ne peut
être obtenu immédiatement, par défaut PostgreSQL attendra indéfiniment qu’il soit libéré.
Ce verrou en attente peut lui‑même imposer une attente à d’autres sessions qui s’intéresseront
au même objet. Si ce verrou en attente est bloquant (cas extrême : un VACUUM FULL sans
SKIP_LOCKED lui‑même bloqué par une session qui tarde à faire un COMMIT), il est possible
d’assister à un phénomène d’empilement de verrous en attente.
Les noms des verrous peuvent prêter à confusion : ROW SHARE par exemple est un
Á verrou de table, pas un verrou d’enregistrement. Il signifie qu’on a pris un verrou sur
une table pour y faire des SELECT FOR UPDATE par exemple. Ce verrou est en conflit
avec les verrous pris pour un DROP TABLE, ou pour un LOCK TABLE.
Le gestionnaire de verrous détecte tout verrou mortel (deadlock) entre deux sessions. Un deadlock est
la suite de prise de verrous entraînant le blocage mutuel d’au moins deux sessions, chacune étant en
attente d’un des verrous acquis par l’autre.
Il est possible d’accéder aux verrous actuellement utilisés sur une instance par la vue pg_locks.
Le gestionnaire de verrous fournit des verrous sur enregistrement. Ceux‑ci sont utilisés pour
verrouiller un enregistrement le temps d’y écrire un xmax, puis libérés immédiatement.
– D’abord, chaque transaction verrouille son objet « identifiant de transaction » de façon exclu‑
sive.
– Une transaction voulant mettre à jour un enregistrement consulte le xmax. Si ce xmax est celui
d’une transaction en cours, elle demande un verrou exclusif sur l’objet « identifiant de transac‑
tion » de cette transaction, qui ne lui est naturellement pas accordé. La transaction est donc
placée en attente.
– Enfin, quand l’autre transaction possédant le verrou se termine (COMMIT ou ROLLBACK), son
verrou sur l’objet « identifiant de transaction » est libéré, débloquant ainsi l’autre transaction,
qui peut reprendre son travail.
® – pg_locks :
– visualisation des verrous en place
– tous types de verrous sur objets
– Complexe à interpréter :
– verrous sur enregistrements pas directement visibles
Vue « pg_catalog.pg_locks »
Colonne | Type | Collationnement | NULL-able | …
--------------------+--------------------------+-----------------+-----------+-
locktype | text | | |
database | oid | | |
relation | oid | | |
page | integer | | |
tuple | smallint | | |
virtualxid | text | | |
transactionid | xid | | |
classid | oid | | |
objid | oid | | |
objsubid | smallint | | |
virtualtransaction | text | | |
pid | integer | | |
mode | text | | |
granted | boolean | | |
fastpath | boolean | | |
waitstart | timestamp with time zone | | |
– locktype est le type de verrou, les plus fréquents étant relation (table ou index), tran-
sactionid (transaction), virtualxid (transaction virtuelle, utilisée tant qu’une transac‑
tion n’a pas eu à modifier de données, donc à stocker des identifiants de transaction dans des
enregistrements) ;
– database est la base dans laquelle ce verrou est pris ;
– relation est l’OID de la relation cible si locktype vaut relation (ou page ou tuple) ;
– page est le numéro de la page dans une relation (pour un verrou de type page ou tuple)
cible ;
– tuple est le numéro de l’enregistrement cible (quand verrou de type tuple) ;
– virtualxid est le numéro de la transaction virtuelle cible (quand verrou de type vir-
tualxid) ;
– transactionid est le numéro de la transaction cible ;
– classid est le numéro d’OID de la classe de l’objet verrouillé (autre que relation) dans
pg_class. Indique le catalogue système, donc le type d’objet, concerné. Aussi utilisé pour les
advisory locks ;
– objid est l’OID de l’objet dans le catalogue système pointé par classid ;
– objsubid correspond à l’ID de la colonne de l’objet objid concerné par le verrou ;
– virtualtransaction est le numéro de transaction virtuelle possédant le verrou (ou ten‑
tant de l’acquérir si granted vaut f) ;
– pid est le PID (l’identifiant de processus système) de la session possédant le verrou ;
– mode est le niveau de verrouillage demandé ;
– granted signifie si le verrou est acquis ou non (donc en attente) ;
– fastpath correspond à une information utilisée surtout pour le débogage (fastpath est le mé‑
canisme d’acquisition des verrous les plus faibles) ;
– waitstart indique depuis quand le verrou est en attente.
La plupart des verrous sont de type relation, transactionid ou virtualxid. Une transaction
qui démarre prend un verrou virtualxid sur son propre virtualxid. Elle acquiert des verrous faibles
(ACCESS SHARE) sur tous les objets sur lesquels elle fait des SELECT, afin de garantir que leur struc‑
ture n’est pas modifiée sur la durée de la transaction. Dès qu’une modification doit être faite, la tran‑
saction acquiert un verrou exclusif sur le numéro de transaction qui vient de lui être affecté. Tout
objet modifié (table) sera verrouillé avec ROW EXCLUSIVE, afin d’éviter les CREATE INDEX non
concurrents, et empêcher aussi les verrouillages manuels de la table en entier (SHARE ROW EX-
CLUSIVE).
® – Nombre :
– max_locks_per_transaction (+ paramètres pour la sérialisation)
– Durée :
– lock_timeout (éviter l’empilement des verrous)
– deadlock_timeout (défaut 1 s)
– Trace :
– log_lock_waits
Nombre de verrous :
max_locks_per_transaction sert à dimensionner un espace en mémoire partagée destinée
aux verrous sur des objets (notamment les tables). Le nombre de verrous est :
max_locks_per_transaction × max_connections
La valeur par défaut de 64 est largement suffisante la plupart du temps. Il peut arriver qu’il faille le
monter, par exemple si l’on utilise énormément de partitions, mais le message d’erreur est explicite.
Le nombre maximum de verrous d’une session n’est pas limité à max_locks_per_transaction.
C’est une valeur moyenne. Une session peut acquérir autant de verrous qu’elle le souhaite pourvu
qu’au total la table de hachage interne soit assez grande. Les verrous de lignes sont stockés sur les
lignes et donc potentiellement en nombre infini.
Pour la sérialisation, les verrous de prédicat possèdent des paramètres spécifiques. Pour économiser
la mémoire, les verrous peuvent être regroupés par bloc ou relation (voir pg_locks pour le niveau
de verrouillage). Les paramètres respectifs sont :
Il faudra bien sûr retenter le VACUUM FULL plus tard, mais la production n’est pas bloquée plus de 3
secondes.
PostgreSQL recherche périodiquement les deadlocks entre transactions en cours. La périodicité par
défaut est de 1 s (paramètre deadlock_timeout), ce qui est largement suffisant la plupart du
temps : les deadlocks sont assez rares, alors que la vérification est quelque chose de coûteux. L’une
des transactions est alors arrêtée et annulée, pour que les autres puissent continuer :
postgres=*# DELETE FROM t_centmille_int WHERE i < 50000;
8
https://docs.postgresql.fr/current/runtime‑config‑locks.html#GUC‑MAX‑PRED‑LOCKS‑PER‑RELATION
S’il ne s’agit pas d’un deadlock, la transaction continuera, et le moment où elle obtiendra son verrou
sera également tracé :
LOG: process 457051 acquired ShareLock on transaction 35373775 after
18131.402 ms
CONTEXT: while deleting tuple (221,55) in relation "t_centmille_int"
STATEMENT: DELETE FROM t_centmille_int ;
LOG: duration: 18203.059 ms statement: DELETE FROM t_centmille_int ;
Principe du TOAST :
Une ligne ne peut déborder d’un bloc, et un bloc fait 8 ko (par défaut). Cela ne suffit pas pour certains
champs beaucoup plus longs, comme certains textes, mais aussi des types composés (json, jsonb,
hstore), ou binaires (bytea), et même numeric.
PostgreSQL sait compresser alors les champs, mais ça ne suffit pas forcément non plus. Le mécanisme
TOAST s’active alors. Il consiste à déporter le contenu de certains champs d’un enregistrement vers
une table système associée à la table principale, gérée de manière transparente pour l’utilisateur. Ce
mécanisme permet d’éviter qu’un enregistrement ne dépasse la taille d’un bloc.
Le mécanisme TOAST a d’autres intérêts :
– la partie principale d’une table ayant des champs très longs est moins grosse, alors que les « gros
champs » ont moins de chance d’être accédés systématiquement par le code applicatif ;
– ces champs peuvent être compressés de façon transparente, avec souvent de gros gains en
place ;
– si un UPDATE ne modifie pas un de ces champs « toastés », la table TOAST n’est pas mise à jour :
le pointeur vers l’enregistrement de cette table est juste « cloné » dans la nouvelle version de
l’enregistrement.
Politiques de stockage :
Chaque champ possède une propriété de stockage :
CREATE TABLE unetable (i int, t text, b bytea, j jsonb);
# \d+ unetable
Table « public.unetable »
Colonne | Type | Col... | NULL-able | Par défaut | Stockage | …
---------+---------+--------+-----------+------------+----------+--
i | integer | | | | plain |
t | text | | | | extended |
b | bytea | | | | extended |
j2 | jsonb | | | | extended |
Méthode d’accès : heap
– PLAIN permettant de stocker uniquement dans la table, sans compression (champs numé‑
riques ou dates notamment) ;
– MAIN permettant de stocker dans la table tant que possible, éventuellement compressé (poli‑
tique rarement utilisée) ;
– EXTERNAL permettant de stocker éventuellement dans la table TOAST, sans compression ;
– EXTENDED permettant de stocker éventuellement dans la table TOAST, éventuellement com‑
pressé (cas général des champs texte ou binaire).
Il est rare d’avoir à modifier ce paramétrage, mais cela arrive. Par exemple, certains longs champs
(souvent binaires, par exemple du JPEG) se compressent si mal qu’il ne vaut pas la peine de gaspiller
du CPU dans cette tâche. Dans le cas extrême où le champ compressé est plus grand que l’original,
PostgreSQL revient à la valeur originale, mais là aussi il y a gaspillage. Il peut alors être intéressant de
passer de EXTENDED à EXTERNAL, pour un gain de temps parfois non négligeable :
ALTER TABLE t1 ALTER COLUMN champ SET STORAGE EXTERNAL ;
La présence de ces tables n’apparaît guère que dans pg_class, par exemple ainsi :
SELECT * FROM pg_class c
WHERE c.relname = 'longs_textes'
-[ RECORD 1 ]-------+---------------
oid | 16614
relname | longs_textes
relnamespace | 2200
reltype | 16616
reloftype | 0
relowner | 10
relam | 2
relfilenode | 16614
reltablespace | 0
relpages | 35
reltuples | 2421
relallvisible | 35
reltoastrelid | 16617
…
-[ RECORD 2 ]-------+---------------
oid | 16617
relname | pg_toast_16614
relnamespace | 99
reltype | 16618
reloftype | 0
relowner | 10
relam | 2
relfilenode | 16617
reltablespace | 0
relpages | 73161
reltuples | 293188
relallvisible | 73161
reltoastrelid | 0
…
La partie TOAST est une table à part entière, avec une clé primaire. On ne peut ni ne doit y toucher !
\d+ pg_toast.pg_toast_16614
SELECT
oid AS table_oid,
c.relnamespace::regnamespace || '.' || relname AS TABLE,
reltoastrelid,
reltoastrelid::regclass::text AS table_toast,
reltuples AS nb_lignes_estimees,
pg_size_pretty(pg_table_size(c.oid)) AS " Table (dont TOAST)",
pg_size_pretty(pg_relation_size(c.oid)) AS " Heap",
pg_size_pretty(pg_relation_size(reltoastrelid)) AS " Toast",
pg_size_pretty(pg_indexes_size(reltoastrelid)) AS " Toast (PK)",
pg_size_pretty(pg_indexes_size(c.oid)) AS " Index",
pg_size_pretty(pg_total_relation_size(c.oid)) AS "Total"
FROM pg_class c
WHERE relkind = 'r'
AND relname = 'longs_textes'
\gx
-[ RECORD 1 ]------+------------------------
table_oid | 16614
table | public.longs_textes
reltoastrelid | 16617
table_toast | pg_toast.pg_toast_16614
nb_lignes_estimees | 2421
Table (dont TOAST) | 578 MB
Heap | 280 kB
Toast | 572 MB
Toast (PK) | 6448 kB
Index | 560 kB
Total | 579 MB
La taille des index sur les champs susceptibles d’être toastés est comptabilisée avec tous les index de
la table (la clé primaire de la table TOAST est à part).
Les tables TOAST restent forcément dans le même tablespace que la table principale. Leur mainte‑
nance (notamment le nettoyage par autovacuum) s’effectue en même temps que la table principale,
comme le montre un VACUUM VERBOSE.
Détails du mécanisme TOAST :
Les détails techniques du mécanisme TOAST sont dans la documentation officielle10 . En résumé, le
mécanisme TOAST est déclenché sur un enregistrement quand la taille d’un enregistrement dépasse
2 ko. Les champs « toastables » peuvent alors être compressés pour que la taille de l’enregistrement
redescende en‑dessous de 2 ko. Si cela ne suffit pas, des champs sont alors découpés et déportés
vers la table TOAST. Dans ces champs de la table principale, l’enregistrement ne contient plus qu’un
pointeur vers la table TOAST associée.
Un champ MAIN peut tout de même être stocké dans la table TOAST, si l’enregistrement dépasse 2 ko :
mieux vaut « toaster » que d’empêcher l’insertion.
Cette valeur de 2 ko convient généralement. Au besoin, on peut l’augmenter (à partir de la version 11)
en utilisant le paramètre de stockage toast_tuple_target ainsi :
ALTER TABLE t1 SET (toast_tuple_target = 3000);
10
https://doc.postgresql.fr/current/storage‑toast.html
default_toast_compression
---------------------------
pglz
c’est‑à‑dire que PostgreSQL utilise la zlib, seule compression disponible jusqu’en version 13 incluse.
À partir de la version 14, il est souvent préférable d’utiliser lz4, un nouvel algorithme, si PostgreSQL
a été compilé avec la bibliothèque du même nom (c’est le cas des paquets distribués par le PGDG).
L’activation demande soit de modifier la valeur par défaut dans postgresql.conf :
default_toast_compression = lz4
De manière générale, l’algorithme lz4 ne compresse pas mieux les données courantes que pglz,
mais cela dépend des usages. Surtout, lz4 est beaucoup plus rapide à compresser, et parfois à dé‑
compresser.
Par exemple, il peut accélérer une restauration logique avec beaucoup de données toastées et com‑
pressées. Si lz4 n’a pas été activé par défaut, il peut être utilisé dès le chargement :
$ PGOPTIONS='-c default_toast_compression=lz4' pg_restore …
lz4 est l’option à conseiller, même si, en toute rigueur, l’arbitrage entre consommations
b CPU en écriture ou lecture et place disque ne peut se faire qu’en testant soigneusement
avec les données réelles.
Une table TOAST peut contenir un mélange de lignes compressées de manière différentes. En effet,
l’utilisation SET COMPRESSION sur une colonne préexistante ne recompresse pas les données de la
table TOAST. De plus, pendant une requête, des données toastées lues par une requête, puis réinsé‑
rées sans être modifiées, sont recopiées vers les champs cibles telles quelles, sans étapes de décom‑
pression/recompression, et ce même si la compression de la cible est différente. Il existe une fonction
4.13 CONCLUSION
4.13.1 Questions
4.14 QUIZ
https://dali.bo/m4_quiz
®
Dans la base de données b2, créer une table t1 avec deux colonnes c1 de type integer et c2 de
type text.
Insérer 5 lignes dans table t1 avec des valeurs de (1, 'un') à (5, 'cinq').
Depuis une autre session, mettre en majuscules le texte de la troisième ligne de la table t1.
Fermer la transaction et ouvrir une nouvelle transaction, cette fois‑ci en REPEATABLE READ.
Depuis une autre session, mettre en majuscules le texte de la quatrième ligne de la table t1.
Revenir à la première session et lire de nouveau les données de la table t1. Que s’est‑il passé ?
Chaque mouvement donne lieu à une ligne de crédit ou de débit. Une ligne de crédit correspondra
à l’insertion d’une ligne avec une valeur mouvement positive. Une ligne de débit correspondra à
l’insertion d’une ligne avec une valeur mouvement négative. Nous exigeons que le client ait tou‑
jours un solde positif. Chaque opération bancaire se déroulera donc dans une transaction, qui se
terminera par l’appel à cette procédure de test :
CREATE PROCEDURE verifie_solde_positif (p_client int)
LANGUAGE plpgsql
AS $$
DECLARE
solde numeric ;
BEGIN
SELECT round(sum (mouvement), 0)
INTO solde
FROM mouvements_comptes
WHERE client = p_client ;
IF solde < 0 THEN
-- Erreur fatale
RAISE EXCEPTION 'Client % - Solde négatif : % !', p_client, solde ;
ELSE
-- Simple message
RAISE NOTICE 'Client % - Solde positif : %', p_client, solde ;
END IF ;
END ;
$$ ;
– Reproduire avec le client 3 le même scénario de deux débits parallèles de 500 €, mais avec
des transactions sérialisables : (BEGIN TRANSACTION ISOLATION LEVEL SERIA-
LIZABLE).
– Avant chaque COMMIT, consulter la vue pg_locks pour la table mouvements_comptes :
SELECT locktype, mode, pid, granted FROM pg_locks
WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'mouvements_comptes')
↪ ;
Commencer une transaction. Mettre en majuscules le texte de la troisième ligne de la table t2.
Ouvrir une autre session et lire les données de la table t2. Que faut‑il observer ?
Afficher xmin et xmax lors de la lecture des données de la table t2, dans chaque session.
Récupérer maintenant en plus le ctid lors de la lecture des données de la table t2, dans chaque
session.
Valider la transaction.
4.15.4 Verrous
Depuis une troisième session, récupérer la liste des sessions en attente avec la vue
pg_stat_activity.
Récupérer la liste des verrous sur cet objet. Quel processus a verrouillé la table t1 ?
Pour créer un verrou, effectuer un LOCK TABLE dans une transaction qu’il faudra laisser ou‑
verte.
$ createdb b2
Dans la base de données b2, créer une table t1 avec deux colonnes c1 de type integer et c2 de
type text.
CREATE TABLE
Insérer 5 lignes dans table t1 avec des valeurs de (1, 'un') à (5, 'cinq').
INSERT 0 5
BEGIN;
BEGIN
c1 | c2
----+--------
1 | un
2 | deux
3 | trois
4 | quatre
5 | cinq
Depuis une autre session, mettre en majuscules le texte de la troisième ligne de la table t1.
UPDATE 1
c1 | c2
----+--------
1 | un
2 | deux
4 | quatre
5 | cinq
3 | TROIS
Les modifications réalisées par la deuxième transaction sont immédiatement visibles par la première
transaction. C’est le cas des transactions en niveau d’isolation READ COMMITED.
Fermer la transaction et ouvrir une nouvelle transaction, cette fois‑ci en REPEATABLE READ.
ROLLBACK;
ROLLBACK;
BEGIN
c1 | c2
----+--------
1 | un
2 | deux
4 | quatre
5 | cinq
3 | TROIS
Depuis une autre session, mettre en majuscules le texte de la quatrième ligne de la table t1.
UPDATE 1
Revenir à la première session et lire de nouveau les données de la table t1. Que s’est‑il passé ?
c1 | c2
----+--------
1 | un
2 | deux
4 | quatre
5 | cinq
3 | TROIS
En niveau d’isolation REPEATABLE READ, la transaction est certaine de ne pas voir les modifications
réalisées par d’autres transactions (à partir de la première lecture de la table).
Une table de comptes bancaires contient 1000 clients, chacun avec 3 lignes de crédit et 600 € au to‑
tal :
CREATE TABLE mouvements_comptes
(client int,
mouvement numeric NOT NULL DEFAULT 0
);
CREATE INDEX on mouvements_comptes (client) ;
Chaque mouvement donne lieu à une ligne de crédit ou de débit. Une ligne de crédit correspondra
à l’insertion d’une ligne avec une valeur mouvement positive. Une ligne de débit correspondra à
l’insertion d’une ligne avec une valeur mouvement négative. Nous exigeons que le client ait tou‑
jours un solde positif. Chaque opération bancaire se déroulera donc dans une transaction, qui se
terminera par l’appel à cette procédure de test :
CREATE PROCEDURE verifie_solde_positif (p_client int)
LANGUAGE plpgsql
AS $$
DECLARE
solde numeric ;
BEGIN
SELECT round(sum (mouvement), 0)
INTO solde
FROM mouvements_comptes
WHERE client = p_client ;
IF solde < 0 THEN
-- Erreur fatale
RAISE EXCEPTION 'Client % - Solde négatif : % !', p_client, solde ;
ELSE
-- Simple message
RAISE NOTICE 'Client % - Solde positif : %', p_client, solde ;
END IF ;
END ;
$$ ;
Une table de comptes bancaires contient 1000 clients, chacun avec 3 lignes de crédit et 600 € au to‑
tal :
CREATE TABLE mouvements_comptes
(client int,
mouvement numeric NOT NULL DEFAULT 0
);
CREATE INDEX on mouvements_comptes (client) ;
Chaque mouvement donne lieu à une ligne de crédit ou de débit. Une ligne de crédit correspondra
à l’insertion d’une ligne avec une valeur mouvement positive. Une ligne de débit correspondra à
l’insertion d’une ligne avec une valeur mouvement négative. Nous exigeons que le client ait tou‑
jours un solde positif. Chaque opération bancaire se déroulera donc dans une transaction, qui se
terminera par l’appel à cette procédure de test :
CREATE PROCEDURE verifie_solde_positif (p_client int)
LANGUAGE plpgsql
AS $$
DECLARE
solde numeric ;
BEGIN
SELECT round(sum (mouvement), 0)
INTO solde
FROM mouvements_comptes
WHERE client = p_client ;
IF solde < 0 THEN
-- Erreur fatale
RAISE EXCEPTION 'Client % - Solde négatif : % !', p_client, solde ;
ELSE
-- Simple message
RAISE NOTICE 'Client % - Solde positif : %', p_client, solde ;
END IF ;
END ;
$$ ;
client | mouvement
--------+-----------
1 | 100
1 | 200
1 | 300
Première transaction :
BEGIN ;
INSERT INTO mouvements_comptes(client, mouvement) VALUES (1, -300) ;
CALL verifie_solde_positif (1) ;
COMMIT ;
La transaction est annulée : il est interdit de retirer plus d’argent qu’il n’y en a.
En effet, cette deuxième session ne voit pas encore le débit de la première session.
Les deux tests étant concluants, les deux sessions committent :
COMMIT ; --session 1
COMMIT
COMMIT ; --session 2
COMMIT
Les deux sessions en parallèle sont donc un moyen de contourner la sécurité, qui porte sur le résultat
d’un ensemble de lignes, et non juste sur la ligne concernée.
– Reproduire avec le client 3 le même scénario de deux débits parallèles de 500 €, mais avec
des transactions sérialisables : (BEGIN TRANSACTION ISOLATION LEVEL SERIA-
LIZABLE).
– Avant chaque COMMIT, consulter la vue pg_locks pour la table mouvements_comptes :
SELECT locktype, mode, pid, granted FROM pg_locks
WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'mouvements_comptes')
↪ ;
Première session :
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
INSERT INTO mouvements_comptes(client, mouvement) VALUES (3, -500) ;
CALL verifie_solde_positif (3) ;
SIReadLock est un verrou lié à la sérialisation : noter qu’il porte sur des lignes, portées par les deux
sessions. AccessShareLock empêche surtout de supprimer la table. RowExclusiveLock est
un verrou de ligne.
Validation de la première session :
COMMIT ;
COMMIT
Dans les verrous, il subsiste toujours les verrous SIReadLock de la session de PID 28304, qui a pour‑
tant committé :
SELECT locktype, mode, pid, granted
FROM pg_locks
WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'mouvements_comptes') ;
ERROR: could not serialize access due to read/write dependencies among transactions
DÉTAIL : Reason code: Canceled on identification as a pivot, during commit attempt.
ASTUCE : The transaction might succeed if retried.
La transaction est annulée pour erreur de sérialisation. En effet, le calcul effectué pendant la seconde
transaction n’est plus valable puisque la première a modifié les lignes qu’elle a lues.
La transaction annulée doit être rejouée de zéro, et elle tombera alors bien en erreur.
CREATE TABLE
INSERT 0 5
i | t
----+--------
1 | un
2 | deux
3 | trois
4 | quatre
5 | cinq
Commencer une transaction. Mettre en majuscules le texte de la troisième ligne de la table t2.
BEGIN ;
UPDATE t2 SET t = upper(t) WHERE i = 3;
UPDATE 1
i | t
----+--------
1 | un
2 | deux
4 | quatre
5 | cinq
3 | TROIS
La ligne mise à jour n’apparaît plus, ce qui est normal. Elle apparaît en fin de table. En effet, quand un
UPDATE est exécuté, la ligne courante est considérée comme morte et une nouvelle ligne est ajoutée,
avec les valeurs modifiées. Comme nous n’avons pas demandé de récupérer les résultats dans un
certain ordre (pas d’ORDER BY), les lignes sont simplement affichées dans leur ordre de stockage
dans les blocs de la table.
Ouvrir une autre session et lire les données de la table t2. Que faut‑il observer ?
i | t
----+--------
1 | un
2 | deux
3 | trois
4 | quatre
5 | cinq
L’ordre des lignes en retour n’est pas le même. Les autres sessions voient toujours l’ancienne version
de la ligne 3, puisque la transaction n’a pas encore été validée.
Afficher xmin et xmax lors de la lecture des données de la table t2, dans chaque session.
xmin | xmax | i | t
------+------+----+--------
753 | 0 | 1 | un
753 | 0 | 2 | deux
753 | 0 | 4 | quatre
753 | 0 | 5 | cinq
754 | 0 | 3 | TROIS
xmin | xmax | i | t
------+------+----+--------
753 | 0 | 1 | un
753 | 0 | 2 | deux
753 | 754 | 3 | trois
753 | 0 | 4 | quatre
753 | 0 | 5 | cinq
La transaction 754 est celle qui a réalisé la modification. La colonne xmin de la nouvelle version de
ligne contient ce numéro. De même pour la colonne xmax de l’ancienne version de ligne. PostgreSQL
se base sur cette information pour savoir si telle transaction peut lire telle ou telle ligne.
Récupérer maintenant en plus le ctid lors de la lecture des données de la table t2, dans chaque
session.
La colonne ctid contient une paire d’entiers. Le premier indique le numéro de bloc, le second le
numéro de l’enregistrement dans le bloc. Autrement dit, elle précise la position de l’enregistrement
sur le fichier de la table.
En récupérant cette colonne, nous voyons que la première session voit la nouvelle position (enregis‑
trement 6 du bloc 0) car elle est dans la transaction 754. Mais pour la deuxième session, cette nouvelle
transaction n’est pas validée, donc l’information d’effacement de la ligne 3 n’est pas prise en compte,
et on la voit toujours.
Valider la transaction.
COMMIT;
COMMIT
CREATE EXTENSION
Cette table est assez petite pour tenir dans le bloc 0 toute entière. pageinspect nous fournit le
détail de ce qu’il y a dans les lignes (la sortie est coupée en deux pour l’affichage) :
SELECT * FROM heap_page_items(get_raw_page('t2', 0)) ;
Tous les champs ne sont pas immédiatement compréhensibles, mais on peut lire facilement ceci :
– Les six lignes sont bien présentes, dont les deux versions de la ligne 3 ;
– Le t_ctid de l’ancienne ligne ne contient plus (0,3) mais l’adresse de la nouvelle ligne (soit
(0,6)).
Mais encore :
– Les lignes sont stockées à rebours depuis la fin du bloc : la première a un offset de 8160 octets
depuis le début, la dernière est à seulement 7960 octets du début ;
– la longueur de la ligne est indiquée par le champ lp_len : la ligne 4 est la plus longue ;
– t_infomask2 est un champ de bits : la valeur 16386 pour l’ancienne version nous indique
que le changement a eu lieu en utilisant la technologie HOT (la nouvelle version de la ligne est
maintenue dans le même bloc et un chaînage depuis l’ancienne est effectué) ;
– le champ t_data contient les valeurs de la ligne : nous devinons i au début (01 à 05), et la fin
correspond aux chaînes de caractères, précédée d’un octet lié à la taille.
La signification de tous les champs n’est pas dans la documentation mais se trouve dans le code de
PostgreSQL11 .
– Lancer VACUUM sur t2.
– Relancer la requête avec pageinspect.
– Comment est réorganisé le bloc ?
VACUUM (VERBOSE) t2 ;
Une seule ligne a bien été nettoyée. La requête suivante nous montre qu’elle a bien disparu :
SELECT * FROM heap_page_items(get_raw_page('t2', 0)) ;
11
https://doxygen.postgresql.org/itemid_8h_source.html
La 3è ligne ici a été remplacée par un simple pointeur sur la nouvelle version de la ligne dans le bloc
(mise à jour HOT).
On peut aussi remarquer que les lignes non modifiées ont été réorganisées dans le bloc : là où se
trouvait l’ancienne version de la 3è ligne (à 8080 octets du début de bloc) se trouve à présent la 4è. Le
VACUUM opère ainsi une défragmentation des blocs qu’il nettoie.
Le vacuum ne se déclenche qu’à partir d’un certain nombre de lignes modifiées ou effacées (50 + 20%
de la table par défaut). On est encore très loin de ce seuil avec cette très petite table.
4.16.4 Verrous
Ouvrir une transaction et lire les données de la table t1. Ne pas terminer la transaction.
BEGIN;
SELECT * FROM t1;
c1 | c2
----+--------
1 | un
2 | deux
3 | TROIS
4 | QUATRE
5 | CINQ
La ligne intéressante est la ligne du DROP TABLE. Elle contient le mot clé waiting. Ce dernier
indique que l’exécution de la requête est en attente d’un verrou sur un objet.
Depuis une troisième session, récupérer la liste des sessions en attente avec la vue
pg_stat_activity.
\x
-[ RECORD 1 ]----+------------------------------
datid | 16387
datname | b2
pid | 2718
usesysid | 10
usename | postgres
application_name | psql
client_addr |
client_hostname |
client_port | -1
backend_start | 2018-11-02 15:56:45.38342+00
xact_start | 2018-11-02 15:57:32.82511+00
query_start | 2018-11-02 15:57:32.82511+00
state_change | 2018-11-02 15:57:32.825112+00
wait_event_type | Lock
wait_event | relation
state | active
backend_xid | 575
backend_xmin | 575
query_id |
query | drop table t1 ;
backend_type | client backend
-[ RECORD 2 ]----+------------------------------
datid | 16387
datname | b2
pid | 2719
usesysid | 10
usename | postgres
application_name | psql
client_addr |
client_hostname |
client_port | -1
backend_start | 2018-11-02 15:56:17.173784+00
xact_start | 2018-11-02 15:57:25.311573+00
query_start | 2018-11-02 15:57:25.311573+00
state_change | 2018-11-02 15:57:25.311573+00
wait_event_type | Client
wait_event | ClientRead
state | idle in transaction
backend_xid |
backend_xmin |
query_id |
query | SELECT * FROM t1;
backend_type | client backend
-[ RECORD 1 ]------+--------------------
locktype | relation
database | 16387
relation | 16394
page |
tuple |
virtualxid |
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 5/7
pid | 2718
mode | AccessExclusiveLock
granted | f
fastpath | f
waitstart |
-[ RECORD 1 ]
relname | t1
Noter que l’objet n’est visible dans pg_class que si l’on est dans la même base de données que
lui. D’autre part, la colonne oid des tables systèmes n’est pas visible par défaut dans les versions
antérieures à la 12, il faut demander explicitement son affichage pour la voir.
Récupérer la liste des verrous sur cet objet. Quel processus a verrouillé la table t1 ?
-[ RECORD 1 ]------+--------------------
locktype | relation
database | 16387
relation | 16394
page |
tuple |
virtualxid |
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 4/10
pid | 2719
mode | AccessShareLock
granted | t
fastpath | f
waitstart |
-[ RECORD 2 ]------+--------------------
locktype | relation
database | 16387
relation | 16394
page |
tuple |
virtualxid |
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 5/7
pid | 2718
mode | AccessExclusiveLock
granted | f
fastpath | f
waitstart |
Le processus de PID 2718 (le DROP TABLE) demande un verrou exclusif sur t1, mais ce verrou n’est
pas encore accordé (granted est à false). La session idle in transaction a acquis un verrou
Access Share, normalement peu gênant, qui n’entre en conflit qu’avec les verrous exclusifs.
-[ RECORD 1 ]----+------------------------------
datid | 16387
datname | b2
pid | 2719
usesysid | 10
usename | postgres
application_name | psql
client_addr |
client_hostname |
client_port | -1
backend_start | 2018-11-02 15:56:17.173784+00
xact_start | 2018-11-02 15:57:25.311573+00
query_start | 2018-11-02 15:57:25.311573+00
state_change | 2018-11-02 15:57:25.311573+00
wait_event_type | Client
wait_event | ClientRead
state | idle in transaction
backend_xid |
backend_xmin |
query_id |
query | SELECT * FROM t1;
backend_type | client backend
Il existe une fonction pratique indiquant quelles sessions bloquent une autre. En l’occurence, notre
DROP TABLE t1 est bloqué par :
SELECT pg_blocking_pids(2718);
-[ RECORD 1 ]----+-------
pg_blocking_pids | {2719}
À partir de là, il est possible d’annuler l’exécution de l’ordre bloqué, le DROP TABLE, avec la fonc‑
tion pg_cancel_backend(). Si l’on veut détruire le processus bloquant, il faudra plutôt utiliser la
fonction pg_terminate_backend() :
SELECT pg_terminate_backend (2719) ;
Dans ce dernier cas, vérifiez que la table a été supprimée, et que la session en statut idle in tran-
saction affiche un message indiquant la perte de la connexion.
Pour créer un verrou, effectuer un LOCK TABLE dans une transaction qu’il faudra laisser ou‑
verte.
197
DALIBO Formations
5.1 AU MENU
VACUUM est la contrepartie de la flexibilité du modèle MVCC. Derrière les différentes options de VA-
CUUM se cachent plusieurs tâches très différentes. Malheureusement, la confusion est facile. Il est
capital de les connaître et de comprendre leur fonctionnement.
Autovacuum permet d’automatiser le VACUUM et allège considérablement le travail de l’administrateur.
Il fonctionne généralement bien, mais il faut savoir le surveiller et l’optimiser.
VACUUM est né du besoin de nettoyer les lignes mortes. Au fil du temps il a été couplé à d’autres ordres
(ANALYZE, VACUUM FREEZE) et s’est occupé d’autres opérations de maintenance (création de la
visibility map par exemple).
autovacuum est un processus de l’instance PostgreSQL. Il est activé par défaut, et il fortement
conseillé de le conserver ainsi. Dans le cas général, son fonctionnement convient et il ne gênera pas
les utilisateurs.
L’autovacuum ne gère pas toutes les variantes de VACUUM (notamment pas le FULL).
La seconde passe se charge de nettoyer les entrées d’index. VACUUM possède une liste de tid (tuple
id) à invalider. Il parcourt donc tous les index de la table à la recherche de ces tid et les supprime.
En effet, les index sont triés afin de mettre en correspondance une valeur de clé (la colonne indexée
par exemple) avec un tid. Il n’est par contre pas possible de trouver un tid directement. Les pages
entièrement vides sont supprimées de l’arbre et stockées dans la liste des pages réutilisables, la Free
Space Map (FSM).
Cette phase peut être ignorée par deux mécanismes. Le premier mécanisme apparaît en version 12
où l’option INDEX_CLEANUP a été ajoutée. Ce mécanisme est donc manuel et permet de gagner du
temps sur l’opération de VACUUM. Cette option s’utilise ainsi :
VACUUM (VERBOSE, INDEX_CLEANUP off) nom_table ;
À partir de la version 14, un autre mécanisme, automatique cette fois, a été ajouté. Le but est
toujours d’exécuter rapidement le VACUUM, mais uniquement pour éviter le wraparound. Quand
la table atteint l’âge, très élevé, de 1,6 milliard de transactions (défaut des paramètres va-
cuum_failsafe_age et vacuum_multixact_failsafe_age), un VACUUM simple va
automatiquement désactiver le nettoyage des index pour nettoyer plus rapidement la table et
permettre d’avancer l’identifiant le plus ancien de la table.
À partir de la version 13, cette phase peut être parallélisée (clause PARALLEL), chaque index pouvant
être traité par un CPU.
Différentes opérations :
® – VACUUM
– lignes mortes, visibility map, hint bits
– ANALYZE
– statistiques
– FREEZE
– gel des lignes
– parfois gênant ou long
– FULL
– bloquant !
– non lancé par l’autovacuum
VACUUM
Par défaut, VACUUM procède principalement au nettoyage des lignes mortes. Pour que cela soit ef‑
ficace, il met à jour la visibility map, et la crée au besoin. Au passage, il peut geler certaines lignes
rencontrées.
L’autovacuum le déclenchera sur les tables en fonction de l’activité.
Le verrou SHARE UPDATE EXCLUSIVE posé protège la table contre les modifications simultanées du
schéma, et ne gêne généralement pas les opérations, sauf les plus intrusives (il empêche par exemple
un LOCK TABLE). L’autovacuum arrêtera spontanément un VACUUM qu’il aurait lancé et qui gênerait ;
mais un VACUUM lancé manuellement continuera jusqu’à la fin.
VACUUM ANALYZE
ANALYZE existe en tant qu’ordre séparé, pour rafraîchir les statistiques sur un échantillon des don‑
nées, à destination de l’optimiseur. L’autovacuum se charge également de lancer des ANALYZE en
fonction de l’activité.
L’ordre VACUUM ANALYZE (ou VACUUM (ANALYZE)) force le calcul des statistiques sur les données
en même temps que le VACUUM.
VACUUM FREEZE
VACUUM FREEZE procède au « gel » des lignes visibles par toutes les transactions en cours sur
l’instance, afin de parer au problème du wraparound des identifiants de transaction Concrètement, il
indique dans un hint bit de chaque ligne qu’elle est plus vieille que tous les numéros de transactions
actuellement actives (avant la 9.4, la colonne système xmin était remplacée par un FrozenXid).
® – VERBOSE
– Optimisations :
– PARALLEL (v13+)
– INDEX_CLEANUP
– PROCESS_TOAST (v14+)
– TRUNCATE (v12+)
– Ponctuellement :
– SKIP_LOCKED (v12+), DISABLE_PAGE_SKIPPING (v11+)
VERBOSE :
Cette option affiche un grand nombre d’informations sur ce que fait la commande. En général c’est
une bonne idée de l’activer :
VACUUM (VERBOSE) pgbench_accounts_5 ;
PARALLEL :
Apparue avec PostgreSQL 13, l’option PARALLEL permet le traitement parallélisé des index. Le
nombre indiqué après PARALLEL précise le niveau de parallélisation souhaité. Par exemple :
VACUUM (VERBOSE, PARALLEL 4) matable ;
DISABLE_PAGE_SKIPPING :
Par défaut, PostgreSQL ne traite que les blocs modifiés depuis le dernier VACUUM, ce qui est un gros
gain en performance (l’information est stockée dans la Visibility Map).
À partir de la version 11, activer l’option DISABLE_PAGE_SKIPPING force l’analyse de tous les blocs
de la table. La table est intégralement reparcourue. Ce peut être utile en cas de problème, notamment
pour reconstruire cette Visibility Map.
SKIP_LOCKED :
À partir de la version 12, l’option SKIP_LOCKED permet d’ignorer toute table pour laquelle la com‑
mande VACUUM ne peut pas obtenir immédiatement son verrou. Cela évite de bloquer le VACUUM sur
une table, et peut éviter un empilement des verrous derrière celui que le VACUUM veut poser, surtout
en cas de VACUUM FULL. La commande passe alors à la table suivante à traiter. Exemple :
# VACUUM (FULL, SKIP_LOCKED) t_un_million_int, t_cent_mille_int ;
Une autre technique est de paramétrer dans la session un petit délai avant abandon :
SET lock_timeout TO '100ms' ;
INDEX_CLEANUP :
L’option INDEX_CLEANUP (par défaut à on jusque PostgreSQL 13 compris) déclenche systématique‑
ment le nettoyage des index. La commande VACUUMva supprimer les enregistrements de l’index qui
pointent vers des lignes mortes de la table. Quand il faut nettoyer des lignes mortes urgemment dans
une grosse table, la valeur off fait gagner beaucoup de temps :
VACUUM (VERBOSE, INDEX_CLEANUP off) unetable ;
Les index peuvent être nettoyés plus tard par un autre VACUUM, ou reconstruits.
Cette option existe aussi sous la forme d’un paramètre de stockage (vacuum_index_cleanup)
propre à la table pour que l’autovacuum en tienne aussi compte.
En version 14, le nouveau défaut est auto, qui indique que PostgreSQL doit décider de faire ou non
le nettoyage des index suivant la quantité d’entrées à nettoyer. Il faut au minimum 2 % d’éléments à
nettoyer pour que le nettoyage ait lieu.
PROCESS_TOAST :
Cette option active ou non le traitement de la partie TOAST associée à la table. Elle est activée par
défaut. Son utilité est la même que pour INDEX_CLEANUP.
TRUNCATE :
L’option TRUNCATE (à on par défaut) permet de tronquer les derniers blocs vides d’une table. TRUN-
CATE off évite d’avoir à poser un verrou exclusif certes court, mais parfois gênant.
Cette option existe aussi sous la forme d’un paramètre de stockage de table (vacuum_truncate).
Mélange des options :
Il est possible de mixer ces options presque à volonté et de préciser plusieurs tables à nettoyer :
VACUUM (VERBOSE, ANALYZE, INDEX_CLEANUP off, TRUNCATE off,
DISABLE_PAGE_SKIPPING) bigtable, smalltable ;
® – pg_stat_activity ou top
– La table est‑elle suffisamment nettoyée ?
– Vue pg_stat_user_tables
– last_vacuum / last_autovacuum
– last_analyze / last_autoanalyze
– log_autovacuum_min_duration
Il est fréquent de se demander si l’autovacuum s’occupe suffisamment d’une table qui grossit ou dont
les statistiques semblent périmées. La vue pg_stat_user_tables contient quelques informa‑
tions. Dans l’exemple ci‑dessous, nous distinguons les dates des VACUUM et ANALYZE déclenchés
automatiquement ou manuellement (en fait par l’application pgbench). Si 44 305 lignes ont été mo‑
difiées depuis le rafraîchissement des statistiques, il reste 2,3 millions de lignes mortes à nettoyer
(contre 10 millions vivantes).
# SELECT * FROM pg_stat_user_tables WHERE relname ='pgbench_accounts' \gx
-[ RECORD 1 ]-------+------------------------------
relid | 489050
schemaname | public
relname | pgbench_accounts
seq_scan | 1
seq_tup_read | 10
idx_scan | 686140
idx_tup_fetch | 2686136
n_tup_ins | 0
n_tup_upd | 2343090
n_tup_del | 452
n_tup_hot_upd | 118551
n_live_tup | 10044489
n_dead_tup | 2289437
n_mod_since_analyze | 44305
n_ins_since_vacuum | 452
– Partie ANALYZE
– pg_stat_progress_analyze (v13)
La vue pg_stat_progress_vacuum contient une ligne par VACUUM (simple ou FREEZE) en cours
d’exécution.
Voici un exemple :
SELECT * FROM pg_stat_progress_vacuum ;
-[ RECORD 1 ]------+--------------
pid | 4299
datid | 13356
datname | postgres
relid | 16384
phase | scanning heap
heap_blks_total | 127293
heap_blks_scanned | 86665
heap_blks_vacuumed | 86664
index_vacuum_count | 0
max_dead_tuples | 291
num_dead_tuples | 53
Dans cet exemple, le VACUUM exécuté par le PID 4299 a parcouru 86 665 blocs (soit 68 % de la table),
et en a traité 86 664.
Dans le cas d’un VACUUM ANALYZE, la seconde partie de recueil des statistiques pourra être suivie
dans pg_stat_progress_analyze (à partir de PostgreSQL 13) :
SELECT * FROM pg_stat_progress_analyze ;
-[ RECORD 1 ]-------------+--------------------------------
pid | 1938258
datid | 748619
datname | grossetable
relid | 748698
phase | acquiring inherited sample rows
sample_blks_total | 1875
sample_blks_scanned | 1418
ext_stats_total | 0
ext_stats_computed | 0
child_tables_total | 16
child_tables_done | 6
current_child_table_relid | 748751
Les vues précédentes affichent aussi bien les opérations lancées manuellement que celles décidées
par l’autovacuum.
Par contre, pour un VACUUM FULL, il faudra suivre la progression au travers de la vue
pg_stat_progress_cluster (à partir de la version 12), qui renvoie par exemple :
$ psql -c 'VACUUM FULL big' &
$ psql
postgres=# \x
Affichage étendu activé.
-[ RECORD 1 ]-------+------------------
pid | 21157
datid | 13444
datname | postgres
relid | 16384
command | VACUUM FULL
phase | seq scanning heap
cluster_index_relid | 0
heap_tuples_scanned | 13749388
heap_tuples_written | 13749388
heap_blks_total | 199105
heap_blks_scanned | 60839
index_rebuild_count | 0
Cette vue est utilisable aussi avec l’ordre CLUSTER, d’où le nom.
5.6 AUTOVACUUM
® – Processus autovacuum
– But : ne plus s’occuper de VACUUM
– Suit l’activité
– Seuil dépassé => worker dédié
– Gère : VACUUM, ANALYZE, FREEZE
– mais pas FULL
® – autovacuum (on !)
– autovacuum_naptime (1 min)
– autovacuum_max_workers (3)
– plusieurs workers simultanés sur une base
– un seul par table
mais cela est très rare. La valeur off n’empêche pas le déclenchement d’un VACUUM FREEZE s’il
devient nécessaire.
autovacuum_naptime est le temps d’attente entre deux périodes de vérification sur la même base
(1 minute par défaut). Le déclenchement de l’autovacuum suite à des modifications de tables n’est
donc pas instantané.
Seuil de déclenchement =
® threshold + scale factor × nb lignes de la table
L’autovacuum déclenche un VACUUM ou un ANALYZE à partir de seuils calculés sur le principe d’un
nombre de lignes minimal (threshold) et d’une proportion de la table existante (scale factor) de lignes
modifiées, insérées ou effacées. (Pour les détails précis sur ce qui suit, voir la documentation offi‑
cielle1 .)
1
https://docs.postgresql.fr/current/routine‑vacuuming.html#AUTOVACUUM
® – Pour VACUUM
– autovacuum_vacuum_scale_factor (20 %)
– autovacuum_vacuum_threshold (50)
– (v13) autovacuum_vacuum_insert_threshold (1000)
– (v13) autovacuum_vacuum_insert_scale_factor (20 %)
– Pour ANALYZE
– autovacuum_analyze_scale_factor (10 %)
– autovacuum_analyze_threshold (50)
– Adapter pour une grosse table :
Donc, par exemple, dans une table d’un million de lignes, modifier 200 050 lignes provoquera le pas‑
sage d’un VACUUM.
Pour les grosses tables avec de l’historique, modifier 20 % de la volumétrie peut être extrêmement
long. Quand l’autovacuum lance enfin un VACUUM, celui‑ci a donc beaucoup de travail et peut du‑
rer longtemps et générer beaucoup d’écritures. Il est donc fréquent de descendre la valeur de va-
cuum_vacuum_scale_factor à quelques pour cent sur les grosses tables. (Une alternative est
de monter autovacuum_vacuum_threshold à un nombre de lignes élevé et de descendre au-
tovacuum_vacuum_scale_factor à 0, mais il faut alors calculer le nombre de lignes qui déclen‑
chera le nettoyage, et cela dépend fortement de la table et de sa fréquence de mise à jour.)
S’il faut modifier un paramètre, il est préférable de ne pas le faire au niveau global mais de cibler les
tables où cela est nécessaire. Par exemple, l’ordre suivant réduit à 5 % de la table le nombre de lignes
à modifier avant que l’autovacuum y lance un VACUUM :
ALTER TABLE nom_table SET (autovacuum_vacuum_scale_factor = 0.05);
À partir de PostgreSQL 13, le VACUUM est aussi lancé quand il n’y a que des insertions, avec deux
nouveaux paramètres et un autre seuil de déclenchement :
nb_enregistrements_insérés (pg_stat_all_tables.n_ins_since_vacuum) >=
autovacuum_vacuum_insert_threshold
+ autovacuum_vacuum_insert_scale_factor × nb_enregs (pg_class.reltuples)
Pour l’ANALYZE, le principe est le même. Il n’y a que deux paramètres, qui prennent en compte toutes
les lignes modifiées ou insérées, pour calculer le seuil :
nb_insert + nb_updates + nb_delete (n_mod_since_analyze) >=
autovacuum_analyze_threshold + nb_enregs × autovacuum_analyze_scale_factor
Dans notre exemple d’une table, modifier 100 050 lignes provoquera le passage d’un ANALYZE.
Là encore, il est fréquent de modifier les paramètres sur les grosses tables pour rafraîchir les statis‑
tiques plus fréquemment.
Les insertions ont toujours été prises en compte pour ANALYZE, puisqu’elles modifient
Á le contenu de la table. Par contre, jusque PostgreSQL 12 inclus, VACUUM ne tenait pas
compte des lignes insérées pour déclencher son nettoyage. Or, cela avait des consé‑
quences pour les tables à insertion seule (gel de lignes retardé, Index Only Scan impos‑
sibles…) Pour cette raison, à partir de la version 13, les insertions sont aussi prises en
compte pour déclencher un VACUUM.
® – VACUUM vs autovacuum
– Mémoire
– Gestion des coûts
– Gel des lignes
Ces paramètres peuvent différer (par le nom ou la valeur) selon qu’ils s’appliquent à un VACUUM lancé
manuellement ou par script, ou à un processus lancé par l’autovacuum.
Urgent Arrière‑plan
Pas de limite Peu agressif
Paramètres Les mêmes + paramètres de surcharge
Quand on lance un ordre VACUUM, il y a souvent urgence, ou l’on est dans une période de mainte‑
nance, ou dans un batch. Les paramètres que nous allons voir ne cherchent donc pas, par défaut, à
économiser des ressources.
À l’inverse, un VACUUM lancé par l’autovacuum ne doit pas gêner une production peut‑être chargée. Il
existe donc des paramètres autovacuum_* surchargeant les précédents, et beaucoup plus conser‑
vateurs.
5.7.2 Mémoire
Les paramètres suivant permettent de provoquer une pause d’un VACUUM pour ne pas gêner les
autres sessions en saturant le disque. Ils affectent un coût arbitraire aux trois actions suivantes :
– vacuum_cost_page_hit : coût d’accès à une page présente dans le cache (défaut : 1) ;
– vacuum_cost_page_miss : coût d’accès à une page hors du cache (défaut : 10 avant la v14,
2 à partir de la v14) ;
– vacuum_cost_page_dirty : coût de modification d’une page, et donc de son écriture (dé‑
faut : 20).
Il est déconseillé de modifier ces paramètres de coût. Ils permettent de « mesurer » l’activité de VA-
CUUM, et le mettre en pause quand il aura atteint cette limite. Ce second point est gouverné par deux
paramètres :
– vacuum_cost_limit : coût à atteindre avant de déclencher une pause (défaut : 200) ;
– vacuum_cost_delay : temps à attendre (défaut : 0 ms !)
En conséquence, les VACUUM lancés manuellement (en ligne de commande ou via vacuumdb) ne
sont pas freinés par ce mécanisme et peuvent donc entraîner de fortes écritures, du moins par défaut.
Mais c’est généralement dans un batch ou en urgence, et il vaut mieux alors être le plus rapide possible.
Il est donc conseillé de laisser vacuum_cost_limit et vacuum_cost_delay ainsi, ou de ne les
modifier que le temps d’une session ainsi :
SET vacuum_cost_limit = 200 ;
SET vacuum_cost_delay = '20ms' ;
VACUUM (VERBOSE) matable ;
(Pour les urgences, rappelons que l’option INDEX_CLEANUP off permet en plus d’ignorer le net‑
toyage des index, à partir de PostgreSQL 12.)
Les VACUUM d’autovacuum, eux, sont par défaut limités en débit pour ne pas gêner l’activité normale
de l’instance. Deux paramètres surchargent les précédents :
– autovacuum_cost_limit vaut par défaut ‑1, donc reprend la valeur 200 de va-
cuum_cost_limit ;
– autovacuum_vacuum_cost_delay vaut par défaut 2 ms (mais 20 ms avant la version 12,
ce qui correspond à l’exemple ci‑dessus).
Principe (rappel) :
Rappelons que les numéros de transaction stockés sur les lignes ne sont stockés que sur 32 bits, et
sont recyclés. Il y a donc un risque de mélanger l’avenir et le futur des transactions lors du rebouclage
(wraparound). Afin d’éviter ce phénomène, VACUUM « gèle » les vieux enregistrements, afin que ceux‑
ci ne se retrouvent pas brusquement dans le futur. Cela implique de réécrire le bloc. Il est inutile de
geler trop tôt une ligne récente, qui sera peut‑être bientôt réécrite.
Paramétrage :
Plusieurs paramètres règlent ce fonctionnement. Leurs valeurs par défaut sont satisfaisantes pour la
plupart des installations et ne sont pour ainsi dire jamais modifiées. Par contre, il est important de
bien connaître le fonctionnement pour ne pas être surpris.
Rappelons que le numéro de transaction le plus ancien connu d’une table est porté par pg-
class.relfrozenxid, et est sur 32 bits. Il faut utiliser la fonction age() pour connaître l’écart
par rapport au numéro de transaction courant (géré sur 64 bits en interne).
SELECT relname, relfrozenxid, round(age(relfrozenxid) /1e6,2) AS "age_Mtrx"
FROM pg_class c
WHERE relname LIKE 'pgbench%' AND relkind='r'
ORDER BY age(relfrozenxid) ;
Une partie du gel se fait lors d’un VACUUM normal. Si ce dernier rencontre un enregistrement plus
vieux que vacuum_freeze_min_age (par défaut 50 millions de transactions écoulées), alors le
tuple peut et doit être gelé. Cela ne concerne que les lignes dans des blocs qui ont des lignes mortes
à nettoyer : les lignes dans des blocs un peu statiques y échappent. (Y échappent aussi les lignes qui
ne sont pas forcément visibles par toutes les transactions ouvertes.)
VACUUM doit donc périodiquement déclencher un nettoyage plus agressif de toute la table (et non pas
uniquement des blocs modifiés depuis le dernier VACUUM), afin de nettoyer tous les vieux enregistre‑
ments. C’est le rôle de vacuum_freeze_table_age (par défaut 150 millions de transactions). Si
la table a atteint cet âge, un VACUUM (manuel ou automatique) lancé dessus deviendra « agressif » :
VACUUM (VERBOSE) pgbench_tellers ;
INFO: aggressively vacuuming "public.pgbench_tellers"
C’est équivalent à l’option DISABLE_PAGE_SKIPPING : les blocs ne contenant que des lignes vi‑
vantes seront tout de même parcourus. Les lignes non gelées qui s’y trouvent et plus vieilles que va-
cuum_freeze_min_age seront alors gelées. Ce peut être long, ou pas, en fonction de l’efficacité
de l’étape précédente.
À côté des numéros de transaction habituels, les identifiants multixact, utilisés pour supporter le
verrouillage de lignes par des transactions multiples évitent aussi le wraparound avec des paramètres
age
---------
2487153
Concrètement, on verra l’âge d’une base de données approcher peu à peu des 200 millions de transac‑
tions, ce qui correspondra à l’âge des plus « vieilles » tables, souvent celles sur lesquelles l’autovacuum
ne passe jamais. L’âge des tables évolue même si l’essentiel de leur contenu, voire la totalité, est déjà
gelé (car il peut rester le pg_class.relfrozenxid à mettre à jour, ce qui sera bien sûr très rapide).
Cet âge va retomber quand un gel sera forcé sur ces tables, puis remonter, etc.
Rappelons que le FREEZE génère de fait la réécriture de tous les blocs concernés. Il
Á peut être quasi‑instantané, mais le déclenchement inopiné d’un VACUUM FREEZE
sur l’intégralité d’une grosse table assez statique est une mauvaise surprise assez fré‑
quente.
Une base chargée avec pg_restore et peu modifiée peut même voir le FREEZE se
déclencher sur toutes les tables en même temps. Cela est moins grave depuis les opti‑
misations de la 9.6, mais, après de très gros imports, il reste utile d’opérer un VACUUM
FREEZE manuel, à un moment où cela gêne peu, pour éviter qu’ils ne se provoquent
plus tard en période chargée.
Résumé :
Que retenir de ce paramétrage complexe ?
– le VACUUM gèlera une partie des lignes un peu anciennes lors de son fonctionnement habituel ;
– un bloc gelé non modifié ne sera plus à regeler ;
– de grosses tables statiques peuvent engendrer soudainement une grosse charge en écriture ; il
vaut mieux être proactif.
Le cas des VACUUM manuels a été vu plus haut : ils peuvent gêner quelques verrous ou opérations
DDL. Il faudra les arrêter manuellement au besoin.
C’est différent si l’autovacuum a lancé le processus : celui‑ci sera arrêté si un utilisateur pose un verrou
en conflit.
La seule exception concerne un VACUUM FREEZE lancé quand la table doit être gelée, donc avec la
mention to prevent wraparound dans pg_stat_activity : celui‑ci ne sera pas interrompu. Il ne
pose qu’un verrou destinée à éviter les modifications de schéma simultanées (SHARE UPDATE EXCLU‑
SIVE). Comme le débit en lecture et écriture est bridé par le paramétrage habituel de l’autovacuum, ce
verrou peut durer assez longtemps (surtout avant PostgreSQL 9.6, où toute la table est relue à chaque
FREEZE). Cela peut s’avérer gênant avec certaines applications. Une solution est de réduire auto-
vacuum_vacuum_cost_delay, surtout avant PostgreSQL 12 (voir plus haut).
Si les opérations sont impactées, on peut vouloir lancer soi‑même un VACUUM FREEZE ma‑
nuel, non bridé. Il faudra alors repérer le PID du VACUUM FREEZE en cours, l’arrêter avec
pg_cancel_backend, puis lancer manuellement l’ordre VACUUM FREEZE sur la table concernée,
(et rapidement avant que l’autovacuum ne relance un processus).
® – Causes :
– sessions idle in transaction sur une longue durée
– slot de réplication en retard/oublié
– transactions préparées oubliées
– erreur à l’exécution du VACUUM
– Conséquences :
– processus autovacuum répétés
– arrêt des transactions
– mode single…
– Supervision :
– check_pg_activity : xmin, max_freeze_age
– surveillez les traces !
Il arrive que le fonctionnement du FREEZE soit gêné par un problème qui lui interdit de recycler les
plus anciens numéros de transactions. Les causes possibles sont :
– des sessions idle in transactions durent depuis des jours ou des semaines (voir le statut idle
in transaction dans pg_stat_activity, et au besoin fermer la session) : au pire, elles
disparaissent après redémarrage ;
– des slots de réplication pointent vers un secondaire très en retard, voire disparu (consulter
pg_replication_slots, et supprimer le slot) ;
– des transactions préparées (pas des requêtes préparées !) n’ont jamais été validées ni annulées,
(voir pg_prepared_xacts, et annuler la transaction) : elles ne disparaissent pas après redé‑
marrage ;
– l’opération de VACUUM tombe en erreur : corruption de table ou index, fonction d’index fonc‑
tionnel buggée, etc. (voir les traces et corriger le problème, supprimer l’objet ou la fonction,
etc.).
Pour effectuer le FREEZE en urgence le plus rapidement possible, on peut utiliser, à partir de Post‑
greSQL 12 :
VACUUM (FREEZE, VERBOSE, INDEX_CLEANUP off, TRUNCATE off) ;
Cette commande force le gel de toutes les lignes, ignore le nettoyage des index et ne supprime pas
les blocs vides finaux (le verrou peut être gênant). Un VACUUM classique serait à prévoir ensuite à
l’occasion.
En toute rigueur, une version sans l’option FREEZE est encore plus rapide : le mode agressif serait dé‑
clenché mais les lignes plus récentes que vacuum_freeze_min_age (50 millions de transaction)
ne seraient pas encore gelées. On peut même monter ce paramètre dans la session pour alléger au
maximum la charge sur une table dont les lignes ont des âges bien étalés.
Ne pas oublier de nettoyer toutes les bases de l’instance.
Dans le pire des cas, plus aucune transaction ne devient possible (y compris les opérations
d’administration comme DROP, ou VACUUM sans TRUNCATE off) :
ERROR: database is not accepting commands to avoid wraparound data loss in database
↪ "db1"
HINT: Stop the postmaster and vacuum that database in single-user mode.
You might also need to commit or roll back old prepared transactions,
or drop stale replication slots.
En dernière extrémité, il reste un délai de grâce d’un million de transactions, qui ne sont accessibles
que dans le très austère mode monoutilisateur2 de PostgreSQL.
Avec la sonde Nagios check_pgactivity3 , et les services max_freeze_age et oldest_xmin, il est pos‑
sible de vérifier que l’âge des bases ne dérive pas, ou de trouver quel processus porte le xmin le plus
ancien. S’il y a un problème, il entraîne généralement l’apparition de nombreux messages dans les
traces : lisez‑les régulièrement !
2
https://docs.postgresql.fr/current/app‑postgres.html#APP‑POSTGRES‑SINGLE‑USER
3
https://github.com/OPMDG/check_pgactivity
L’autovacuum fonctionne convenablement pour les charges habituelles. Il ne faut pas s’étonner qu’il
fonctionne longtemps en arrière‑plan : il est justement conçu pour ne pas se presser. Au besoin, ne
pas hésiter à lancer manuellement l’opération, donc sans bridage en débit.
Si les disques sont bons, on peut augmenter le débit autorisé en jouant :
Comme son déclenchement est très lié à l’activité, il faut vérifier que l’autovacuum passe assez
souvent sur les tables sensibles en surveillant pg_stat_all_tables.last_autovacuum et
last_autoanalyze. Si les statistiques traînent à se rafraîchir, ne pas hésiter à activer plus souvent
l’autovacuum sur les grosses tables problématiques ainsi :
-- analyze après 5 % de modification au lieu du défaut de 10 %
ALTER TABLE table_name SET (autovacuum_analyze_scale_factor = 0.05) ;
® – Mode manuel
– batchs / tables temporaires / tables à insertions seules (<v13)
– si pressé !
– Danger du FREEZE brutal
– prévenir
– VACUUM FULL : dernière extrémité
L’autovacuum n’est pas toujours assez rapide à se déclencher, par exemple entre les différentes étapes
d’un batch : on intercalera des VACUUM ANALYZE manuels. Il faudra le faire systématiquement pour
les tables temporaires (que l’autovacuum ne voit pas). Pour les tables où il n’y a que des insertions,
avant PostgreSQL 13, l’autovacuum ne lance spontanément que l’ANALYZE : il faudra effectuer un
VACUUM explicite pour profiter de certaines optimisations.
Un point d’attention reste le gel brutal de grosses quantités de données chargées ou modifiées en
même temps. Un VACUUM FREEZE préventif dans une période calme reste la meilleure solution.
Un VACUUM FULL sur une grande table est une opération très lourde, à réserver à la récupération
d’une partie significative de son espace, qui ne serait pas réutilisé plus tard.
5.11 CONCLUSION
5.11.1 Questions
5.12 QUIZ
https://dali.bo/m5_quiz
®
Exécuter un VACUUM VERBOSE sur la table t3. Quelle est l’information la plus importante ?
Récupérer la taille de la table t5 et l’espace libre rapporté par pg_freespacemap. Que faut‑il
en déduire ?
CREATE TABLE
ALTER TABLE
INSERT 0 1000000
SELECT pg_size_pretty(pg_table_size('t3'));
pg_size_pretty
----------------
35 MB
DELETE 500000
SELECT pg_size_pretty(pg_table_size('t3'));
pg_size_pretty
----------------
35 MB
DELETE seul ne permet pas de regagner de la place sur le disque. Les lignes supprimées sont uni‑
quement marquées comme étant mortes. Comme l’autovacuum est ici désactivé, PostgreSQL n’a pas
encore nettoyé ces lignes.
Exécuter un VACUUM VERBOSE sur la table t3. Quelle est l’information la plus importante ?
L’indication :
removed 500000 row versions in 2213 pages
indique 500 000 lignes ont été nettoyées dans 2213 blocs (en gros, la moitié des blocs de la table).
Pour compléter, l’indication suivante :
found 500000 removable, 500000 nonremovable row versions in 4425 out of 4425 pages
reprend l’indication sur 500 000 lignes mortes, et précise que 500 000 autres ne le sont pas. Les 4425
pages parcourues correspondent bien à la totalité des 35 Mo de la table complète. C’est la première
fois que VACUUM passe sur cette table, il est normal qu’elle soit intégralement parcourue.
SELECT pg_size_pretty(pg_table_size('t3'));
pg_size_pretty
----------------
35 MB
VACUUM ne permet pas non plus de gagner en espace disque. Principalement, il renseigne la structure
FSM (free space map) sur les emplacements libres dans les fichiers des tables.
SELECT pg_size_pretty(pg_table_size('t3'));
pg_size_pretty
----------------
17 MB
Là, par contre, nous gagnons en espace disque. Le VACUUM FULL reconstruit la table et la fragmen‑
tation disparaît.
Créer une table t4 avec une colonne id de type integer.
CREATE TABLE
ALTER TABLE
INSERT 0 1000000
SELECT pg_size_pretty(pg_table_size('t4'));
pg_size_pretty
----------------
35 MB
DELETE 500000
SELECT pg_size_pretty(pg_table_size('t4'));
pg_size_pretty
----------------
35 MB
VACUUM
SELECT pg_size_pretty(pg_table_size('t4'));
pg_size_pretty
----------------
17 MB
En fait, il existe un cas où il est possible de gagner de l’espace disque suite à un VACUUM simple : quand
l’espace récupéré se trouve en fin de table et qu’il est possible de prendre rapidement un verrou exclu‑
sif sur la table pour la tronquer. C’est assez peu fréquent mais c’est une optimisation intéressante.
Créer une table t5 avec deux colonnes : c1 de type integer et c2 de type text.
CREATE TABLE
ALTER TABLE
INSERT INTO t5(c1, c2) SELECT i, 'Ligne '||i FROM generate_series(1, 1000000) AS i;
INSERT 0 1000000
CREATE EXTENSION
Cette extension installe une fonction nommée pg_freespace, dont la version la plus simple ne
demande que la table en argument, et renvoie l’espace libre dans chaque bloc, en octets, connu de la
Free Space Map.
SELECT count(blkno), sum(avail) FROM pg_freespace('t5'::regclass);
count | sum
-------+-----
6274 | 0
et donc 6274 blocs (soit 51,4 Mo) sans aucun espace vide.
UPDATE 200000
count | sum
-------+-----
7451 | 32
La table comporte donc 20 % de blocs en plus, où sont stockées les nouvelles versions des lignes
modifiées. Le champ avail indique qu’il n’y a quasiment pas de place libre. (Ne pas prendre la valeur
de 32 octets au pied de la lettre, la Free Space Map ne cherche pas à fournir une valeur précise.)
Exécuter un VACUUM sur la table t5.
VACUUM VERBOSE t5;
count | sum
-------+---------
7451 | 8806816
Il y a toujours autant de blocs, mais environ 8,8 Mo sont à présent repérés comme libres.
Il faut donc bien exécuter un VACUUM pour que PostgreSQL nettoie les blocs et mette à jour la structure
FSM, ce qui nous permet de déduire le taux de fragmentation de la table.
SELECT pg_size_pretty(pg_table_size('t5'));
pg_size_pretty
----------------
58 MB
Récupérer la taille de la table t5 et l’espace libre rapporté par pg_freespacemap. Que faut‑il
en déduire ?
count | sum
-------+-----
6274 | 0
SELECT pg_size_pretty(pg_table_size('t5'));
pg_size_pretty
----------------
49 MB
VACUUM FULL a réécrit la table sans les espaces morts, ce qui nous a fait gagner entre 8 et 9 Mo. La
taille de la table maintenant correspond bien à celle de l’ancienne table, moins la place prise par les
lignes mortes.
CREATE TABLE
INSERT 0 1000000
\x
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 0
seq_tup_read | 0
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 0
n_tup_del | 0
n_tup_hot_upd | 0
n_live_tup | 1000000
n_dead_tup | 0
n_mod_since_analyze | 0
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:42:43.612269+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:42:43.719195+01
vacuum_count | 0
autovacuum_count | 1
analyze_count | 0
autoanalyze_count | 1
-[ RECORD 1 ]-------+--------
oid | 4160608
relname | t6
relnamespace | 2200
reltype | 4160610
reloftype | 0
relowner | 10
relam | 2
relfilenode | 4160608
reltablespace | 0
relpages | 4425
reltuples | 1e+06
...
L’autovacuum se base entre autres sur cette valeur pour décider s’il doit passer ou pas. Si elle n’est
pas encore à jour, il faut lancer manuellement :
ANALYZE t6 ;
UPDATE 150000
Le démon autovacuum ne se déclenche pas instantanément après les écritures, attendons un peu :
SELECT pg_sleep(60) ;
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 1
seq_tup_read | 1000000
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 150000
n_tup_del | 0
n_tup_hot_upd | 0
n_live_tup | 1000000
n_dead_tup | 150000
n_mod_since_analyze | 0
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:42:43.612269+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:43:43.561288+01
vacuum_count | 0
autovacuum_count | 1
analyze_count | 0
autoanalyze_count | 2
Seul last_autoanalyze a été modifié, et il reste entre 150 000 lignes morts (n_dead_tup). En
effet, le démon autovacuum traite séparément l’ANALYZE (statistiques sur les valeurs des données)
et le VACUUM (recherche des espaces morts). Si l’on recalcule les seuils de déclenchement, on trouve
pour l’autoanalyze :
autovacuum_analyze_scale_factor × nombre de lignes
+ autovacuum_analyze_threshold
soit par défaut 10 % × 1 000 000 + 50 = 100 050, dépassé ici.
Pour l’autovacuum, le seuil est de :
autovacuum_vacuum_insert_scale_factor × nombre de lignes
+ autovacuum_vacuum_insert_threshold
soit 20 % × 1 000 000 + 50 = 200 050, qui n’est pas atteint.
UPDATE 60000
L’autovacuum ne passe pas tout de suite, les 210 000 lignes mortes au total sont bien visibles :
SELECT * FROM pg_stat_user_tables WHERE relname = 't6';
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 3
seq_tup_read | 3000000
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 210000
n_tup_del | 0
n_tup_hot_upd | 65
n_live_tup | 1000000
n_dead_tup | 210000
n_mod_since_analyze | 60000
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:42:43.612269+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:43:43.561288+01
vacuum_count | 0
autovacuum_count | 1
analyze_count | 0
autoanalyze_count | 2
Mais comme le seuil de 200 050 lignes modifiées à été franchi, le démon lance un VACUUM :
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 3
seq_tup_read | 3000000
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 210000
n_tup_del | 0
n_tup_hot_upd | 65
n_live_tup | 896905
n_dead_tup | 0
n_mod_since_analyze | 60000
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:47:43.740962+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:43:43.561288+01
vacuum_count | 0
autovacuum_count | 2
analyze_count | 0
autoanalyze_count | 2
Noter que n_dead_tup est revenu à 0. last_auto_analyze indique qu’un nouvel ANALYZE n’a
pas été exécuté : seules 60 000 lignes ont été modifiées (voir n_mod_since_analyze), en‑dessous
du seuil de 100 050.
Descendre le facteur d’échelle de la table t6 à 10 % pour le VACUUM.
ALTER TABLE
UPDATE 200000
SELECT pg_sleep(60);
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 4
seq_tup_read | 4000000
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 410000
n_tup_del | 0
n_tup_hot_upd | 65
n_live_tup | 1000000
n_dead_tup | 0
n_mod_since_analyze | 0
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:53:43.563671+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:53:43.681023+01
vacuum_count | 0
autovacuum_count | 3
analyze_count | 0
autoanalyze_count | 3
Le démon a relancé un VACUUM et un ANALYZE. Avec un facteur d’échelle à 10 %, il ne faut plus at‑
tendre que la modification de 100 050 lignes pour que le VACUUM soit déclenché par le démon. C’était
déjà le seuil pour l’ANALYZE.
Ce module introduit le partitionnement déclaratif introduit avec PostgreSQL 10, et amélioré dans les
versions suivantes. PostgreSQL 13 au minimum est conseillé pour ne pas être gêné par une des limites
levées dans les versions précédentes (et non développées ici).
Le partitionnement par héritage, au fonctionnement totalement différent, reste utilisable, mais ne
doit plus servir aux nouveaux développements, du moins pour les cas décrits ici.
243
DALIBO Formations
Maintenir de très grosses tables peut devenir fastidieux, voire impossible : VACUUM FULL trop long,
espace disque insuffisant, autovacuum pas assez réactif, réindexation interminable… Il est aussi aber‑
rant de conserver beaucoup de données d’archives dans des tables lourdement sollicitées pour les
données récentes.
Le partitionnement consiste à séparer une même table en plusieurs sous‑tables (partitions) manipu‑
lables en tant que tables à part entière.
Maintenance
La maintenance s’effectue sur les partitions plutôt que sur l’ensemble complet des données. En par‑
ticulier, un VACUUM FULL ou une réindexation peuvent s’effectuer partition par partition, ce qui
permet de limiter les interruptions en production, et lisser la charge. pg_dump ne sait pas paralléli‑
ser la sauvegarde d’une table volumineuse et non partitionnée, mais parallélise celle de différentes
partitions d’une même table.
C’est aussi un moyen de déplacer une partie des données dans un autre tablespace pour des raisons
de place, ou pour déporter les parties les moins utilisées de la table vers des disques plus lents et
moins chers.
Parcours complet de partitions
Certaines requêtes (notamment décisionnelles) ramènent tant de lignes, ou ont des critères si com‑
plexes, qu’un parcours complet de la table est souvent privilégié par l’optimiseur.
Un partitionnement, souvent par date, permet de ne parcourir qu’une ou quelques partitions au lieu
de l’ensemble des données. C’est le rôle de l’optimiseur de choisir la partition (partition pruning), par
exemple celle de l’année en cours, ou des mois sélectionnés.
Suppression des partitions
La suppression de données parmi un gros volume peut poser des problèmes d’accès concurrents ou
de performance, par exemple dans le cas de purges.
En configurant judicieusement les partitions, on peut résoudre cette problématique en suppri‑
mant une partition (DROP TABLE nompartition ;), ou en la détachant (ALTER TABLE
table_partitionnee DETACH PARTITION nompartition ;) pour l’archiver (et la
réattacher au besoin) ou la supprimer ultérieurement.
D’autres optimisations sont décrites dans ce billet de blog d’Adrien Nayrat1 : statistiques plus précises
au niveau d’une partition, réduction plus simple de la fragmentation des index, jointure par rappro‑
chement des partitions…
La principale difficulté d’un système de partitionnement consiste à partitionner avec un impact mini‑
mal sur la maintenance du code par rapport à une table classique.
1
https://blog.anayrat.info/2021/09/01/cas‑dusages‑du‑partitionnement‑natif‑dans‑postgresql/
® – Table partitionnée
– structure uniquement
– index/contraintes répercutés sur les partitions
– Partitions :
– 1 partition = 1 table classique, utilisable directement
– clé de partitionnement (inclue dans PK/UK)
– partition par défaut
– sous‑partitions possibles
– FDW comme partitions possible
– attacher/détacher une partition
En partitionnement déclaratif, une table partitionnée ne contient pas de données par elle‑même. Elle
définit la structure (champs, types) et les contraintes et index, qui sont répercutées sur ses parti‑
tions.
Une partition est une table à part entière, rattachée à une table partitionnée. Sa structure suit auto‑
matiquement celle de la table partitionnée et ses modifications. Cependant, des index ou contraintes
supplémentaires propres à cette partition peuvent être ajoutées de la même manière que pour une
table classique.
La partition se définit par une « clé de partitionnement », sur une ou plusieurs colonnes. Les lignes de
même clé se retrouvent dans la même partition. La clé peut se définir comme :
Le routage des données insérées ou modifiées vers la bonne partition est géré de façon automatique
en fonction de la définition des partitions. La création d’une partition par défaut permet d’éviter des
erreurs si aucune partition ne convient.
De même, à la lecture de la table partitionnée, les différentes partitions nécessaires sont accédées de
manière transparente.
Pour le développeur, la table principale peut donc être utilisée comme une table classique. Il vaut
mieux cependant qu’il connaisse le mode de partitionnement, pour utiliser la clé autant que possible.
La complexité supplémentaire améliorera les performances. L’accès direct aux partitions par leur nom
de table reste possible, et peut parfois améliorer les performances. Un développeur pourra aussi pur‑
ger des données plus rapidement, en effectuant un simple DROP de la partition concernée.
Le partitionnement par liste définit les valeurs d’une colonne acceptables dans chaque partition.
Utilisations courantes : partitionnement par année, par statut, par code géographique…
Utilisations courantes : partitionnement par date, par plages de valeurs continues, alphabétiques…
L’exemple ci‑dessus utilise le partitionnement par mois. Chaque partition est définie par des plages
de date. Noter que la borne supérieure ne fait pas partie des données de la partition. Elle doit donc
être aussi la borne inférieure de la partie suivante.
La description de la table partitionnée devient :
=# \d+ logs
Table partitionnée « public.logs »
Colonne | Type | ... | Stockage | …
---------+--------------------------+-----+----------+ …
d | timestamp with time zone | ... | plain | …
contenu | text | ... | extended | …
Clé de partition : RANGE (d)
Partitions: logs_201901 FOR VALUES FROM ('2019-01-01 00:00:00+01') TO ('2019-02-01
↪ 00:00:00+01'),
logs_201902 FOR VALUES FROM ('2019-02-01 00:00:00+01') TO ('2019-03-01
↪ 00:00:00+01'),
…
logs_201912 FOR VALUES FROM ('2019-12-01 00:00:00+01') TO ('2020-01-01
↪ 00:00:00+01'),
logs_autres DEFAULT
La partition par défaut reçoit toutes les données qui ne vont dans aucune autre partition : cela évite
des erreurs d’insertion. Il vaut mieux que la partition par défaut reste très petite.
Il est possible de définir des plages sur plusieurs champs :
CREATE TABLE tt_a PARTITION OF tt
FOR VALUES FROM (1,'2020-08-10') TO (100, '2020-08-11') ;
Ce type de partitionnement vise à répartir la volumétrie dans plusieurs partitions de manière homo‑
gène, quand il n’y a pas de clé évidente. En général, il y aura plus que 3 partitions.
Des INSERT dans la table partitionnée seront redirigés directement dans les bonnes partitions avec
un impact en performances quasi négligeable.
Lors des lectures ou jointures, il est important de préciser autant que possible la clé de jointure, si elle
est pertinente. Dans le cas contraire, toutes les tables de la partition seront interrogées.
Dans cet exemple, la table comprend 10 partitions :
=# EXPLAIN (COSTS OFF) SELECT COUNT(*) FROM pgbench_accounts ;
QUERY PLAN
---------------------------------------------------------------------------------
Finalize Aggregate
-> Gather
Workers Planned: 2
-> Parallel Append
-> Partial Aggregate
-> Parallel Index Only Scan using pgbench_accounts_1_pkey on
↪ pgbench_accounts_1 pgbench_accounts
-> Partial Aggregate
-> Parallel Index Only Scan using pgbench_accounts_2_pkey on
↪ pgbench_accounts_2 pgbench_accounts_1
-> Partial Aggregate
-> Parallel Index Only Scan using pgbench_accounts_3_pkey on
↪ pgbench_accounts_3 pgbench_accounts_2
-> Partial Aggregate
-> Parallel Index Only Scan using pgbench_accounts_4_pkey on
↪ pgbench_accounts_4 pgbench_accounts_3
-> Partial Aggregate
-> Parallel Index Only Scan using pgbench_accounts_5_pkey on
↪ pgbench_accounts_5 pgbench_accounts_4
-> Partial Aggregate
QUERY PLAN
---------------------------------------------------------------------------------
Index Scan using pgbench_accounts_1_pkey on pgbench_accounts_1 pgbench_accounts
Index Cond: (aid = 599999)
Si l’on connaît la clé et que le développeur sait en déduire la table, il est aussi possible d’accéder
directement à la partition :
QUERY PLAN
QUERY PLAN
---------------------------------------------------------------------------------
Index Scan using pgbench_accounts_6_pkey on pgbench_accounts_6
Index Cond: (aid = 599999)
Exemples :
– dans une table partitionnée par statut de commande, beaucoup de requêtes ne s’occupent que
d’un statut particulier, et peuvent donc n’appeler que la partition concernée (attention si le sta‑
tut change…) ;
– dans une table partitionnée par mois de création d’une facture, la date de la facture permet de
s’adresser directement à la bonne partition.
Il est courant que les ORM ne sachent pas exploiter cette fonctionnalité.
Attacher une table existante à une table partitionnée implique de définir une clé de partitionnement.
PostgreSQL vérifiera que les valeurs présentes correspondent bien à cette clé. Cela peut être long, sur‑
tout que le verrou nécessaire sur la table est gênant. Pour accélérer les choses, il est conseillé d’ajouter
au préalable une contrainte CHECK correspondant à la clé, voire d’ajouter d’avance les index qui se‑
raient ajoutés lors du rattachement.
Détacher une partition est beaucoup plus rapide qu’en attacher une. Cependant, là encore, le verrou
peut être gênant.
Certaines limitations du partitionnement sont liées à son principe. Les partitions ont forcément le
même schéma de données que leur partition mère. Il n’y a pas de notion d’héritage multiple.
La création des partitions n’est pas automatique (par exemple dans un partitionnement
Á par date). Il faudra prévoir de les créer par avance.
Pour contourner cette limite, il reste possible de manipuler directement les partitions s’il est facile de
trouver leur nom.
6.4 CONCLUSION
Le partitionnement déclaratif apparu en version 10 est mûr dans les dernières versions. Il introduit une
complexité supplémentaire, mais peut rendre de grands services quand la volumétrie augmente.
6.5 QUIZ
https://dali.bo/v0_quiz
®
255
DALIBO Formations
7.1 INTRODUCTION
® – Sauvegarde traditionnelle
– sauvegarde pg_dump à chaud
– sauvegarde des fichiers à froid
– Insuffisant pour les grosses bases
– long à sauvegarder
– encore plus long à restaurer
– Perte de données potentiellement importante
– car impossible de réaliser fréquemment une sauvegarde
– Une solution : la sauvegarde PITR
La sauvegarde traditionnelle, qu’elle soit logique ou physique à froid, répond à beaucoup de besoins.
Cependant, ce type de sauvegarde montre de plus en plus ses faiblesses pour les gros volumes : la
sauvegarde est longue à réaliser et encore plus longue à restaurer. Et plus une sauvegarde met du
temps, moins fréquemment on l’exécute. La fenêtre de perte de données devient plus importante.
PostgreSQL propose une solution à ce problème avec la sauvegarde physique à chaud. On peut
l’utiliser comme un simple mode de sauvegarde supplémentaire, mais elle permet bien d’autres
possibilités, d’où le nom de PITR (Point In Time Recovery).
7.1.1 Au menu
Ce module fait le tour de la sauvegarde PITR, de la mise en place de l’archivage (de manière manuelle
ou avec l’outil pg_receivewal) à la sauvegarde des fichiers (là aussi, en manuel, ou avec l’outil
pg_basebackup). Il discute aussi de la restauration d’une telle sauvegarde. Nous évoquerons très
rapidement quelques outils externes pour faciliter ces sauvegardes.
7.2 PITR
PITR est l’acronyme de Point In Time Recovery, autrement dit restauration à un point dans le temps.
C’est une sauvegarde à chaud et surtout en continu. Là où une sauvegarde logique du type pg_dump
se fait au mieux une fois toutes les 24 h, la sauvegarde PITR se fait en continu grâce à l’archivage des
journaux de transactions. De ce fait, ce type de sauvegarde diminue très fortement la fenêtre de perte
de données.
Bien qu’elle se fasse à chaud, la sauvegarde est cohérente.
7.2.1 Principes
Quand une transaction est validée, les données à écrire dans les fichiers de données sont d’abord
écrites dans un journal de transactions. Ces journaux décrivent donc toutes les modifications surve‑
nant sur les fichiers de données, que ce soit les objets utilisateurs comme les objets systèmes. Pour
reconstruire un système, il suffit donc d’avoir ces journaux et d’avoir un état des fichiers du répertoire
des données à un instant t. Toutes les actions effectuées après cet instant t pourront être rejouées
en demandant à PostgreSQL d’appliquer les actions contenues dans les journaux. Les opérations sto‑
ckées dans les journaux correspondent à des modifications physiques de fichiers, il faut donc partir
d’une sauvegarde au niveau du système de fichier, un export avec pg_dump n’est pas utilisable.
Il est donc nécessaire de conserver ces journaux de transactions. Or PostgreSQL les recycle dès qu’il
n’en a plus besoin. La solution est de demander au moteur de les archiver ailleurs avant ce recyclage.
On doit aussi disposer de l’ensemble des fichiers qui composent le répertoire des données (incluant
les tablespaces si ces derniers sont utilisés).
La restauration a besoin des journaux de transactions archivés. Il ne sera pas possible de restaurer
et éventuellement revenir à un point donné avec la sauvegarde seule. En revanche, une fois la sau‑
vegarde des fichiers restaurée et la configuration réalisée pour rejouer les journaux archivés, il sera
possible de les rejouer tous ou seulement une partie d’entre eux (en s’arrêtant à un certain moment).
Ils doivent impérativement être rejoués dans l’ordre de leur écriture (et donc de leur nom).
7.2.2 Avantages
® – Sauvegarde à chaud
– Rejeu d’un grand nombre de journaux
– Moins de perte de données
Tout le travail est réalisé à chaud, que ce soit l’archivage des journaux ou la sauvegarde des fichiers
de la base. En effet, il importe peu que les fichiers de données soient modifiés pendant la sauvegarde
car les journaux de transactions archivés permettront de corriger toute incohérence par leur applica‑
tion.
Il est possible de rejouer un très grand nombre de journaux (une journée, une semaine, un mois, etc.).
Évidemment, plus il y a de journaux à appliquer, plus cela prendra du temps. Mais il n’y a pas de limite
au nombre de journaux à rejouer.
Dernier avantage, c’est le système de sauvegarde qui occasionnera le moins de perte de données. Gé‑
néralement, une sauvegarde pg_dump s’exécute toutes les nuits, disons à 3 h du matin. Supposons
qu’un gros problème survienne à midi. S’il faut restaurer la dernière sauvegarde, la perte de données
sera de 9 h. Le volume maximum de données perdu correspond à l’espacement des sauvegardes. Avec
l’archivage continu des journaux de transactions, la fenêtre de perte de données va être fortement
réduite. Plus l’activité est intense, plus la fenêtre de temps sera petite : il faut changer de fichier de
journal pour que le journal précédent soit archivé et les fichiers de journaux sont de taille fixe.
Pour les systèmes n’ayant pas une grosse activité, il est aussi possible de forcer un changement de
journal à intervalle régulier, ce qui a pour effet de forcer son archivage, et donc dans les faits de pouvoir
s’assurer une perte correspondant au maximum à cet intervalle.
7.2.3 Inconvénients
Certains inconvénients viennent directement du fait qu’on copie les fichiers : sauvegarde et restaura‑
tion complète (impossible de ne restaurer qu’une seule base ou que quelques tables), restauration
sur la même architecture (32/64 bits, little/big endian). Il est même fortement conseillé de restaurer
dans la même version du même système d’exploitation, sous peine de devoir réindexer l’instance (dif‑
férence de définition des locales notamment).
Elle nécessite en plus un plus grand espace de stockage car il faut sauvegarder les fichiers (dont les
index) ainsi que les journaux de transactions sur une certaine période, ce qui peut être volumineux
(en tout cas beaucoup plus que des pg_dump).
En cas de problème dans l’archivage et selon la méthode choisie, l’instance ne voudra pas effacer les
journaux non archivés. Il y a donc un risque d’accumulation de ceux‑ci. Il faudra donc surveiller la taille
du pg_wal. En cas de saturation, PostgreSQL s’arrête !
Enfin, la sauvegarde PITR est plus complexe à mettre en place qu’une sauvegarde pg_dump. Elle né‑
cessite plus d’étapes, une réflexion sur l’architecture à mettre en œuvre et une meilleure compréhen‑
sion des mécanismes internes à PostgreSQL pour en avoir la maîtrise.
Description :
pg_basebackup est un produit qui a beaucoup évolué dans les dernières versions de PostgreSQL.
De plus, le paramétrage par défaut depuis la version 10 le rend immédiatement utilisable.
Il permet de réaliser toute la sauvegarde de la base, à distance, via deux connexions de réplication :
une pour les données, une pour les journaux de transactions qui sont générés pendant la copie. Sa
compression permet d’éviter une durée de transfert ou une place disque occupée trop importante.
Cela a évidemment un coût, notamment au niveau CPU, sur le serveur ou sur le client suivant le besoin.
Il est simple à mettre en place et à utiliser, et permet d’éviter de nombreuses étapes que nous verrons
par la suite.
Par contre, il ne permet pas de réaliser une sauvegarde incrémentale, et ne permet pas de conti‑
nuer à archiver les journaux, contrairement aux outils de PITR classiques. Cependant, ceux‑ci peuvent
l’utiliser pour réaliser la première copie des fichiers d’une instance.
Mise en place :
pg_basebackup nécessite des connexions de réplication. Il peut utiliser un slot de réplication, une
technique qui fiabilise la sauvegarde ou la réplication en indiquant à l’instance quels journaux elle doit
conserver. Par défaut, tout est en place pour cela dans PostgreSQL 10 et suivants pour une connexion
en local :
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
Ensuite, il faut configurer le fichier pg_hba.conf pour accepter la connexion du serveur où est exé‑
cutée pg_basebackup. Dans notre cas, il s’agit du même serveur avec un utilisateur dédié :
host replication sauve 127.0.0.1/32 scram-sha-256
Enfin, il faut créer un utilisateur dédié à la réplication (ici sauve) qui sera le rôle créant la connexion
et lui attribuer un mot de passe :
CREATE ROLE sauve LOGIN REPLICATION;
\password sauve
Dans un but d’automatisation, le mot de passe finira souvent dans un fichier .pgpass ou équi‑
valent.
Il ne reste plus qu’à :
– lancer pg_basebackup, ici en lui demandant une archive au format tar ;
– archiver les journaux en utilisant une connexion de réplication par streaming ;
– forcer le checkpoint.
Cela donne la commande suivante, ici pour une sauvegarde en local :
$ pg_basebackup --format=tar --wal-method=stream \
--checkpoint=fast --progress -h 127.0.0.1 -U sauve \
-D /var/lib/postgresql/backups/
Le résultat est ici un ensemble des deux archives : les journaux sont à part et devront être dépaquetés
dans le pg_wal à la restauration.
$ ls -l /var/lib/postgresql/backups/
total 4163772
-rw------- 1 postgres postgres 659785216 Oct 9 11:37 base.tar
-rw------- 1 postgres postgres 16780288 Oct 9 11:37 pg_wal.tar
La cible doit être vide. En cas d’arrêt avant la fin, il faudra tout recommencer de zéro, c’est une limite
de l’outil.
Restauration :
Pour restaurer, il suffit de remplacer le PGDATA corrompu par le contenu de l’archive, ou de créer
une nouvelle instance et de remplacer son PGDATA par cette sauvegarde. Au démarrage, l’instance
repérera qu’elle est une sauvegarde restaurée et réappliquera les journaux. L’instance contiendra les
données telles qu’elles étaient à la fin du pg_basebackup.
Noter que les fichiers de configuration ne sont PAS inclus s’ils ne sont pas dans le PGDATA, notamment
sur Debian et ses versions dérivées.
Différences entre les versions :
À partir de la v10, un slot temporaire sera créé par défaut pour garantir que le serveur gardera les
journaux jusqu’à leur copie intégrale.
À partir de la version 13, la commande pg_basebackup crée un fichier manifeste contenant la liste
des fichiers sauvegardés, leur taille et une somme de contrôle. Cela permet de vérifier la sauvegarde
avec l’outil pg_verifybackup (ce dernier ne fonctionne hélas que sur une sauvegarde au format
plain, ou décompressée).
Lisez bien la documentation de pg_basebackup1 pour votre version précise de PostgreSQL, des op‑
tions ont changé de nom au fil des versions.
1
https://docs.postgresql.fr/current/app‑pgbasebackup.html
2
https://dali.bo/i4_html
2 étapes :
® – Archivage des journaux de transactions
– archivage interne
– ou outil pg_receivewal
– Sauvegarde des fichiers
– pg_basebackup
– ou manuellement (outils de copie classiques)
Même si la mise en place est plus complexe qu’un pg_dump, la sauvegarde PITR demande peu
d’étapes. La première chose à faire est de mettre en place l’archivage des journaux de transactions.
Un choix est à faire entre un archivage classique et l’utilisation de l’outil pg_receivewal.
Lorsque cette étape est réalisée (et fonctionnelle), il est possible de passer à la seconde : la sauvegarde
des fichiers. Là‑aussi, il y a différentes possibilités : soit manuellement, soit pg_basebackup, soit
son propre script ou un outil extérieur.
® – Deux méthodes :
– processus interne archiver
– outil pg_receivewal (flux de réplication)
La méthode historique est la méthode utilisant le processus archiver. Ce processus fonctionne sur
le serveur à sauvegarder et est de la responsabilité du serveur PostgreSQL. Seule sa (bonne) configu‑
ration incombe au DBA.
Une autre méthode existe : pg_receivewal. Cet outil livré aussi avec PostgreSQL se comporte
comme un serveur secondaire. Il reconstitue les journaux de transactions à partir du flux de répli‑
cation.
Chaque solution a ses avantages et inconvénients qu’on étudiera après avoir détaillé leur mise en
place.
Dans le cas de l’archivage historique, le serveur PostgreSQL va exécuter une commande qui va copier
les journaux à l’extérieur de son répertoire de travail :
Dans le cas de l’archivage avec pg_receivewal, c’est cet outil qui va écrire les journaux dans un ré‑
pertoire de travail. Cette écriture ne peut se faire qu’en local. Cependant, le répertoire peut se trouver
dans un montage NFS.
L’exemple pris ici utilise le répertoire /mnt/nfs1/archivage comme répertoire de copie. Ce ré‑
pertoire est en fait un montage NFS. Il faut donc commencer par créer ce répertoire et s’assurer que
l’utilisateur Unix (ou Windows) postgres peut écrire dedans :
# mkdir /mnt/nfs1/archivage
# chown postgres:postgres /mnt/nfs1/archivage
® – configuration (postgresql.conf)
– wal_level = replica
– archive_mode = on (ou always)
– archive_command = '… une commande …'
– ou : archive_library = '… une bibliothèque …' (v15+)
– archive_timeout = '… min'
– Ne pas oublier de forcer l’écriture de l’archive sur disque
– Code retour de l’archivage entre 0 (ok) et 125
Après avoir créé le répertoire d’archivage, il faut configurer PostgreSQL pour lui indiquer comment
archiver.
Niveau d’archivage :
La valeur par défaut de wal_level est adéquate :
wal_level = replica
Ce paramètre indique le niveau des informations écrites dans les journaux de transactions. Avec un
niveau minimal, les journaux ne servent qu’à garantir la cohérence des fichiers de données en cas
de crash. Dans le cas d’un archivage, il faut écrire plus d’informations, d’où la nécessité du niveau
replica (défaut à partir de PostgreSQL 10).
Le niveau logical, nécessaire à la réplication logique3 , convient également.
Mode d’archivage :
Il s’active ainsi sur une instance seule ou primaire :
archive_mode = on
(La valeur always permet d’archiver depuis un secondaire). Le changement nécessite un redémar‑
rage !
Enfin, une commande d’archivage doit être définie par le paramètre archive_command. ar-
chive_command sert à archiver un seul fichier à chaque appel. PostgreSQL l’appelle une fois
pour chaque fichier WAL, impérativement dans l’ordre des fichiers. En cas d’échec, elle est répétée
indéfiniment jusqu’à réussite, avant de passer à l’archivage du fichier suivant. C’est la technique
encore la plus utilisée.
(Noter qu’à partir de la version 15, il existe une alternative, avec l’utilisation du paramètre ar-
chive_library. Il est possible d’indiquer une bibliothèque partagée qui fera ce travail d’archivage.
Une telle bibliothèque, écrite en C, devrait être plus puissante et performante. Un module basique
est fourni avec PostgreSQL : basic_archive4 ). Notre blog présente un exemple fonctionnel de module
d’archivage5 utilisant une extension en C pour compresser les journaux de transactions. Mais en
production, il vaudra mieux utiliser une bibliothèque fournie par un outil PITR reconnu. Cependant,
à notre connaissance (en août 2023), aucun outil connu n’utilise encore cette fonctionnalité.)
Exemples d’archive_command :
PostgreSQL laisse le soin à l’administrateur de définir la méthode d’archivage des journaux de tran‑
sactions suivant son contexte. Si vous utilisez un outil de sauvegarde, la commande vous sera pro‑
bablement fournie. Une simple commande de copie suffit dans la plupart des cas. La directive ar-
chive_command peut alors être positionnée comme suit :
archive_command = 'cp %p /mnt/nfs1/archivage/%f'
Le joker %p est remplacé par le chemin complet vers le journal de transactions à archiver, alors que le
joker %f correspond au nom du journal de transactions une fois archivé.
3
https://dali.bo/w5_html
4
https://docs.postgresql.fr/current/basic‑archive.html
5
https://blog.dalibo.com/2023/07/28/hackingpg2.html
En toute rigueur, une copie du fichier ne suffit pas. Par exemple, dans le cas de la commande cp, le
nouveau fichier n’est pas immédiatement écrit sur disque. La copie est effectuée dans le cache disque
du système d’exploitation. En cas de crash juste après la copie, il est tout à fait possible de perdre
l’archive. Il est donc essentiel d’ajouter une étape de synchronisation du cache sur disque.
La commande d’archivage suivante est donnée dans la documentation officielle à titre d’exemple :
archive_command = 'test ! -f /mnt/server/archivedir/%f && cp %p
↪ /mnt/server/archivedir/%f'
Cette commande a deux inconvénients. Elle ne garantit pas que les données seront syn‑
Á chronisées sur disque. De plus si le fichier existe ou a été copié partiellement à cause
d’une erreur précédente, la copie ne s’effectuera pas.
Cette protection est une bonne chose. Cependant, il faut être vigilant lorsque l’on rétablit le fonction‑
nement de l’archiver suite à un incident ayant provoqué des écritures partielles dans le répertoire
d’archive, comme une saturation de l’espace disque.
Il est aussi possible d’y placer le nom d’un script bash, perl ou autre. L’intérêt est de pouvoir faire plus
qu’une simple copie. On peut y ajouter la demande de synchronisation du cache sur disque. Il peut
aussi être intéressant de tracer l’action de l’archivage par exemple, ou encore de compresser le journal
avant archivage.
Il faut s’assurer d’une seule chose : la commande d’archivage doit retourner 0 en cas de
Á réussite et surtout une valeur différente de 0 en cas d’échec.
Si le code retour de la commande est compris entre 1 et 125, PostgreSQL va tenter pé‑
riodiquement d’archiver le fichier jusqu’à ce que la commande réussisse (autrement dit,
renvoie 0).
Tant qu’un fichier journal n’est pas considéré comme archivé avec succès, PostgreSQL
ne le supprimera ou recyclera pas !
Il ne cherchera pas non plus à archiver les fichiers suivants.
Il est donc important de surveiller le processus d’archivage et de faire remonter les problèmes à un
opérateur. Les causes d’échec sont nombreuses : problème réseau, montage inaccessible, erreur de
# barman
archive_command='/usr/bin/barman-wal-archive backup prod %p'
Enfin, le paramétrage suivant archive « dans le vide ». Cette astuce est utilisée lors de
b certains dépannages, ou pour éviter le redémarrage que nécessiterait la désactivation
de archive_mode.
archive_mode = on
archive_command = '/bin/true'
(La valeur par défaut, 0, désactive ce comportement.) Une conséquence sera l’archivage de journaux
de transactions partiellement remplis. Comme la taille reste fixe (16 Mo par fichier par défaut), la
consommation en terme d’espace disque sera donc plus importante (la compression par l’outil
d’archivage compense généralement cela), et le temps de restauration plus long.
® – Redémarrage de PostgreSQL
– si modification de wal_level et/ou archive_mode
– ou rechargement de la configuration
Il ne reste plus qu’à indiquer à PostgreSQL de recharger sa configuration pour que l’archivage soit
en place (avec SELECT pg_reload_conf(); ou la commande reload adaptée au système).
Dans le cas où l’un des paramètres wal_level et archive_mode a été modifié, il faudra relancer
PostgreSQL.
® – Vue pg_stat_archiver
– pg_wal/archive_status/
– Taille de pg_wal
– si saturation : Arrêt !
– Traces
S’il y a un problème d’archivage d’un journal, les suivants ne seront pas archivés non
¾ plus, et vont s’accumuler dans pg_wal ! De plus, une saturation de la partition portant
pg_wal mènera à l’arrêt de l’instance PostgreSQL !
-[ RECORD 1 ]------+------------------------------
archived_count | 156
last_archived_wal | 0000000200000001000000D9
last_archived_time | 2020-01-17 18:26:03.715946+00
failed_count | 6
last_failed_wal | 0000000200000001000000D7
last_failed_time | 2020-01-17 18:24:24.463038+00
stats_reset | 2020-01-17 16:08:37.980214+00
Comme dit plus haut, pour que cette supervision soit fiable, la commande exécutée doit renvoyer un
code retour inférieur ou égal à 125. Dans le cas contraire, le processus archiver redémarre et l’erreur
n’apparaît pas dans la vue !
Traces :
On trouvera la sortie et surtout les messages d’erreurs du script d’archivage dans les traces (qui dé‑
pendent bien sûr du script utilisé) :
2020-01-17 18:24:18.427 UTC [15431] LOG: archive command failed with exit code 3
2020-01-17 18:24:18.427 UTC [15431] DETAIL: The failed archive command was:
rsync pg_wal/0000000200000001000000D7 /opt/pgsql/archives/0000000200000001000000D7
rsync: change_dir#3 "/opt/pgsql/archives" failed: No such file or directory (2)
rsync error: errors selecting input/output files, dirs (code 3) at main.c(695)
[Receiver=3.1.2]
2020-01-17 18:24:19.456 UTC [15431] LOG: archive command failed with exit code 3
2020-01-17 18:24:19.456 UTC [15431] DETAIL: The failed archive command was:
rsync pg_wal/0000000200000001000000D7 /opt/pgsql/archives/0000000200000001000000D7
rsync: change_dir#3 "/opt/pgsql/archives" failed: No such file or directory (2)
rsync error: errors selecting input/output files, dirs (code 3) at main.c(695)
[Receiver=3.1.2]
2020-01-17 18:24:20.463 UTC [15431] LOG: archive command failed with exit code 3
C’est donc le premier endroit à regarder en cas de souci ou lors de la mise en place de l’archivage.
pg_wal/archive_status :
Enfin, on peut monitorer les fichiers présents dans pg_wal/archive_status. Les fichiers
.ready, de taille nulle, indiquent en permanence quels sont les journaux prêts à être archivés.
Théoriquement, leur nombre doit donc rester faible et retomber rapidement à 0 ou 1. Le service
ready_archives de la sonde Nagios check_pgactivity6 se base sur ce répertoire.
pg_ls_dir
--------------------------------
0000000200000001000000DE.done
0000000200000001000000DF.done
0000000200000001000000E0.done
0000000200000001000000E1.ready
0000000200000001000000E2.ready
0000000200000001000000E3.ready
0000000200000001000000E4.ready
0000000200000001000000E5.ready
0000000200000001000000E6.ready
00000002.history.done
6
https://github.com/OPMDG/check_pgactivity
7.4.6 pg_receivewal
pg_receivewal est un outil permettant de se faire passer pour un serveur secondaire utilisant la
réplication en flux (streaming replication) dans le but d’archiver en continu les journaux de transac‑
tions. Il fonctionne habituellement sur un autre serveur, où seront archivés les journaux. C’est une
alternative à l’archiver.
Comme il utilise le protocole de réplication, les journaux archivés ont un retard bien inférieur à celui in‑
duit par la configuration du paramètre archive_command ou du paramètre archive_library,
les journaux de transactions étant écrits au fil de l’eau avant d’être complets. Cela permet donc de faire
de l’archivage PITR avec une perte de données minimum en cas d’incident sur le serveur primaire. On
peut même utiliser une réplication synchrone (paramètres synchronous_commit et synchro-
nous_standby_names) pour ne perdre aucune transaction, si l’on accepte un impact certain sur
la latence des transactions.
Cet outil utilise les mêmes options de connexion que la plupart des outils PostgreSQL, avec en plus
l’option -D pour spécifier le répertoire où sauvegarder les journaux de transactions. L’utilisateur spé‑
cifié doit bien évidemment avoir les attributs LOGIN et REPLICATION.
Comme il s’agit de conserver toutes les modifications effectuées par le serveur dans le cadre d’une sau‑
vegarde permanente, il est nécessaire de s’assurer qu’on ne puisse pas perdre des journaux de transac‑
tions. Il n’y a qu’un seul moyen pour cela : utiliser la technologie des slots de réplication. En utilisant
un slot de réplication, pg_receivewal s’assure que le serveur ne va pas recycler des journaux dont
pg_receivewal n’aurait pas reçu les enregistrements. On retrouve donc le risque d’accumulation
des journaux sur le serveur principal si pg_receivewal ne fonctionne pas.
Voici l’aide de cet outil en v15 :
$ pg_receivewal --help
pg_receivewal reçoit le flux des journaux de transactions PostgreSQL.
Usage :
pg_receivewal [OPTION]...
Options :
-D, --directory=RÉPERTOIRE reçoit les journaux de transactions dans ce
répertoire
-E, --endpos=LSN quitte après avoir reçu le LSN spécifié
--if-not-exists ne pas renvoyer une erreur si le slot existe
Options de connexion :
-d, --dbname=CHAÎNE_CONNEX chaîne de connexion
-h, --host=HÔTE hôte du serveur de bases de données ou
répertoire des sockets
-p, --port=PORT numéro de port du serveur de bases de données
-U, --username=UTILISATEUR se connecte avec cet utilisateur
-w, --no-password ne demande jamais le mot de passe
-W, --password force la demande du mot de passe (devrait
survenir automatiquement)
Actions optionnelles :
--create-slot crée un nouveau slot de réplication
(pour le nom du slot, voir --slot)
--drop-slot supprime un nouveau slot de réplication
(pour le nom du slot, voir --slot)
max_wal_senders = 10
max_replication_slots = 10
– pg_hba.conf :
– Utilisateur de réplication :
Les connexions de réplication nécessitent une configuration particulière au niveau des accès.
D’où la modification du fichier pg_hba.conf. Le sous‑réseau (192.168.0.0/24) est à modifier
suivant l’adressage utilisé. Il est d’ailleurs préférable de n’indiquer que le serveur où est installé
pg_receivewal (plutôt que l’intégralité d’un sous‑réseau).
L’utilisation d’un utilisateur de réplication n’est pas obligatoire mais fortement conseillée pour des
raisons de sécurité.
® – Redémarrage de PostgreSQL
– Slot de réplication
SELECT pg_create_physical_replication_slot('archivage');
Pour que les modifications soient prises en compte, nous devons redémarrer le serveur.
Enfin, nous devons créer le slot de réplication qui sera utilisé par pg_receivewal. La fonction
pg_create_physical_replication_slot() est là pour ça. Il est à noter que la liste des slots
est disponible dans le catalogue système pg_replication_slots.
® – Exemple de lancement
Les journaux de transactions sont alors créés en temps réel dans le répertoire indiqué (ici,
/data/archives) :
En cas d’incident sur le serveur primaire, il est alors possible de partir d’une sauvegarde physique et
de rejouer les journaux de transactions disponibles (sans oublier de supprimer l’extension .partial
du dernier journal).
Il faut mettre en place un script de démarrage pour que pg_receivewal soit redémarré en cas de
redémarrage du serveur.
® – Méthode archiver
– simple à mettre en place
– perte au maximum d’un journal de transactions
– Méthode pg_receivewal
– mise en place plus complexe
– perte minimale voire nulle
La méthode archiver est la méthode la plus simple à mettre en place. Elle se lance au lancement du ser‑
veur PostgreSQL, donc il n’est pas nécessaire de créer et installer un script de démarrage. Cependant,
un journal de transactions n’est archivé que quand PostgreSQL l’ordonne, soit parce qu’il a rempli le
journal en question, soit parce qu’un utilisateur a forcé un changement de journal (avec la fonction
pg_switch_wal ou suite a un pg_stop_backup), soit parce que le délai maximum entre deux
archivages a été dépassé (paramètre archive_timeout). Il est donc possible de perdre un grand
nombre de transactions (même si cela reste bien inférieur à la perte qu’une restauration d’une sauve‑
garde logique occasionnerait).
La méthode pg_receivewal est plus complexe à mettre en place. Il faut exécuter ce démon, géné‑
ralement sur un autre serveur. Un script de démarrage doit donc être configuré. Par contre, elle a le
gros avantage de ne perdre pratiquement aucune transaction, surtout en mode synchrone. Les en‑
registrements de transactions sont envoyés en temps réel à pg_receivewal. Ce dernier les place
dans un fichier de suffixe .partial, qui est ensuite renommé pour devenir un journal de transac‑
tions complet.
® – 3 étapes :
– fonction de démarrage
– copie des fichiers par outil externe
– fonction d’arrêt
– Exclusive : simple… & obsolète ! (< v15)
– Concurrente : plus complexe à scripter
– Aucun impact pour les utilisateurs ; pas de verrou
– Préférer des outils dédiés qu’un script maison
Une fois l’archivage en place, une sauvegarde à chaud a lieu en trois temps :
Les sauvegardes manuelles servent cependant encore quand on veut utiliser une sauvegarde par
snapshot, ou avec rsync (car pg_basebackup ne sait pas synchroniser vers une sauvegarde inter‑
rompue ou ancienne), et quand les outils conseillés ne sont pas utilisables ou disponibles sur le sys‑
tème.
SELECT pg_backup_start (
® – un_label : texte
– fast : forcer un checkpoint ?
La sauvegarde PITR est donc devenue plus complexe au fil des versions, et il est donc recommandé
d’utiliser plutôt pg_basebackup ou des outils la supportant (barman, pgBackRest…).
Snapshot :
Il est assez fréquent que les sauvegardes se fassent par snapshot au niveau de la baie, de l’hyperviseur,
directement ou via divers outils intégrés (Veeam, Tina…), ou encore de l’OS (LVM, ZFS…). Il est cru‑
cial que cette sauvegarde soit bien cohérente, y compris entre les tablespaces. Cela est de la seule
responsabilité de l’outil utilisé. Il faut en étudier sa documentation pour savoir exactement ce qu’il
fait.
Dans les cas simples, la restauration de ce snapshot équivaudra pour PostgreSQL à un redémarrage
brutal, mais pour une sauvegarde PITR, il faudra encadrer le snapshot des appels aux fonctions de dé‑
marrage et d’arrêt ci‑dessus. La facilité d’implémentation dépend de l’outil utilisé. Il faut aussi vérifier
qu’il sait gérer les sauvegardes non exclusives pour utiliser PostgreSQL 15 et supérieurs.
Le point noir de la sauvegarde par snapshot est d’être liée au même système matériel
Á que l’instance PostgreSQL (disque, hyperviseur, datacenter…) Une défaillance grave du
matériel peut donc emporter, corrompre ou bloquer la sauvegarde en même temps que
la sauvegarde. La sécurité de l’instance est donc reportée sur celle de l’infrastructure
sous‑jacente. Une copie parallèle des données de manière plus classique est conseillée
pour éviter un désastre total.
Copie manuelle :
La sauvegarde se fait à chaud : il est donc possible que pendant ce temps des fichiers
b changent, disparaissent avant d’être copiés ou apparaissent sans être copiés. Cela n’a
pas d’importance en soi car les journaux de transactions corrigeront cela (leur archivage
doit donc commencer avant le début de la sauvegarde et se poursuivre sans interrup‑
tion jusqu’à la fin).
Il faut s’assurer que l’outil de sauvegarde supporte cela, c’est‑à‑dire qu’il soit capable
de différencier les codes d’erreurs dus à « des fichiers ont bougé ou disparu lors de la
sauvegarde » des autres erreurs techniques. tar par exemple convient : il retourne 1
pour le premier cas d’erreur, et 2 quand l’erreur est critique. rsync est très courant
également.
Sur les plateformes Microsoft Windows, peu d’outils sont capables de copier des fichiers en cours de
modification. Assurez‑vous d’en utiliser un possédant cette fonctionnalité. À noter : l’outil tar (ainsi
que d’autres issus du projet GNU) est disponible nativement à travers le projet unxutils8 .
Exclusions :
Des fichiers et répertoires sont à ignorer, pour gagner du temps ou faciliter la restauration. Voici la
liste exhaustive (disponible aussi dans la documentation officielle9 ) :
On n’oubliera pas les fichiers de configuration s’ils ne sont pas dans le PGDATA.
8
http://unxutils.sourceforge.net/
9
https://docs.postgresql.fr/current/continuous‑archiving.html#BACKUP‑LOWLEVEL‑BASE‑BACKUP
Ne pas oublier !!
® SELECT * FROM pg_backup_stop (
PostgreSQL va alors :
– marquer cette fin de backup dans le journal des transactions (étape capitale pour la restaura‑
tion) ;
– forcer la finalisation du journal de transactions courant et donc son archivage, afin que la sau‑
vegarde (fichiers + archives) soit utilisable même en cas de crash juste l’appel à la fonction :
pg_backup_stop() ne rendra pas la main (par défaut) tant que ce dernier journal n’aura
pas été archivé avec succès.
La fonction renvoie :
| 134152 /tbl/quota +
|
Ces informations se retrouvent aussi dans un fichier .backup mêlé aux journaux :
# cat /var/lib/postgresql/12/main/pg_wal/00000001000000220000002B.00000028.backup
pg_basebackup a été décrit plus haut. Il a l’avantage d’être simple à utiliser, de savoir quels fichiers
ne pas copier, de fiabiliser la sauvegarde par un slot de réplication. Il ne réclame en général pas de
configuration supplémentaire.
Si l’archivage est déjà en place, copier les journaux est inutile (--wal-method=none). Nous verrons
plus tard comment lui indiquer où les chercher.
L’inconvénient principal de pg_basebackup reste son incapacité à reprendre une sauvegarde inter‑
rompue ou à opérer une sauvegarde différentielle ou incrémentale.
La fréquence dépend des besoins. Une sauvegarde par jour est le plus commun, mais il est possible
d’espacer encore plus la fréquence.
Cependant, il faut conserver à l’esprit que plus la sauvegarde est ancienne, plus la restauration sera
longue car un plus grand nombre de journaux seront à rejouer.
® – Vue pg_stat_progress_basebackup
– à partir de la v13
La version 13 permet de suivre la progression de la sauvegarde de base, quelque soit l’outil utilisé à
condition qu’il passe par le protocole de réplication.
Cela permet ainsi de savoir à quelle phase la sauvegarde se trouve, quelle volumétrie a été envoyée,
celle à envoyer, etc.
La restauration se déroule en trois voire quatre étapes suivant qu’elle est effectuée sur le même ser‑
veur ou sur un autre serveur.
Dans le cas où la restauration a lieu sur le même serveur, quelques étapes préliminaires sont à effec‑
tuer.
Il faut arrêter PostgreSQL s’il n’est pas arrêté. Cela arrivera quand la restauration a pour but, par
exemple, de récupérer des données qui ont été supprimées par erreur.
Ensuite, il faut supprimer (ou archiver) l’ancien répertoire des données pour pouvoir y placer
l’ancienne sauvegarde des fichiers. Écraser l’ancien répertoire n’est pas suffisant, il faut le supprimer,
ainsi que les répertoires des tablespaces au cas où l’instance en possède.
La sauvegarde des fichiers peut enfin être restaurée. Il faut bien porter attention à ce que les fichiers
soient restaurés au même emplacement, tablespaces compris.
Une fois cette étape effectuée, il peut être intéressant de faire un peu de ménage. Par exemple, le fi‑
chier postmaster.pid peut poser un problème au démarrage. Conserver les journaux applicatifs
n’est pas en soi un problème mais peut porter à confusion. Il est donc préférable de les supprimer.
Quant aux journaux de transactions compris dans la sauvegarde, bien que ceux en provenance des
archives seront utilisés même s’ils sont présents aux deux emplacements, il est préférable de les sup‑
primer. La commande sera similaire à celle‑ci :
$ rm postmaster.pid log/* pg_wal/[0-9A-F]*
Enfin, s’il est possible d’accéder au journal de transactions courant au moment de l’arrêt de l’ancienne
instance, il est intéressant de le restaurer dans le répertoire pg_wal fraîchement nettoyé. Ce dernier
sera pris en compte en toute fin de restauration des journaux depuis les archives et permettra donc
de restaurer les toutes dernières transactions validées sur l’ancienne instance, mais pas encore archi‑
vées.
Jusqu’en version 11 incluse, la restauration se configure dans un fichier spécifique, appelé reco-
very.conf, impérativement dans le répertoire des données.
À partir de la version 12, on utilise directement postgresql.conf, ou un fichier inclus, ou post-
gresql.auto.conf. Par contre, il faut créer un fichier vide recovery.signal.
Ce sont ces fichiers recovery.signal ou recovery.conf qui permettent à PostgreSQL de sa‑
voir qu’il doit se lancer dans une restauration, et n’a pas simplement subi un arrêt brutal (auquel cas
il ne restaurerait que les journaux en place).
Si vous êtes sur une version antérieure à la version 12, vous pouvez vous inspirer du fichier
exemple fourni avec PostgreSQL. Pour une version 10 par exemple, sur Red Hat et dérivés,
c’est /usr/pgsql-10/share/recovery.conf.sample. Sur Debian et dérivés, c’est
/usr/share/postgresql/10/recovery.conf.sample. Il contient certains paramètres
liés à la mise en place d’un serveur secondaire (standby) inutiles ici. Sinon, les paramètres sont dans
les différentes parties du postgresql.conf.
Si le but est de restaurer tous les journaux archivés, il n’est pas nécessaire d’aller plus loin dans la
configuration. La restauration se poursuivra jusqu’à l’épuisement de tous les journaux disponibles.
® – Jusqu’où restaurer :
– recovery_target_name, recovery_target_time
– recovery_target_xid, recovery_target_lsn
– recovery_target_inclusive
– Le backup de base doit être antérieur !
– Suivi de timeline :
– recovery_target_timeline : latest ?
– Et on fait quoi ?
– recovery_target_action : pause
– pg_wal_replay_resume pour ouvrir immédiatement
– ou modifier & redémarrer
Si l’on ne veut pas simplement restaurer tous les journaux, par exemple pour s’arrêter avant une fausse
manipulation désastreuse, plusieurs paramètres permettent de préciser le point d’arrêt :
Dans les cas complexes, nous verrons plus tard que choisir la timeline (avec recovery_target_timeline,
en général à latest) peut être utile.
Il est possible de demander à la restauration de s’arrêter une fois arrivée au stade voulu avec :
recovery_target_action = pause
C’est même l’action par défaut si une des options d’arrêt ci‑dessus a été choisie : cela permet à
l’utilisateur de vérifier que le serveur est bien arrivé au point qu’il désirait. Les alternatives sont
promote et shutdown.
Si la cible est atteinte mais que l’on décide de continuer la restauration jusqu’à un autre point (évidem‑
ment postérieur), il faut modifier la cible de restauration dans le fichier de configuration, et redémar‑
rer PostgreSQL. C’est le seul moyen de rejouer d’autres journaux sans ouvrir l’instance en écriture.
(Le terme promote pour une restauration est un peu abusif.) pg_wal_replay_resume() — malgré
ce que pourrait laisser croire son nom ! — provoque ici l’arrêt immédiat de la restauration, donc ignore
les opérations contenues dans les WALs que l’on n’a pas souhaités restaurer, puis le serveur s’ouvre
en écriture sur une nouvelle timeline.
® – Démarrer PostgreSQL
– Rejeu des journaux
– Vérifier que le point de cohérence est atteint !
La dernière étape est particulièrement simple. Il suffit de démarrer PostgreSQL. PostgreSQL va com‑
prendre qu’il doit rejouer les journaux de transactions.
Les journaux doivent se dérouler au moins jusqu’à rencontrer le « point de cohérence », c’est‑à‑dire
la mention insérée par pg_backup_stop(). Avant cela, il n’est pas possible de savoir si les fichiers
issus du base backup sont à jour ou pas, et il est impossible de démarrer l’instance avant ce point. Le
message apparaît dans les traces et, dans le doute, on doit vérifier sa présence :
2020-01-17 16:08:37.285 UTC [15221] LOG: restored log file
↪ "000000010000000100000031"…
2020-01-17 16:08:37.789 UTC [15221] LOG: restored log file
↪ "000000010000000100000032"…
2020-01-17 16:08:37.949 UTC [15221] LOG: consistent recovery state reached
at 1/32BFDD88
2020-01-17 16:08:37.949 UTC [15217] LOG: database system is ready to accept
read only connections
2020-01-17 16:08:38.009 UTC [15221] LOG: restored log file
↪ "000000010000000100000033"…
PostgreSQL continue ensuite jusqu’à arriver à la limite fixée, jusqu’à ce qu’il ne trouve plus de journal
à rejouer, ou que le bloc de journal lu soit incohérent (ce qui indique qu’on est arrivé à la fin d’un
11
https://www.postgresql.org/docs/current/runtime‑config‑wal.html#RUNTIME‑CONFIG‑WAL‑RECOVERY‑TARGET
journal qui n’a pas été terminé, le journal courant au moment du crash par exemple). Par défaut en
v12 il vérifiera qu’il n’existe pas une timeline supérieure sur laquelle basculer (par exemple s’il s’agit
de la deuxième restauration depuis la sauvegarde du PGDATA). Puis il va s’ouvrir en écriture (sauf si
vous avez demandé recovery_target_action = pause).
2020-01-17 16:08:45.938 UTC [15221] LOG: restored log file "00000001000000010000003C"
from archive
2020-01-17 16:08:46.116 UTC [15221] LOG: restored log file
↪ "00000001000000010000003D"…
2020-01-17 16:08:46.547 UTC [15221] LOG: restored log file
↪ "00000001000000010000003E"…
2020-01-17 16:08:47.262 UTC [15221] LOG: restored log file
↪ "00000001000000010000003F"…
2020-01-17 16:08:47.842 UTC [15221] LOG: invalid record length at 1/3F0000A0:
wanted 24, got 0
2020-01-17 16:08:47.842 UTC [15221] LOG: redo done at 1/3F000028
2020-01-17 16:08:47.842 UTC [15221] LOG: last completed transaction was
at log time 2020-01-17 14:59:30.093491+00
2020-01-17 16:08:47.860 UTC [15221] LOG: restored log file
↪ "00000001000000010000003F"…
cp: cannot stat ‘/opt/pgsql/archives/00000002.history’: No such file or directory
2020-01-17 16:08:47.966 UTC [15221] LOG: selected new timeline ID: 2
2020-01-17 16:08:48.179 UTC [15221] LOG: archive recovery complete
cp: cannot stat ‘/opt/pgsql/archives/00000001.history’: No such file or directory
2020-01-17 16:08:51.613 UTC [15217] LOG: database system is ready
to accept connections
La durée de la restauration est fortement dépendante du nombre de journaux. Ils sont rejoués séquen‑
tiellement. Mais avant cela, un fichier journal peut devoir être récupéré, décompressé, et restauré
dans pg_wal.
Il est donc préférable qu’il n’y ait pas trop de journaux à rejouer, et donc qu’il n’y ait pas trop d’espaces
entre sauvegardes complètes successives.
La version 15 a optimisé le rejeu en permettant l’activation du prefetch des blocs de données lors du
rejeu des journaux.
Un outil comme pgBackRest en mode asynchrone permet de paralléliser la récupération des jour‑
naux, ce qui permet de les récupérer via le réseau et de les décompresser par avance pendant que
PostgreSQL traite les journaux précédents.
Lorsque le mode recovery s’arrête, au point dans le temps demandé ou faute d’archives disponibles,
l’instance accepte les écritures. De nouvelles transactions se produisent alors sur les différentes bases
de données de l’instance. Dans ce cas, l’historique des données prend un chemin différent par rapport
aux archives de journaux de transactions produites avant la restauration. Par exemple, dans ce nou‑
vel historique, il n’y a pas le DROP TABLE malencontreux qui a imposé de restaurer les données.
Cependant, cette transaction existe bien dans les archives des journaux de transactions.
On a alors plusieurs historiques des transactions, avec des « bifurcations » aux moments où on a réa‑
lisé des restaurations. PostgreSQL permet de garder ces historiques grâce à la notion de timeline. Une
timeline est donc l’un de ces historiques, elle se matérialise par un ensemble de journaux de tran‑
sactions, identifiée par un numéro. Le numéro de la timeline est le premier nombre hexadécimal du
nom des segments de journaux de transactions (le second est le numéro du journal et le troisième
le numéro du segment). Lorsqu’une instance termine une restauration PITR, elle peut archiver immé‑
diatement ces journaux de transactions au même endroit, les fichiers ne seront pas écrasés vu qu’ils
seront nommés différemment. Par exemple, après une restauration PITR s’arrêtant à un point situé
dans le segment 000000010000000000000009 :
$ ls -1 /backup/postgresql/archived_wal/
00000001000000010000003C
00000001000000010000003D
00000001000000010000003E
00000001000000010000003F
000000010000000100000040
00000002.history
00000002000000010000003F
000000020000000100000040
000000020000000100000041
À la sortie du mode recovery, l’instance doit choisir une nouvelle timeline. Les timelines connues avec
leur point de départ sont suivies grâce aux fichiers history, nommés d’après le numéro hexadécimal
sur huit caractères de la timeline et le suffixe .history, et archivés avec les fichiers WAL. En partant
de la timeline qu’elle quitte, l’instance restaure les fichiers history des timelines suivantes pour choisir
la première disponible, et archive un nouveau fichier .history pour la nouvelle timeline sélection‑
née, avec l’adresse du point de départ dans la timeline qu’elle quitte :
$ cat 00000002.history
1 0/9765A80 before 2015-10-20 16:59:30.103317+02
Cependant, jusqu’en version 11 comprise, la valeur par défaut est current et la res‑
Á tauration se fait dans la même timeline que le base backup. Si entre‑temps il y a eu une
bascule ou une précédente restauration, la nouvelle timeline ne sera pas automatique‑
ment suivie !
Pour choisir une autre timeline, il faut donner le numéro de la timeline cible comme valeur du pa‑
ramètre recovery_target_timeline. Cela permet d’effectuer plusieurs restaurations succes‑
sives à partir du même base backup et d’archiver au même endroit sans mélanger les journaux.
Le numéro de timeline dans les traces ou affiché par pg_controldata est en dé‑
¾ cimal. Mais les fichiers .history portent un numéro en hexadécimal (par exemple
00000014.history pour la timeline 20). On peut fournir les deux à reco-
very_target_timeline (20 ou '0x14'). Attention, il n’y a pas de contrôle !
Attention : pour restaurer dans une timeline précise, il faut que le fichier history correspondant soit
encore présent dans les archives, sous peine d’erreur.
écriture, elle va générer de nouveaux WAL, qui seront associés à la nouvelle timeline : ils n’écrasent pas
les fichiers WAL archivés de la timeline 1, ce qui permet de les réutiliser pour une autre restauration
en cas de besoin (par exemple si la transaction x42 utilisée comme point d’arrêt était trop loin dans
le passé, et que l’on désire restaurer de nouveau jusqu’à un point plus récent).
Un peu plus tard, on a de nouveau besoin d’effectuer une restauration dans le passé ‑ par exemple, une
nouvelle livraison applicative a été effectuée, mais le bug rencontré précédemment n’était toujours
pas corrigé. On restaure donc de nouveau les fichiers de l’instance à partir de la même sauvegarde,
puis on configure PostgreSQL pour suivre la timeline 2 (paramètre recovery_target_timeline
= 2) jusqu’à la transaction x55. Lors du recovery, l’instance va :
On démarre ensuite l’instance et on l’ouvre en écriture, on constate alors que celle‑ci bascule sur la
timeline 3, la bifurcation s’effectuant cette fois à la transaction x55.
Enfin, on se rend compte qu’un problème bien plus ancien et subtil a été introduit précédemment aux
deux restaurations effectuées. On décide alors de restaurer l’instance jusqu’à un point dans le temps
situé bien avant, jusqu’à la transaction x20. On restaure donc de nouveau les fichiers de l’instance
à partir de la même sauvegarde, et on configure le serveur pour restaurer jusqu’à la transaction x20.
Lors du recovery, l’instance va :
Comme la création des deux timelines précédentes est archivée dans les fichiers history, l’ouverture
de l’instance en écriture va basculer sur une nouvelle timeline (4). Suite à cette restauration, toutes
les modifications de données provoquées par des transactions effectuées sur la timeline 1 après la
transaction x20, ainsi que celles effectuées sur les timelines 2 et 3, ne sont donc pas présentes dans
l’instance.
Une fois le nouveau primaire en place, la production peut reprendre, mais il faut vérifier que la sauve‑
garde PITR est elle aussi fonctionnelle.
Ce nouveau primaire a généralement commencé à archiver ses journaux à partir du dernier journal
récupéré de l’ancien primaire, renommé avec l’extension .partial, juste avant la bascule sur la
nouvelle timeline. Il faut bien sûr vérifier que l’archivage des nouveaux journaux fonctionne.
Sur l’ancien primaire, les derniers journaux générés juste avant l’incident n’ont pas forcément été ar‑
chivés. Ceux‑ci possèdent un fichier témoin .ready dans pg_wal/archive_status. Même s’ils
ont été copiés manuellement vers le nouveau primaire avant sa promotion, celui‑ci ne les a pas archi‑
vés.
Rappelons qu’un « trou » dans le flux des journaux dans le dépôt des archives empêchera la restaura‑
tion d’aller au‑delà de ce point !
Il est possible de forcer l’archivage des fichiers .ready depuis l’ancien primaire, avant la bascule, en
exécutant à la main les restore_command que PostgreSQL aurait générées, mais la facilité pour le
faire dépend de l’outil.
La copie de journaux à la main est donc risquée. Il ne faut pas hésiter à reprendre une sauvegarde
complète du nouveau primaire pour repartir d’une base sûre.
Quant aux éventuels autres secondaires, il faut bien vérifier que leur configuration a été modifiée et
qu’ils suivent. S’ils sont en log shipping, la remarque sur l’archivage ci‑dessus est encore plus impor‑
tante.
® – Gagner en place
– …en compressant les journaux de transactions
– Les outils dédiés à la sauvegarde
L’un des problèmes de la sauvegarde PITR est la place prise sur disque par les journaux de transactions.
Avec un journal généré toutes les 5 minutes, cela représente 16 Mo (par défaut) toutes les 5 minutes,
soit 192 Mo par heure, ou 5 Go par jour. Il n’est pas forcément possible de conserver autant de journaux.
Une solution est la compression à la volée et il existe deux types de compression.
La méthode la plus simple et la plus sûre pour les données est une compression non destructive,
comme celle proposée par les outils gzip, bzip2, lzma, etc. L’algorithme peut être imposé ou inclus
dans l’outil PITR choisi. La compression peut ne pas être très intéressante en terme d’espace disque
gagné. Néanmoins, un fichier de 16 Mo aura généralement une taille compressée comprise entre 3 et
6 Mo. Attention par ailleurs au temps de compression des journaux, qui peut entraîner un retard consé‑
quent de l’archivage par rapport à l’écriture des journaux en cas d’écritures lourdes : une compression
élevée mais lente peut être dangereuse.
Noter que la compression des journaux à la source existe aussi (paramètre wal_compression,
désactivé par défaut), qui s’effectue au niveau de la page, avec un coût en CPU à l’écriture des
journaux.
Il n’est pas conseillé de réinventer la roue et d’écrire soi‑même des scripts de sauvegarde, qui doivent
prévoir de nombreux cas et bien gérer les erreurs. La sauvegarde concurrente est également difficile
à manier. Des outils reconnus existent, dont nous évoquerons brièvement les plus connus. Il en existe
d’autres. Ils ne font pas partie du projet PostgreSQL à proprement parler et doivent être installés sé‑
parément.
Les outils décrits succinctement plus bas fournissent :
– un outil pour procéder aux sauvegardes, gérer la péremption des archives… ;
– un outil qui sera appelé par archive_command.
Leur philosophie peut différer, notamment en terme de centralisation ou de compromis entre simpli‑
cité et fonctionnalités. Ces dernières s’enrichissent d’ailleurs au fil du temps.
7.7.3 pgBackRest
pgBackRest12 est un outil de gestion de sauvegardes PITR écrit en perl et en C, par David Steele de
Crunchy Data.
Il met l’accent sur les performances avec de gros volumes et les fonctionnalités, au prix d’une com‑
plexité à la configuration :
– un protocole dédié pour le transfert et la compression des données ;
– des opérations parallélisables en multi‑thread ;
– la possibilité de réaliser des sauvegardes complètes, différentielles et incrémentielles ;
– la possibilité d’archiver ou restaurer les WAL de façon asynchrone, et donc plus rapide ;
– la possibilité d’abandonner l’archivage en cas d’accumulation et de risque de saturation de
pg_wal ;
– la gestion de dépôts de sauvegarde multiples (pour sécuriser notamment),
– le support intégré de dépôts S3 ou Azure ;
– la sauvegarde depuis un serveur secondaire ;
– le chiffrement des sauvegardes ;
– la restauration en mode delta, très pratique pour restaurer un serveur qui a décroché mais n’a
que peu divergé ;
– la reprise d’une sauvegarde échouée.
Le projet est récent, très actif, considéré comme fiable, et les fonctionnalités proposées sont intéres‑
santes.
Pour la supervision de l’outil, une sonde Nagios est fournie par un des développeurs : check_pgbackrest13 .
7.7.4 barman
barman est un outil créé par 2ndQuadrant (racheté depuis par EDB). Il a pour but de faciliter la mise
en place de sauvegardes PITR. Il gère à la fois la sauvegarde et la restauration.
La commande barman dispose de plusieurs actions :
12
https://pgbackrest.org/
13
https://github.com/pgstef/check_pgbackrest/
Contrairement aux autre outils présentés ici, barman permet d’utiliser pg_receivewal.
Il supporte aussi les dépôts S3 ou blob Azure.
Site web de barman14
7.7.5 pitrery
pitrery a été créé par la société Dalibo. Il mettait l’accent sur la simplicité de sauvegarde et la restau‑
ration de la base.
Après 10 ans de développement actif, le projet Pitrery est désormais placé en mainte‑
Á nance LTS (Long Term Support) jusqu’en novembre 2026. Plus aucune nouvelle fonction‑
nalité n’y sera ajoutée, les mises à jour concerneront les correctifs de sécurité unique‑
ment. Il est désormais conseillé de lui préférer pgBackRest. Il n’est plus compatible avec
PostgreSQL 15 et supérieur.
14
https://www.pgbarman.org/
15
https://dalibo.github.io/pitrery/
7.8 CONCLUSION
® – Une sauvegarde
– fiable
– éprouvée
– rapide
– continue
– Mais
– plus complexe à mettre en place que pg_dump
– qui restaure toute l’instance
Cette méthode de sauvegarde est la seule utilisable dès que les besoins de performance de sauve‑
garde et de restauration augmentent (Recovery Time Objective ou RTO), ou que le volume de perte de
données doit être drastiquement réduit (Recovery Point Objective ou RPO).
7.8.1 Questions
7.9 QUIZ
https://dali.bo/i2_quiz
®
But : Créer une sauvegarde physique à chaud à un moment précis de la base avec
® pg_basebackup, et la restaurer.
– Arrêter l’instance.
– Faire une copie à froid des données (par exemple avec cp -rfp) vers /var/lib/pgsql/15/data.old
Une fois l’instance restaurée et démarrée, vérifier les traces : la base doit accepter les connexions.
Tenter une nouvelle restauration depuis l’archive pg_basebackup sans restaurer les journaux
de transaction. Que se passe‑t‑il ?
Vérifier les traces, ainsi que les données restaurées une fois le service démarré.
On n’aura ici pas besoin de l’archivage. S’il est déjà actif, on peut se contenter d’inhiber ainsi la com‑
mande d’archivage :
archive_mode = on
archive_command = '/bin/true'
Cela va ouvrir l’accès sans mot de passe depuis l’utilisateur système postgres.
Redémarrer PostgreSQL :
# systemctl restart postgresql-15
-[ RECORD 1 ]--------+---------------------------------
pid | 19763
phase | waiting for checkpoint to finish
backup_total |
backup_streamed | 0
tablespaces_total | 0
↪
tablespaces_streamed | 0
↪
-[ RECORD 1 ]--------+-------------------------
pid | 19763
phase | streaming database files
backup_total | 1611215360
backup_streamed | 29354496
tablespaces_total | 1
tablespaces_streamed | 0
…
$ ls -lha /var/lib/pgsql/15/backups/basebackup
…
-rw-------. 1 postgres postgres 180K Jan 5 17:00 backup_manifest
-rw-------. 1 postgres postgres 91M Jan 5 17:00 base.tar.gz
-rw-------. 1 postgres postgres 23M Jan 5 17:00 pg_wal.tar.gz
On obtient donc :
max
----------------------------
2023-01-05 17:01:51.595414
– Arrêter l’instance.
– Faire une copie à froid des données (par exemple avec cp -rfp) vers /var/lib/pgsql/15/data.old
(cette copie resservira plus tard).
On restaure dans le répertoire de données l’archive de base, puis les journaux dans leur sous‑
répertoire. La suppression des journaux est optionnelle, mais elle nous permettra de ne pas
mélanger les traces d’avant et d’après la restauration.
$ rm -rf /var/lib/pgsql/15/data/*
$ tar -C /var/lib/pgsql/15/data \
-xzf /var/lib/pgsql/15/backups/basebackup/base.tar.gz
$ tar -C /var/lib/pgsql/15/data/pg_wal \
-xzf /var/lib/pgsql/15/backups/basebackup/pg_wal.tar.gz
$ rm -rf /var/lib/pgsql/15/data/log/*
Une fois l’instance restaurée et démarrée, vérifier les traces : la base doit accepter les connexions.
$ tail -F /var/lib/pgsql/15/data/log/postgresql-*.log
…
2023-01-05 17:10:51.412 UTC [20057] LOG: database system was interrupted; last
↪ known up at 2023-01-05 16:59:03 UTC
2023-01-05 17:10:51.510 UTC [20057] LOG: redo starts at 0/830000B0
2023-01-05 17:10:52.105 UTC [20057] LOG: consistent recovery state reached at
↪ 0/8E8450F0
2023-01-05 17:10:52.105 UTC [20057] LOG: redo done at 0/8E8450F0 system usage: CPU:
↪ user: 0.28 s, system: 0.24 s, elapsed: 0.59 s
2023-01-05 17:10:52.181 UTC [20055] LOG: checkpoint starting: end-of-recovery
↪ immediate wait
2023-01-05 17:10:53.446 UTC [20055] LOG: checkpoint complete: wrote 16008 buffers
↪ (97.7%); …
2023-01-05 17:10:53.466 UTC [20051] LOG: database system is ready to accept
↪ connections
PostgreSQL considère qu’il a été interrompu brutalement et part en recovery. Noter en particulier la
mention consistent recovery state reached : la sauvegarde est bien cohérente.
max
----------------------------
2023-01-05 17:00:40.936925
Grâce aux journaux (pg_wal) restaurés, l’ensemble des modifications survenues pendant la sauve‑
garde ont bien été récupérées. Par contre, les données générées après la sauvegarde n’ont, elles, pas
été récupérées.
Tenter une nouvelle restauration depuis l’archive pg_basebackup sans restaurer les journaux
de transaction. Que se passe‑t‑il ?
Résultat :
Job for postgresql-15.service failed because the control process exited with error
↪ code.
See "systemctl status postgresql-15.service" and "journalctl -xe" for details.
# tail -F /var/lib/pgsql/15/data/log/postgresql-*.log
…
2023-01-05 17:16:52.048 UTC [20177] LOG: database system was interrupted; last
↪ known up at 2023-01-05 16:59:03 UTC
2023-01-05 17:16:52.134 UTC [20177] LOG: invalid checkpoint record
2023-01-05 17:16:52.134 UTC [20177] FATAL: could not locate required checkpoint
↪ record
2023-01-05 17:16:52.134 UTC [20177] HINT: If you are restoring from a backup, touch
↪ "/var/lib/pgsql/15/data/recovery.signal" and add required recovery options.
If you are not restoring from a backup, try removing the file
↪ "/var/lib/pgsql/15/data/backup_label".
Be careful: removing "/var/lib/pgsql/15/data/backup_label" will result in a
↪ corrupt cluster if restoring from a backup.
PostgreSQL ne trouve pas les journaux nécessaires à sa restauration à un état cohérent, le service
refuse de démarrer. Il a trouvé un checkpoint dans le fichier backup_label créé au début de la
sauvegarde, mais aucun checkpoint postérieur dans les journaux (et pour cause).
Les traces contiennent ensuite des suggestions qui peuvent être utiles.
Cependant, un fichier recovery.signal ne sert à rien sans recovery_command, et nous n’en
avons pas encore paramétré ici.
Là encore, en production, ce sera un partage distant. L’utilisateur système postgres doit avoir le droit
d’y écrire.
L’archivage se définit dans postgresql.conf :
archive_mode = on
archive_command = 'rsync %p /var/lib/pgsql/15/archives/%f'
$ ls -lha /var/lib/pgsql/15/archives
…
-rw-------. 1 postgres postgres 16M Jan 5 18:32 0000000100000000000000BB
-rw-------. 1 postgres postgres 16M Jan 5 18:32 0000000100000000000000BC
-rw-------. 1 postgres postgres 16M Jan 5 18:32 0000000100000000000000BD
…
$ rm -rf /var/lib/pgsql/15/backups/basebackup
$ pg_basebackup -D /var/lib/pgsql/15/backups/basebackup -Fp \
--checkpoint=fast --progress --max-rate=16M
Démarrer le service :
# systemctl start postgresql-15
Vérifier les traces, ainsi que les données restaurées une fois le service démarré.
Les traces sont plus complexes à cause de la restauration depuis les archives :
# tail -F /var/lib/pgsql/15/data/log/postgresql-*.log
…
2023-01-05 18:43:24.572 UTC [22535] LOG: database system was interrupted; last
↪ known up at 2023-01-05 18:32:52 UTC
rsync: link_stat "/var/lib/pgsql/15/archives/00000002.history" failed: No such file
↪ or directory (2)
rsync error: some files/attrs were not transferred (see previous errors) (code 23)
↪ at main.c(1189) [sender=3.1.3]
2023-01-05 18:43:24.657 UTC [22535] LOG: starting archive recovery
2023-01-05 18:43:24.728 UTC [22535] LOG: restored log file
↪ "0000000100000000000000BE" from archive
2023-01-05 18:43:24.797 UTC [22535] LOG: redo starts at 0/BE003E00
2023-01-05 18:43:24.960 UTC [22535] LOG: restored log file
↪ "0000000100000000000000BF" from archive
2023-01-05 18:43:25.167 UTC [22535] LOG: restored log file
↪ "0000000100000000000000C0" from archive
2023-01-05 18:43:25.408 UTC [22535] LOG: restored log file
↪ "0000000100000000000000C1" from archive
…
2023-01-05 18:43:27.036 UTC [22535] LOG: restored log file
↪ "0000000100000000000000C8" from archive
2023-01-05 18:43:27.240 UTC [22535] LOG: restored log file
↪ "0000000100000000000000C9" from archive
2023-01-05 18:43:27.331 UTC [22535] LOG: consistent recovery state reached at
↪ 0/C864D0F8
2023-01-05 18:43:27.331 UTC [22530] LOG: database system is ready to accept
↪ read-only connections
2023-01-05 18:43:27.551 UTC [22535] LOG: restored log file
↪ "0000000100000000000000CA" from archive
Les messages d’erreur de rsync ne sont pas inquiétants : celui‑ci ne trouve simplement pas
les fichiers demandés à la restore_command. PostgreSQL sait ainsi qu’il n’y a pas de fichier
00000002.history et donc pas de timeline de ce numéro. Il devine aussi qu’il a restauré tous les
journaux quand la récupération de l’un d’entre eux échoue.
La progression de la restauration peut être suivie grâce aux différents messages ci‑dessous, de démar‑
rage, d’atteinte du point de cohérence, de statut… jusqu’à l’heure exacte de restauration. Enfin, il y a
bascule sur une nouvelle timeline, et un checkpoint.
LOG: starting archive recovery
LOG: redo starts at 0/BE003E00
LOG: consistent recovery state reached at 0/C864D0F8
LOG: redo in progress, elapsed time: 10.25 s, current LSN: 0/E0FF3438
LOG: redo done at 0/F0A6C9E0 …
LOG: last completed transaction was at log time 2023-01-05 18:41:23.077219+00
LOG: selected new timeline ID: 2
LOG: archive recovery complete
LOG: checkpoint complete:
Cette fois, toutes les données générées après la sauvegarde ont bien été récupérées :
$ psql -d bench -c 'SELECT max(mtime) FROM pgbench_history;'
max
----------------------------
2023-01-05 18:41:23.068948
313
DALIBO Formations
8.1 INTRODUCTION
Ce module se propose de faire une description des bonnes et mauvaises pratiques en cas de coup
dur :
– crash de l’instance ;
– suppression / corruption de fichiers ;
– problèmes matériels ;
– sauvegardes corrompues…
Seront également présentées les situations classiques de désastres, ainsi que certaines méthodes et
outils dangereux et déconseillés.
L’objectif est d’aider à convaincre de l’intérêt qu’il y a à anticiper les problèmes, à mettre en place une
politique de sauvegarde pérenne, et à ne pas tenter de manipulation dangereuse sans comprendre
précisément à quoi l’on s’expose.
Ce module est en grande partie inspiré de The Worst Day of Your Life, une présentation de Christophe
Pettus au FOSDEM 20141
8.1.1 Au menu
1
http://thebuild.com/presentations/worst‑day‑fosdem‑2014.pdf
8.2.1 Documentation
Cette documentation devrait détailler l’architecture dans son ensemble, et particulièrement la poli‑
tique de sauvegarde choisie, l’emplacement de celles‑ci, les procédures de restauration et éventuel‑
lement de bascule vers un environnement de secours.
Les procédures d’exploitation doivent y être expliquées, de façon détaillée mais claire, afin qu’il n’y
ait pas de doute sur les actions à effectuer une fois la cause du problème identifié.
La méthode d’accès aux informations utiles (traces de l’instance, du système, supervision…) devrait
également être soigneusement documentée afin que le diagnostic du problème soit aussi simple que
possible.
Toutes ces informations doivent être organisées de façon claire, afin qu’elles soient immédiatement
accessibles et exploitables aux intervenants lors d’un problème.
Il est évidemment tout aussi important de penser à versionner et sauvegarder cette documentation,
afin que celle‑ci soit toujours accessible même en cas de désastre majeur (perte d’un site).
La gestion d’un désastre est une situation particulièrement stressante, le risque d’erreur humaine est
donc accru.
Un DBA devant restaurer d’urgence l’instance de production en pleine nuit courera plus de risques de
faire une fausse manipulation s’il doit taper une vingtaine de commandes en suivant une procédure
dans une autre fenêtre (voire un autre poste) que s’il n’a qu’un script à exécuter.
En conséquence, il est important de minimiser le nombre d’actions manuelles à effectuer dans les
procédures, en privilégiant l’usage de scripts d’exploitation ou d’outils dédiés (comme pgBackRest
ou barman pour restaurer une instance PostgreSQL).
Néanmoins, même cette pratique ne suffit pas à exclure tout risque.
L’utilisation de ces scripts ou de ces outils doit également être comprise, correctement documentée,
et les procédures régulièrement testées. Le test idéal consiste à remonter fréquemment des environ‑
nements de développement et de test ; vos développeurs vous en seront d’ailleurs reconnaissants.
Dans le cas contraire, l’utilisation d’un script ou d’un outil peut aggraver le problème, parfois de façon
dramatique — par exemple, l’écrasement d’un environnement sain lors d’une restauration parce que
la procédure ne mentionne pas que le script doit être lancé depuis un serveur particulier.
L’aspect le plus important est de s’assurer par des tests réguliers et manuels que les procédures sont
à jour, n’ont pas de comportement inattendu, et sont maîtrisées par toute l’équipe d’exploitation.
Tout comme pour la documentation, les scripts d’exploitation doivent également être sauvegardés et
versionnés.
La supervision est un sujet vaste, qui touche plus au domaine de la haute disponibilité.
Un désastre sera d’autant plus difficile à gérer qu’il est détecté tard. La supervision en place doit donc
être pensée pour détecter tout type de défaillance (penser également à superviser la supervision !).
Attention à bien calibrer les niveaux d’alerte, la présence de trop de messages augmente le risque que
l’un d’eux passe inaperçu, et donc que l’incident ne soit détecté que tardivement.
Pour aider la phase de diagnostic de l’origine du problème, il faut prévoir d’historiser un maximum
d’informations.
La présentation de celles‑ci est également importante : il est plus facile de distinguer un pic brutal du
nombre de connexions sur un graphique que dans un fichier de traces de plusieurs Go !
8.2.4 Automatisation
Si on poursuit jusqu’au bout le raisonnement précédent sur le risque à faire effectuer de nombreuses
opérations manuelles lors d’un incident, la conclusion logique est que la solution idéale serait de les
éliminer complètement, et d’automatiser complètement le déclenchement et l’exécution de la procé‑
dure.
Un problème est que toute solution visant à automatiser une tâche se base sur un nombre limité de
paramètres et sur une vision restreinte de l’architecture.
De plus, il est difficile à un outil de bascule automatique de diagnostiquer correctement certains types
d’incident, par exemple une partition réseau. L’outil peut donc détecter à tort à un incident, surtout s’il
est réglé de façon à être assez sensible, et ainsi provoquer lui‑même une coupure de service inutile.
Dans le pire des cas, l’outil peut être amené à prendre une mauvaise décision amenant à une situation
de désastre, comme un split brain (deux instances PostgreSQL se retrouvent ouvertes en écriture en
même temps sur les mêmes données).
Il est donc fortement préférable de laisser un administrateur prendre les décisions potentiellement
dangereuses, comme une bascule ou une restauration.
En dépit de toutes les précautions que l’on peut être amené à prendre, rien ne peut garantir qu’aucun
problème ne surviendra.
Il faut donc être capable d’identifier le problème lorsqu’il survient, et être prêt à y répondre.
® – Crash de l’instance
– Résultats de requêtes erronnés
– Messages d’erreurs dans les traces
– Dégradation importante des temps d’exécution
– Processus manquants
– ou en court d’exécution depuis trop longtemps
De très nombreux éléments peuvent aider à identifier que l’on est en situation d’incident grave.
Le plus flagrant est évidemment le crash complet de l’instance PostgreSQL, ou du serveur
l’hébergeant, et l’impossibilité pour PostgreSQL de redémarrer.
Les désastres les plus importants ne sont toutefois pas toujours aussi simples à détecter.
Les crash peuvent se produire uniquement de façon ponctuelle, et il existe des cas où l’instance
redémarre immédiatement après (typiquement suite au kill -9 d’un processus backend
PostgreSQL).
Cas encore plus délicat, il peut également arriver que les résultats de requêtes soient erronés (par
exemple en cas de corruption de fichiers d’index) sans qu’aucune erreur n’apparaisse.
Les symptômes classiques permettant de détecter un problème majeur sont :
– la présence de messages d’erreurs dans les traces de PostgreSQL (notamment des messages
PANIC ou FATAL, mais les messages ERROR et WARNING sont également très significatifs, par‑
ticulièrement s’ils apparaissent soudainement en très grand nombre) ;
Une fois que l’incident est repéré, il est important de ne pas foncer tête baissée dans des manipula‑
tions.
Il faut bien sûr prendre en considération la criticité du problème, notamment pour définir la priorité
des actions (par exemple, en cas de perte totale d’un site, quelles sont les applications à basculer en
priorité ?), mais quelle que soit la criticité ou l’impact, il ne faut jamais effectuer une action sans en
avoir parfaitement saisi l’impact et s’être assuré qu’elle répondait bien au problème rencontré.
Si le travail s’effectue en équipe, il faut bien faire attention à répartir les tâches clairement, afin d’éviter
des manipulations concurrentes ou des oublis qui pourraient aggraver la situation.
Il faut également éviter de multiplier les canaux de communication, cela risque de favoriser la perte
d’information, ce qui est critique dans une situation de crise.
Surtout, une règle majeure est de prendre le temps de noter systématiquement toutes les actions
entreprises.
Les commandes passées, les options utilisées, l’heure d’exécution, toutes ces informations sont très
importantes, déjà pour pouvoir agir efficacement en cas de fausse manipulation, mais également
pour documenter la gestion de l’incident après coup, et ainsi en conserver une trace qui sera pré‑
cieuse si celui‑ci venait à se reproduire.
S’il y a suspicion de potentielle corruption de données, il est primordial de s’assurer au plus vite de
couper tous les accès applicatifs vers l’instance afin de ne pas aggraver la situation.
Il est généralement préférable d’avoir une coupure de service plutôt qu’un grand volume de données
irrécupérables.
Ensuite, il faut impérativement faire une sauvegarde complète de l’instance avant de procéder à toute
manipulation. En fonction de la nature du problème rencontré, le type de sauvegarde pouvant être
effectué peut varier (un export de données ne sera possible que si l’instance est démarrée et que
les fichiers sont lisibles par exemple). En cas de doute, la sauvegarde la plus fiable qu’il est possible
d’effectuer est une copie des fichiers à froid (instance arrêtée) ‑ toute autre action (y compris un export
de données) pourrait avoir des conséquences indésirables.
Si des manipulations doivent être tentées pour tenter de récupérer des données, il faut impérative‑
ment travailler sur une copie de l’instance, restaurée à partir de cette sauvegarde. Ne jamais travailler
directement sur une instance de production corrompue, la moindre action (même en lecture) pourrait
aggraver le problème !
2
https://wiki.postgresql.org/wiki/Corruption
La première chose à identifier est l’instant précis où le problème a commencé à se manifester. Cette
information est en effet déterminante pour identifier la cause du problème, et le résoudre — notam‑
ment pour savoir à quel instant il faut restaurer l’instance si cela est nécessaire.
Il convient pour cela d’utiliser les outils de supervision et de traces (système, applicatif et Post‑
greSQL) pour remonter au moment d’apparition des premiers symptômes. Attention toutefois à ne
pas confondre les symptômes avec le problème lui‑même ! Les symptômes les plus visibles ne sont
pas forcément apparus les premiers. Par exemple, la charge sur la machine est un symptôme, mais
n’est jamais la cause du problème. Elle est liée à d’autres phénomènes, comme des problèmes avec
les disques ou un grand nombre de connexions, qui peuvent avoir commencé à se manifester bien
avant que la charge ne commence réellement à augmenter.
Si la nature du problème n’est pas évidente à ce stade, il faut examiner l’ensemble de l’architecture en
cause, sans en exclure d’office certains composants (baie de stockage, progiciel…), quels que soient
leur complexité / coût / stabilité supposés. Si le comportement observé côté PostgreSQL est difficile à
expliquer (crashs plus ou moins aléatoires, nombreux messages d’erreur sans lien apparent…), il est
préférable de commencer par s’assurer qu’il n’y a pas un problème de plus grande ampleur (système
de stockage, virtualisation, réseau, système d’exploitation).
Un bon indicateur consiste à regarder si d’autres instances / applications / processus rencontrent des
problèmes similaires.
Ensuite, une fois que l’ampleur du problème a été cernée, il faut procéder méthodiquement pour en
déterminer la cause et les éléments affectés.
Pour cela, les informations les plus utiles se trouvent dans les traces, généralement de PostgreSQL
ou du système, qui vont permettre d’identifier précisément les éventuels fichiers ou relations corrom‑
pus.
Cette recommandation peut paraître aller de soi, mais si les problèmes sont provoqués par une dé‑
faillance matérielle, il est impératif de s’assurer que le travail de correction soit effectué sur un envi‑
ronnement non affecté.
Cela peut s’avérer problématique dans le cadre d’architecture mutualisant les ressources, comme des
environnements virtualisés ou utilisant une baie de stockage.
Prendre également la précaution de vérifier que l’intégrité des sauvegardes n’est pas affectée par le
problème.
Si la situation semble échapper à tout contrôle, et dépasser les compétences de l’équipe en cours
d’intervention, il faut chercher de l’aide auprès de personnes compétentes, par exemple auprès
d’autres équipes, du support.
En aucun cas, il ne faut se mettre à suivre des recommandations glanées sur Internet, qui ne se rap‑
porteraient que très approximativement au problème rencontré, voire pas du tout. Si nécessaire, on
trouve en ligne des forums et des listes de discussions spécialisées sur lesquels il est également pos‑
sible d’obtenir des conseils — il est néanmoins indispensable de prendre en compte que les personnes
intervenant sur ces médias le font de manière bénévole. Il est déraisonnable de s’attendre à une ré‑
action immédiate, aussi urgent le problème soit‑il, et les suggestions effectuées le sont sans aucune
garantie.
Dans l’idéal, des procédures détaillant les actions à effectuer ont été écrites pour le cas de figure ren‑
contré. Dans ce cas, une fois que l’on s’est assuré d’avoir identifié la procédure appropriée, il faut la
dérouler méthodiquement, point par point, et valider à chaque étape que tout se déroule comme
prévu.
Si une étape de la procédure ne se passe pas comme prévu, il ne faut pas tenter de poursuivre tout
de même son exécution sans avoir compris ce qui s’est passé et les conséquences. Cela pourrait être
dangereux.
Il faut au contraire prendre le temps de comprendre le problème en procédant comme décrit précé‑
demment, quitte à remettre en cause toute l’analyse menée auparavant, et la procédure ou les scripts
utilisés.
C’est également pour parer à ce type de cas de figure qu’il est important de travailler sur une copie et
non sur l’environnement de production directement.
Ce n’est heureusement pas fréquent, mais il est possible que l’origine du problème soit liée à un bug
de PostgreSQL lui‑même.
Dans ce cas, la méthodologie appropriée consiste à essayer de reproduire le problème le plus fidèle‑
ment possible et de façon systématique, pour le cerner au mieux.
Il est ensuite très important de le signaler au plus vite à la communauté, généralement sur la liste
pgsql‑bugs@postgresql.org (cela nécessite une inscription préalable), en respectant les règles défi‑
nies dans la documentation3 .
Pour les problèmes relevant du domaine de la sécurité (découverte d’une faille), la liste adéquate est
security@postgresql.org.
3
https://www.postgresql.org/docs/current/static/bug‑reporting.html
® – Après correction
– Tester complètement l’intégrité des données
– pour détecter tous les problèmes
– Validation avec export logique complet
– Ou physique
pg_basebackup
Une fois les actions correctives réalisées (restauration, recréation d’objets, mise à jour des données…),
il faut tester intensivement pour s’assurer que le problème est bien complètement résolu.
Il est donc extrêmement important d’avoir préparé des cas de tests permettant de reproduire le pro‑
blème de façon certaine, afin de valider la solution appliquée.
En cas de suspicion de corruption de données, il est également important de tenter de procéder à la
lecture de la totalité des données depuis PostgreSQL.
Un premier outil pour cela est une sauvegarde avec pg_basebackup (voir plus loin).
Alternativement, la commande suivante, exécutée avec l’utilisateur système propriétaire de l’instance
(généralement postgres) effectue une lecture complète de toutes les tables (mais sans les index ni
les vues matérialisées), sans nécessiter de place sur disque supplémentaire :
$ pg_dumpall > /dev/null
Cette commande ne devrait renvoyer aucune erreur. En cas de problème, notamment une somme de
contrôle qui échoue, une erreur apparaîtra :
pg_dump: WARNING: page verification failed, calculated checksum 20565 but expected
↪ 17796
pg_dump: erreur : Sauvegarde du contenu de la table « corrompue » échouée :
échec de PQgetResult().
pg_dump: erreur : Message d'erreur du serveur :
ERROR: invalid page in block 0 of relation base/104818/104828
Même si la lecture des données par pg_dumpall ou pg_dump ne renvoie aucune erreur, il est tou‑
jours possible que des problèmes subsistent, par exemple des corruptions silencieuses, des index in‑
cohérents avec les données…
Dans les situations les plus extrêmes (problème de stockage, fichiers corrompus), il est important de
tester la validité des données dans une nouvelle instance en effectuant un export/import complet des
données.
Par exemple, initialiser une nouvelle instance avec initdb, sur un autre système de stockage, voire
sur un autre serveur, puis lancer la commande suivante (l’application doit être coupée, ce qui est nor‑
malement le cas depuis la détection de l’incident si les conseils précédents ont été suivis) pour expor‑
ter et importer à la volée :
$ pg_dumpall -h <serveur_corrompu> -U postgres | psql -h <nouveau_serveur> \
-U postgres postgres
$ vacuumdb --analyze -h <nouveau_serveur> -U postgres postgres
D’éventuels problèmes peuvent être détectés lors de l’import des données, par exemple si des cor‑
ruptions entraînent l’échec de la reconstruction de clés étrangères. Il faut alors procéder au cas par
cas.
Enfin, même si cette étape s’est déroulée sans erreur, tout risque n’est pas écarté, il reste la possibilité
de corruption de données silencieuses. Sauf si la fonctionnalité de checksum de PostgreSQL a été
activée sur l’instance (ce n’est pas activé par défaut !), le seul moyen de détecter ce type de problème
est de valider les données fonctionnellement.
Dans tous les cas, en cas de suspicion de corruption de données en profondeur, il est fortement préfé‑
rable d’accepter une perte de données et de restaurer une sauvegarde d’avant le début de l’incident,
plutôt que de continuer à travailler avec des données dont l’intégrité n’est pas assurée.
® – Paniquer
– Prendre une décision hâtive
– exemple, supprimer des fichiers du répertoire pg_wal
– Lancer une commande sans la comprendre, par exemple :
– pg_resetwal
– l’extension pg_surgery
– DANGER, dernier espoir
Quelle que soit la criticité du problème rencontré, la panique peut en faire quelque chose de pire.
Il faut impérativement garder son calme, et résister au mieux au stress et aux pressions qu’une situa‑
tion de désastre ne manque pas de provoquer.
Il est également préférable d’éviter de sauter immédiatement à la conclusion la plus évidente. Il ne
faut pas hésiter à retirer les mains du clavier pour prendre de la distance par rapport aux conséquences
du problème, réfléchir aux causes possibles, prendre le temps d’aller chercher de l’information pour
réévaluer l’ampleur réelle du problème.
La plus mauvaise décision que l’on peut être amenée à prendre lors de la gestion d’un incident est
celle que l’on prend dans la précipitation, sans avoir bien réfléchi et mesuré son impact. Cela peut
provoquer des dégâts irrécupérables, et transformer une situation d’incident en situation de crise
majeure.
Un exemple classique de ce type de comportement est le cas où PostgreSQL est arrêté suite au rem‑
plissage du système de fichiers contenant les fichiers WAL, pg_wal.
Le réflexe immédiat d’un administrateur non averti pourrait être de supprimer les plus vieux fichiers
dans ce répertoire, ce qui répond bien aux symptômes observés mais reste une erreur dramatique qui
va rendre le démarrage de l’instance impossible.
Quoi qu’il arrive, ne jamais exécuter une commande sans être certain qu’elle correspond bien à la
situation rencontrée, et sans en maîtriser complètement les impacts. Même si cette commande pro‑
vient d’un document mentionnant les mêmes messages d’erreur que ceux rencontrés (et tout parti‑
culièrement si le document a été trouvé via une recherche hâtive sur Internet) !
Là encore, nous disposons comme exemple d’une erreur malheureusement fréquente, l’exécution de
la commande pg_resetwal sur une instance rencontrant un problème. Comme l’indique la docu‑
mentation, « [cette commande] ne doit être utilisée qu’en dernier ressort quand le serveur ne démarre
plus du fait d’une telle corruption » et « il ne faut pas perdre de vue que la base de données peut contenir
des données incohérentes du fait de transactions partiellement validées » (documentation4 ). Nous re‑
viendrons ultérieurement sur les (rares) cas d’usage réels de cette commande, mais dans l’immense
majorité des cas, l’utiliser va aggraver le problème, en ajoutant des problématiques de corruption
logique des données !
Il convient donc de bien s’assurer de comprendre les conséquences de l’exécution de chaque action
effectuée.
4
https://docs.postgresql.fr/current/app‑pgresetwal.html
Il est important de pousser la réflexion jusqu’à avoir complètement compris l’origine du problème et
ses conséquences.
En premier lieu, même si les symptômes semblent avoir disparus, il est tout à fait possible que le
problème soit toujours sous‑jacent, ou qu’il ait eu des conséquences moins visibles mais tout aussi
graves (par exemple, une corruption logique de données).
Ensuite, même si le problème est effectivement corrigé, prendre le temps de comprendre et de docu‑
menter l’origine du problème (rapport « post‑mortem ») a une valeur inestimable pour prendre les me‑
sures afin d’éviter que le problème ne se reproduise, et retrouver rapidement les informations utiles
s’il venait à se reproduire malgré tout.
® – Ne pas documenter
– le résultat de l’investigation
– les actions effectuées
Après s’être assuré d’avoir bien compris le problème rencontré, il est tout aussi important de le docu‑
menter soigneusement, avec les actions de diagnostic et de correction effectuées.
Ne pas le faire, c’est perdre une excellente occasion de gagner un temps précieux si le problème venait
à se reproduire.
C’est également un risque supplémentaire dans le cas où les actions correctives menées n’auraient
pas suffi à complètement corriger le problème ou auraient eu un effet de bord inattendu.
Dans ce cas, avoir pris le temps de noter le détail des actions effectuées fera là encore gagner un temps
précieux.
Les problèmes pouvant survenir sont trop nombreux pour pouvoir tous les lister, chaque élément
matériel ou logiciel d’une architecture pouvant subir de nombreux types de défaillances.
Cette section liste quelques pistes classiques d’investigation à ne pas négliger pour s’efforcer de cerner
au mieux l’étendue du problème, et en déterminer les conséquences.
8.4.1 Prérequis
La première étape est de déterminer aussi précisément que possible les symptômes observés, sans
en négliger, et à partir de quel moment ils sont apparus.
Cela donne des informations précieuses sur l’étendue du problème, et permet d’éviter de se focaliser
sur un symptôme particulier, parce que plus visible (par exemple l’arrêt brutal de l’instance), alors que
la cause réelle est plus ancienne (par exemple des erreurs IO dans les traces système, ou une montée
progressive de la charge sur le serveur).
Une fois les principaux symptômes identifiés, il est utile de prendre un moment pour déterminer si ce
problème est déjà connu.
Notamment, identifier dans la base de connaissances si ces symptômes ont déjà été rencontrés dans
le passé (d’où l’importance de bien documenter les problèmes).
Au‑delà de la documentation interne, il est également possible de rechercher si ces symptômes ont
déjà été rencontrés par d’autres.
Pour ce type de recherche, il est préférable de privilégier les sources fiables (documentation officielle,
listes de discussion, plate‑forme de support…) plutôt qu’un quelconque document d’un auteur non
identifié.
Dans tous les cas, il faut faire très attention à ne pas prendre les informations trouvées pour argent
comptant, et ce même si elles proviennent de la documentation interne ou d’une source fiable !
Il est toujours possible que les symptômes soient similaires mais que la cause soit différente. Il s’agit
donc ici de mettre en place une base de travail, qui doit être complétée par une observation directe
et une analyse.
8.4.3 Matériel
Les défaillances du matériel, et notamment du système de stockage, sont de celles qui peuvent
avoir les impacts les plus importants et les plus étendus sur une instance et sur les données qu’elle
contient.
Ce type de problème peut également être difficile à diagnostiquer en se contentant d’observer les
symptômes les plus visibles. Il est facile de sous‑estimer l’ampleur des dégâts.
Parmi les bonnes pratiques, il convient de vérifier la configuration et l’état du système disque (SAN,
carte RAID, disques).
Quelques éléments étant une source habituelle de problèmes :
Il faut évidemment rechercher la présence de toute erreur matérielle, au niveau des disques, de la
mémoire, des CPU…
Vérifier également la version des firmwares installés. Il est possible qu’une nouvelle version corrige
le problème rencontré, ou à l’inverse que le déploiement d’une nouvelle version soit à l’origine du
problème.
Dans le même esprit, il faut vérifier si du matériel a récemment été changé. Il arrive que de nouveaux
éléments soient défaillants.
Il convient de noter que l’investigation à ce niveau peut être grandement complexifiée par l’utilisation
de certaines technologies (virtualisation, baies de stockage), du fait de la mutualisation des res‑
sources, et de la séparation des compétences et des informations de supervision entre différentes
équipes.
8.4.4 Virtualisation
® – Mutualisation excessive
– Configuration du stockage virtualisé
– Rechercher les erreurs aussi niveau superviseur
– Mises à jour non appliquées
– ou appliquées récemment
– Modifications de configuration récentes
Tout comme pour les problèmes au niveau du matériel, les problèmes au niveau du système de vir‑
tualisation peuvent être complexes à détecter et à diagnostiquer correctement.
Le principal facteur de problème avec la virtualisation est lié à une mutualisation excessive des res‑
sources.
Il est ainsi possible d’avoir un total de ressources allouées aux VM supérieur à celles disponibles sur
l’hyperviseur, ce qui amène à des comportements de fort ralentissement, voire de blocage des sys‑
tèmes virtualisés.
Si ce type d’architecture est couplé à un système de gestion de bascule automatique (Pacemaker,
repmgr…), il est possible d’avoir des situations de bascules impromptues, voire des situations de
split brain, qui peuvent provoquer des pertes de données importantes. Il est donc important de prê‑
ter une attention particulière à l’utilisation des ressources de l’hyperviseur, et d’éviter à tout prix la
sur‑allocation.
Par ailleurs, lorsque l’architecture inclut une brique de virtualisation, il est important de prendre en
compte que certains problèmes ne peuvent être observés qu’à partir de l’hyperviseur, et pas à partir
du système virtualisé. Par exemple, les erreurs matérielles ou système risquent d’être invisibles depuis
une VM, il convient donc d’être vigilant, et de rechercher toute erreur sur l’hôte.
Il faut également vérifier si des modifications ont été effectuées peu avant l’incident, comme des mo‑
difications de configuration ou l’application de mises à jour.
Comme indiqué dans la partie traitant du matériel, l’investigation peut être grandement freinée par la
séparation des compétences et des informations de supervision entre différentes équipes. Une bonne
communication est alors la clé de la résolution rapide du problème.
Après avoir vérifié les couches matérielles et la virtualisation, il faut ensuite s’assurer de l’intégrité du
système d’exploitation.
La première des vérifications à effectuer est de consulter les traces du système pour en extraire les
éventuels messages d’erreur :
– sous Linux, on trouvera ce type d’informations en sortie de la commande dmesg, et dans les
fichiers traces du système, généralement situés sous /var/log ;
– sous Windows, on consultera à cet effet le journal des événements (les event logs).
Tout comme pour les autres briques, il faut également voir s’il existe des mises à jour des paquets
qui n’auraient pas été appliquées, ou à l’inverse si des mises à jour, installations ou modifications de
configuration ont été effectuées récemment.
Parmi les problèmes fréquemment rencontrés se trouve l’impossibilité pour PostgreSQL d’accéder en
lecture ou en écriture à un ou plusieurs fichiers.
La première chose à vérifier est de déterminer si le système de fichiers sous‑jacent ne serait pas rempli
à 100% (commande df sous Linux) ou monté en lecture seule (commande mount sous Linux).
On peut aussi tester les opérations d’écriture et de lecture sur le système de fichiers pour déterminer
si le comportement y est global :
– pour tester une écriture dans le répertoire PGDATA, sous Linux :
$ touch $PGDATA/test_write
Pour identifier précisément les fichiers présentant des problèmes, il est possible de tester la lecture
complète des fichiers dans le point de montage :
$ tar cvf /dev/null $PGDATA
Sous Linux, l’installation d’outils d’aide au diagnostic sur les serveurs est très important pour mener
une analyse efficace, particulièrement le paquet systat qui permet d’utiliser la commande sar.
La lecture des traces système et des traces PostgreSQL permettent également d’avancer dans le diag‑
nostic.
Un problème de consommation excessive des ressources peut généralement être anticipée grâce à
une supervision sur l’utilisation des ressources et des seuils d’alerte appropriés. Il arrive néanmoins
parfois que la consommation soit très rapide et qu’il ne soit pas possible de réagir suffisamment rapi‑
dement.
Dans le cas d’une consommation mémoire d’un serveur Linux qui menacerait de dépasser la quan‑
tité totale de mémoire allouable, le comportement par défaut de Linux est d’autoriser par défaut la
tentative d’allocation.
Si l’allocation dépasse effectivement la mémoire disponible, alors le système va déclencher un pro‑
cessus Out Of Memory Killer (OOM Killer) qui va se charger de tuer les processus les plus consomma‑
teurs.
Dans le cas d’un serveur dédié à une instance PostgreSQL, il y a de grandes chances que le processus
en question appartienne à l’instance.
S’il s’agit d’un OOM Killer effectuant un arrêt brutal (kill -9) sur un backend, l’instance PostgreSQL
va arrêter immédiatement tous les processus afin de prévenir une corruption de la mémoire et les
redémarrer.
S’il s’agit du processus principal de l’instance (postmaster), les conséquences peuvent être bien plus
dramatiques, surtout si une tentative est faite de redémarrer l’instance sans vérifier si des processus
actifs existent encore.
8.4.8 PostgreSQL
Tout comme pour l’analyse autour du système d’exploitation, la première chose à faire est rechercher
toute erreur ou message inhabituel dans les traces de l’instance. Ces messages sont habituellement
assez informatifs, et permettent de cerner la nature du problème. Par exemple, si PostgreSQL ne par‑
vient pas à écrire dans un fichier, il indiquera précisément de quel fichier il s’agit.
Si l’instance est arrêtée suite à un crash, et que les tentatives de redémarrage échouent avant qu’un
message puisse être écrit dans les traces, il est possible de tenter de démarrer l’instance en exécu‑
tant directement le binaire postgres afin que les premiers messages soient envoyés vers la sortie
standard.
Il convient également de vérifier si des mises à jour qui n’auraient pas été appliquées ne corrigeraient
pas un problème similaire à celui rencontré.
Identifier les mises à jours appliquées récemment et les modifications de configuration peut égale‑
ment aider à comprendre la nature du problème.
Si des corruptions de données sont relevées suite à un crash de l’instance, il convient particulièrement
de vérifier la valeur du paramètre fsync.
En effet, si celui‑ci est désactivé, les écritures dans les journaux de transactions ne sont pas effectuées
de façon synchrone, ce qui implique que l’ordre des écritures ne sera pas conservé en cas de crash.
Le processus de recovery de PostgreSQL risque alors de provoquer des corruptions si l’instance est
malgré tout redémarrée.
Ce paramètre ne devrait jamais être positionné à une autre valeur que on, sauf dans des cas extrême‑
ment particuliers (en bref, si l’on peut se permettre de restaurer intégralement les données en cas de
crash, par exemple dans un chargement de données initial).
À partir de la version 9.5, le bloc peut être compressé avant d’être écrit dans le journal de transaction.
Comme il n’y avait qu’un seul algorithme de compression, le paramètre wal_compression était
un booléen pour activer ou non la compression. À partir de la version 15, d’autres algorithmes sont
disponibles et il faut donc configurer le paramètre wal_compression avec le nom de l’algorithme
de compression utilisable (parmi pglz, lz4, zstd).
PostgreSQL ne verrouille pas tous les fichiers dès son ouverture. Sans mécanisme de sécurité, il est
donc possible de modifier un fichier sans que PostgreSQL s’en rende compte, ce qui aboutit à une
corruption silencieuse.
Les sommes de contrôles (checksums) permettent de se prémunir contre des corruptions silencieuses
de données. Leur mise en place est fortement recommandée sur une nouvelle instance. Malheureu‑
sement, jusqu’en version 11 comprise, on ne peut le faire qu’à l’initialisation de l’instance. La version
12 permet de les mettre en place, base arrêtée, avec l’utilitaire pg_checksums5 .
À titre d’exemple, créons une instance sans utiliser les checksums, et une autre qui les utilisera :
$ initdb -D /tmp/sans_checksums/
$ initdb -D /tmp/avec_checksums/ --data-checksums
On récupère le chemin du fichier de la table pour aller le corrompre à la main (seul celui sans check‑
sums est montré en exemple).
SELECT pg_relation_filepath('test');
pg_relation_filepath
----------------------
base/12036/16317
Instance arrêtée (pour ne pas être gêné par le cache), on va s’attacher à corrompre ce fichier, en rem‑
plaçant la valeur « toto » par « goto » avec un éditeur hexadécimal :
$ hexedit /tmp/sans_checksums/base/12036/16317
$ hexedit /tmp/avec_checksums/base/12036/16399
Enfin, on peut ensuite exécuter des requêtes sur ces deux clusters.
Sans checksums :
5
https://docs.postgresql.fr/current/app‑pgchecksums.html
TABLE test;
name
------
qoto
Avec checksums :
TABLE test;
Depuis la version 11, les sommes de contrôles, si elles sont là, sont vérifiées par défaut lors d’un
pg_basebackup. En cas de corruption des données, l’opération sera interrompue. Il est possible
de désactiver cette vérification avec l’option --no-verify-checksums pour obtenir une copie,
aussi corrompue que l’original, mais pouvant servir de base de travail.
En pratique, si vous utilisez PostgreSQL 9.5 au moins et si votre processeur supporte les instructions
SSE 4.2 (voir dans /proc/cpuinfo), il n’y aura pas d’impact notable en performances. Par contre
vous générerez un peu plus de journaux.
L’activation ou non des sommes de contrôle peut se faire indépendamment sur un serveur primaire
et son secondaire, mais il est fortement conseillé de les activer simultanément des deux côtés pour
éviter de gros problèmes dans certains scénarios de restauration.
– kill -9
– rm -rf
– rsync
– find (souvent couplé avec des commandes destructices comme rm, mv, gzip…)
8.5 OUTILS
L’outil pg_controldata lit les informations du fichier de contrôle d’une instance PostgreSQL.
Cet outil ne se connecte pas à l’instance, il a juste besoin d’avoir un accès en lecture sur le répertoire
PGDATA de l’instance.
Les informations qu’il récupère ne sont donc pas du temps réel, il s’agit d’une vision de l’instance telle
qu’elle était la dernière fois que le fichier de contrôle a été mis à jour. L’avantage est qu’elle peut être
utilisée même si l’instance est arrêtée.
pg_controldata affiche notamment les informations initialisées lors d’initdb, telles que la version du
catalogue, ou la taille des blocs, qui peuvent être cruciales si l’on veut restaurer une instance sur un
nouveau serveur à partir d’une copie des fichiers.
Il affiche également de nombreuses informations utiles sur le traitement des journaux de transactions
et des checkpoints, par exemple :
En complément, le dernier état connu de l’instance est également affiché. Les états potentiels sont :
Bien entendu, comme ces informations ne sont pas mises à jour en temps réel, elles peuvent être
erronées.
Cet asynchronisme est intéressant pour diagnostiquer un problème, par exemple si pg_controldata
renvoie l’état in production mais que l’instance est arrêtée, cela signifie que l’arrêt n’a pas été
effectué proprement (crash de l’instance, qui sera donc suivi d’un recovery au démarrage).
Exemple de sortie de la commande :
$ /usr/pgsql-10/bin/pg_controldata /var/lib/pgsql/10/data
® – pg_dump
– pg_dumpall
– COPY
– psql / pg_restore
– --section=pre-data / data / post-data
Les outils pg_dump et pg_dumpall permettent d’exporter des données à partir d’une instance dé‑
marrée.
Dans le cadre d’un incident grave, il est possible de les utiliser pour :
Par exemple, un moyen rapide de s’assurer que tous les fichiers des tables de l’instance
b sont lisibles est de forcer leur lecture complète, notamment grâce à la commande sui‑
vante :
Attention, les fichiers associés aux index ne sont pas parcourus pendant cette opération.
Á Par ailleurs, ne pas avoir d’erreur ne garantit en aucun cas pas l’intégrité fonctionnelle
des données : les corruptions peuvent très bien être silencieuses ou concerner les in‑
dex. Une vérification exhaustive implique d’autres outils comme pg_checksums ou
pg_basebackup (voir plus loin).
Si pg_dumpall ou pg_dump renvoient des messages d’erreur et ne parviennent pas à exporter cer‑
taines tables, il est possible de contourner le problème à l’aide de la commande COPY, en sélection‑
nant exclusivement les données lisibles autour du bloc corrompu.
Il convient ensuite d’utiliser psql ou pg_restore pour importer les données dans une nouvelle
instance, probablement sur un nouveau serveur, dans un environnement non affecté par le problème.
Pour parer au cas où le réimport échoue à cause de contraintes non respectées, il est souvent préfé‑
rable de faire le réimport par étapes :
Il peut être utile de générer les scripts en pur SQL avant de les appliquer, éventuellement par étape :
Pour rappel, même après un export / import de données réalisé avec succès, des corruptions logiques
peuvent encore être présentes. Il faut donc être particulièrement vigilant et prendre le temps de vali‑
der l’intégrité fonctionnelle des données.
® – Extension
– Vision du contenu d’un bloc
– Sans le dictionnaire, donc sans décodage des données
– Affichage brut
– Utilisé surtout en debug, ou dans les cas de corruption
– Fonctions de décodage pour les tables, les index (B‑tree, hash, GIN, GiST), FSM
– Nécessite de connaître le code de PostgreSQL
-[ RECORD 1 ]------
lp | 1
lp_off | 8152
lp_flags | 1
lp_len | 40
t_xmin | 837
t_xmax | 839
t_field3 | 0
t_ctid | (0,7)
t_infomask2 | 3
t_infomask | 1282
t_hoff | 24
t_bits |
t_oid |
t_data | \x01000000010000000100000001000000
-[ RECORD 2 ]------
lp | 2
lp_off | 8112
lp_flags | 1
lp_len | 40
t_xmin | 837
t_xmax | 839
t_field3 | 0
t_ctid | (0,8)
t_infomask2 | 3
t_infomask | 1282
t_hoff | 24
t_bits |
t_oid |
t_data | \x02000000010000000100000002000000
Et son entête :
SELECT * FROM page_header(get_raw_page('dspam_token_data',0));
-[ RECORD 1 ]--------
lsn | F1A/5A6EAC40
checksum | 0
flags | 0
lower | 56
upper | 7872
special | 8192
pagesize | 8192
version | 4
prune_xid | 839
-[ RECORD 1 ]-----
magic | 340322
version | 2
root | 243
level | 2
fastroot | 243
fastlevel | 2
-[ RECORD 1 ]-+-----
blkno | 3
type | i
live_items | 202
dead_items | 0
avg_item_size | 19
page_size | 8192
free_size | 3312
btpo_prev | 0
btpo_next | 44565
btpo | 1
btpo_flags | 0
Le type de la page est i, c’est‑à‑dire «internal», donc une page interne de l’arbre. Continuons notre
descente, allons voir la page 38065 :
-[ RECORD 1 ]-+-----
blkno | 38065
type | l
live_items | 169
dead_items | 21
avg_item_size | 20
page_size | 8192
free_size | 3588
btpo_prev | 118
btpo_next | 119
btpo | 0
btpo_flags | 65
Nous avons trouvé une feuille (type l). Les ctid pointés sont maintenant les adresses dans la table :
pg_resetwal est un outil fourni avec PostgreSQL. Son objectif est de pouvoir démarrer une instance
après un crash si des corruptions de fichiers (typiquement WAL ou fichier de contrôle) empêchent ce
démarrage.
Cette action n’est pas une action de réparation ! La réinitialisation des journaux de
¾ transactions implique que des transactions qui n’étaient que partiellement validées ne
seront pas détectées comme telles, et ne seront donc pas annulées lors du recovery.
La conséquence est que les données de l’instance ne sont plus cohérentes. Il est fort
¾ possible d’y trouver des violations de contraintes diverses (notamment clés étrangères),
ou d’autres cas d’incohérences plus difficiles à détecter.
Il s’utilise manuellement, en ligne de commande. Sa fonctionnalité principale est d’effacer les fichiers
WAL courants, et il se charge également de réinitialiser les informations correspondantes du fichier
de contrôle.
Il est possible de lui spécifier les valeurs à initialiser dans le fichier de contrôle si l’outil ne parvient
pas à les déterminer (par exemple, si tous les WAL dans le répertoire pg_wal ont été supprimés).
Attention, pg_resetwal ne doit jamais être utilisé sur une instance démarrée. Avant d’exécuter
l’outil, il faut toujours vérifier qu’il ne reste aucun processus de l’instance.
Après la réinitialisation des WAL, une fois que l’instance a démarré, il ne faut surtout pas ouvrir les
accès à l’application ! Comme indiqué, les données présentent sans aucun doute des incohérences,
et toute action en écriture à ce point ne ferait qu’aggraver le problème.
L’étape suivante est donc de faire un export immédiat des données, de les restaurer dans une nou‑
velle instance initialisée à cet effet (de préférence sur un nouveau serveur, surtout si l’origine de la
corruption n’a pas été clairement identifiée), et ensuite de procéder à une validation méthodique des
données.
Il est probable que certaines données incohérentes puissent être identifiées à l’import, lors de la
phase de recréation des contraintes : celles‑ci échoueront si les données ne les respectent, ce qui
permettra de les identifier.
En ce qui concerne les incohérences qui passeront au travers de ces tests, il faudra les trouver et les
corriger manuellement, en procédant à une validation fonctionnelle des données.
Cette extension regroupe des fonctions qui permettent de modifier le satut d’un tuple dans une re‑
lation. Il est par exemple possible de rendre une ligne morte ou de rendre visible des tuples qui sont
invisibles à cause des informations de visibilité.
Depuis la version 11, pg_checksums permet de vérifier les sommes de contrôles existantes sur les
bases de données à froid : l’instance doit être arrêtée proprement auparavant. (En version 11 l’outil
s’appelait pg_verify_checksums.)
Par exemple, suite à une modification de deux blocs dans une table avec l’outil hexedit, on peut
rencontrer ceci :
$ /usr/pgsql-12/bin/pg_checksums -D /var/lib/pgsql/12/data -c
À partir de PostgreSQL 12, l’outil pg_checksums peut aussi ajouter ou supprimer les sommes de
contrôle sur une instance existante arrêtée (donc après le initdb), ce qui n’était pas possible dans
les versions antérieures.
Une alternative, toujours à partir de la version 11, est d’effectuer une sauvegarde physique avec
pg_basebackup, ce qui est plus lourd, mais n’oblige pas à arrêter la base.
Le module amcheck était apparu en version 10 pour vérifier la cohérence des index et de leur struc‑
ture interne, et ainsi détecter des bugs, des corruptions dues au système de fichier voire à la mémoire.
Il définit deux fonctions :
– bt_index_check est destinée aux vérifications de routine, et ne pose qu’un verrou Access‑
ShareLock peu gênant ;
– bt_index_parent_check est plus minutieuse, mais son exécution gêne les modifications
dans la table (verrou ShareLock sur la table et l’index) et elle ne peut pas être exécutée sur un
serveur secondaire.
En v11 apparaît le nouveau paramètre heapallindex. S’il vaut true, chaque fonction effectue
une vérification supplémentaire en recréant temporairement une structure d’index et en la compa‑
rant avec l’index original. bt_index_check vérifiera que chaque entrée de la table possède une
entrée dans l’index. bt_index_parent_check vérifiera en plus qu’à chaque entrée de l’index cor‑
respond une entrée dans la table.
Les verrous posés par les fonctions ne changent pas. Néanmoins, l’utilisation de ce mode a un impact
sur la durée d’exécution des vérifications. Pour limiter l’impact, l’opération n’a lieu qu’en mémoire, et
dans la limite du paramètre maintenance_work_mem (soit entre 256 Mo et 1 Go, parfois plus, sur
les serveurs récents). C’est cette restriction mémoire qui implique que la détection de problèmes est
probabiliste pour les plus grosses tables (selon la documentation, la probabilité de rater une incohé‑
rence est de 2 % si l’on peut consacrer 2 octets de mémoire à chaque ligne). Mais rien n’empêche de
relancer les vérifications régulièrement, diminuant ainsi les chances de rater une erreur.
amcheck ne fournit aucun moyen de corriger une erreur, puisqu’il détecte des choses qui ne de‑
vraient jamais arriver. REINDEX sera souvent la solution la plus simple et facile, mais tout dépend
de la cause du problème.
Soit unetable_pkey, un index de 10 Go sur un entier :
CREATE EXTENSION amcheck ;
SELECT bt_index_check('unetable_pkey');
Durée : 63753,257 ms (01:03,753)
Cette section décrit quelques‑unes des pires situations de corruptions que l’on peut être amené à
observer.
Dans la quasi‑totalité des cas, la seule bonne réponse est la restauration de l’instance à partir d’une
sauvegarde fiable.
8.6.1 Avertissement
La plupart des manipulations mentionnées dans cette partie sont destructives, et peuvent (et vont)
provoquer des incohérences dans les données.
Tous les experts s’accordent pour dire que l’utilisation de telles méthodes pour récupérer une instance
tend à aggraver le problème existant ou à en provoquer de nouveaux, plus graves. S’il est possible de
l’éviter, ne pas les tenter (ie : préférer la restauration d’une sauvegarde) !
S’il n’est pas possible de faire autrement (ie : pas de sauvegarde utilisable, données vitales à
extraire…), alors TRAVAILLER SUR UNE COPIE.
Il ne faut pas non plus oublier que chaque situation est unique, il faut prendre le temps de bien cerner
l’origine du problème, documenter chaque action prise, s’assurer qu’un retour arrière est toujours
possible.
Les index sont des objets de structure complexe, ils sont donc particulièrement vulnérables aux cor‑
ruptions.
Lorsqu’un index est corrompu, on aura généralement des messages d’erreur de ce type :
ERROR: invalid page header in block 5869177 of relation base/17291/17420
Il peut arriver qu’un bloc corrompu ne renvoie pas de message d’erreur à l’accès, mais que les données
elles‑mêmes soient altérées, ou que des filtres ne renvoient pas les données attendues.
Ce cas est néanmoins très rare dans un bloc d’index.
Dans la plupart des cas, si les données de la table sous‑jacente ne sont pas affectées, il est possible
de réparer l’index en le reconstruisant intégralement grâce à la commande REINDEX.
Les corruptions de blocs vont généralement déclencher des erreurs du type suivant :
ERROR: invalid page header in block 32570 of relation base/16390/2663
ERROR: could not read block 32570 of relation base/16390/2663:
read only 0 of 8192 bytes
Si la relation concernée est une table, tout ou partie des données contenues dans ces blocs est
perdu.
L’apparition de ce type d’erreur est un signal fort qu’une restauration est certainement nécessaire.
Néanmoins, s’il est nécessaire de lire le maximum de données possibles de la table, il est possible
d’utiliser l’option de PostgreSQL zero_damaged_pages pour demander au moteur de réinitialiser
les blocs invalides à zéro lorsqu’ils sont lus au lieu de tomber en erreur. Il s’agit d’un des très rares
paramètres absents de postgresql.conf.
Par exemple :
Si cela se termine sans erreur, les blocs invalides ont été réinitialisés.
Les données qu’ils contenaient sont évidemment perdues, mais la table peut désormais être accé‑
dée dans son intégralité en lecture, permettant ainsi par exemple de réaliser un export des données
pour récupérer ce qui peut l’être.
Attention, du fait des données perdues, le résultat peut être incohérent (contraintes non respec‑
tées…).
Par ailleurs, par défaut PostgreSQL ne détecte pas les corruptions logiques, c’est‑à‑dire n’affectant pas
la structure des données mais uniquement le contenu.
Il ne faut donc pas penser que la procédure d’export complet de données suivie d’un import sans
erreur garantit l’absence de corruption.
Dans certains cas, il arrive que la corruption soit suffisamment importante pour que le simple accès
au bloc fasse crasher l’instance.
Dans ce cas, le seul moyen de réinitialiser le bloc est de le faire manuellement au niveau du fichier,
instance arrêtée, par exemple avec la commande dd.
Pour identifier le fichier associé à la table corrompue, il est possible d’utiliser la fonction
pg_relation_filepath() :
pg_relation_filepath
----------------------
base/16390/40995
Le résultat donne le chemin vers le fichier principal de la table, relatif au PGDATA de l’instance.
Attention, une table peut contenir plusieurs fichiers. Par défaut une instance PostgreSQL sépare les
fichiers en segments de 1 Go. Une table dépassant cette taille aura donc des fichiers supplémentaires
(base/16390/40995.1, base/16390/40995.2…).
Pour trouver le fichier contenant le bloc corrompu, il faudra donc prendre en compte le numéro du
bloc trouvé dans le champ ctid, multiplier ce numéro par la taille du bloc (paramètre block_size,
8 ko par défaut), et diviser le tout par la taille du segment.
Cette manipulation est évidemment extrêmement risquée, la moindre erreur pouvant rendre irrécu‑
pérables de grandes portions de données.
Il est donc fortement déconseillé de se lancer dans ce genre de manipulations à moins d’être absolu‑
ment certain que c’est indispensable.
Encore une fois, ne pas oublier de travailler sur une copie, et pas directement sur l’instance de pro‑
duction.
L’utilitaire pg_resetwal a comme fonction principale de supprimer les fichiers WAL courants et d’en
créer un nouveau, avant de mettre à jour le fichier de contrôle pour permettre le redémarrage.
Au minimum, cette action va provoquer la perte de toutes les transactions validées effectuées depuis
le dernier checkpoint.
Il est également probable que des incohérences vont apparaître, certaines relativement simples à dé‑
tecter via un export/import (incohérences dans les clés étrangères par exemple), certaines complète‑
ment invisibles.
L’utilisation de cet utilitaire est extrêmement dangereuse, n’est pas recommandée, et ne peut jamais
être considérée comme une action corrective. Il faut toujours privilégier la restauration d’une sauve‑
garde plutôt que son exécution.
Si l’utilisation de pg_resetwal est néanmoins nécessaire (par exemple pour récupérer des données
absentes de la sauvegarde), alors il faut travailler sur une copie des fichiers de l’instance, récupérer ce
qui peut l’être à l’aide d’un export de données, et les importer dans une autre instance.
Les données récupérées de cette manière devraient également être soigneusement validées avant
d’être importée de façon à s’assurer qu’il n’y a pas de corruption silencieuse.
Il ne faut en aucun cas remettre une instance en production après une réinitialisation
¾ des WAL.
® – Fichier global/pg_control
– Contient les informations liées au dernier checkpoint
– Sans lui, l’instance ne peut pas démarrer
– Recréation avec pg_resetwal… parfois
– Restauration nécessaire
$ psql postgres
psql: FATAL: database "postgres" does not exist
Encore une fois, utiliser pg_resetwal n’est en aucun cas une solution, mais doit uniquement être
considéré comme un contournement temporaire à une situation désastreuse.
Une instance altérée par cet outil ne doit pas être considérée comme saine.
Le fichier CLOG (Commit Log) dans PGDATA/pg_xact/ contient le statut des différentes transac‑
tions, notamment si celles‑ci sont en cours, validées ou annulées.
S’il est altéré ou supprimé, il est possible que des transactions qui avaient été marquées comme an‑
nulées soient désormais considérées comme valides, et donc que les modifications de données cor‑
respondantes deviennent visibles aux autres transactions.
C’est évidemment un problème d’incohérence majeur, tout problème avec ce fichier devrait donc être
soigneusement analysé.
Il est préférable dans le doute de procéder à une restauration et d’accepter une perte de données
plutôt que de risquer de maintenir des données incohérentes dans la base.
Le catalogue système contient la définition de toutes les relations, les méthodes d’accès, la corres‑
pondance entre un objet et un fichier sur disque, les types de données existantes…
S’il est incomplet, corrompu ou inaccessible, l’accès aux données en SQL risque de ne pas être possible
du tout.
8.7 CONCLUSION
8.8 QUIZ
https://dali.bo/i5_quiz
®
Créer une base pgbench et la remplir avec l’outil de même, avec un facteur d’échelle 10 et avec
les clés étrangères entre tables ainsi :
/usr/pgsql-15/bin/pgbench -i -s 10 -d pgbench --foreign-keys
Arrêter PostgreSQL.
Avec un outil hexedit (à installer au besoin, l’aide s’obtient par F1), modifier une ligne dans le
PREMIER bloc de la table.
– Arrêter PostgreSQL.
– Voir ce que donne pg_checksums (pg_verify_checksums en v11).
Avant de redémarrer PostgreSQL, supprimer les sommes de contrôle dans la copie (en désespoir
de cause).
Tenter une récupération avec SET zero_damaged_pages. Quelles données ont pu être per‑
dues ?
Retrouver les fichiers des tables pgbench_branches (par exemple avec pg_file_relationpath).
– Arrêter PostgreSQL.
– Avec hexedit, dans le premier bloc en tête de fichier, remplacer les derniers caractères non
nuls (C0 9E 40) par FF FF FF.
– En toute fin de fichier, remplacer le dernier 01 par un FF.
– Redémarrer PostgreSQL.
Avec l’extension amcheck, essayer de voir si le problème peut être détecté. Si non, pourquoi ?
Vérifier que l’instance utilise bien les checksums. Au besoin les ajouter avec pg_checksums.
# SHOW data_checksums ;
data_checksums
----------------
on
Si la réponse est off, on peut (à partir de la v12) mettre les checksums en place :
$ /usr/pgsql-15/bin/pg_checksums -D /var/lib/pgsql/15/data.BACKUP/ --enable
↪ --progress
58/58 MB (100%) computed
Checksum operation completed
Files scanned: 964
Blocks scanned: 7524
pg_checksums: syncing data directory
pg_checksums: updating control file
Checksums enabled in cluster
Créer une base pgbench et la remplir avec l’outil de même, avec un facteur d’échelle 10 et avec
les clés étrangères entre tables ainsi :
/usr/pgsql-15/bin/pgbench -i -s 10 -d pgbench --foreign-keys
min | max
-----+---------
1 | 1000000
Un SELECT montre que les valeurs sont triées mais c’est dû à l’initialisation.
SELECT pg_relation_filepath('pgbench_accounts') ;
pg_relation_filepath
----------------------
base/16454/16489
Arrêter PostgreSQL.
Cela permet d’être sûr qu’il ne va pas écraser nos modifications lors d’un checkpoint.
Avec un outil hexedit (à installer au besoin, l’aide s’obtient par F1), modifier une ligne dans le
PREMIER bloc de la table.
Aller par exemple sur la 2è ligne, modifier 80 9F en FF FF. Sortir avec Ctrl‑X, confirmer la sauve‑
garde.
WARNING: page verification failed, calculated checksum 62947 but expected 57715
ERROR: invalid page in block 0 of relation base/16454/16489
– Arrêter PostgreSQL.
– Voir ce que donne pg_checksums (pg_verify_checksums en v11).
# systemctl stop postgresql-15
Dans l’idéal, la copie devrait se faire vers un autre support, une corruption rend celui‑ci suspect. Dans
le cadre du TP, ceci suffira :
$ cp -upR /var/lib/pgsql/15/data/ /var/lib/pgsql/15/data.BACKUP/
$ chmod -R -w /var/lib/pgsql/15/data/
Avant de redémarrer PostgreSQL, supprimer les sommes de contrôle dans la copie (en désespoir
de cause).
$ /usr/pgsql-15/bin/pg_checksums -D /var/lib/pgsql/15/data.BACKUP/ --disable
pg_checksums: syncing data directory
pg_checksums: updating control file
Checksums disabled in cluster
Ce ne sera pas forcément cette erreur, plus rien n’est sûr en cas de corruption. L’avantage des sommes
de contrôle est justement d’avoir une erreur moins grave et plus ciblée.
Un pg_dumpall renverra le même message.
Tenter une récupération avec SET zero_damaged_pages. Quelles données ont pu être per‑
dues ?
Apparemment une ligne a disparu, celle portant la valeur 1 pour la clé. Il est rare que la perte soit aussi
évidente !
Retrouver les fichiers des tables pgbench_branches (par exemple avec pg_file_relationpath).
# SELECT pg_relation_filepath('pgbench_branches') ;
pg_relation_filepath
----------------------
base/16454/16490
– Arrêter PostgreSQL.
– Avec hexedit, dans le premier bloc en tête de fichier, remplacer les derniers caractères non
nuls (C0 9E 40) par FF FF FF.
– En toute fin de fichier, remplacer le dernier 01 par un FF.
– Redémarrer PostgreSQL.
$ hexedit /var/lib/pgsql/15/data.BACKUP/base/16454/16490
En effet, le premier lit la (petite) table directement, le second passe par l’index, comme un EXPLAIN le
montrerait. Les deux objets diffèrent.
Et le contenu de la table est devenu :
# SELECT * FROM pgbench_branches ;
9 | 0 |
(9 lignes)
Le 1 est devenu 255 (c’est notre première modification) mais la ligne 10 a disparu !
Les requêtes peuvent renvoyer un résultat incohérent avec leur critère :
pgbench=# SET enable_seqscan TO off;
SET
pgbench=# SELECT * FROM pgbench_branches
WHERE bid = 1 ;
Avec l’extension amcheck, essayer de voir si le problème peut être détecté. Si non, pourquoi ?
(1 ligne)
(1 ligne)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
COPY 999999
ALTER TABLE
CREATE INDEX
ERROR: insert or update on table "pgbench_accounts"
violates foreign key constraint "pgbench_accounts_bid_fkey"
DÉTAIL : Key (bid)=(1) is not present in table "pgbench_branches".
La contrainte de clé étrangère entre les deux tables ne peut être respectée : bid est à 1 sur de nom‑
breuses lignes de pgbench_accounts mais n’existe plus dans la table pgbench_branches ! Ce
genre d’incohérence doit être recherchée très tôt pour ne pas surgir bien plus tard, quand on doit
restaurer pour d’autres raisons.
371
DALIBO Formations
Téléchargement gratuit
Les versions électroniques de nos publications sont disponibles gratuitement sous licence open
source ou sous licence Creative Commons.