Dalibo SCOP
Formation | Formation DBA2 |
Titre | PostgreSQL Avancé |
Révision | 24.12 |
ISBN | N/A |
https://dali.bo/dba2_pdf | |
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. Les solutions de TP ne figurent pas forcément dans la version imprimée, mais sont dans les versions numériques (PDF ou HTML).
Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :
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 titulaire 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 et schémas dont la redistribution est soumise à des licences différentes qui sont alors précisées.
PostgreSQL® Postgres® et le logo Slonik sont des marques déposées par PostgreSQL Community Association of Canada.
Ce document ne couvre que les versions supportées de PostgreSQL au moment de sa rédaction, soit les versions 13 à 17.
Sur les versions précédentes susceptibles d’être encore rencontrées en production, seuls quelques points très importants sont évoqués, en plus éventuellement de quelques éléments historiques.
Sauf précision contraire, le système d’exploitation utilisé est Linux.
Le présent module vise à donner un premier aperçu global du fonctionnement interne de PostgreSQL.
Après quelques rappels sur l’installation, nous verrons essentiellement les processus et les fichiers utilisé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 recommande 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. Habituellement, tout
est placé dans /usr/pgsql-<version majeure>
pour les
distributions Red Hat et dans
/usr/lib/postgresql/<version majeure>
pour les
distributions Debian.
Dans le cas d’une distribution Debian, une instance est immédiatement
créée dans
/var/lib/postgresql/<version 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 utiliser 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.
L’architecture PostgreSQL est une architecture multiprocessus et non multithread.
Cela signifie que chaque processus de PostgreSQL s’exécute dans un contexte mémoire isolé, et que la communication entre ces processus repose sur des mécanismes systèmes inter-processus : sémaphores, zones de mémoire partagée, sockets. Ceci s’oppose à l’architecture multithread, où l’ensemble du moteur s’exécute dans un seul processus, avec plusieurs threads (contextes) d’exécution, où tout est partagé par défaut.
Le principal avantage de cette architecture multiprocessus est la stabilité : un processus, en cas de problème, ne corrompt que sa mémoire (ou la mémoire partagée), le plantage d’un processus n’affecte pas directement les autres. Son principal défaut est une allocation statique des ressources de mémoire partagée : elles ne sont pas redimensionnables à chaud.
Tous les processus de PostgreSQL accèdent à une zone de « mémoire partagée ». Cette zone contient les informations devant être partagées entre les clients, comme un cache de données, ou des informations sur l’état de chaque session par exemple.
PostgreSQL utilise une architecture client-serveur. Nous ne nous connectons à PostgreSQL qu’à travers d’un protocole bien défini, nous n’accédons jamais aux fichiers de données.
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.
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.
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.
Pour chaque nouvelle session à l’instance, le processus
postmaster
crée un processus fils qui s’occupe de gérer
cette session. Il y a donc un processus dédié à chaque connexion
cliente, et ce processus est détruit à fin de cette connexion.
Ce processus dit backend reçoit les ordres SQL, les interprète, exécute les requêtes, trie les données, et enfin retourne les résultats. 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).
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 paramètre max_connections
. Le défaut
est 100.
Certaines connexions sont réservées. Les administrateurs doivent
pouvoir se connecter à l’instance même si la limite est atteinte.
Quelques connexions sont donc réservées aux superutilisateurs (paramètre
superuser_reserved_connections
, à 3 par défaut). On peut
aussi octroyer le rôle pg_use_reserved_connections
à
certains utilisateurs pour leur garantir l’accès à un nombre de
connexions réservées, à définir avec le paramètre
reserved_connections
(vide par défaut), cela à partir de
PostreSQL 16.
Attention : modifier un de ces 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 PostgreSQL. Il faut donc réfléchir au bon dimensionnement avant la mise en production.
La valeur 100 pour max_connections
est généralement
suffisante. Il peut être intéressant de la diminuer pour se permettre de
monter work_mem
et autoriser plus de mémoire de tri. Il est
possible de monter max_connections
pour qu’un plus grand
nombre de clients puisse se connecter en même temps.
Il s’agit aussi d’arbitrer entre le nombre de requêtes à exécuter à un instant t, le nombre de CPU disponibles, la complexité des requêtes, et le nombre de processus que peut gérer l’OS. Ce dimensionnement 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 :
Le plus réputé est PgBouncer, mais il est aussi souvent inclus dans des serveurs d’application (par exemple Tomcat).
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, paramètre
shared_buffers
), et d’autres zones plus petites : cache des
journaux de transactions, donné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.
Une instance est composée des éléments suivants :
Le répertoire de données :
Il contient les fichiers obligatoires au bon fonctionnement de l’instance : fichiers de données, journaux de transaction….
Les fichiers de configuration :
Selon la distribution, ils sont stockés dans le répertoire de données
(Red Hat et dérivés comme CentOS ou Rocky Linux), ou dans
/etc/postgresql
(Debian et dérivés).
Un fichier PID :
Il permet de savoir si une instance est démarrée ou non, et donc à empêcher un second jeu de processus d’y accéder.
Le paramètre external_pid_file
permet d’indiquer un
emplacement où PostgreSQL créera un second fichier de PID, généralement
à l’extérieur de son répertoire de données.
Des tablespaces :
Ils sont totalement optionnels. Ce sont des espaces de stockage supplémentaires, stockés habituellement dans d’autres systèmes de fichiers.
Le fichier de statistiques d’exécution :
Généralement dans pg_stat_tmp/
.
Les fichiers de trace :
Typiquement, des fichiers avec une variante du nom
postgresql.log
, souvent datés. Ils sont par défaut dans le
répertoire de l’instance, sous log/
. Sur Debian, ils sont
redirigés vers la sortie d’erreur du système. Ils peuvent être redirigés
vers un autre mécanisme du système d’exploitation (syslog sous Unix,
journal des événements sous Windows),
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 (ou depuis un conteneur comme docker).
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 officielle.
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.
Sous Debian, il est sous /etc
.
postgresql.auto.conf
stocke les paramètres de
configuration fixés en utilisant la commande ALTER SYSTEM
.
Il surcharge donc postgresql.conf
. Comme pour
postgresql.conf
, sa modification impose de recharger la
configuration ou redémarrer l’instance. Il est techniquement possible,
mais déconseillé, de le modifier à la main.
postgresql.auto.conf
est toujours dans le répertoire des
données, même si postgresql.conf
est ailleurs.
pg_hba.conf
contient les règles d’authentification à la
base selon leur identité, la base, la provenance, etc.
pg_ident.conf
est plus rarement utilisé. Il complète
pg_hba.conf
, par exemple pour rapprocher des utilisateurs
système ou propres à PostgreSQL.
Ces deux derniers fichiers peuvent eux-mêmes inclure d’autres fichiers (depuis PostgreSQL 16). Leur utilisation est décrite dans notre première formation.
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 PGDATA/base/
ou à
la racine de chaque tablespace.
Le fichier postmaster.pid
est créé au démarrage de
PostgreSQL. PostgreSQL y indique le PID du processus père sur la
première ligne, l’emplacement du répertoire des données sur la deuxième
ligne et la date et l’heure du lancement de postmaster sur la troisième
ligne ainsi que beaucoup d’autres informations. Par exemple :
~$ 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
$ ipcs -p |grep 7771
54919263 postgres 7771 10640
$ ipcs | grep 54919263 0x0052e2c1 54919263 postgres 600 56 6
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'
Par contre, ce fichier ne contient que le PID du processus père.
Quant au fichier postmaster.opts
, il contient les
arguments en ligne de commande correspondant au dernier lancement de
PostgreSQL. Il n’est jamais supprimé. Par exemple :
$ cat $PGDATA/postmaster.opts
/usr/local/pgsql/bin/postgres "-D" "/var/lib/postgresql/15/data"
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 :
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
…)_fsm
, il s’agit du fichier
stockant la Free Space Map (liste des blocs
réutilisables)._vm
, il s’agit du fichier
stockant la Visibility Map (liste des blocs intégralement
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 relfilenode
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 endommagé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
$ /usr/pgsql-15/bin/oid2name -f 16477 -d employes
From database "employes":
Filenode Table Name
----------------------------- 16477 employes_big_pkey
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.
Si nous demandons au sein d’une première session :
SELECT * FROM generate_series(1,1e9) ORDER BY random() LIMIT 1 ;
alors nous pourrons suivre les fichiers temporaires depuis une autre session :
SELECT * FROM pg_ls_tmpdir() ;
name | size | modification
-------------------+------------+------------------------
pgsql_tmp12851.16 | 1073741824 | 2020-09-02 15:43:27+02
pgsql_tmp12851.11 | 1073741824 | 2020-09-02 15:42:32+02
pgsql_tmp12851.7 | 1073741824 | 2020-09-02 15:41:49+02
pgsql_tmp12851.5 | 1073741824 | 2020-09-02 15:41:29+02
pgsql_tmp12851.9 | 1073741824 | 2020-09-02 15:42:11+02
pgsql_tmp12851.0 | 1073741824 | 2020-09-02 15:40:36+02
pgsql_tmp12851.14 | 1073741824 | 2020-09-02 15:43:06+02
pgsql_tmp12851.4 | 1073741824 | 2020-09-02 15:41:19+02
pgsql_tmp12851.13 | 1073741824 | 2020-09-02 15:42:54+02
pgsql_tmp12851.3 | 1073741824 | 2020-09-02 15:41:09+02
pgsql_tmp12851.1 | 1073741824 | 2020-09-02 15:40:47+02
pgsql_tmp12851.15 | 1073741824 | 2020-09-02 15:43:17+02
pgsql_tmp12851.2 | 1073741824 | 2020-09-02 15:40:58+02
pgsql_tmp12851.8 | 1073741824 | 2020-09-02 15:42:00+02
pgsql_tmp12851.12 | 1073741824 | 2020-09-02 15:42:43+02
pgsql_tmp12851.10 | 1073741824 | 2020-09-02 15:42:21+02
pgsql_tmp12851.6 | 1073741824 | 2020-09-02 15:41:39+02 pgsql_tmp12851.17 | 546168976 | 2020-09-02 15:43:32+02
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.
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
-rw------- 1 postgres postgres 33554432 Mar 26 16:25 000000020000014300000022
-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
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 journal à proprement parler, soit un ensemble de fichiers
représentant 4 Go. La dernière partie correspond 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
. 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()
:
SELECT * FROM pg_ls_archive_statusdir() ORDER BY 1 ; #
name | size | modification
--------------------------------+------+------------------------
000000010000000000000067.done | 0 | 2020-09-02 15:52:57+02
000000010000000000000068.done | 0 | 2020-09-02 15:52:57+02
000000010000000000000069.done | 0 | 2020-09-02 15:52:58+02
00000001000000000000006A.ready | 0 | 2020-09-02 15:53:53+02
00000001000000000000006B.ready | 0 | 2020-09-02 15:53:53+02
00000001000000000000006C.ready | 0 | 2020-09-02 15:53:54+02
00000001000000000000006D.ready | 0 | 2020-09-02 15:53:54+02
00000001000000000000006E.ready | 0 | 2020-09-02 15:53:54+02
00000001000000000000006F.ready | 0 | 2020-09-02 15:53:54+02
000000010000000000000070.ready | 0 | 2020-09-02 15:53:55+02 000000010000000000000071.ready | 0 | 2020-09-02 15:53:55+02
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 ».
Les fichiers contenus dans le répertoire pg_xact
ne
doivent jamais être effacés. Ils sont cruciaux au bon
fonctionnement de la base.
D’autres répertoires contiennent des fichiers essentiels à la gestion des transactions :
pg_commit_ts
contient l’horodatage de la validation de
chaque transaction ;pg_multixact
est utilisé dans l’implémentation des
verrous partagés (SELECT ... FOR SHARE
) ;pg_serial
est utilisé dans l’implémentation de SSI
(Serializable Snapshot Isolation
) ;pg_snapshots
est utilisé pour stocker les snapshots
exportés de transactions ;pg_subtrans
stocke l’imbrication des transactions lors
de sous-transactions (les SAVEPOINTS
) ;pg_twophase
est utilisé pour l’implémentation du
Two-Phase Commit, aussi appelé
transaction préparée
, 2PC
, ou transaction
XA
dans le monde Java par exemple.La version 10 a été l’occasion du changement de nom de quelques
répertoires pour des raisons de cohérence et pour réduire les risques de
fausses manipulations. Jusqu’en 9.6, pg_wal
s’appelait
pg_xlog
, pg_xact
s’appelait
pg_clog
.
Les fonctions et outils ont été renommés en conséquence :
xlog
a été
remplacé par wal
(par exemple pg_switch_xlog
est devenue pg_switch_wal
) ;location
a été remplacé
par lsn
.pg_logical
contient des informations sur la réplication
logique.
pg_replslot
contient des informations sur les slots de
réplications, qui sont un moyen de fiabiliser la réplication physique ou
logique.
Sans réplication en place, ces répertoires sont quasi-vides. Là encore, il ne faut pas toucher à leur contenu.
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 | /mnt/hdd/pg | | 3576 kB |
pg_default | postgres | | | 6536 MB | pg_global | postgres | | | 587 kB |
sudo ls -R /mnt/hdd/pg
/mnt/hdd/pg:
PG_15_202209061
/mnt/hdd/pg/PG_15_202209061:
5
/mnt/hdd/pg/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
PS P:\PGDATA13> dir 'pg_tblspc/*' | ?{$_.LinkType} | select FullName,LinkType,Target
FullName LinkType Target-------- -------- ------
:\PGDATA13\pg_tblspc\105921 Junction {T:\TBL1} P
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/
).
La création d’autres tablespaces est totalement optionnelle.
Leur utilité et leur gestion seront abordés plus loin.
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
fichiers 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
À l’arrêt, les fichiers sont copiés dans le répertoire
pg_stat/
.
PostgreSQL gérant ces statistiques en mémoire partagée à partir de la
version 15, le collecteur n’existe plus. Mais les deux répertoires sont
encore utilisés par des extensions comme pg_stat_statements
ou pg_stat_kcache
.
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 NOTIFY
)
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 première formation.
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 échappements 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.
Ce schéma ne soit à présent plus avoir aucun secret pour vous.
L’installation est détaillée ici pour Rocky Linux 8 et 9 (similaire à Red Hat et à d’autres variantes comem Oracle Linux et Fedora), et Debian/Ubuntu.
Elle ne dure que quelques minutes.
ATTENTION : Red Hat, CentOS, Rocky Linux fournissent
souvent 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 fortement conseillée.
Installation du dépôt communautaire :
Les dépôts de la communauté sont sur https://yum.postgresql.org/. Les commandes qui suivent sont inspirées de celles générées par l’assistant sur https://www.postgresql.org/download/linux/redhat/, en précisant :
Les commandes sont à lancer sous root :
# dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms\
/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# dnf -qy module disable postgresql
Installation de PostgreSQL 17 (client, serveur, librairies, extensions) :
# dnf install -y postgresql17-server postgresql17-contrib
Les outils clients et les librairies nécessaires seront automatiquement installés.
Une fonctionnalité avancée optionnelle, le JIT (Just In Time compilation), nécessite un paquet séparé.
# dnf install postgresql17-llvmjit
Création d’une première instance :
Il est conseillé de déclarer PG_SETUP_INITDB_OPTIONS
,
notamment pour mettre en place les sommes de contrôle et forcer les
traces en anglais :
# export PGSETUP_INITDB_OPTIONS='--data-checksums --lc-messages=C'
# /usr/pgsql-17/bin/postgresql-17-setup initdb # cat /var/lib/pgsql/17/initdb.log
Ce dernier fichier permet de vérifier que tout s’est bien passé et doit finir par :
Success. You can now start the database server using:
/usr/pgsql-17/bin/pg_ctl -D /var/lib/pgsql/17/data/ -l logfile start
Chemins :
Objet | Chemin |
---|---|
Binaires | /usr/pgsql-17/bin |
Répertoire de l’utilisateur postgres | /var/lib/pgsql |
PGDATA par défaut |
/var/lib/pgsql/17/data |
Fichiers de configuration | dans PGDATA/ |
Traces | dans PGDATA/log |
Configuration :
Modifier postgresql.conf
est facultatif pour un premier
lancement.
Commandes d’administration habituelles :
Démarrage, arrêt, statut, rechargement à chaud de la configuration, redémarrage :
# systemctl start postgresql-17
# systemctl stop postgresql-17
# systemctl status postgresql-17
# systemctl reload postgresql-17 # systemctl restart postgresql-17
Test rapide de bon fonctionnement et connexion à psql :
# systemctl --all |grep postgres # sudo -iu postgres psql
Démarrage de l’instance au lancement du système d’exploitation :
# systemctl enable postgresql-17
Ouverture du firewall pour le port 5432 :
Voir si le firewall est actif :
# systemctl status firewalld
Si c’est le cas, autoriser un accès extérieur :
# firewall-cmd --zone=public --add-port=5432/tcp --permanent
# firewall-cmd --reload # firewall-cmd --list-all
(Rappelons que listen_addresses
doit être également
modifié dans postgresql.conf
.)
Création d’autres instances :
Si des instances de versions majeures différentes doivent
être installées, il faut d’abord installer les binaires pour chacune
(adapter le numéro dans dnf install …
) et appeler le script
d’installation de chaque version. l’instance par défaut de chaque
version vivra dans un sous-répertoire numéroté de
/var/lib/pgsql
automatiquement créé à l’installation. Il
faudra juste modifier les ports dans les postgresql.conf
pour que les instances puissent tourner simultanément.
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
faut les installer dans des PGDATA
différents.
/var/lib/pgsqsl/17/
(ou
l’équivalent pour d’autres versions majeures).Pour créer une seconde instance, nommée par exemple infocentre :
# cp /lib/systemd/system/postgresql-17.service \ /etc/systemd/system/postgresql-17-infocentre.service
Environment=PGDATA=/var/lib/pgsql/17/infocentre
# export PGSETUP_INITDB_OPTIONS='--data-checksums --lc-messages=C' # /usr/pgsql-17/bin/postgresql-17-setup initdb postgresql-17-infocentre
Option 2 : restauration d’une sauvegarde : la procédure dépend de votre outil.
Adaptation de
/var/lib/pgsql/17/infocentre/postgresql.conf
(port
surtout).
Commandes de maintenance de cette instance :
# systemctl [start|stop|reload|status] postgresql-17-infocentre # systemctl [enable|disable] postgresql-17-infocentre
Sauf précision, tout est à effectuer en tant qu’utilisateur root.
Référence : https://apt.postgresql.org/
Installation du dépôt communautaire :
L’installation des dépôts du PGDG est prévue dans le paquet Debian :
# apt update
# apt install -y gnupg2 postgresql-common # /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
Ce dernier ordre créera le fichier du dépôt
/etc/apt/sources.list.d/pgdg.list
adapté à la distribution
en place.
Installation de PostgreSQL 17 :
La méthode la plus propre consiste à modifier la configuration par défaut avant l’installation :
Dans /etc/postgresql-common/createcluster.conf
,
paramétrer au moins les sommes de contrôle et les traces en
anglais :
initdb_options = '--data-checksums --lc-messages=C'
Puis installer les paquets serveur et clients et leurs dépendances :
# apt install postgresql-17 postgresql-client-17
La première instance est automatiquement créée, démarrée et déclarée
comme service à lancer au démarrage du système. Elle porte un nom (par
défaut main
).
Elle est immédiatement accessible par l’utilisateur système postgres.
Chemins :
Objet | Chemin |
---|---|
Binaires | /usr/lib/postgresql/17/bin/ |
Répertoire de l’utilisateur postgres | /var/lib/postgresql |
PGDATA de l’instance par défaut | /var/lib/postgresql/17/main |
Fichiers de configuration | dans
/etc/postgresql/17/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, qui demandent en paramètre la version et le nom de l’instance :
# pg_ctlcluster 17 main [start|stop|reload|status|restart]
Démarrage de l’instance avec le serveur :
C’est en place par défaut, et modifiable dans
/etc/postgresql/17/main/start.conf
.
Ouverture du firewall :
Debian et Ubuntu n’installent pas de firewall par défaut.
Statut des instances du serveur :
# pg_lsclusters
Test rapide de bon fonctionnement et connexion à psql :
# systemctl --all |grep postgres # sudo -iu postgres psql
Destruction d’une instance :
# pg_dropcluster 17 main
Création d’autres instances :
Ce qui suit est valable pour remplacer l’instance par défaut par une autre, par exemple pour mettre les checksums en place :
/etc/postgresql-common/createcluster.conf
permet de mettre
en place tout d’entrée les checksums, les messages en anglais,
le format des traces ou un emplacement séparé pour les journaux :initdb_options = '--data-checksums --lc-messages=C'
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
waldir = '/var/lib/postgresql/wal/%v/%c/pg_wal'
# pg_createcluster 17 infocentre
Il est également possible de préciser certains paramètres du fichier
postgresql.conf
, voire les chemins des fichiers (il est
conseillé de conserver les chemins par défaut) :
# pg_createcluster 17 infocentre \
--port=12345 \
--datadir=/PGDATA/17/infocentre \
--pgoption shared_buffers='8GB' --pgoption work_mem='50MB' \ -- --data-checksums --waldir=/ssd/postgresql/17/infocentre/journaux
adapter au besoin
/etc/postgresql/17/infocentre/postgresql.conf
;
démarrage :
# pg_ctlcluster 17 infocentre 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 :
$ sudo -iu postgres psql
psql (17.0)
Type "help" for help. postgres=#
Ce qui suit permet la connexion directement depuis un utilisateur du système :
Pour des tests (pas en production !), il suffit de passer à
trust
le type de la connexion en local dans le
pg_hba.conf
:
local all postgres trust
La connexion en tant qu’utilisateur postgres
(ou tout
autre) n’est alors plus sécurisée :
dalibo:~$ psql -U postgres
psql (17.0)
Type "help" for help. postgres=#
Une authentification par mot de passe est plus sécurisée :
pg_hba.conf
, paramétrer une authentification par
mot de passe pour les accès depuis localhost
(déjà en place
sous Debian) :
# IPv4 local connections:
host all all 127.0.0.1/32 scram-sha-256
# IPv6 local connections: host all all ::1/128 scram-sha-256
(Ne pas oublier de recharger la configuration en cas de modification.)
postgres
de
l’instance :
dalibo:~$ sudo -iu postgres psql
psql (17.0)
Type "help" for help.
postgres=# \password
Enter new password for user "postgres":
Enter it again:
postgres=# quit
dalibo:~$ psql -h localhost -U postgres
Password for user postgres:
psql (17.0)
Type "help" for help. postgres=#
.pgpass
dans le répertoire personnel doit contenir
les informations sur cette connexion :localhost:5432:*:postgres:motdepassetrèslong
Ce fichier doit être protégé des autres utilisateurs :
$ chmod 600 ~/.pgpass
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 :
/var/lib/pgsql/17/data/log
ou
/var/log/postgresql/
) ;pg_hba.conf
ou
postgresql.conf
impliquant de recharger la configuration
peut être réalisée par une de ces trois méthodes en fonction du
système : root:~# systemctl reload postgresql-17
root:~# pg_ctlcluster 17 main reload
postgres:~$ psql -c 'SELECT pg_reload_conf()'
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/m1_solutions.
Si ce n’est pas déjà fait, démarrer l’instance PostgreSQL.
Lister les processus du serveur PostgreSQL. Qu’observe-t-on ?
Se connecter à l’instance PostgreSQL.
Dans un autre terminal lister de nouveau les processus du serveur PostgreSQL. Qu’observe-t-on ?
Créer une nouvelle base de données nommée b0.
Se connecter à la base de données b0 et créer une table
t1
avec une colonneid
de typeinteger
.
Insérer 10 millions de lignes dans la table
t1
avec :INSERT INTO t1 SELECT generate_series(1, 10000000) ;
Dans un autre terminal lister de nouveau les processus du serveur PostgreSQL. Qu’observe-t-on ?
Configurer la valeur du paramètre
max_connections
à15
.
Redémarrer l’instance PostgreSQL.
Vérifier que la modification de la valeur du paramètre
max_connections
a été prise en compte.
Se connecter 15 fois à l’instance PostgreSQL sans fermer les sessions, par exemple en lançant plusieurs fois :
psql -c 'SELECT pg_sleep(1000)' &
Se connecter une seizième fois à l’instance PostgreSQL. Qu’observe-t-on ?
Configurer la valeur du paramètre
max_connections
à sa valeur initiale.
Aller dans le répertoire des données de l’instance PostgreSQL. Lister les fichiers.
Aller dans le répertoire
base
. Lister les fichiers.
À quelle base est lié chaque répertoire présent dans le répertoire
base
? (Voir l’oid de la base danspg_database
ou l’utilitaire en ligne de commandeoid2name
)
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 colonneid
de typeinteger
.
Récupérer le chemin vers le fichier correspondant à la table
t1
(il existe une fonctionpg_relation_filepath
).
Regarder la taille du fichier correspondant à la table
t1
. Pourquoi est-il vide ?
Insérer une ligne dans la table
t1
. Quelle taille fait le fichier de la tablet1
?
Insérer 500 lignes dans la table
t1
avecgenerate_series
. Quelle taille fait le fichier de la tablet1
?
Pourquoi cette taille pour simplement 501 entiers de 4 octets chacun ?
Si ce n’est pas déjà fait, démarrer l’instance PostgreSQL.
Sous Rocky Linux, CentOS ou Red Hat en tant qu’utilisateur root :
# systemctl start postgresql-15
Lister les processus du serveur PostgreSQL. Qu’observe-t-on ?
En tant qu’utilisateur postgres :
$ ps -o pid,cmd fx
PID CMD
27886 -bash
28666 \_ 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
Se connecter à 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.
Créer une nouvelle base de données nommée b0.
Depuis le shell, en tant que postgres :
$ createdb b0
Alternativement, depuis la session déjà ouverte dans
psql
:
CREATE DATABASE b0;
Se connecter à la base de données b0 et créer une table
t1
avec une colonneid
de typeinteger
.
Pour se connecter depuis le shell :
psql b0
ou depuis la session psql
:
\c b0
Création de la table :
CREATE TABLE t1 (id integer);
Insérer 10 millions de lignes dans la table
t1
avec :INSERT INTO t1 SELECT generate_series(1, 10000000) ;
INSERT INTO t1 SELECT generate_series(1, 10000000);
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).
Configurer la valeur du paramètre
max_connections
à15
.
La première méthode est d’ouvrir le fichier de configuration
postgresql.conf
et de modifier la valeur du paramètre
max_connections
:
max_connections = 15
La seconde méthode se fait directement depuis psql en tant que superutilisateur :
ALTER SYSTEM SET max_connections TO 15 ;
Ce dernier ordre fera apparaître une ligne dans le fichier
/var/lib/pgsql/15/data/postgresql.auto.conf
.
Dans les deux cas, la prise en compte n’est pas automatique. Pour ce paramètre, il faut redémarrer l’instance PostgreSQL.
Redémarrer l’instance PostgreSQL.
En tant qu’utilisateur root :
# systemctl restart postgresql-15
Vérifier que la modification de la valeur du paramètre
max_connections
a été prise en compte.
=# SHOW max_connections ; postgres
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)' &
Il est possible de le faire manuellement ou de l’automatiser avec ce petit script shell :
$ for i in $(seq 1 15); do psql -c "SELECT pg_sleep(1000);" postgres & done
[1] 998
[2] 999
...
[15] 1012
Se connecter une seizième fois à l’instance PostgreSQL. Qu’observe-t-on ?
$ 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.
Configurer la valeur du paramètre
max_connections
à sa valeur initiale.
Si le fichier de configuration postgresql.conf
avait été
modifié, restaurer la valeur du paramètre max_connections
à
100.
Si ALTER SYSTEM
a été utilisée, il faudrait pouvoir
entrer dans psql :
ALTER SYSTEM RESET max_connections ;
Mais cela ne fonctionne pas si toutes les connexions réservées à
l’utilisateur ont été consommées (paramètre
superuser_reserved_connections
, par défaut à 3). Dans ce
cas, il n’y a plus qu’à aller dans le fichier de configuration
/var/lib/pgsql/15/data/postgresql.auto.conf
, pour supprimer
la ligne avec le paramètre max_connections
avant de
redémarrer l’instance PostgreSQL.
Il est déconseillé de modifier postgresql.auto.conf
à la
main, mais pour le TP, nous nous permettons quelques libertés.
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
Aller dans le répertoire des données de l’instance PostgreSQL. Lister les fichiers.
En tant qu’utilisateur système postgres :
echo $PGDATA
/var/lib/pgsql/15/data
$ cd $PGDATA
$ 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
Aller dans le répertoire
base
. Lister les fichiers.
$ 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 danspg_database
ou l’utilitaire en ligne de commandeoid2name
)
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 :
pg_database
:$ psql postgres
psql (15.0) Type "help" for help.
=# SELECT oid, datname FROM pg_database ORDER BY oid::text; postgres
oid | datname
-------+-----------
1 | template1
16404 | b0
4 | template0 5 | postgres
oid2name
(à installer au besoin via le
paquet postgresql15-contrib
) :
$ /usr/pgsql-15/bin/oid2name
All databases:
Oid Database Name Tablespace
----------------------------------
16404 b0 pg_default
5 postgres pg_default
4 template0 pg_default 1 template1 pg_default
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 colonneid
de typeinteger
.
$ psql b1
psql (15.0) Type "help" for help.
=# CREATE TABLE t1(id integer); b1
CREATE TABLE
Récupérer le chemin vers le fichier correspondant à la table
t1
(il existe une fonctionpg_relation_filepath
).
La fonction a pour définition :
b1=# \df pg_relation_filepath
List of functions
Schema | Name | Result data type | Argument data types | Type
------------+----------------------+------------------+---------------------+------ pg_catalog | pg_relation_filepath | text | regclass | func
L’argument regclass
peut être l’OID de la table, ou son
nom.
L’emplacement du fichier sur le disque est donc :
=# SELECT current_setting('data_directory') || '/' || pg_relation_filepath('t1')
b1AS chemin;
chemin
----------------------------------------- /var/lib/pgsql/15/data/base/16405/16406
Regarder la taille du fichier correspondant à la table
t1
. Pourquoi est-il vide ?
$ 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 tablet1
?
=# INSERT INTO t1 VALUES (1);
b1INSERT 0 1
$ 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
avecgenerate_series
. Quelle taille fait le fichier de la tablet1
?
=# INSERT INTO t1 SELECT generate_series(1, 500);
b1INSERT 0 500
$ 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
Le fichier fait maintenant 24 ko, soit 3 blocs de 8 ko.
Pourquoi cette taille pour simplement 501 entiers de 4 octets chacun ?
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 :
=# SELECT CASE WHEN attnum<0 THEN 'systeme' ELSE 'utilisateur' END AS type,
b1
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;
type | attname | attnum | typname | typlen | longueur_tot
-------------+----------+--------+---------+--------+--------------
systeme | tableoid | -6 | oid | 4 | 26
systeme | cmax | -5 | cid | 4 | 26
systeme | xmax | -4 | xid | 4 | 26
systeme | cmin | -3 | cid | 4 | 26
systeme | xmin | -2 | xid | 4 | 26
systeme | ctid | -1 | tid | 6 | 26 utilisateur | id | 1 | int4 | 4 | 4
Vous pouvez voir ces colonnes système en les appelant explicitement :
SELECT cmin, cmax, xmin, xmax, ctid, *
FROM t1 ;
L’en-tête de chaque ligne pèse 26 octets dans le cas général. 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).
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)) 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.)
Les fichiers de configuration sont habituellement les 4 suivants :
postgresql.conf
: il contient une liste de paramètres,
sous la forme paramètre=valeur
;pg_hba.conf
: il contient les règles d’authentification
à la base.pg_ident.conf
: il complète pg_hba.conf
,
quand nous déciderons de nous reposer sur un mécanisme
d’authentification extérieur à la base (identification par le système ou
par un annuaire par exemple) ;postgresql.auto.conf
: il stocke les paramètres de
configuration fixés en utilisant la commande ALTER SYSTEM
et surcharge donc postgresql.conf
. C’est le fichier le plus important. Il contient le paramétrage de
l’instance. PostgreSQL le cherche au démarrage dans le PGDATA. Par
défaut, dans les versions compilées, ou depuis les paquets sur Red Hat,
CentOS ou Rocky Linux, il sera dans le répertoire principal avec les
données (/var/lib/pgsql/15/data/postgresql.conf
par
exemple). Debian le place dans /etc
(/etc/postgresql/15/main/postgresql.conf
pour l’instance
par défaut).
Dans le doute, il est possible de consulter la valeur du paramètre
config_file
, ici dans la configuration par défaut sur Rocky
Linux :
# SHOW config_file;
config_file
--------------------------------------------- /var/lib/postgresql/15/data/postgresql.conf
Ce fichier contient un paramètre par ligne, sous le format :
clé = valeur
Les commentaires commencent par « # » (croisillon) et les chaînes de caractères doivent être encadrées de « ’ » (single quote). Par exemple :
data_directory = '/var/lib/postgresql/15/main'
listen_addresses = 'localhost'
port = 5432
shared_buffers = 128MB
Les valeurs de ce fichier ne seront pas forcément les valeurs actives !
Nous allons en effet voir que l’on peut les surcharger.
Le paramétrage ne dépend pas seulement du contenu de
postgresql.conf
.
Nous pouvons inclure d’autres fichiers depuis
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
ajustements 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
postgresql.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.
Ces paramètres peuvent être surchargés par le fichier
postgresql.auto.conf
, qui contient le résultat des
commandes de ce type :
ALTER SYSTEM SET paramètre = valeur ;
Ce fichier est principalement utilisé par les administrateurs et les
outils qui n’ont pas accès au système de fichiers du serveur. (À partir
de PostgreSQL 17, l’utilisation de ALTER SYSTEM
peut être
interdite, en passant le paramètre allow_alter_system
à
off
. Le but est d’éviter des erreurs de manipulation quand
la configuration est gérée par un outil extérieur, comme Ansible ou
Patroni. À noter qu’un superutilisateur malveillant saura tout de même
contourner cette limite.)
Si des options sont passées directement en arguments à
pg_ctl
(situation rare), elles seront prises en compte en
priorité par rapport à celles de ces fichiers de configuration.
Il est possible de surcharger les options modifiables à chaud par utilisateur, par base, et par combinaison « utilisateur+base », avec par exemple :
ALTER ROLE nagios SET log_min_duration_statement TO '1min';
ALTER DATABASE dwh SET work_mem TO '1GB';
ALTER ROLE patron IN DATABASE dwh SET work_mem TO '2GB';
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 au sein d’une session :
SET parametre = valeur ;
ou même juste au sein d’une transaction :
SET LOCAL parametre = valeur ;
Au final, l’ordre des surcharges est le suivant :
paramètre par défaut
-> postgresql.conf
-> ALTER SYSTEM SET (postgresql.auto.conf)
-> option de pg_ctl / postmaster
-> paramètre par base
-> paramètre par rôle
-> paramètre base+rôle
-> paramètre dans la chaîne de connexion
-> paramètre de session (SET) -> paramètre de transaction (SET LOCAL)
À titre d’exemple, voici ce qu’il se passe en appliquant la requête suivante :
SET client_min_messages = debug2;
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');
name | source | context | setting | boot_val | reset_val
---------------------+----------+----------+----------+----------+-----------
client_min_messages | default | user | debug2 | notice | notice
log_checkpoints | default | sighup | off | off | off wal_segment_size | override | internal | 16777216 | 16777216 | 16777216
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 debug2
. 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 reload
, 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.auto.conf
mais un rechargement de la
configuration n’est pas forcément suffisant pour qu’ils soient pris en
compte :
ALTER SYSTEM SET work_mem TO '16MB' ;
ALTER SYSTEM SET max_connections TO 200 ;
SELECT pg_reload_conf() ;
pg_reload_conf
---------------- t
SELECT * FROM pg_file_settings
WHERE name IN ('work_mem','max_connections')
ORDER BY name ;
-[ 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 |
PostgreSQL offre une certaine granularité dans sa configuration,
ainsi certains paramètres peuvent être surchargés par rapport au fichier
postgresql.conf
. Il est utile de connaître l’ordre de
précédence. Par exemple, un utilisateur peut spécifier un paramètre dans
sa session avec l’ordre SET
, celui-ci sera prioritaire par
rapport à la configuration présente dans le fichier
postgresql.conf
.
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 DBA1).
Les principales sections sont :
Connections and authentication
S’y trouveront les classiques listen_addresses
,
port
, max_connections
,
password_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 module.
Tout est prévu pour faciliter la mise en place d’une réplication sans avoir besoin de modifier cette partie sur le primaire.
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 augmenter 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.
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 formation.
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 tablespaces.
Un tablespace, vu de PostgreSQL, est un espace de stockage des objets (tables et index principalement). 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 '/mnt/ssd/pg';
L’idée est de séparer physiquement les objets suivant leur utilisation. Les cas d’utilisation des tablespaces dans PostgreSQL sont :
cannot extend file
.Sans un réel besoin physique, il n’y a pas besoin de créer des tablespaces, et de complexifier l’administration.
Un tablespace n’est pas adapté à une séparation logique des objets.
Si vous tenez à distinguer les fichiers de chaque base sans besoin
physique, rappelez-vous que PostgreSQL crée déjà un sous-répertoire par
base de données dans PGDATA/base/
.
PostgreSQL ne connaît pas de notion de tablespace en lecture seule, ni de tablespace transportable entre deux bases ou deux instances.
Il y a quelques pièges à éviter à la définition d’un tablespace :
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, par exemple
dans /mnt/ssd/pg
plutôt que directement dans le point de
montage /mnt/ssd
. (Voir Utilisation
de systèmes de fichiers secondaires dans la documentation
officielle, ou le bug à
l’origine de ce conseil.)
Surtout, le tablespace 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
Il est aussi déconseillé de mettre le numéro de version de PostgreSQL
dans le chemin du tablespace. PostgreSQL le gère déjà à l’intérieur du
tablespace. Cela évite des incohérences dans les noms des chemins si
vous migrez plus tard avec pg_upgrade
.
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) :
# /mnt/ssd/ doit exister (point de montage d'un SSD par exemple)
# chown postgres:postgres /mnt/ssd/pg
# chmod 700 /mnt/ssd/pg
Les ordres SQL plus haut permettent de :
Quelques choses à savoir :
La table ou l’index est totalement verrouillé le temps du déplacement.
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 index existants ne « suivent » pas automatiquement une table déplacée, il faut les déplacer séparément.
Depuis PostgreSQL 14, il est possible de préciser un tablespace
de réindexation lors d’une réindexation
(REINDEX … 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';
schemaname | indexname | tablespace
------------+--------------+------------
public | matable_idx | chaud public | matable_pkey |
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 :
ALTER DATABASE erp_prod SET default_tablespace TO ssd ; -- base
ALTER DATABASE erp_archives SET default_tablespace TO froid ; -- base
ALTER ROLE etl SET default_tablespace TO ssd ; -- niveau rôle
ALTER ROLE audit IN DATABASE erp_prod SET default_tablespace TO froid ; -- niveau rôle dans une base
SET default_tablespace TO ssd ; -- session
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 partition 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 RAM disque (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 RAM disque est encore plus dangereux pour les tablespaces de données, bien sûr.
Il faudra ouvrir les droits aux utilisateurs ainsi :
GRANT CREATE ON TABLESPACE ssd_tri1 TO dupont ;
Plusieurs tablespaces temporaires peuvent être paramétrés. Noter que la déclaration se fait sans guillemet. Chaque transaction en choisira un de façon aléatoire à la création d’un objet temporaire, puis utilisera alternativement les autres pour chaque nouveau fichier. C’est un bon moyen de lisser la charge :
Si un des tablespaces temporaires sature, la requête tombe en erreur immédiatement : PostgreSQL ne regarde pas si autre tablespace temporaire a de la place libre.
Il vaut donc mieux regrouper les espaces disponibles dans un même système de fichiers, et n’avoir qu’un grand tablespace temporaire.
Dans le cas de disques de performances différentes, il faut adapter les paramètres concernés aux caracté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, défaut 1) ;random_page_cost
(coût d’accès à un bloc isolé, défaut
4) ;effective_io_concurrency
(nombre d’I/O simultanées,
défaut 1) et maintenance_io_concurrency
(idem, pour une
opération de maintenance, défaut 10).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 documentation,
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. Ces valeurs doivent être
réduites sur un système très chargé. Une valeur excessive mène au
gaspillage de CPU. Le défaut deffective_io_concurrency
est
seulement de 1, et la valeur maximale est 1000.
(Avant la version 13, le principe était le même, mais la valeur exacte de ce paramètre devait être 2 à 5 fois plus basse comme le précise la formule des notes de version de la version 13.)
maintenance_io_concurrency
est similaire à
effective_io_concurrency
, mais pour les opérations de
maintenance. Celles‑ci peuvent ainsi se voir accorder plus de ressources
qu’une simple requête. Il faut penser à le monter aussi si on adapte
effective_io_concurrency
.
Par exemple, un système paramétré pour des disques classiques aura comme paramètres par défaut :
random_page_cost = 4
effective_io_concurrency = 1
maintenance_io_concurrency = 10
et un tablespace sur un SSD les surchargera ainsi :
ALTER TABLESPACE ssd SET ( random_page_cost = 1 );
ALTER TABLESPACE ssd SET ( effective_io_concurrency = 500,
= 500 ) ; maintenance_io_concurrency
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
distributions) ;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.
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
plusieurs 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 :
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.
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
officielle.
Les différents processus de PostgreSQL collectent des statistiques d’activité qui ont pour but de mesurer 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 !
Pour des raisons de performance, ces stastistiques restent en mémoire (ou dans des fichiers temporaires jusque PostgreSQL 14) et ne sont stockées durablement qu’en cas d’arrêt propre. Un arrêt brutal implique donc leur réinitialisation !
Voici les paramètres concernés par cette collecte d’informations.
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_activity_query_size
est la taille maximale du
texte de requête pouvant être stocké dans pg_stat_activity
.
1024 caractères est un défaut souvent insuffisant, à monter vers 10 000
si les requêtes sont longues, voire plus ; cette modification nécessite
un redémarrage vu qu’elle touche au dimensionnement de la mémoire
partagée.
Disponible depuis la version 14, compute_query_id
permet
d’activer le calcul de l’identifiant de la requête. Ce dernier sera
visible dans le champ query_id
de la vue
pg_stat_activity
, ainsi que dans les traces.
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 langages procéduraux, all
pour
tracer aussi les routines en C et en SQL.
update_process_title
permet de modifier le titre du
processus, visible par exemple avec ps -ef
sous Unix. Il
est à on
par défaut sous Unix, mais il faut le laisser à
off
sous Windows pour des raisons de performance.
Avant la version 15, stats_temp_directory
servait à
indiquer le répertoire de stockage temporaire des statistiques, avant
copie dans pg_stat/
lors d’un arrêt propre. Ce répertoire
peut devenir gros, est réécrit fréquemment, et peut devenir source de
contention. Il est conseillé de le stocker ailleurs que dans le
répertoire de l’instance PostgreSQL, par exemple sur un RAM disque ou
tmpfs
(c’est le défaut sous Debian).
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 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…
Statistiques sur les objets :
Pour les statistiques sur les objets, le système fournit à chaque fois trois vues différentes :
pg_statio_all_tables
par exemple ;pg_statio_sys_tables
par exemple ;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
.
SELECT * FROM pg_stat_user_tables WHERE relname = 'pgbench_accounts' \gx
-[ RECORD 1 ]-------+------------------------------
relid | 200784
schemaname | public
relname | pgbench_accounts
seq_scan | 6
last_seq_scan | 2024-11-14 11:37:16.851753+01
seq_tup_read | 104077729
idx_scan | 1
last_idx_scan | 2024-11-13 15:48:32.962717+01
idx_tup_fetch | 1319553
n_tup_ins | 0
n_tup_upd | 0
n_tup_del | 0
n_tup_hot_upd | 0
n_tup_newpage_upd | 0
n_live_tup | 0
n_dead_tup | 0
n_mod_since_analyze | 0
n_ins_since_vacuum | 0
last_vacuum | ø
last_autovacuum | ø
last_analyze | ø
last_autoanalyze | ø
vacuum_count | 0
autovacuum_count | 0
analyze_count | 0 autoanalyze_count | 0
Les accès physiques aux objets sont visibles dans les vues
pg_statio_xxx_indexes
, pg_statio_xxx_tables
et
pg_statio_xxx_sequences
. Une vision plus globale est
disponible dans pg_stat_io
(apparue avec PostgreSQL 16).
SELECT * FROM pg_statio_user_tables WHERE relname = 'pgbench_accounts' \gx
-[ RECORD 1 ]---+-----------------
relid | 200784
schemaname | public
relname | pgbench_accounts
heap_blks_read | 2993285
heap_blks_hit | 8720337
idx_blks_read | 277804
idx_blks_hit | 6114553
toast_blks_read | ø
toast_blks_hit | ø
tidx_blks_read | ø tidx_blks_hit | ø
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.
SELECT * FROM pg_stat_database WHERE datname ='pgbench' \gx
-[ RECORD 1 ]-------+------------------------------
relid | 200784
schemaname | public
relname | pgbench_accounts
seq_scan | 7
last_seq_scan | 2024-11-14 15:25:08.632708+01
seq_tup_read | 199954505
idx_scan | 1001638
last_idx_scan | 2024-11-14 15:33:15.346497+01
idx_tup_fetch | 38260013
n_tup_ins | 0
n_tup_upd | 91491186
n_tup_del | 0
n_tup_hot_upd | 304994
n_tup_newpage_upd | 91186192
n_live_tup | 98976910
n_dead_tup | 16426
n_mod_since_analyze | 1481762
n_ins_since_vacuum | 0
last_vacuum | 2024-11-14 14:41:20.893236+01
last_autovacuum | 2024-11-14 16:06:01.192891+01
last_analyze | 2024-11-14 14:41:45.544659+01
last_autoanalyze | ø
vacuum_count | 1
autovacuum_count | 1
analyze_count | 1 autoanalyze_count | 0
pg_stat_bgwriter
stocke les statistiques d’écriture des
buffers des background writer, et du checkpointer
(jusqu’en version 16 incluse) et des sessions elles-mêmes. On peut ainsi
voir si les backends écrivent beaucoup ou peu. À partir de
PostgreSQL 17, apparaît pg_stat_checkpointer
qui reprend
les champs sur les checkpoints et en ajoute quelques-uns. Cette
vue permet de vérifier que les checkpoints sont réguliers, donc
peu gênants.
Exemple (version 17) :
TABLE pg_stat_bgwriter \gx
-[ RECORD 1 ]----+------------------------------
buffers_clean | 3004
maxwritten_clean | 26
buffers_alloc | 24399160 stats_reset | 2024-11-05 15:12:27.556173+01
TABLE pg_stat_checkpointer \gx
-[ RECORD 1 ]-------+------------------------------
num_timed | 282
num_requested | 2
restartpoints_timed | 0
restartpoints_req | 0
restartpoints_done | 0
write_time | 605908
sync_time | 3846
buffers_written | 20656 stats_reset | 2024-11-05 15:12:27.556173+01
Écritures :
pg_stat_bgwriter
stocke les statistiques d’écriture des
buffers des Background Writer, Checkpointer et des sessions
elles-mêmes.
Requêtes
pg_stat_activity
est une des vues les plus utilisées et
est souvent le point de départ d’une recherche. Elle donne la liste des
processus en cours sur l’instance, en incluant entre autres :
pid
) ;application_name
;SELECT datname, pid, usename, application_name,
query
backend_start, state, backend_type, 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
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 |
Les textes des requêtes sont tronqués à 1024 caractères : c’est un
problème courant. Il est conseillé de monter le paramètre
track_activity_query_size
à plusieurs kilooctets.
Cette vue fournit aussi les wait events, qui indiquent ce
qu’une session est en train d’attendre. Cela peut être très divers et
inclut la levée d’un verrou sur un objet, celle d’un verrou interne, la
fin d’une entrée-sortie… L’absence de wait event indique que la
requête s’exécute. À noter qu’une session avec un wait event
peut rester en statut active
.
Les détails sur les champs wait_event_type
(type
d’événement en attente) et wait_event
(nom de l’événement
en attente) sont disponibles dans le tableau des événements
d’attente. de la documentation.
À partir de PostgreSQL 17, la vue pg_wait_events
peut
être directement jointe à pg_stat_activity
, et son champ
description
évite d’aller voir la documentation :
SELECT datname, application_name, pid,
query, w.description
wait_event_type, wait_event, FROM pg_stat_activity a
LEFT OUTER JOIN pg_wait_events w
ON (a.wait_event_type = w.type AND a.wait_event = w.name)
WHERE backend_type='client backend'
AND wait_event IS NOT NULL
ORDER BY wait_event DESC LIMIT 4 \gx
-[ RECORD 1 ]----+-------------------------------------------------------------
datname | pgbench_20000_hdd
application_name | pgbench
pid | 786146
wait_event_type | LWLock
wait_event | WALWriteLock
query | UPDATE pgbench_accounts SET abalance = abalance + 4055 WHERE…
description | ø
-[ RECORD 2 ]----+-------------------------------------------------------------
datname | pgbench_20000_hdd
application_name | pgbench
pid | 786190
wait_event_type | IO
wait_event | WalSync
query | UPDATE pgbench_accounts SET abalance = abalance + -1859 WHERE…
description | Waiting for a WAL file to reach durable storage
-[ RECORD 3 ]----+-------------------------------------------------------------
datname | pgbench_20000_hdd
application_name | pgbench
pid | 786145
wait_event_type | IO
wait_event | DataFileRead
query | UPDATE pgbench_accounts SET abalance = abalance + 3553 WHERE…
description | Waiting for a read from a relation data file
-[ RECORD 4 ]----+-------------------------------------------------------------
datname | pgbench_20000_hdd
application_name | pgbench
pid | 786143
wait_event_type | IO
wait_event | DataFileRead
query | UPDATE pgbench_accounts SET abalance = abalance + 1929 WHERE… description | Waiting for a read from a relation data file
Le processus de la ligne 2 attend une synchronisation sur disque du journal de transaction (WAL), et les deux suivants une lecture d’un fichier de données. (La description vide en ligne 1 est un souci de la version 17.2).
Pour entrer dans le détail des champs liés aux connexions :
backend_type
est le type de processus : on filtrera
généralement sur client backend
, mais on y trouvera aussi
des processus de tâche de fond comme checkpointer
,
walwriter
, autovacuum launcher
et autres
processus de PostgreSQL, ou encore des workers lancés par des
extensions ;datname
est le nom de la base à laquelle la session est
connectée, et datid
est son identifiant (OID) ;pid
est le processus du backend, c’est-à-dire
du processus PostgreSQL chargé de discuter avec le client, qui durera le
temps de la session (sauf parallélisation) ;usename
est le nom de l’utilisateur connecté, et
usesysid
est son OID dans pg_roles
;application_name
est un nom facultatif, et il est
recommandé que l’application cliente le renseigne autant que possible
avec SET application_name TO 'nom_outil_client'
;client_addr
est l’adresse IP du client connecté
(NULL
si connexion sur socket Unix), et
client_hostname
est le nom associé à cette IP, renseigné
uniquement si log_hostname
a été passé à on
(cela peut ralentir les connexions à cause de la résolution DNS) ;client_port
est le numéro de port sur lequel le client
est connecté, toujours s’il s’agit d’une connexion IP.Une requête parallélisée occupe plusieurs processus, et apparaîtra
sur plusieurs lignes de pid
différents. Le champ
leader_pid
indique le processus principal. Les autres
processus disparaîtront dès la requête terminée.
Pour les champs liés aux durées de session, transactions et requêtes :
backend_start
est le timestamp de l’établissement de la
session ;xact_start
est le timestamp de début de la
transaction ;query_start
est le timestamp de début de la requête en
cours, ou de la dernière requête exécutée ;status
vaut soit active
, soit
idle
(la session ne fait rien) soit
idle in transaction
(en attente pendant une transaction) ;
backend_xid
est l’identifiant de la transaction en
cours, s’il y en a une ;backend_xmin
est l’horizon des transactions visibles,
et dépend aussi des autres transactions en cours. Rappelons qu’une session durablement en statut
idle in transaction
bloque le fonctionnement de
l’autovacuum car backend_xmin
est bloqué. Cela peut mener à
des tables fragmentées et du gaspillage de place disque.
Depuis PostgreSQL 14, pg_stat_activity
peut afficher un
champ query_id
, c’est-à-dire un identifiant de requête
normalisée (dépouillée des valeurs de paramètres). Il faut que le
paramètre compute_query_id
soit à on
ou
auto
(le défaut, et alors une extension peut l’activer). Ce
champ est utile pour retrouver une requête dans la vue de l’extension
pg_stat_statements
, par exemple.
Certains champs de cette vue ne sont renseignés que si le paramètre
track_activities
est à on
(valeur par défaut,
qu’il est conseillé de laisser ainsi).
À noter qu’il ne faut pas interroger pg_stat_activity
au
sein d’une transaction, son contenu pourrait sembler figé.
Verrous :
pg_locks
permet de voir les verrous posés sur les objets
(principalement les relations comme les tables et les index). Le
processus (pid
) est la clé commune pour la rapprocher de
pg_stat_activity
.
Archivage et réplication :
pg_stat_archiver
donne des informations sur l’archivage
des journaux de transaction et notamment sur les erreurs d’archivage.
L’exemple suivant montre un archivage en erreur :
TABLE pg_stat_archiver \gx
-[ RECORD 1 ]------+------------------------------
archived_count | 1637
last_archived_wal | 0000000100000007000000E3
last_archived_time | 2024-11-14 16:00:00.418887+01
failed_count | 13254
last_failed_wal | 0000000100000007000000E4
last_failed_time | 2024-11-14 16:01:37.347793+01 stats_reset | 2024-11-05 14:58:00.515774+01
pg_stat_replication
donne des informations sur les
serveurs secondaires connectés. Les statistiques sur les conflits entre
application de la réplication et requêtes en lecture seule sont
disponibles dans pg_stat_database_conflicts
.
Si les réplications se font par des slots de réplication (optionnels
en réplication physique), pg_stat_replication_slots
donne
des informations sur leur état et leur retard.
Autres vues :
Des vues plus spécialisées existent :
pg_stat_ssl
donne des informations sur les connexions
SSL : version SSL, suite de chiffrement, nombre de bits pour
l’algorithme de chiffrement, compression, Distinguished Name (DN) du
certificat client.
pg_stat_progress_vacuum
,
pg_stat_progress_analyze
,
pg_stat_progress_create_index
,
pg_stat_progress_cluster
,
pg_stat_progress_basebackup
et
pg_stat_progress_copy
donnent respectivement des
informations sur la progression des VACUUM
, des
ANALYZE
, des créations d’index, des commandes de
VACUUM FULL
et CLUSTER
, de la commande de
réplication BASE BACKUP
et des COPY
.
pg_stat_slru
permet de suivre les accès à différents
petits caches internes de PostgreSQL (à partir de PostgreSQL 17).
Réinitialisation :
Ces vues contiennent des compteurs cumulatifs. L’évolution en fonction du temps est souvent gérée par les nombreux outils qui font appel à ces vues. Il existe une fonction pour réinitialiser les compteurs de certaines vues (pas toutes) :
SELECT pg_stat_reset_shared('archiver') ;
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 jointure, 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 colonne (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
------------------------+----------+-----------+----------+---------
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, c’est le nombre de valeurs
distinctes ; si négatif, c’est la fraction de valeurs distinctes pour
cette colonne dans la table. Il est possible de forcer la valeur de ce
champ s’il est constaté que la collecte des statistiques le calcule mal.
Par exemple, pour indiquer à l’optimiseur que chaque valeur apparaît
statistiquement deux fois :ALTER TABLE matable ALTER COLUMN yyy SET (n_distinct = -0.5) ;
ANALYZE matable ;
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
paramè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é comme ci-après (sachant que le temps
de planification augmente exponentiellement avec ce paramètre, et qu’il
vaut mieux ne pas dépasser la valeur 1000) :ALTER TABLE matable ALTER COLUMN macolonne SET statistics 300 ;
histogram_bounds
: les limites d’histogramme sur la
colonne. Les histogrammes permettent 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
commencent 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 physiquement 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
;elem_count_histogram
: les limites d’histogramme sur la
colonne si elle est de type tableau.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
).
Le langage SQL décrit le résultat souhaité. Par exemple :
SELECT path, filename
FROM file
JOIN path ON (file.pathid=path.pathid)
WHERE path LIKE '/usr/%'
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 (PostgreSQL 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 :
path
est ramenée par le
critère path LIKE '/usr/%
’ ?file.pathid
, sur
path.pathid
?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, supprimer 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).
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 :
seq_page_cost
(1 par défaut) : coût de la lecture d’une
page disque de façon séquentielle (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éatoire (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 enregistrement 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 classique à plateaux. 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, 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
préconisée :
# pour un SSD
seq_page_cost = 1
random_page_cost = 1.1
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 documentation,
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. Ces valeurs doivent être
réduites sur un système très chargé. Une valeur excessive mène au
gaspillage de CPU. Le défaut deffective_io_concurrency
est
seulement de 1, et la valeur maximale est 1000.
(Avant la version 13, le principe était le même, mais la valeur exacte de ce paramètre devait être 2 à 5 fois plus basse comme le précise la formule des notes de version de la version 13.)
Le paramètre maintenance_io_concurrency
a le même sens
que effective_io_concurrency
, mais pour les opérations de
maintenance. 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 comme effective_io_concurrency
sur des disques
plus rapides qu’un simple disque magnétique.
seq_page_cost
, random_page_cost
,
effective_io_concurrency
et
maintenance_io_concurrency
peuvent être paramétrés par
tablespace, afin de refléter les caracté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 exhaustive une requête à une jointure dure 200 microsecondes environ, contre 7 secondes pour 12 jointures. 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 planification issu de notre base de connaissance.
Les paramètres suivants peuvent être modifiés par session.
Nombre de tables considérées :
Les paramètres join_collapse_limit
et
from_collapse_limit
sont trop peu connus, mais peuvent
améliorer radicalement les performances si vous joignez souvent plus de
huit tables.
En principe, le planificateur ne tient pas compte de l’ordre des
tables dans la clause FROM
et peut décider de commencer la
requête par n’importe laquelle. Cependant, la complexité des plans
d’exécution à étudier explose avec le nombre de tables et de
combinaisons de jointures, avec toutes leurs possibilités. Les
paramètres from_collapse_limit
et
join_collapse_limit
définissent le nombre de tables prises
en compte à la fois.
join_collapse_limit
(à 8 par défaut) définit le
nombre de tables prises en compte dans le FROM
(vision
simplifiée). S’il y a plus de tables, elles seront jointes au premier
résultat dans un deuxième temps. Cela peut donner des résultats
catastrophiques si le critère de filtrage le plus utile est sur la
neuvième table du FROM
!
from_collapse_limit
remonte les tables dans une
sous-requête dans le FROM
, pourvu de ne pas dépasser cette
valeur. On le définit habituellement à la même valeur que
join_collapse_limit
.
Comme exemple de plan désastreux à cause d’un critère de filtrage sur
une table non prise en compte, voir ce plan. Monter
join_collapse_limit
de 8 à 9 bascule vers un plan bien meilleur.
Une autre solution aurait été de remonter la table
INNER JOIN contacts
, qui porte le critère, plus haut dans
le FROM
. Mais il n’est pas toujours possible de modifier le
code, qui d’ailleurs peut être dynamique.
Pour éviter de se soucier de ce problème, il est fréquent de monter
les valeurs de join_collapse_limit
et
from_collapse_limit
à 10 ou 12 quand on utilise parfois
autant de tables. Des valeurs plus élevées (jusque 20 ou plus) sont plus
dangereuses, car le temps de planification peut monter très haut
(centaines de millisecondes, voire pire). Mais elles se justifient dans
certains domaines. Il est alors préférable de positionner ces valeurs
élevées uniquement au niveau de la session, de l’écran, ou de
l’utilisateur avec SET
ou
ALTER ROLE … SET …
.
À l’inverse, descendre join_collapse_limit
à 1 permet de
dicter
l’ordre d’exécution au planificateur, une mesure de dernier
recours.
Parallèment, il ne sera pas inutile de juger de la pertinence d’une requête avec autant de tables.
Optimiseur génétique :
Ce qui suit suppose que join_collapse_limit
dépasse
12.
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 principe, 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 ci-dessous, il est déconseillé d’y
toucher sans une bonne connaissance des algorithmes génétiques.
geqo
, par défaut à on
, permet
d’activer/désactiver GEQO ;geqo_threshold
, par défaut à 12, est le nombre
d’éléments minimum à joindre dans un FROM
avant d’optimiser
celui-ci par GEQO au lieu du planificateur exhaustif ;geqo_seed
(la « graine » pour la génération aléatoire,
0 par défaut) permet de forcer la recherche d’autres plans.À geqo_seed
identique, les
plans générés seront toujours les mêmes (toutes autres choses
identiques par ailleurs).
En production, on ne montera pas geqo_threshold
ou à
peine, et en limitant dans une session. Le nombre de plans à étudier
sans l’optimisation du GEQO peut devenir colossal et consommer beaucoup
de RAM et de CPU.
Tous les paramètres suivants peuvent être modifiés par session.
Requêtes préparées :
Pour les requêtes préparées, par défaut 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. Le
paramètre de configuration plan_cache_mode
permet de
modifier cela :
auto
active le comportement par défaut ;force_custom_plan
force le recalcul systématique d’un
plan personnalisé pour la requête (on n’économise plus le temps de
planification, mais le plan est calculé pour être optimal pour les
paramètres, tout en conservant la protection contre les injections SQL
permise par les requêtes préparées) ;force_generic_plan
force l’utilisation d’un seul et
même plan dès le départ.Partitionnement :
Avant la version 10, PostgreSQL ne connaissait qu’un partitionnement
par héritage, où l’on crée une table parente et des tables filles
héritent de celle-ci, possédant des contraintes CHECK
comme
critères de partitionnement, par exemple
CHECK (date >='2011-01-01' and date < '2011-02-01')
pour une table fille d’un partitionnement par mois. Afin que PostgreSQL
ne parcourt que les partitions correspondant à la clause
WHERE
d’une requête, le paramètre
constraint_exclusion
doit valoir partition
(la
valeur par défaut) ou on
. partition
est moins
coûteux dans un contexte d’utilisation classique car les contraintes
d’exclusion ne seront examinées que dans le cas de requêtes
UNION ALL
, qui sont les requêtes générées par le
partitionnement.
Pour le partitionnement déclaratif (à favoriser à partir de
PostgreSQL 10), enable_partition_pruning
, activé par
défaut, est le paramètre équivalent.
Curseurs :
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 enregistrements. Si
c’est le cas, le plan d’exécution optimal ne sera plus le même. Le
paramètre cursor_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.
Synchronisation des parcours de table :
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
.
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);
-- insertion des données de tests
INSERT INTO test2 SELECT 1, i FROM generate_series(1, 500000) i;
-- analyse des données
ANALYZE test2;
-- désactivation de la parallélisation (pour faciliter la lecture du plan)
SET max_parallel_workers_per_gather TO 0;
-- récupération du plan d'exécution
EXPLAIN ANALYZE SELECT * FROM test2 WHERE a<3;
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);
-- récupération du plan d'exécution
EXPLAIN ANALYZE SELECT * FROM test2 WHERE a<3;
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;
-- récupération du plan d'exécution
EXPLAIN ANALYZE SELECT * FROM test2 WHERE a<3;
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)
Planning Time: 0.313 ms Execution Time: 82.598 ms
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
.La version en ligne des solutions de ces TP est disponible sur https://dali.bo/m2_solutions.
Créer un tablespace nommé
ts1
pointant vers/opt/ts1
.
Se connecter à la base de données
b1
. Créer une tablet_dans_ts1
avec une colonneid
de type integer dans le tablespacets1
.
Récupérer le chemin du fichier correspondant à la table
t_dans_ts1
avec la fonctionpg_relation_filepath
.
Supprimer le tablespace
ts1
. Qu’observe-t-on ?
Créer une table
t3
avec une colonneid
de type integer.
Insérer 1000 lignes dans la table
t3
avecgenerate_series
.
Lire les statistiques d’activité de la table
t3
à l’aide de la vue systèmepg_stat_user_tables
.
Créer un utilisateur pgbench et créer une base
pgbench
lui appartenant.
É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
etpg_roles
).
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
).
Pour voir les sessions connectées :
- dans un autre terminal, ouvrir une session
psql
sur la baseb0
, 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 ?
Se connecter à la base de données
b1
et créer une tablet4
avec une colonneid
de type entier.
Empêcher l’autovacuum d’analyser automatiquement la table
t4
.
Insérer 1 million de lignes dans
t4
avecgenerate_series
.
Rechercher la ligne ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
Exécuter la commande
ANALYZE
sur la tablet4
.
Rechercher la ligne ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
Ajouter un index sur la colonne
id
de la tablet4
.
Rechercher la ligne ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
Modifier le contenu de la table
t4
avecUPDATE t4 SET id = 100000;
Rechercher les lignes ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
Exécuter la commande
ANALYZE
sur la tablet4
.
Rechercher les lignes ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
Créer un tablespace nommé
ts1
pointant vers/opt/ts1
.
En tant qu’utilisateur root :
# mkdir /opt/ts1
# chown postgres:postgres /opt/ts1
En tant qu’utilisateur postgres :
$ psql
=# CREATE TABLESPACE ts1 LOCATION '/opt/ts1';
postgresCREATE TABLESPACE
=# \db postgres
Liste des tablespaces
Nom | Propriétaire | Emplacement
------------+--------------+-------------
pg_default | postgres |
pg_global | postgres | ts1 | postgres | /opt/ts1
Se connecter à la base de données
b1
. Créer une tablet_dans_ts1
avec une colonneid
de type integer dans le tablespacets1
.
=# CREATE TABLE t_dans_ts1 (id integer) TABLESPACE ts1;
b1CREATE TABLE
Récupérer le chemin du fichier correspondant à la table
t_dans_ts1
avec la fonctionpg_relation_filepath
.
b1=# SELECT current_setting('data_directory') || '/'
|| pg_relation_filepath('t_dans_ts1') AS chemin;
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/
$ 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 :
=# DROP TABLESPACE ts1 ;
b1tablespace "ts1" is not empty
ERROR:
=# ALTER TABLE t_dans_ts1 SET TABLESPACE pg_default ;
b1ALTER TABLE
=# DROP TABLESPACE ts1 ;
b1DROP TABLESPACE
Créer une table
t3
avec une colonneid
de type integer.
=# CREATE TABLE t3 (id integer);
b1CREATE TABLE
Insérer 1000 lignes dans la table
t3
avecgenerate_series
.
=# INSERT INTO t3 SELECT generate_series(1, 1000);
b1INSERT 0 1000
Lire les statistiques d’activité de la table
t3
à l’aide de la vue systèmepg_stat_user_tables
.
=# \x
b1is on.
Expanded display =# SELECT * FROM pg_stat_user_tables WHERE relname = 't3'; b1
-[ RECORD 1 ]-----+-------
relid | 24594
schemaname | public
relname | t3
seq_scan | 0
seq_tup_read | 0
idx_scan |
idx_tup_fetch |
n_tup_ins | 1000
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.
Créer un utilisateur pgbench et créer une base
pgbench
lui appartenant.
=# CREATE ROLE pgbench LOGIN ;
b1CREATE ROLE
=# CREATE DATABASE pgbench OWNER pgbench ;
b1CREATE DATABASE
É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
etpg_roles
).
La liste des bases de données se trouve dans la table
pg_database
:
SELECT db.oid, db.datname, datdba
FROM pg_database db ;
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 ;
d’où par exemple :
datname | rolname | encoding
-----------+----------+----------
b1 | postgres | 6
b0 | postgres | 6
template0 | postgres | 6
template1 | postgres | 6
postgres | postgres | 6 pgbench | pgbench | 6
L’encodage est numérique, il reste à le rendre lisible.
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
).
Il est possible de positionner le paramètre
\set ECHO_HIDDEN on
, ou sortir de la console et la lancer
de nouveau psql avec l’option -E
:
$ psql -E
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",
as "Owner",
pg_catalog.pg_get_userbyid(d.datdba) as "Encoding",
pg_catalog.pg_encoding_to_char(d.encoding) as "Collate",
d.datcollate as "Ctype",
d.datctype '\n') AS "Access privileges"
pg_catalog.array_to_string(d.datacl, EFROM pg_catalog.pg_database d
ORDER BY 1;
**************************
L’encodage se retrouve donc en appelant la fonction
pg_encoding_to_char
:
=# SELECT db.datname, r.rolname, db.encoding, pg_catalog.pg_encoding_to_char(db.encoding)
b1FROM pg_database db, pg_roles r
WHERE db.datdba = r.oid ;
datname | rolname | encoding | pg_encoding_to_char
-----------+----------+----------+---------------------
b1 | postgres | 6 | UTF8
b0 | postgres | 6 | UTF8
template0 | postgres | 6 | UTF8
template1 | postgres | 6 | UTF8
postgres | postgres | 6 | UTF8 pgbench | pgbench | 6 | UTF8
Pour voir les sessions connectées :
- dans un autre terminal, ouvrir une session
psql
sur la baseb0
, 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
La table a de nombreux champs, affichons les plus importants :
SELECT datname, pid, state, usename, application_name AS appp, backend_type, query
# FROM pg_stat_activity ;
datname | pid | state | usename | appp | backend_type | query
---------+------+--------+----------+------+------------------------------+--------
| 6179 | | | | autovacuum launcher |
| 6181 | | postgres | | logical replication launcher |
b0 | 6870 | idle | postgres | psql | client backend |
b1 | 6872 | active | postgres | psql | client backend | SELECT…
| 6177 | | | | background writer |
| 6176 | | | | checkpointer |
| 6178 | | | | walwriter | (7 rows)
La session dans b1
est idle
, c’est-à-dire
en attente. La seule session active (au moment où elle tournait) 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;
Ce qui donnerait par exemple :
datname | count
---------+-------
pgbench | 10 b0 | 5
Se connecter à la base de données
b1
et créer une tablet4
avec une colonneid
de type entier.
=# CREATE TABLE t4 (id integer);
b1CREATE TABLE
Empêcher l’autovacuum d’analyser automatiquement la table
t4
.
=# ALTER TABLE t4 SET (autovacuum_enabled=false);
b1ALTER TABLE
NB : ceci n’est à faire qu’à titre d’exercice ! En production, c’est une très mauvaise idée.
Insérer 1 million de lignes dans
t4
avecgenerate_series
.
=# INSERT INTO t4 SELECT generate_series(1, 1000000);
b1INSERT 0 1000000
Rechercher la ligne ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
=# EXPLAIN SELECT * FROM t4 WHERE id = 100000; b1
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)
Exécuter la commande
ANALYZE
sur la tablet4
.
=# ANALYZE t4;
b1ANALYZE
Rechercher la ligne ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
=# EXPLAIN SELECT * FROM t4 WHERE id = 100000; b1
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 tablet4
.
=# CREATE INDEX ON t4(id);
b1CREATE INDEX
Rechercher la ligne ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
=# EXPLAIN SELECT * FROM t4 WHERE id = 100000; b1
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
avecUPDATE t4 SET id = 100000;
=# UPDATE t4 SET id = 100000;
b1UPDATE 1000000
Toutes les lignes ont donc à présent la même valeur.
Rechercher les lignes ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
=# EXPLAIN ANALYZE SELECT * FROM t4 WHERE id = 100000; b1
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.
Exécuter la commande
ANALYZE
sur la tablet4
.
=# ANALYZE t4;
b1ANALYZE
Rechercher les lignes ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
=# EXPLAIN ANALYZE SELECT * FROM t4 WHERE id = 100000; b1
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.
La zone de mémoire partagée statique est allouée au démarrage de
l’instance. Le type de mémoire partagée est configuré avec le paramètre
shared_memory_type
. Sous Linux, il s’agit par défaut de
mmap
, sachant qu’une très petite partie utilise toujours
sysv
(System V). (Il est en principe possible de basculer
uniquement en sysv
mais ceci n’est pas recommandé et
nécessite un paramétrage du noyau Linux.) Sous Windows, le type est
windows
.
Les principales zones de mémoire partagées décrites ici sont fixes, et les tailles calculées en fonction de paramètres. Nous verrons en détail l’utilité de certaines de ces zones dans les chapitres suivants.
Shared buffers :
Les shared buffers sont le cache des fichiers de données présents sur le disque. Ils représentent de loin la volumétrie la plus importante.
Paramètre associé : shared_buffers
(à adapter
systématiquement)
Wal buffers :
Les wal buffers sont le cache des journaux de transaction.
Paramètre associé : wal_buffers
(rarement modifié)
Données liées aux sessions :
Cet espace mémoire sert à gérer les sessions ouvertes, celles des utilisateurs, mais aussi celles ouvertes par de nombreux processus internes.
Principaux paramètres associés :
max_connections
, soit le nombre de connexions
simultanées possibles (défaut : 100, souvent suffisant mais à
adapter) ;track_activity_query_size
(défaut : 1024 ; souvent
monté à 10 000 s’il y a des requêtes de grande taille) ;Verrous :
La gestion des verrous est décrite dans un autre module. Lors des mises à jour ou suppressions dans les tables, les verrous sur les lignes sont généralement stockés dans les lignes mêmes ; mais de nombreux autres verrous sont stockés en mémoire partagée. Les paramètres associés pour le dimensionnement sont surtout :
max_connections
à nouveau ;max_locks_per_transaction
, soit le nombre de verrous
possible pour une transaction (défaut : 64, généralement
suffisant) ;max_pred_locks_per_relation
, nombre de verrous possible
pour une table si le niveau d’isolation « sérialisation » est
choisi ;Les SLRU :
Les SLRU (Simple Least Recently Used Buffers) sont de petits
caches pour certaines métadonneés de diverses natures : transactions et
sous-transactions en cours, horodatage des commits,
notifications par NOTIFY
…
Le mécanisme a été revu en PostgreSQL 17 et, depuis, il est possible
de modifier la taille des SLRU avec les paramètres
commit_timestamp_buffers
,
multixact_member_buffers
,
multixact_offset_buffers
, notify_buffers
,
serializable_buffers
, subtransaction_buffers
et transaction_buffers
.
Les valeurs par défaut ne font parfois que 16 blocs. Les augmenter
peut être intéressant si l’on repère une contention sur l’un d’eux. Un
indice est la présence récurrente de wait events avec « SLRU »
dans le nom. (Pour la liste des wait events, voir la table
pg_wait_events
, à partir de la version 17 également. Ils se
voient dans pg_stat_activity
. Il existe aussi une vue
pg_stat_slru
.)
Modification :
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 complète
de mémoire partagée allouée (un peu supérieure à
shared_buffers
en pratique). Dans le cadre de l’utilisation
des Huge Pages, il est possible de consulter le paramètre
shared_memory_size_in_huge_pages
pour connaître le nombre
de pages mémoires nécessaires (mais on ne peut savoir ici si elles sont
utilisées) :
postgres=# \dconfig shared*
Liste des paramètres de configuration
Paramètre | Valeur
----------------------------------+---------------------------------
shared_buffers | 12GB
shared_memory_size | 12835MB
shared_memory_size_in_huge_pages | 6418 ...
Des zones de mémoire partagée non statiques peuvent exister : par
exemple, à l’exécution d’une requête parallélisée, les processus
impliqués utilisent de la mémoire partagée dynamique. Depuis PostgreSQL
14, une partie peut être pré-allouée avec le paramètre
min_dynamic_shared_memory
(0 par défaut).
Les processus de PostgreSQL ont accès à la mémoire partagée, définie
principalement par shared_buffers
, mais ils ont aussi leur
mémoire propre. Cette mémoire n’est utilisable que par le processus
l’ayant allouée.
work_mem
, qui
définit la taille maximale de la mémoire de travail d’un
ORDER BY
, de certaines jointures, pour la déduplication…
que peut utiliser un processus sur un nœud de requête, principalement
lors d’opérations de tri ou regroupement.maintenance_work_mem
qui est
la mémoire utilisable pour les opérations de maintenance lourdes :
VACUUM
, CREATE INDEX
, REINDEX
,
ajouts de clé étrangère…Cette mémoire liée au processus est rendue immédiatement après la fin de l’ordre concerné.
logical_decoding_work_mem
(défaut :
64 Mo), utilisable pour chacun des flux de réplication logique (s’il y
en a, ils sont rarement nombreux). Opérations de maintenance & maintenance_work_mem :
maintenance_work_mem
peut être monté à 256 Mo à 1 Go,
voire plus sur les machines récentes, car il concerne des opérations
lourdes (indexation, nettoyage des index par VACUUM
…).
Leurs consommations de RAM s’additionnent, mais en pratique ces
opérations sont rarement exécutées plusieurs fois simultanément.
Monter au-delà de 1 Go n’a d’intérêt que pour la création ou la réindexation de très gros index.
autovacuum_work_mem
est la mémoire que s’autorise à
prendre l’autovacuum pour les nettoyages d’index, et ce pour chaque
worker de l’autovacuum (3 maximum par défaut). Par défaut, ce
paramètre reprend maintenance_work_mem
, et est généralement
laissé tel quel.
Paramétrage de work_mem :
Pour work_mem
, c’est beaucoup plus compliqué.
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 120 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 simultanément, le danger est de saturer la RAM.
Il n’existe en effet pas de limite à la consommation des sessions de
PostgreSQL, ni globalement ni par session !
Or le paramétrage de l’overcommit sous Linux est par défaut très permissif, le noyau ne bloquera rien. La première conséquence de la saturation de mémoire est l’assèchement du cache système (complémentaire de celui de PostgreSQL), 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.
Toutefois, si l’administrateur paramètre correctement l’overcommit (voir https://dali.bo/j1_html#configuration-de-la-surréservation-mémoire), Linux refusera d’allouer la RAM et la requête tombera en erreur, mais le cache système sera préservé, et PostgreSQL ne tombera pas.
Suivant la complexité des requêtes, il est possible qu’un processus
utilise plusieurs fois work_mem
(par exemple si une requête
fait une jointure et un tri, ou qu’un nœud est parallélisé). À
l’inverse, beaucoup de requêtes ne nécessitent aucune mémoire de
travail.
La valeur de work_mem
dépend donc beaucoup de la mémoire
disponible, des requêtes et du nombre de connexions actives.
Si le nombre de requêtes simultanées est important,
work_mem
devra être faible. Avec peu de requêtes
simultanées, work_mem
pourra être augmenté sans risque.
Il n’y a pas de formule de calcul miracle. Une première estimation courante, bien que très conservatrice, peut être :
work_mem
= mémoire /max_connections
On obtient alors, sur un serveur dédié avec 16 Go de RAM et 200 connexions autorisées :
work_mem = 80MB
Mais max_connections
est fréquemment surdimensionné, et
beaucoup de sessions sont inactives. work_mem
est alors
sous-dimensionné.
Plus finement, Christophe Pettus propose en première intention :
work_mem
= 4 × mémoire libre /max_connections
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.
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 requête ou
d’un traitement particulier :
SET work_mem TO '30MB' ;
hash_mem_multiplier :
hash_mem_multiplier
est un paramètre multiplicateur, qui
peut s’appliquer à certaines opérations (le hachage, lors de jointures
ou agrégations). Par défaut, il vaut 1 en versions 13 et 14, et 2 à
partir de la 15. Le seuil de consommation fixé par work_mem
est alors multiplié d’autant. hash_mem_multiplier
permet de
donner plus de RAM à ces opérations sans augmenter globalement
work_mem
. Il peut lui aussi être modifié dans une session.
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 augmentée dans la session
avant la création de la table temporaire.
Il ne faut pas laisser inutilement ouvertes des sessions ayant créé des tables temporaires, sinon la mémoire n’est jamais rendue.
PostgreSQL dispose de son propre mécanisme de cache. Toute donnée lue l’est de ce cache. Si la donné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 modifié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. Il n’y a pas de direct I/O comme dans
certains SGBD concurrents. 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. La redondance est
habituellement limitée, mais il existe des cas problématiques. En
conséquence, des travaux sont en cours pour contourner le cache du
système quand cela est pertinent. Cela implique de réimplémenter
certaines fonctionnalités fournies par le noyau (prefetching,
read-ahead), en les optimisant pour PostgreSQL. Une première
étape apparaît en version 17. PostgreSQL sait depuis longtemps lire
plusieurs blocs d’un coup (fonction pread()
), mais ce n’est
pas très bien adapté à la nature fragmentée de son cache. Avec
l’utilisation des vectored I/O,
PostgreSQL peut lire plusieurs blocs contigus sur le système de
fichiers, et les écrire à des positions disjointes en mémoire
virtuelle, avec un seul appel système preadv()
(voir la page
de manuel), (Avec un OS qui ne connait pas la fonction, comme
Windows, PostgreSQL se contente d’appeler pread()
plusieurs
fois.) Avec la valeur par défaut d’un nouveau paramètre,
io_combine_limit
, et des blocs de taille standard, on a
jusqu’à 16 fois moins d’appels système pour la lecture des blocs en
dehors du cache PostgreSQL. En version 17, seule l’implémentation du
parcours séquentiel, celle de ANALYZE
, et celle de
pg_prewarm
utilisent cette nouvelle API. Nous conseillons
de ne pas changer la valeur de ce paramètre sans tests sérieux montrant
qu’un changement est bénéfique.
Dimensionner correctement ce cache est important pour de nombreuses raisons.
Un cache trop petit :
Un cache trop grand :
shared_buffers
a un coût de traitement ;shared_buffers
:
Un bon point de départ est 25 % de la mémoire vive totale. Ne pas dépasser 40 %, car le cache du système d’exploitation est aussi utilisé.
Sur une machine dédiée de 32 Go de RAM, cela donne donc :
shared_buffers = 8GB
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 performances, mais il faudra tester avec votre charge (en lecture, en écriture, et avec le bon nombre de clients).
Le cache système limite la plupart du temps l’impact d’un mauvais
paramétrage de shared_buffers
, et il est moins grave de
sous-dimensionner un peu shared_buffers
que de le
sur-dimensionner.
Attention : une valeur élevée de shared_buffers
(au-delà
de 8 Go) nécessite de paramétrer finement le système d’exploitation
(Huge Pages notamment) et d’autres paramètres liés aux journaux
et checkpoints comme max_wal_size
. Il faut aussi
s’assurer qu’il restera de la mémoire pour le reste des opérations
(tri…) et donc adapter work_mem
.
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 ⅔ 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'
Cela sera à ajuster en fonction du comportement observé de l’application.
Les principales notions à connaître pour comprendre le mécanisme de gestion du cache de PostgreSQL sont :
Buffer pin
Un processus voulant accéder à un bloc du cache (buffer) doit d’abord épingler (to pin) ce bloc pour forcer son maintien en cache. Pour ce faire, il incrémente le compteur buffer pin, puis 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 a été modifié en mémoire, mais pas encore sur disque. Le fichier de données n’est donc plus à jour (mais a bien été pérennisée sur disque dans les journaux de transactions). Il faudra écrire ce bloc avant de pouvoir le supprimer du cache. Nous verrons plus loin comment.
Un buffer est clean (propre) s’il n’a pas été modifié. Le bloc peut être supprimé du cache sans nécessiter le coût d’une écriture sur disque.
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 disponible. 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 unpinned 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 ¼ de shared_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
ou un ANALYZE
, la même
technique est utilisée, mais les écritures se font dans le ring
buffer. Sa taille est de 2 Mo (256 ko seulement jusque
PostgreSQL 16 inclus). À partir de PostgreSQL 16, elle peut être
augmentée pour accélérer les opérations avec le paramètre
vacuum_buffer_usage_limit
ou ponctuellement ainsi :
vacuumdb --buffer-usage-limit='16MB'
ou ainsi :
ANALYZE, BUFFER_USAGE_LIMIT '16MB') ; VACUUM (
En montant à quelques mégaoctets, l’accélération de la vitesse du
VACUUM
peut
être notable. C’est aussi très intéressant pour accélérer
ANALYZE
.
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 PostgreSQL et un README dans le code de PostgreSQL entrent plus en détail sur tous ces sujets tout en restant lisibles.
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 :
=# CREATE EXTENSION pg_buffercache ;
pgbench
=# SELECT
pgbench
relname,
isdirty,count(bufferid) AS blocs,
count(bufferid) * current_setting ('block_size')::int) AS taille
pg_size_pretty(FROM pg_buffercache b
INNER JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE relname NOT LIKE 'pg\_%'
GROUP BY
relname,
isdirtyORDER BY 1, 2 ;
relname | isdirty | blocs | taille
-----------------------+---------+-------+---------
pgbench_accounts | f | 8398 | 66 MB
pgbench_accounts | t | 4622 | 36 MB
pgbench_accounts_pkey | f | 2744 | 21 MB
pgbench_branches | f | 14 | 112 kB
pgbench_branches | t | 2 | 16 kB
pgbench_branches_pkey | f | 2 | 16 kB
pgbench_history | f | 267 | 2136 kB
pgbench_history | t | 102 | 816 kB
pgbench_tellers | f | 13 | 104 kB pgbench_tellers_pkey | f | 2 | 16 kB
L’extension pg_prewarm
permet de précharger un objet
dans le cache de PostgreSQL (si le cache est assez gros, 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 documentation).
Ces deux outils sont décrits dans le module de formation X2.
Afin de limiter les attentes des sessions interactives, PostgreSQL
dispose de deux processus complémentaires, le
background writer
et le checkpointer
. Tous
deux ont pour rôle d’écrire sur le disque les buffers dirty
(« sales ») de façon asynchrone. Le but est de ne pas impacter les temps
de traitement des requêtes des utilisateurs, donc que les écritures
soient lissées sur de grandes plages de temps, pour ne pas saturer les
disques, mais en nettoyant assez souvent le cache pour qu’il y ait
toujours assez de blocs libérables en cas de besoin.
Le checkpointer
écrit généralement l’essentiel des blocs
dirty. Lors des checkpoints, il synchronise sur disque
tous les blocs modifiés. Son rôle est de lisser cette charge sans
saturer les disques.
Comme le checkpointer
n’est pas censé passer très
souvent, le background writer
anticipe les besoins des
sessions, et écrit lui-même une partie des blocs dirty.
Lors d’écritures intenses, il est possible que ces deux mécanismes
soient débordés. Les processus backend ne trouvent alors plus
en cache de bloc libre ou libérable, et doivent 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 background writer
plus
agressif.
Nous verrons plus loin les paramètres concernés.
La journalisation, sous PostgreSQL, permet de garantir l’intégrité des fichiers, et la durabilité des opérations :
COMMIT
) est
écrite physiquement, et un arrêt brutal immédiatement ne va pas la faire
disparaître (excepté la perte de tous les disques de stockage et
réplicas, bien sûr).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.
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 magnétiques). Il n’y a que le fichier en cours à
synchroniser à chaque COMMIT
.
Ce n’est généralement que bien plus tard que les modificationss
seront écrites de façon asynchrone, soit par un processus recherchant un
buffer libre, soit par le background writer
, soit par le
checkpointer
. Ce dernier processus sait étaler la charge en
écriture dans les fichiers de données sur plusieurs minutes, et dans
l’idéal il est seul à s’en charger.
Il existe plusieurs configurations et astuces pour arbitrer entre le
niveau de durabilité des données exigé et les contraintes de
performances : secondaire en réplication synchrone pour une sécurité
maximale, désactivation partielle du mécanisme d’enregistrement
synchrone des journaux dans une session, tables de travail non
journalisées (unlogged), regroupement des insertions pour
réduire l’impact des COMMIT
… Aucune ne remet en cause
l’intégrité définie dans les modèle de données.
Rappelons que les journaux de transaction sont des fichiers de 16 Mo
par défaut, stockés dans PGDATA/pg_wal
, 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
Le sous-répertoire archive_status
est lié à
l’archivage.
D’autres plus petits répertoires comme pg_xact
, qui
contient les statuts des transactions passées, ou
pg_commit_ts
, pg_multixact
,
pg_serial
, pg_snapshots
,
pg_subtrans
ou encore pg_twophase
sont
également impliqués.
Tous ces répertoires sont critiques, gérés par PostgreSQL, et ne doivent pas être modifiés !
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 interrompue, 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 au dernier point de reprise valide 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.
Un checkpoint n’est pas un « instantané » cohérent de l’ensemble des fichiers. C’est simplement l’endroit à partir duquel les journaux doivent être rejoués. Il faut donc pouvoir garantir que tous les blocs modifiés dans le cache au démarrage du checkpoint auront été synchronisés sur le disque quand le checkpoint sera terminé, et marqué comme dernier checkpoint valide. Un checkpoint peut donc durer plusieurs minutes, sans que cela ne bloque l’activité.
C’est le processus checkpointer
qui est responsable de
l’écriture des buffers devant être synchronisés durant un
checkpoint.
Plusieurs paramètres influencent le comportement des checkpoints.
Dans l’idéal les checkpoints sont périodiques. Le temps maximum entre
deux checkpoints est fixé par checkpoint_timeout
(par
défaut 300 secondes). C’est parfois un peu court pour les instances
actives.
Le checkpoint intervient aussi quand il y a beaucoup d’écritures et
que le volume des journaux dépasse le seuil défini par le paramètre
max_wal_size
(1 Go par défaut). Un checkpoint est alors
déclenché.
L’ordre CHECKPOINT
déclenche aussi un
checkpoint sans attendre. En fait, il sert surtout à des
utilitaires.
Une fois le checkpoint terminé, les journaux sont à priori inutiles.
Ils peuvent être effacés pour redescendre 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 inté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
.
Le dimensionnement de ces paramètres est très dépendant du contexte, de l’activité habituelle, et de la régularité des écritures. Le but est d’éviter des gros pics d’écriture, et donc d’avoir des checkpoints essentiellement périodiques, même si des opérations ponctuelles peuvent y échapper (gros chargements, grosse maintenance…).
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 à sa première modification après un checkpoint, mais par la suite seules les modifications de ce bloc sont journalisées. Espacer les checkpoints peut économiser beaucoup de place disque quand les journaux sont archivés, et du réseau s’ils sont répliqués. 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
à plusieurs
gigaoctets.
Si l’on monte max_wal_size
, par cohérence, il faudra
penser à augmenter aussi checkpoint_timeout
, et
vice-versa.
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
checkpoint_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_*
. Il y
a un intérêt à continuer de le faire, car PostgreSQL n’est pas seul à
écrire de gros fichiers (exports pg_dump
, copie de
fichiers…).
Quand le checkpoint démarre, il vise à lisser au maximum le débit en
écriture. La durée d’écriture des données se calcule à partir d’une
fraction de la durée d’exécution des précédents checkpoints, fraction
fixée par le paramètre checkpoint_completion_target
. Sa
valeur par défaut est celle préconisée par la documentation pour un
lissage maximum, soit 0,9 (depuis la version 14, et auparavant le défaut
de 0,5 était fréquemment corrigé). Par défaut, PostgreSQL prévoit donc
une durée maximale de 300 × 0,9 = 270 secondes pour opérer son
checkpoint, mais cette valeur pourra évoluer ensuite suivant la durée
réelle des checkpoints précédents.
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 à
checkpoint_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.
max_wal_size
n’est pas une limite en
dur de la taille de pg_wal/
.
La partition de pg_wal/
doit être taillée généreusement.
Sa saturation entraîne l’arrêt immédiat de l’instance !
Comme le checkpointer
ne s’exécute pas très souvent, et
ne s’occupe pas des blocs salis depuis son exécution courante, il est
épaulé par le background writer
. Celui-ci pour but de
nettoyer une partie des blocs dirty pour faire de la place à
d’autres. Il s’exécute beaucoup plus fréquemment que le
checkpointer
mais traite moins de blocs à chaque fois.
À intervalle régulier, le background writer
synchronise
un nombre de buffers proportionnel à l’activité sur l’intervalle
précédent. Quatre paramètres régissent son comportement :
bgwriter_delay
(défaut : 200 ms) : la fréquence à
laquelle se réveille le background writer
;bgwriter_lru_maxpages
(défaut : 100) : le nombre
maximum de pages pouvant être écrites sur chaque tour d’activité. Ce
paramètre permet d’éviter que le background writer
ne
veuille synchroniser trop de pages si l’activité des sessions est trop
intense : dans ce cas, autant les laisser effectuer elles-mêmes les
synchronisations, étant donné que la charge est forte ;bgwriter_lru_multiplier
(defaut : 2) : le coefficient
multiplicateur utilisé pour calculer le nombre de buffers à libérer par
rapport aux demandes d’allocation sur la période précédente ;bgwriter_flush_after
(défaut : 512 ko sous Linux, 0 ou
désactivé ailleurs) : à partir de quelle quantité de données écrites une
synchronisation sur disque est demandée.Pour les paramètres bgwriter_lru_maxpages
et
bgwriter_lru_multiplier
, lru signifie Least
Recently Used (« moins récemment utilisé »). Ainsi le
background writer
synchronisera les pages du cache qui ont
été utilisées le moins récemment.
Ces paramètres permettent de rendre le background writer
plus agressif si la supervision montre que les processus
backends écrivent trop souvent les blocs de données eux-mêmes,
faute de blocs libres dans le cache, ce qui évidemment ralentit
l’exécution du point de vue du client.
Évidemment, certaines écritures massives ne peuvent être absorbées
par le background writer
. Elles provoquent des écritures
par les processus backend et des checkpoints déclenchés.
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 validation 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 Mo)
pouvant ê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) ;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 donné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 écritures 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
paramè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
compresse les blocs complets enregistrés
dans les journaux de transactions, réduisant le volume des WAL, la
charge en écriture sur les disques, la volumétrie des journaux archivés
des sauvegardes PITR.
Comme il y a moins de journaux, leur rejeu 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.
Les détails et un exemple figurent dans ce billet du blog Dalibo.
Depuis PostgreSQL 15, on peut même choisir l’algorithme :
pglz
, lz4
ou zstd
.
on
est le synonyme de pglz
… qui est sans doute
le moins bon des trois (voir ce
petit test), surtout en terme de consommation CPU.
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.
Le paramètre synchronous_commit
(défaut :
on
) indique si la validation de la transaction en cours
doit déclencher une écriture synchrone dans le journal. Le défaut permet
de garantir la pérennité des données dès la fin du
COMMIT
.
Mais ce paramètre peut être modifié dans chaque session par une
commande SET
, et passé à off
s’il est
possible d’accepter une petite perte de données pourtant
committées. La perte peut monter à 3 × wal_writer_delay
(600 ms) ou wal_writer_flush_after
(1 Mo) octets écrits. On
accélère ainsi notablement les flux des petites transactions. Les
transactions où le paramètre reste à on
continuent de
profiter de la sécurité maximale. La base restera, quoi qu’il arrive,
cohérente. (Ce paramètre permet aussi de régler le niveau des
transactions synchrones avec des secondaires.)
Il existe aussi commit_delay
(défaut : 0
)
et commit_siblings
(défaut : 5
) comme
mécanisme de regroupement de transactions.
S’il y au moins commit_siblings
transactions en cours,
PostgreSQL attendra jusqu’à commit_delay
(en microsecondes)
avant de valider une transaction pour permettre à d’autres transactions
de s’y rattacher. Ce mécanisme, désactivé par défaut, accroît la latence
de certaines transactions afin que plusieurs soient écrites ensembles,
et n’apporte un gain de performance global qu’avec de nombreuses petites
transactions en parallèle, et des disques classiques un peu lents. (En
cas d’arrêt brutal, il n’y a pas à proprement parler de perte de données
puisque les transactions délibérément retardées n’ont pas été signalées
comme validées.)
Le système de journalisation de PostgreSQL étant très fiable, des fonctionnalités très intéressantes ont été bâties dessus.
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 modification.
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 Recovery.
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écessaires 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 logique.
(Avant la version 9.6 existaient les niveaux intermédiaires
archive
et hot_standby
, respectivement 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_mode
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.
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 :
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 sauvegarde
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
permet 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écrochage 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
.
pgbench est un outil de test livré avec PostgreSQL. Son but est de faciliter la mise en place de benchmarks simples et rapides. Par défaut, il installe une base très 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 2017.
L’outil est installé avec les paquets habituels de PostgreSQL, client ou serveur suivant la distribution.
Sur les distributions à paquets RPM (RockyLinux…), l’outil n’est pas dans le chemin par défaut, il faudra fournir le chemin complet (qui ne sera pas répété ici):
/usr/pgsql-17/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ée :
CREATE ROLE pgbench LOGIN PASSWORD 'unmotdepassebienc0mplexe';
CREATE DATABASE pgbench OWNER pgbench ;
Le pg_hba.conf
doit éventuellement être adapté. La base
par défaut s’initialise ainsi (ajouter --port
et
--host
au besoin) :
pgbench -U -d pgbench --initialize --scale=100 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 secondes :
pgbench -U pgbench -c 20 -j 4 -T100 pgbench
Pour afficher, ajouter --debug
:
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);
À la fin, s’affichent notamment le nombre de transactions (avec et
sans le temps de connexion) et la durée moyenne d’exécution du point de
vue du client (latency
) :
scaling factor: 100
query mode: simple
number of clients: 20
number of threads: 4
duration: 10 s
number of transactions actually processed: 20433
latency average = 9.826 ms
tps = 2035.338395 (including connections establishing) tps = 2037.198912 (excluding connections establishing)
Modifier le paramétrage est facile grâce à la variable
d’environnement PGOPTIONS
:
PGOPTIONS='-c synchronous_commit=off -c commit_siblings=20' \
pgbench -U pgbench -c 20 -j 4 -T100 pgbench 2>/dev/null
latency average = 6.992 ms
tps = 2860.465176 (including connections establishing) tps = 2862.964803 (excluding connections establishing)
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.
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/m3_solutions.
Se connecter à la base de données b0 et créer une table
t2
avec une colonneid
de typeinteger
.
Insérer 500 lignes dans la table
t2
avecgenerate_series
.
Pour réinitialiser les statistiques de
t2
:
- utiliser la fonction
pg_stat_reset_single_table_counters
- l’OID en paramètre est dans la table des relations
pg_class
, ou peut être trouvé avec't2'::regclass
Afin de vider le cache, redémarrer l’instance PostgreSQL.
Se connecter à la base de données b0 et lire les données de la table
t2
.
Récupérer les statistiques IO pour la table
t2
dans la vue systèmepg_statio_user_tables
. Qu’observe-t-on ?
Lire de nouveau les données de la table
t2
et consulter ses statistiques. Qu’observe-t-on ?
Lire de nouveau les données de la table
t2
et consulter ses statistiques. Qu’observe-t-on ?
Ouvrir un premier terminal et laisser défiler le fichier de traces.
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).
Insérer un million de lignes dans la table
t2
avecgenerate_series
.
Activer le chronométrage dans la session (
\timing on
). Lire les données de la tablet2
en triant par la colonneid
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 colonneid
. Qu’observe-t-on ?
Se connecter à la base de données
b1
. Installer l’extensionpg_buffercache
.
Créer une table
t2
avec une colonneid
de typeinteger
.
Insérer un million de lignes dans la table
t2
avecgenerate_series
.
Pour vider le cache de PostgreSQL, redémarrer l’instance.
Pour vider le cache du système d’exploitation, sous root :
# sync && echo 3 > /proc/sys/vm/drop_caches
Se connecter à la base de données b1. En utilisant l’extension
pg_buffercache
, que contient le cache de PostgreSQL ? (Compter les blocs pour chaque table ; au besoin s’inspirer de la requête du cours.)
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 ?
Configurer la valeur du paramètre
shared_buffers
à un quart de la RAM.
Redémarrer l’instance 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 ?
Modifier le contenu de la table
t2
, par exemple avec :UPDATE t2 SET id = 0 WHERE id < 1000 ;
Que contient le cache de PostgreSQL ?
Exécuter un checkpoint. Que contient le cache de PostgreSQL ?
Insérer 10 millions de lignes dans la table
t2
avecgenerate_series
. Que se passe-t-il au niveau du répertoirepg_wal
?
Exécuter un checkpoint. Que se passe-t-il au niveau du répertoire
pg_wal
?
Se connecter à la base de données b0 et créer une table
t2
avec une colonneid
de typeinteger
.
$ psql b0
=# CREATE TABLE t2 (id integer);
b0CREATE TABLE
Insérer 500 lignes dans la table
t2
avecgenerate_series
.
=# INSERT INTO t2 SELECT generate_series(1, 500);
b0INSERT 0 500
Pour réinitialiser les statistiques de
t2
:
- utiliser la fonction
pg_stat_reset_single_table_counters
- l’OID en paramètre est dans la table des relations
pg_class
, ou peut être trouvé avec't2'::regclass
Cette fonction attend un OID comme paramètre :
=# \df pg_stat_reset_single_table_counters b0
List of functions
-[ RECORD 1 ]-------+---------------------
Schema | pg_catalog
Name | pg_relation_filepath
Result data type | text
Argument data types | regclass Type | func
L’OID est une colonne présente dans la table
pg_class
:
=# SELECT relname, pg_stat_reset_single_table_counters(oid)
b0FROM pg_class WHERE relname = 't2';
relname | pg_stat_reset_single_table_counters
---------+------------------------------------- t2 |
Il y a cependant un raccourci à connaître :
SELECT pg_stat_reset_single_table_counters('t2'::regclass) ;
Afin de vider le cache, redémarrer l’instance PostgreSQL.
# systemctl restart postgresql-15
Se connecter à la base de données b0 et lire les données de la table
t2
.
=# SELECT * FROM t2;
b0...] [
Récupérer les statistiques IO pour la table
t2
dans la vue systèmepg_statio_user_tables
. Qu’observe-t-on ?
=# \x
b0is on.
Expanded display
=# SELECT * FROM pg_statio_user_tables WHERE relname = 't2' ; b0
-[ 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 |
3 blocs ont été lus en dehors du cache de PostgreSQL (colonne
heap_blks_read
).
Lire de nouveau les données de la table
t2
et consulter ses statistiques. Qu’observe-t-on ?
=# SELECT * FROM t2;
b0...]
[=# SELECT * FROM pg_statio_user_tables WHERE relname = 't2'; b0
-[ 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 ?
=# SELECT * FROM t2;
b0...]
[=# SELECT * FROM pg_statio_user_tables WHERE relname = 't2'; b0
-[ 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.
Ouvrir un premier terminal et laisser défiler le fichier de traces.
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 :
=# SET client_min_messages TO log;
postgresSET
=# SET log_temp_files TO 0;
postgresSET
Les paramètres log_temp_files
et
client_min_messages
peuvent aussi être mis en place une
fois pour toutes dans postgresql.conf
(recharger la
configuration). En fait, c’est généralement conseillé.
Insérer un million de lignes dans la table
t2
avecgenerate_series
.
=# INSERT INTO t2 SELECT generate_series(1, 1000000); b0
INSERT 0 1000000
Activer le chronométrage dans la session (
\timing on
). Lire les données de la tablet2
en triant par la colonneid
Qu’observe-t-on ?
=# \timing on
b0=# SELECT * FROM t2 ORDER BY id; b0
LOG: temporary file: path "base/pgsql_tmp/pgsql_tmp1197.0", size 14032896
id
---------
1
1
2
2
3
[...] Time: 436.308 ms
Le message LOG
apparaît aussi dans la trace, et en
général il se trouvera là.
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).
=# SET work_mem TO '100MB';
b0SET
Lire de nouveau les données de la table
t2
en triant par la colonneid
. Qu’observe-t-on ?
=# SELECT * FROM t2 ORDER BY id; b0
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.
Se connecter à la base de données
b1
. Installer l’extensionpg_buffercache
.
=# CREATE EXTENSION pg_buffercache;
b1CREATE EXTENSION
Créer une table
t2
avec une colonneid
de typeinteger
.
=# CREATE TABLE t2 (id integer);
b1CREATE TABLE
Insérer un million de lignes dans la table
t2
avecgenerate_series
.
=# INSERT INTO t2 SELECT generate_series(1, 1000000);
b1INSERT 0 1000000
Pour vider le cache de PostgreSQL, redémarrer l’instance.
# systemctl restart postgresql-15
Pour vider le cache du système d’exploitation, sous root :
# sync && echo 3 > /proc/sys/vm/drop_caches
Se connecter à la base de données b1. En utilisant l’extension
pg_buffercache
, que contient le cache de PostgreSQL ? (Compter les blocs pour chaque table ; au besoin s’inspirer de la requête du cours.)
=# SELECT relfilenode, count(*)
b1FROM pg_buffercache
GROUP BY 1
ORDER BY 2 DESC
LIMIT 10;
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 ?
=# \timing on
b1is on.
Timing
=# SELECT * FROM t2; b1
id
---------
1
2
3
4
5
[...] Time: 277.800 ms
=# SELECT relfilenode, count(*) FROM pg_buffercache
b1GROUP BY 1 ORDER BY 2 DESC LIMIT 10 ;
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 :
=# SELECT pg_size_pretty(pg_table_size('t2')); b1
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 ?
=# SELECT * FROM t2; b1
id
---------
[...] Time: 184.529 ms
=# SELECT relfilenode, count(*) FROM pg_buffercache
b1GROUP BY 1 ORDER BY 2 DESC LIMIT 10 ;
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.
Configurer la valeur du paramètre
shared_buffers
à un quart de la RAM.
Pour cela, il faut ouvrir le fichier de configuration
postgresql.conf
et modifier la valeur du paramètre
shared_buffers
à un quart de la mémoire. Par exemple :
shared_buffers = 2GB
Redémarrer l’instance PostgreSQL.
# systemctl restart postgresql-15
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 ?
=# \timing on
b1=# SELECT * FROM t2; b1
id
---------
1
[...] Time: 340.444 ms
=# SELECT relfilenode, count(*) FROM pg_buffercache
b1GROUP BY 1 ORDER BY 2 DESC LIMIT 10 ;
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écution.
PostgreSQL est optimisé principalement pour une utilisation multiutilisateur. Dans ce cadre, il faut pouvoir exécuter plusieurs requêtes en même temps. Une requête ne doit donc pas monopoliser tout le cache, juste une partie. 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 ;
Que contient le cache de PostgreSQL ?
=# UPDATE t2 SET id=0 WHERE id < 1000;
b1UPDATE 999
=# SELECT
b1
relname,
isdirty,count(bufferid) AS blocs,
count(bufferid) * current_setting ('block_size')::int) AS taille
pg_size_pretty(FROM pg_buffercache b
INNER JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE relname NOT LIKE 'pg\_%'
GROUP BY
relname,
isdirtyORDER BY 1, 2 ;
relname | isdirty | blocs | taille
---------+---------+-------+--------
t2 | f | 4419 | 35 MB t2 | t | 15 | 120 kB
15 blocs ont été modifiés (isdirty
est à
true
), le reste n’a pas bougé.
Exécuter un checkpoint. Que contient le cache de PostgreSQL ?
=# CHECKPOINT;
b1CHECKPOINT
=# SELECT
b1
relname,
isdirty,count(bufferid) AS blocs,
count(bufferid) * current_setting ('block_size')::int) AS taille
pg_size_pretty(FROM pg_buffercache b
INNER JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE relname NOT LIKE 'pg\_%'
GROUP BY
relname,
isdirtyORDER BY 1, 2 ;
relname | isdirty | blocs | taille
---------+---------+-------+-------- t2 | f | 4434 | 35 MB
Les blocs dirty ont tous été écrits sur le disque et sont devenus « propres ».
Insérer 10 millions de lignes dans la table
t2
avecgenerate_series
. Que se passe-t-il au niveau du répertoirepg_wal
?
=# INSERT INTO t2 SELECT generate_series(1, 10000000);
b1INSERT 0 10000000
$ 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
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000059
drwx------ 2 postgres postgres 6 Apr 16 15:01 archive_status
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
?
=# CHECKPOINT;
b1CHECKPOINT
$ 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 recyclés : renommés, il sont prêts à être remplis à nouveau. Noter que leur date de création n’a pas été mise à jour !
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.
MVCC est un sigle signifiant MultiVersion Concurrency Control, ou « contrôle de concurrence multiversion ».
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 enregistrement. Mais celui-ci ne devra pas être visible des autres transactions tant que le travail de modification 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. Modifier 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.
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 verrous sont peu nombreuses, la pénalité de verrouillage à payer étant peu coûteuse.
Elle a par contre des inconvénients :
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 transaction ;C’est l’implémentation d’Oracle, par exemple. Un enregistrement, quand il doit être modifié, est recopié 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 :
Elle a aussi des défauts :
SNAPSHOT TOO OLD
sous Oracle, par exemple) ;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 modification 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 multiples :
Cette implémentation a quelques défauts :
Chaque transaction, en plus d’être atomique, s’exécute séparément des autres. Le niveau de séparation 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 PostgreSQL.
Ce niveau d’isolation n’est disponible que pour les SGBD non-MVCC. Il est très dangereux : il est possible 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 instantané
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 :
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.
Cependant, 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
Il faut donc que l’application soit capable de la rejouer au besoin.
PostgreSQL fournit un mode d’isolation appelé
SERIALIZABLE
:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
…COMMIT / ROLLBACK ;
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 rejouer
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érialisation 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 PostgreSQL, et la documentation officielle 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 :
READ ONLY
dès que
possible, voire en SERIALIZABLE READ ONLY DEFERRABLE
(au
risque d’un délai au démarrage) ;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 informations 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 :
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
FROM deuxblocs
# SELECT ctid, i,j WHERE ctid in ( '(1, 1)', '(0, 226)', '(1, 1)', '(1, 226)', '(1, 227)', '(2, 0)' );
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
!
La documentation officielle contient évidemment tous les détails.
Pour observer le contenu d’un bloc, à titre pédagogique, vous pouvez utiliser l’extension pageinspect.
Table initiale :
xmin | xmax | Nom | Solde |
---|---|---|---|
100 | M. Durand | 1500 | |
100 | Mme Martin | 2200 |
PostgreSQL stocke des informations de visibilité dans chaque version d’enregistrement.
xmin
: l’identifiant de la transaction créant cette
version.xmax
: l’identifiant de la transaction invalidant cette
version.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.
xmin | xmax | Nom | Solde |
---|---|---|---|
100 | 150 | M. Durand | 1500 |
100 | Mme Martin | 2200 | |
150 | M. Durand | 1300 |
Nous décidons d’enregistrer un virement de 200 € du compte de M. Durand vers celui de Mme Martin. 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
effectuerons :
UPDATE soldes SET solde = solde - 200 WHERE nom = 'M. Durand';
xmin | xmax | Nom | Solde |
---|---|---|---|
100 | 150 | M. Durand | 1500 |
100 | 150 | Mme Martin | 2200 |
150 | M. Durand | 1300 | |
150 | Mme Martin | 2400 |
Puis nous effectuerons :
UPDATE soldes SET solde = solde + 200 WHERE nom = 'Mme Martin';
Nous avons maintenant deux versions de chaque enregistrement. Notre session ne voit bien sûr plus que les nouvelles versions de ces enregistrements, sauf si elle décidait d’annuler la transaction, auquel cas elle reverrait les anciennes données.
Pour une autre session, la version visible de ces enregistrements dépend de plusieurs critères :
Dans le cas le plus simple, 150 ayant été validée, une transaction
160 ne verra pas les premières versions : xmax
valant 150,
ces enregistrements ne sont pas visibles. Elle verra les secondes
versions, puisque xmin
= 150, et pas de
xmax
.
xmin | xmax | Nom | Solde |
---|---|---|---|
100 | 150 | M. Durand | 1500 |
100 | 150 | Mme Martin | 2200 |
150 | M. Durand | 1300 | |
150 | Mme Martin | 2400 |
xmax
dans la version courante de la
ligne ;COMMIT
ou le ROLLBACK
consiste à écrire
le statut de la transaction dans le CLOG (Commit Log), une zone
mémoire dédiée au statut des transactions. Le CLOG permet à PostgreSQL de savoir si les numéros de transaction
portés par une ligne sont validés ou non. Le CLOG est stocké 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 :
TRANSACTION_STATUS_IN_PROGRESS
signifie que la
transaction est en cours, c’est l’état initial ;TRANSACTION_STATUS_COMMITTED
signifie que la la
transaction a été validée ;TRANSACTION_STATUS_ABORTED
signifie que la transaction
a été annulée ;TRANSACTION_STATUS_SUB_COMMITTED
signifie que la
transaction comporte des sous-transactions, afin de valider l’ensemble
des sous-transactions de façon atomique.Nous avons donc un million d’états de transactions par fichier de 256 ko.
Ainsi, 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. Les lignes insérées par cette
transaction seront simplement ignorées, de même que des versions de
lignes créées par un UPDATE
. Les versions de lignes
marquées comme périmées par ce même UPDATE
dans la
transaction resteront visibles.
Toute modification dans le 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/
).
Pour éviter de se référer au CLOG trop souvent, chaque ligne porte
des hint bits contenant, entre autres, le statut des numéros de
transaction portés par la ligne. Mais ce n’est pas la requête qui a
modifié les lignes qui les renseigne. La première requête qui lit une
ligne après une modification, même un SELECT
, lit le CLOG
et met à jour ses hint bits, ce qui occasionne de nouvelles
écritures. (Et génèrent même de nouveaux journaux si
wal_log_hints
est on
ou si les sommes de
contrôle sont activées, ce qui est recommandé.)
Reprenons les avantages du MVCC tel qu’implémenté par PostgreSQL :
xmax
correspondant à une transaction en cours ;(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é étrangè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 avantages cités précédemment sont obtenus au prix de concessions. Bien sûr, divers mécanismes tentent de mitiger ces soucis.
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.
Les tables sont forcément plus volumineuses que dans l’implémentation par undo, pour deux raisons :
Ces enregistrements sont recyclés à chaque passage de
VACUUM
.
Toute écriture a tendance à en déclencher d’autres. D’abord, toute modification est écrite dans les journaux de transaction, comme le fait tout moteur de base de données relationnelle fiable, et éventuellement archivée et/ou répliquée.
Des suppressions, insertions ou mises à jour, même suivies d’un
ROLLBACK
, écrivent toujours dans les fichiers de données
eux-mêmes. (Ce sont les numéros de transaction et le CLOG qui définiront
la visibilité des lignes.) Le VACUUM
nettoie les lignes, et
cela genère aussi des écritures et des journaux. Nous verrons également
le coût du problème de wraparound. La mise à jour des hints
bits, même lors d’un SELECT
, génère de nouvelles
écritures voire des journaux.
Comme les index pointent vers un bloc et une ligne, les index sont également souvent à modifier suite à une écriture (et cela est aussi journalisé).
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. Il existe également des
mécanismes de nettoyage des index quand une requête rencontre des
tuples périmés.
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é sur chaque ligne 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 transactions. Sur les machines actuelles, avec une activité soutenue, cela peut être atteint relativement rapidement.
En fait, ce compteur est cyclique. Cela peut se voir ainsi :
SELECT pg_current_xact_id(), *
FROM pg_control_checkpoint()\gx
-[ RECORD 1 ]--------+-------------------------
pg_current_xact_id | 10549379902
checkpoint_lsn | 62/B902F128
redo_lsn | 62/B902F0F0
redo_wal_file | 0000000100000062000000B9
timeline_id | 1
prev_timeline_id | 1
full_page_writes | t
next_xid | 2:1959445310
next_oid | 24614
next_multixact_id | 1
next_multi_offset | 0
oldest_xid | 1809610092
oldest_xid_dbid | 16384
oldest_active_xid | 1959445310
oldest_multi_xid | 1
oldest_multi_dbid | 13431
oldest_commit_ts_xid | 0
newest_commit_ts_xid | 0 checkpoint_time | 2023-12-29 16:39:25+01
pg_current_xact_id()
renvoie le numéro de transaction en
cours, soit 10 549 379 902 (sur 64 bits). Le premier chiffre de la ligne
next_xid
indique qu’il y a déjà eu deux « époques », et le
numéro de transaction sur 32 bits dans ce troisième cycle est
1 959 445 310. On vérifie que 10 549 379 902 = 1 959 445 310 + 2×2³².
Le nombre d’époques n’est enregistré ni dans les tables ni dans les lignes. Pour savoir quelles transactions sont visibles depuis la transaction en cours (ou exactement depuis le « snapshot » de l’état de la base associé), PostgreSQL considère que les 2 milliards de numéros de transactions supérieurs à celle en cours correspondent à des transactions futures, et les 2 milliards de numéros inférieurs à des transactions dans le passé.
Si on se contentait de boucler, une fois dépassé le numéro de transaction 2³¹-1 (environ 2 milliards), de nombreux enregistrements deviendraient invisibles, car validés par des transactions à présent situées dans le futur. Il y a donc besoin d’un mécanisme supplémentaire.
PostgreSQL se débrouille afin de ne jamais laisser dans les tables des numéros de transactions visibles situées dans ce qui serait le futur de la transaction actuelle.
En temps normal, les numéros de transaction visibles se situent tous dans des valeurs un peu inférieures à la transaction en cours. Les lignes plus vieilles sont « gelées », c’est-à-dire marquées comme assez anciennes pour qu’on ignore le numéro de transaction. (Cela implique bien sûr de réécrire la ligne sur le disque.) Il n’y a alors plus de risque de présence de numéros de transaction qui serait abusivement dans le futur de la transaction actuelle. Les numéros de transaction peuvent être réutilisés sans risque de mélange avec ceux issus du cycle précédent.
Ce cycle des numéros de transactions peut se poursuivre indéfiniment.
Ces recyclages sont visibles dans
pg_control_checkpoint()
, comme vu plus haut. La même
fonction renvoie oldest_xid
, qui est le numéro de la
transaction (32 bits) la plus ancienne dans l’instance, qui est par
définition celui de la base la plus vieille, ou plutôt de la ligne la
plus vieille dans la base :
SELECT datname, datfrozenxid, age (datfrozenxid)
FROM pg_database ORDER BY 3 DESC ;
datname | datfrozenxid | age -----------+--------------+-----------
1809610092 | 149835222
pgbench | 1957896953 | 1548361
template0 | 1959012415 | 432899
template1 | 1959445305 | 9 postgres |
La ligne la plus ancienne dans la base pgbench a moins de 150 millions de transactions d’âge, il n’y a pas de confusion possible.
Un peu plus de 50 millions de transactions plus tard, la même requête renvoie :
datname | datfrozenxid | age
-----------+--------------+----------
template0 | 1957896953 | 52338522
template1 | 1959012415 | 51223060
postgres | 1959445305 | 50790170 pgbench | 1959625471 | 50610004
Les lignes les plus anciennes de pgbench
semblent avoir
disparu.
Concrètement, l’opération de « gel » des lignes qui possèdent des
identifiants de transaction suffisamment anciens est 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. Les écritures générées sont très variables,
et parfois gênantes.
Si cela ne suffit pas, le moteur déclenche automatiquement un
VACUUM FREEZE
sur une table avec des lignes trop âgées, et
ce, même si autovacuum est désactivé.
Ces opérations suffisent dans l’immense majorité des cas. Dans le cas
contraire, l’autovacuum
devient de plus en plus agressif.
Si 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 transaction 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 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 autovacuum. La
documentation officielle contient aussi un paragraphe sur ce
sujet.
Les fonctionnalités que nous allons décrire visent à mitiger les inconvénients du MVCC de PostgreSQL.
Le mécanisme des Heap-Only Tuples (HOT) permet de stocker,
sous condition, plusieurs versions du même enregistrement dans le même
bloc, avec une forme de chaînage entre plusieurs versions d’une ligne
dans le bloc. Ceci permet au fur et à mesure des mises à jour de
supprimer automatiquement les anciennes versions, sans attendre
VACUUM
. Cela permet aussi de ne pas toucher aux index, qui
pointent donc grâce à cela sur plusieurs versions du même
enregistrement. Il y a donc un gain en écriture comme en utilisation du
cache. Les conditions sont les suivantes :
Afin que cette derniè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
officielle). Par défaut, le fillfactor
vaut 100% pour
économiser la place. Sacrifier un peu d’espace en le baissant peut être
rentable car le mécanisme HOT peut réduire la fragmentation.
Le nombre de mises à jour HOT sur une table peut être suivi en
comparant les champs n_tup_hot_upd
(décompte des mises à
jour HOT) et n_tup_upd
(toutes les mises à jour) dans la
vue pg_stat_user_tables.
Documentation officielle : Heap-Only Tuples (HOT)
Chaque table possède une Free Space Map avec une liste des
espaces libres de chaque bloc, organisée en arbre. PostgreSQL l’utilise
pour savoir où insérer de nouvelles lignes ou versions de lignes. Elle
est stockée dans les fichiers *_fsm
associés à chaque
table.
Documentation officielle : Carte des espaces libres
La Visibility Map permet de savoir si tous les enregistrements d’un bloc sont visibles de toutes les sessions en cours. C’est le cas si toutes les transactions de modifications sont terminées, et si les lignes mortes ont été nettoyées. Il n’y a donc plus besoin de consulter les numéros de transaction et la CLOG pour toutes les lignes du bloc. En cas de doute, ou d’enregistrement non visible, le bloc n’est pas marqué comme totalement visible.
Cela accélère grandement le VACUUM
: sa phase 1 ne
parcourt pas toute la table, mais uniquement les enregistrements pour
lesquels la Visibility Map est à faux, car des données
sont potentiellement obsolètes dans le bloc. Les blocs intégralement
visibles peuvent être ignorés. Après nettoyage d’un bloc,
VACUUM
positionne sa réference dans la Visibility
Map à vrai, si toutes les lignes sont bien visibles pour
toutes les sessions.
À l’inverse, un Index Only Scan (parcours d’index seuls)
utilise cette Visibility Map pour savoir si les données de
l’index suffisent ou s’il faut aller vérifier la visibilité de la ligne
dans la table. Dans ce dernier cas, le plan indique des Heap
Fetches, qui dégradent les performances. Un VACUUM
fréquent améliore donc les performances si des Index Only Scans
sont utilisés. (Les Index Scans ou Bitmap Scans ne
sont pas concernés.)
La même Visibility Map sert aussi à noter les bloc
entièrement « gelés » pour accélérer les VACUUM FREEZE
, là
encore pour ne pas avoir à les relire.
Documentation officielle : Carte de visibilité
Le gestionnaire de verrous de PostgreSQL est capable de gérer des verrous sur des tables, sur des enregistrements, 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.
Le verrouillage réel est implémenté comme suit :
D’abord, chaque transaction verrouille son objet « identifiant de transaction » de façon exclusive.
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 transaction » 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.
Ce mécanisme ne nécessite pas un nombre de verrous mémoire proportionnel au nombre d’enregistrements à verrouiller, et simplifie le travail du gestionnaire de verrous, celui-ci ayant un nombre bien plus faible de verrous à gérer.
Le mécanisme exposé ici est évidemment simplifié.
C’est une vue globale à l’instance.
# \d pg_locks
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), transactionid
(transaction), virtualxid
(transaction virtuelle, utilisée
tant qu’une transaction 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 virtualxid
) ;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 tentant 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 structure n’est pas
modifiée sur la durée de la transaction. Dès qu’une modification doit
être faite, la transaction 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 EXCLUSIVE
).
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
Si les transactions préparées (liées aux transactions en
deux phases) sont activées, en montant
max_prepared_transactions
au-delà de 0, le nombre de
verrous devient :
max_locks_per_transaction × (max_connections + max_prepared_transactions)
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 :
max_pred_locks_per_transaction
(64 par défaut) ;max_pred_locks_per_page
(par défaut 2, donc 2 lignes
verrouillées entraînent le verrouillage de tout le bloc, du moins pour
la sérialisation) ;max_pred_locks_per_relation
(voir la documentation
pour les détails).Durées maximales de verrou :
Si une session attend un verrou depuis plus longtemps que
lock_timeout
, la requête est annulée. Il est courant de
poser cela avant un ordre assez intrusif, même bref, sur une base
utilisée. Par exemple, il faut éviter qu’un VACUUM FULL
,
s’il est bloqué par une transaction un peu longue, ne bloque lui-même
toutes les transactions suivantes (phénomène d’empilement des verrous)
:
=# SET lock_timeout TO '3s' ;
postgresSET
=# VACUUM FULL t_grosse_table ;
postgresto lock timeout ERROR: canceling statement due
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 :
=*# DELETE FROM t_centmille_int WHERE i < 50000; postgres
ERROR: deadlock detected
DÉTAIL : Process 453259 waits for ShareLock on transaction 3010357;
blocked by process 453125.
Process 453125 waits for ShareLock on transaction 3010360;
blocked by process 453259.
ASTUCE : See server log for query details. CONTEXTE : while deleting tuple (0,1) in relation "t_centmille_int"
Trace des verrous :
Pour tracer les attentes de verrous un peu longue, il est fortement
conseillé de passer log_lock_waits
à on
(le
défaut est off
).
Le seuil est également défini par deadlock_timeout
(1 s
par défaut) Ainsi, une session toujours en attente de verrou au-delà de
cette durée apparaîtra dans les traces :
LOG: process 457051 still waiting for ShareLock on transaction 35373775
after 1000.121 ms
DETAIL: Process holding the lock: 457061. Wait queue: 457051.
CONTEXT: while deleting tuple (221,55) in relation "t_centmille_int" STATEMENT: DELETE FROM t_centmille_int ;
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 ;
Paramètre | Cible du seuil |
---|---|
lock_timeout |
Attente de verrou |
statement_timeout |
Ordre en cours |
idle_session_timeout |
Session inactive |
idle_in_transaction_session_timeout |
Transaction en cours, inactive |
transaction_timeout
(v17) |
Transaction en cours |
PostgreSQL possède divers seuils pour éviter des ordres, transactions ou sessions trop longues. Le dépassement d’un seuil provoque la fin et l’annulation de l’ordre, transaction ou session. Par défaut, aucun n’est activé.
Ne définissez jamais des paramètres globalement, les ordres de
maintenance ou les batchs de nuit seraient aussi concernés, tout comme
les superutilisateurs. Utilisez un ordre SET
dans la
session, ou ALTER ROLE … IN DATABASE … SET …
.
Il est important que l’application sache gérer l’arrêt de connexion ou l’annulation d’un ordre ou d’une transaction et réagir en conséquence (nouvelle tentative, abandon avec erreur…). Dans le cas contraire, l’application pourrait rester déconnectée, et suivant les cas des données pourraient être perdues.
lock_timeout
permet d’interrompre une requête qui
mettrait trop de temps à acquérir son verrou. Il faut l’activer par
précaution avant une opération lourde (un VACUUM FULL
notamment) pour éviter un « empilement des verrous ». En effet, si
l’ordre ne peut pas s’exécuter immédiatement, il bloque toutes les
autres requêtes sur la table.
statement_timeout
est la durée maximale d’un ordre.
Défini au niveau d’un utilisateur ou d’une session, cet ordre permet
d’éviter que des requêtes aberrantes chargent la base pendant des heures
ou des jours ; ou encore que des requêtes « s’empilent » sur une base
totalement saturée ou avec trop de contention. Autre exemple : les
utilisateurs de supervision et autres utilisateurs non critiques.
À noter : l’extrait suivant d’une sauvegarde par pg_dump
montre que l’outil inhibe ces paramètres par précaution, afin que la
restauration n’échoue pas.
-- Dumped from database version 17.2 (Debian 17.2-1.pgdg120+1)
-- Dumped by pg_dump version 17.2 (Debian 17.2-1.pgdg120+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET transaction_timeout = 0;
…
L’idée peut être reprise pour des scripts batchs, par exemple.
Il est généralement conseillé d’utiliser des sessions assez longues, car la création d’une connexion à PostgreSQL est une opération lourde. Si l’applicatif ne cesse de se connecter/déconnecter, il faudra penser à un pooler (pgBouncer, ou côté applicatif).
Des sessions très longues et inactives ne sont en général pas un
souci. Un pooler garde justement ses sessions longtemps. Mais si les
sessions ne se ferment jamais, le nombre maximal de sessions
(max_connections
) peut être atteint, et de nouvelles
connexions refusées.
De plus, pour les performances, il n’est pas très bon qu’il y ait des milliers de sessions ouvertes, malgré les progrès des versions récentes de PostgreSQL. Enfin, si la consommation mémoire du backend associé à une session est raisonnable, il arrive, exceptionnellement, qu’il se mette à occuper durablement de la mémoire (par exemple en cas d’accès à des milliers de tables et index durant son existence).
La durée maximale des sessions peut être réglée par
idle_session_timeout
, ou au niveau du pooler s’il y en a
un.
Les transactions ouvertes et figées durablement sans faire de
COMMIT
ou ROLLBACK
sont un problème sérieux,
car l’autovacuum ne pourra pas nettoyer de ligne que cette session
pourrait encore voir. Si l’applicatif ne peut être corrigé, ou si des
utilisateurs ouvrent un outil quelconque sans le refermer, une solution
est de définir idle_in_transaction_session_timeout
, par
exemple à une ou plusieurs heures.
Pour les performances, il faut éviter les sessions trop courtes car
le coût de la synchronisation sur disque des journaux lors d’un
COMMIT
est coûteux (sauf table unlogged ou
synchronous_commit
). Typiquement, un traitement de batch ou
de chargement regroupera de nombreuses opérations en une seule
transaction.
D’un autre côté, chaque transaction maintient ses verrous jusqu’à sa fin, et peut donc bloquer d’autres transactions et ralentir toute l’application. En utilisation transactionnelle, il vaut donc mieux que les transactions soient courtes et n’en fassent pas plus que ce qui est dicté par le besoin fonctionnel, et si possible le plus tard possible dans la transaction.
transaction_timeout
(à partir de PostgreSQL 17) peut
alors servir comme sécurité en cas de problème. À noter que cette limite
ne concerne pas les transactions préparées, liées au transactions en
deux phases, et dont la longueur est parfois un souci.
Principe :
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 qui peuvent être
beaucoup plus longs, comme certains textes, mais aussi des types
composés (json
, jsonb
, hstore
),
ou binaires (bytea
), et même numeric
.
Le mécanisme TOAST consiste en deux mécanismes. Le premier compresse le contenu des champs, mais ça ne suffit pas forcément. Le second consiste à découper les champs trop longs et à déporter les morceaux dans une autre table associée à la table principale, table gérée de manière transparente pour l’utilisateur. La combinaison des deux mécanismes est possible. Ces deux mécanismes permettent au final d’éviter qu’un enregistrement ne dépasse la taille d’un bloc.
Politiques de stockage :
Chaque champ possède une propriété de stockage :
CREATE TABLE demotoast (i int, t text, png bytea, j jsonb);
# \d+ unetable
Table « public.demotoast »
Colonne | Type | Col… | NULL-able | Par défaut | Stockage | …
---------+---------+--------+-----------+------------+----------+--
i | integer | | | | plain |
t | text | | | | extended |
png | bytea | | | | extended |
j | jsonb | | | | extended | Méthode d’accès : heap
Les différentes politiques de stockage sont :
PLAIN
: stockage uniquement dans la table, sans
compression (champs numériques ou dates notamment) ;MAIN
: stockage dans la table autant que possible,
éventuellement compressé (politique rarement utilisée) ;EXTERNAL
: stockage au besoin dans la table TOAST, sans
chercher à compresser ;EXTENDED
: stockage au besoin dans la table TOAST,
éventuellement compressé (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,
des PNG, des PDF…) sont déjà compressés et 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 du CPU a été consommé inutilement. Il peut alors être
intéressant de passer de EXTENDED
à EXTERNAL
,
pour un gain de temps parfois non négligeable :
ALTER TABLE demotoast ALTER COLUMN png SET STORAGE EXTERNAL ;
Lors d’un changement de mode de stockage, les données existantes ne sont pas modifiées. Il faut modifier les champs d’une manière ou d’une autre pour forcer le changement.
Performances :
Le découpage et la compression restent des opérations coûteuses. Il reste déconseillé de stocker des données binaires de grande taille dans une base de données ! De plus, cela surcharge les journaux de transaction et les sauvegardes.
Il faut rappeler qu’accéder aux champs « toastés » sans en avoir besoin est une mauvaise pratique :
INSERT INTO demotoast
SELECT i, rpad('0',100000,'0') AS t,
'/tmp/slonik.png'),
pg_read_binary_file (SELECT jsonb_agg(to_json(rq)) FROM (SELECT * FROM pg_stat_all_tables) AS rq)
(FROM generate_series (1,1000) i;
EXPLAIN (ANALYZE,VERBOSE,BUFFERS,SERIALIZE) SELECT i FROM demotoast ;
QUERY PLAN
-----------------------------------------------------------------------
Seq Scan on public.demotoast (cost=0.00..177.00 rows=1000 width=4) (actual time=0.008..0.178 rows=1000 loops=1)
Output: i
Buffers: shared hit=167
Query Identifier: -2257406617252911738
Planning Time: 0.029 ms
Serialization: time=0.073 ms output=9kB format=text Execution Time: 0.321 ms
EXPLAIN (ANALYZE,VERBOSE,BUFFERS,SERIALIZE) SELECT * FROM demotoast ;
QUERY PLAN
-----------------------------------------------------------------------
Seq Scan on public.demotoast (cost=0.00..177.00 rows=1000 width=1196) (actual time=0.012..0.182 rows=1000 loops=1)
Output: i, t, png, j
Buffers: shared hit=167
Query Identifier: 698565989149231362
Planning Time: 0.055 ms
Serialization: time=397.480 ms output=307084kB format=text
Buffers: shared hit=14500 Execution Time: 397.776 ms
L’effet du SELECT *
est ici désastreux, avec 14 500
blocs lus supplémentaires. (Noter que l’effet n’est visible dans
EXPLAIN (ANALYZE)
qu’avec l’option SERIALIZE
,
qui n’est disponible qu’à partir de PostgreSQL 17. Sans cette option,
EXPLAIN(ANALYZE)
affiche un résultat trompeur par rapport
aux requêtes réelles !)
Principe des tables de débordement :
Le débordement dans des tables séparées de la table principale a d’autres intérêts :
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.Les tables pg_toast_XXX :
À chaque table utilisateur se voit associée une table TOAST, et ce
dès sa création si la table possède un champ « toastable ». Les
enregistrements y sont stockés en morceaux (chunks) d’un peu
moins de 2 ko. Tous les champs « toastés » d’une table se retrouvent
dans la même table pg_toast_XXX
, dans un espace de nommage
séparé nommé pg_toast
.
Pour l’utilisateur, les tables TOAST sont totalement transparentes. Un développeur doit juste savoir qu’il n’a pas besoin de déporter des champs texte (ou JSON, ou binaires…) imposants dans une table séparée pour des raisons de volumétrie de la table principale : PostgreSQL le fait déjà, et de manière efficace ! Il est donc souvent inutile de se donner la peine de compresser les données au niveau applicatif juste pour réduire le stockage.
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 = 'demotoast'
OR c.oid = (SELECT reltoastrelid FROM pg_class
WHERE relname = 'demotoast');
-[ RECORD 1 ]-------+-----------------
oid | 1315757
relname | pg_toast_1315754
relnamespace | 99
reltype | 0
reloftype | 0
relowner | 10
relam | 2
relfilenode | 1315757
reltablespace | 0
relpages | 9500
reltuples | 38000
relallvisible | 9500
reltoastrelid | 0
relhasindex | t
…
-[ RECORD 2 ]-------+-----------------
oid | 1315754
relname | demotoast
relnamespace | 2200
reltype | 1315756
reloftype | 0
relowner | 10
relam | 2
relfilenode | 1315754
reltablespace | 0
relpages | 167
reltuples | 1000
relallvisible | 0
reltoastrelid | 1315757
relhasindex | f …
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_1315754
Table TOAST « pg_toast.pg_toast_1315754 »
Colonne | Type | Stockage
------------+---------+----------
chunk_id | oid | plain
chunk_seq | integer | plain
chunk_data | bytea | plain
Table propriétaire : « public.demotoast »
Index :
"pg_toast_1315754_index" PRIMARY KEY, btree (chunk_id, chunk_seq) Méthode d'accès : heap
L’index de la table TOAST est toujours utilisé pour accéder aux chunks.
La volumétrie des différents éléments (partie principale, TOAST, index éventuels) peut se calculer grâce à cette requête dérivée du wiki :
SELECT
oid AS table_oid,
:regnamespace || '.' || relname AS TABLE,
c.relnamespace:
reltoastrelid,:regclass::text AS table_toast,
reltoastrelid:AS nb_lignes_estimees,
reltuples oid)) AS " Table (dont TOAST)",
pg_size_pretty(pg_table_size(c.oid)) AS " Heap",
pg_size_pretty(pg_relation_size(c.AS " Toast",
pg_size_pretty(pg_relation_size(reltoastrelid)) AS " Toast (PK)",
pg_size_pretty(pg_indexes_size(reltoastrelid)) oid)) AS " Index",
pg_size_pretty(pg_indexes_size(c.oid)) AS "Total"
pg_size_pretty(pg_total_relation_size(c.FROM pg_class c
WHERE relkind = 'r'
AND relname = 'demotoast'
\gx
-[ RECORD 1 ]-------+--------------------------
table_oid | 1315754
table | public.demotoast
reltoastrelid | 1315757
table_toast | pg_toast.pg_toast_1315754
nb_lignes_estimees | 1000
Table (dont TOAST) | 76 MB
Heap | 1336 kB
Toast | 74 MB
Toast (PK) | 816 kB
Index | 0 bytes Total | 76 MB
On constate que la plus grande partie de la volumétrie de la table est en fait dans la partie TOAST.
La taille des éventuels 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).
Détails du mécanisme TOAST :
Les détails techniques du mécanisme TOAST sont dans la documentation officielle. 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, même compressé, 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 en utilisant le paramètre de stockage
toast_tuple_target
ainsi :
ALTER TABLE demotoast SET (toast_tuple_target = 3000);
mais cela est rarement utile.
Le mécanisme peut différer entre deux lignes pour un même champ, en fonction de la taille réelle de celui-ci sur la ligne.
Il est possible d’avoir le détail champ par champ :
SELECT i,
AS png_c,
pg_column_compression (png) AS ck_png,
pg_column_toast_chunk_id (png) AS png_size,
pg_column_size(png) AS j_c,
pg_column_compression (j) AS ck_j,
pg_column_toast_chunk_id (j) AS j_size
pg_column_size (j) FROM demotoast
LIMIT 3 ;
i | png_c | ck_png | png_size | j_c | ck_j | j_size
---+-------+---------+----------+------+---------+--------
1 | | 1315760 | 58635 | pglz | 1315759 | 14418
2 | | 1315762 | 58635 | pglz | 1315761 | 14418 3 | | 1315764 | 58635 | pglz | 1315763 | 14418
(La fonction pg_column_compression()
existe depuis
PostgreSQL 14 et indique l’algorithme de compression éventuel (voir plus
bas), et pg_column_toast_chunk_id()
depuis
PostgreSQL 17.)
On constate que le champ png
n’est pas compressé
(puisqu’on l’a défini comme EXTERNAL
), et que le champ de
j
de type jsonb
est compressé avec
l’algorithme indiqué. Chaque champ de chaque ligne a son
chunk_id
propre qui permet de compter le nombre de
chunks
SELECT chunk_id, count(*)
FROM pg_toast.pg_toast_1315754
WHERE chunk_id IN ( 1315760, 1315759 )
GROUP BY 1;
chunk_id | count
----------+-------
1315759 | 8 1315760 | 30
En multipliant par 2 ko, on retrouve à peu près les tailles sur
disque (compressées ou pas) renvoyées par
pg_column_size()
.
<!– TODO : faire une requête pour savoir le % de compressé et de déporté dans chaque champ de chaque table ? ça réclame du dynamique… – > KB ? –>
Administration des tables TOAST :
Les tables TOAST restent forcément dans le même tablespace que la
table principale. Leur maintenance (notamment le nettoyage par
autovacuum
) s’effectue en même temps que la table
principale, comme le montre un VACUUM (VERBOSE)
. Une
variante de l’ordre permet d’ailleurs de passer outre le nettoyage d’une
table TOAST pour accélérer un nettoyage urgent (depuis
PostgreSQL 14) :
off) longs_textes ; VACUUM (VERBOSE, PROCESS_TOAST
Depuis la version 14, il est possible de modifier l’algorithme de
compression. Ceci est défini par le nouveau paramètre
default_toast_compression
dont la valeur par défaut
est :
SHOW default_toast_compression ;
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, un nouvel algorithme lz4
est
disponible (et intégré dans les paquets distribués par le PGDG).
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.
Nous conseillons d’utiliser lz4
pour la compression de
vos TOAST, même si, en toute rigueur, l’arbitrage entre consommations
CPU en écriture ou lecture et place disque ne peut se faire qu’en
testant soigneusement avec les données réelles.
Pour changer l’algorithme, vous pouvez :
postgresql.conf
:default_toast_compression = lz4
CREATE TABLE t1 (
GENERATED ALWAYS AS identity,
c1 bigint
c2 text COMPRESSION lz4 ) ;
ALTER TABLE t1 ALTER COLUMN c2 SET COMPRESSION lz4 ;
lz4
accélère aussi les restaurations logiques comportant
beaucoup de données toastées et compressé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 …
Des lignes d’une même table peuvent être compressées de manières
différentes. En effet, l’utilisation de SET COMPRESSION
sur
une colonne préexistante ne recompresse pas les données de la table
TOAST associée. De plus, 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écompression/recompression, et ce même
si la compression de la cible est différente, même s’il s’agit d’une
autre table. Même un VACUUM FULL
sur la table principale
réécrit la table TOAST, en recopiant simplement les champs compressés
tels quels.
Pour forcer la recompression de toutes les données d’une colonne, il
faut modifier leur contenu. Et en fait, c’est peu intéressant car
l’écart de volumétrie est généralement faible. Noter qu’il existe une
fonction pg_column_compression (nom_champ)
pour consulter la compression d’un champ sur chaque ligne concernée.
Pour aller plus loin :
lz4
.La version en ligne des solutions de ces TP est disponible sur https://dali.bo/m4_solutions.
Créer une nouvelle base de données nommée b2.
Dans la base de données b2, créer une table
t1
avec deux colonnesc1
de type integer etc2
de typetext
.
Insérer 5 lignes dans table
t1
avec des valeurs de(1, 'un')
à(5, 'cinq')
.
Ouvrir une transaction.
Lire les données de la table
t1
.
Depuis une autre session, mettre en majuscules le texte de la troisième ligne de la table
t1
.
Revenir à la première session et lire de nouveau toute la table
t1
.
Fermer la transaction et ouvrir une nouvelle transaction, cette fois-ci en
REPEATABLE READ
.
Lire les données de la table
t1
.
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é ?
Une table de comptes bancaires contient 1000 clients, chacun avec 3 lignes de crédit et 600 € au total :
CREATE TABLE mouvements_comptes
int,
(client numeric NOT NULL DEFAULT 0
mouvement
);CREATE INDEX on mouvements_comptes (client) ;
-- 3 clients, 3 lignes de +100, +200, +300 €
INSERT INTO mouvements_comptes (client, mouvement)
SELECT i, j * 100
FROM generate_series(1, 1000) i
CROSS JOIN generate_series(1, 3) j ;
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 toujours 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 plpgsqlAS $$
DECLARE
numeric ;
solde BEGIN
SELECT round(sum (mouvement), 0)
INTO solde
FROM mouvements_comptes
WHERE client = p_client ;
IF solde < 0 THEN
-- Erreur fatale
EXCEPTION 'Client % - Solde négatif : % !', p_client, solde ;
RAISE ELSE
-- Simple message
'Client % - Solde positif : %', p_client, solde ;
RAISE NOTICE END IF ;
END ;
$$ ;
Au sein de trois transactions successives :
- insérer successivement 3 mouvements de débit de 300 € pour le client 1
- chaque transaction doit finir par
CALL verifie_solde_positif (1);
avant leCOMMIT
- la sécurité fonctionne-t-elle ?
Pour le client 2, ouvrir deux transactions en parallèle :
- dans chacune, procéder à retrait de 500 € ;
- dans chacune, appeler
CALL verifie_solde_positif (2)
;- dans chacune, valider la transaction ;
- la règle du solde positif est-elle respecté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 SERIALIZABLE
).Avant chaque
COMMIT
, consulter la vuepg_locks
pour la tablemouvements_comptes
:SELECT locktype, mode, pid, granted FROM pg_locks WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'mouvements_comptes') ;
Créer une nouvelle table
t2
avec deux colonnes : un entier et un texte.
Insérer 5 lignes dans la table
t2
, de(1, 'un')
à(5, 'cinq')
.
Lire les données de la table
t2
.
Commencer une transaction. Mettre en majuscules le texte de la troisième ligne de la table
t2
.
Lire les données de la table
t2
. Que faut-il remarquer ?
Ouvrir une autre session et lire les données de la table
t2
. Que faut-il observer ?
Afficher
xmin
etxmax
lors de la lecture des données de la tablet2
, dans chaque session.
Récupérer maintenant en plus le
ctid
lors de la lecture des données de la tablet2
, dans chaque session.
Valider la transaction.
Installer l’extension
pageinspect
.
La documentation indique comment décoder le contenu du bloc 0 de la table
t2
:SELECT * FROM heap_page_items(get_raw_page('t2', 0)) ;
Que faut-il remarquer ?
- Lancer
VACUUM
surt2
.- Relancer la requête avec
pageinspect
.- Comment est réorganisé le bloc ?
Pourquoi l’autovacuum n’a-t-il pas nettoyé encore la table ?
Ouvrir une transaction et lire les données de la table
t1
. Ne pas terminer la transaction.
Ouvrir une autre transaction, et tenter de supprimer la table
t1
.
Lister les processus du serveur PostgreSQL. Que faut-il remarquer ?
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 en attente pour la requête bloquée.
Récupérer le nom de l’objet dont le verrou n’est pas récupéré.
Récupérer la liste des verrous sur cet objet. Quel processus a verrouillé la table
t1
?
Retrouver les informations sur la session bloquante.
Retrouver cette information avec la fonction
pg_blocking_pids
.
Détruire la session bloquant le
DROP TABLE
.
Pour créer un verrou, effectuer un
LOCK TABLE
dans une transaction qu’il faudra laisser ouverte.
Construire une vue
pg_show_locks
basée surpg_stat_activity
,pg_locks
,pg_class
qui permette de connaître à tout moment l’état des verrous en cours sur la base : processus, nom de l’utilisateur, âge de la transaction, table verrouillée, type de verrou.
Créer une nouvelle base de données nommée b2.
$ createdb b2
Dans la base de données b2, créer une table
t1
avec deux colonnesc1
de type integer etc2
de typetext
.
CREATE TABLE t1 (c1 integer, c2 text);
CREATE TABLE
Insérer 5 lignes dans table
t1
avec des valeurs de(1, 'un')
à(5, 'cinq')
.
INSERT INTO t1 (c1, c2) VALUES
1, 'un'), (2, 'deux'), (3, 'trois'), (4, 'quatre'), (5, 'cinq'); (
INSERT 0 5
Ouvrir une transaction.
BEGIN;
BEGIN
Lire les données de la table
t1
.
SELECT * FROM t1;
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 t1 SET c2 = upper(c2) WHERE c1 = 3;
UPDATE 1
Revenir à la première session et lire de nouveau toute la table
t1
.
SELECT * FROM t1;
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 ISOLATION LEVEL REPEATABLE READ;
BEGIN
Lire les données de la table
t1
.
SELECT * FROM t1;
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 t1 SET c2 = upper(c2) WHERE c1 = 4;
UPDATE 1
Revenir à la première session et lire de nouveau les données de la table
t1
. Que s’est-il passé ?
SELECT * FROM t1;
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 total :
CREATE TABLE mouvements_comptes
int,
(client numeric NOT NULL DEFAULT 0
mouvement
);CREATE INDEX on mouvements_comptes (client) ;
-- 3 clients, 3 lignes de +100, +200, +300 €
INSERT INTO mouvements_comptes (client, mouvement)
SELECT i, j * 100
FROM generate_series(1, 1000) i
CROSS JOIN generate_series(1, 3) j ;
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 toujours 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 plpgsqlAS $$
DECLARE
numeric ;
solde BEGIN
SELECT round(sum (mouvement), 0)
INTO solde
FROM mouvements_comptes
WHERE client = p_client ;
IF solde < 0 THEN
-- Erreur fatale
EXCEPTION 'Client % - Solde négatif : % !', p_client, solde ;
RAISE ELSE
-- Simple message
'Client % - Solde positif : %', p_client, solde ;
RAISE NOTICE END IF ;
END ;
$$ ;
Au sein de trois transactions successives :
- insérer successivement 3 mouvements de débit de 300 € pour le client 1
- chaque transaction doit finir par
CALL verifie_solde_positif (1);
avant leCOMMIT
- la sécurité fonctionne-t-elle ?
Ce client a bien 600 € :
SELECT * FROM mouvements_comptes WHERE client = 1 ;
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) ;
NOTICE: Client 1 - Solde positif : 300 CALL
COMMIT ;
Lors d’une seconde transaction : les mêmes ordres renvoient :
NOTICE: Client 1 - Solde positif : 0
Avec le troisième débit :
BEGIN ;
INSERT INTO mouvements_comptes(client, mouvement) VALUES (1, -300) ;
CALL verifie_solde_positif (1) ;
ERROR: Client 1 - Solde négatif : -300 ! CONTEXTE : PL/pgSQL function verifie_solde_positif(integer) line 11 at RAISE
La transaction est annulée : il est interdit de retirer plus d’argent qu’il n’y en a.
Pour le client 2, ouvrir deux transactions en parallèle :
- dans chacune, procéder à retrait de 500 € ;
- dans chacune, appeler
CALL verifie_solde_positif (2)
;- dans chacune, valider la transaction ;
- la règle du solde positif est-elle respectée ?
Chaque transaction va donc se dérouler dans une session différente.
Première transaction :
BEGIN ; --session 1
INSERT INTO mouvements_comptes(client, mouvement) VALUES (2, -500) ;
CALL verifie_solde_positif (2) ;
NOTICE: Client 2 - Solde positif : 100
On ne commite pas encore.
Dans la deuxième session, il se passe exactement la même chose :
BEGIN ; --session 2
INSERT INTO mouvements_comptes(client, mouvement) VALUES (2, -500) ;
CALL verifie_solde_positif (2) ;
NOTICE: Client 2 - Solde positif : 100
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
Au final, le solde est négatif, ce qui est pourtant strictement interdit !
CALL verifie_solde_positif (2) ;
ERROR: Client 2 - Solde négatif : -400 ! CONTEXTE : PL/pgSQL function verifie_solde_positif(integer) line 11 at RAISE
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 SERIALIZABLE
).Avant chaque
COMMIT
, consulter la vuepg_locks
pour la tablemouvements_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) ;
NOTICE: Client 3 - Solde positif : 100
On ne committe pas encore.
Deuxième session :
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
INSERT INTO mouvements_comptes(client, mouvement) VALUES (3, -500) ;
CALL verifie_solde_positif (3) ;
NOTICE: Client 3 - Solde positif : 100
Les verrous en place sont les suivants :
SELECT locktype, mode, pid, granted
FROM pg_locks
WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'mouvements_comptes') ;
locktype | mode | pid | granted
----------+------------------+---------+---------
relation | AccessShareLock | 28304 | t
relation | RowExclusiveLock | 28304 | t
relation | AccessShareLock | 28358 | t
relation | RowExclusiveLock | 28358 | t
tuple | SIReadLock | 28304 | t
tuple | SIReadLock | 28358 | t
tuple | SIReadLock | 28358 | t
tuple | SIReadLock | 28304 | t
tuple | SIReadLock | 28358 | t tuple | SIReadLock | 28304 | t
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 pourtant
committé :
SELECT locktype, mode, pid, granted
FROM pg_locks
WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'mouvements_comptes') ;
locktype | mode | pid | granted
----------+------------------+---------+---------
relation | AccessShareLock | 28358 | t
relation | RowExclusiveLock | 28358 | t
tuple | SIReadLock | 28304 | t
tuple | SIReadLock | 28358 | t
tuple | SIReadLock | 28358 | t
tuple | SIReadLock | 28304 | t
tuple | SIReadLock | 28358 | t tuple | SIReadLock | 28304 | t
Tentative de validation de la seconde session :
COMMIT ;
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.
Créer une nouvelle table
t2
avec deux colonnes : un entier et un texte.
CREATE TABLE t2 (i int, t text) ;
CREATE TABLE
Insérer 5 lignes dans la table
t2
, de(1, 'un')
à(5, 'cinq')
.
INSERT INTO t2(i, t)
VALUES
1, 'un'), (2, 'deux'), (3, 'trois'), (4, 'quatre'), (5, 'cinq'); (
INSERT 0 5
Lire les données de la table
t2
.
SELECT * FROM t2;
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
Lire les données de la table
t2
. Que faut-il remarquer ?
SELECT * FROM t2;
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 ?
SELECT * FROM t2;
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
etxmax
lors de la lecture des données de la tablet2
, dans chaque session.
Voici ce que renvoie la session qui a fait la modification :
SELECT xmin, xmax, * FROM t2;
xmin | xmax | i | t
------+------+----+--------
753 | 0 | 1 | un
753 | 0 | 2 | deux
753 | 0 | 4 | quatre
753 | 0 | 5 | cinq 754 | 0 | 3 | TROIS
Et voici ce que renvoie l’autre session :
SELECT xmin, xmax, * FROM t2;
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 tablet2
, dans chaque session.
Voici ce que renvoie la session qui a fait la modification :
SELECT ctid, xmin, xmax, * FROM t2;
ctid | xmin | xmax | i | t
-------+------+------+---+--------
(0,1) | 753 | 0 | 1 | un
(0,2) | 753 | 0 | 2 | deux
(0,4) | 753 | 0 | 4 | quatre
(0,5) | 753 | 0 | 5 | cinq (0,6) | 754 | 0 | 3 | TROIS
Et voici ce que renvoie l’autre session :
SELECT ctid, xmin, xmax, * FROM t2 ;
ctid | xmin | xmax | i | t
-------+------+------+----+--------
(0,1) | 753 | 0 | 1 | un
(0,2) | 753 | 0 | 2 | deux
(0,3) | 753 | 754 | 3 | trois
(0,4) | 753 | 0 | 4 | quatre (0,5) | 753 | 0 | 5 | cinq
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 (enregistrement 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
Installer l’extension
pageinspect
.
CREATE EXTENSION pageinspect ;
CREATE EXTENSION
La documentation indique comment décoder le contenu du bloc 0 de la table
t2
:SELECT * FROM heap_page_items(get_raw_page('t2', 0)) ;
Que faut-il remarquer ?
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)) ;
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid |
----+--------+----------+--------+--------+--------+----------+--------+-
1 | 8160 | 1 | 31 | 753 | 0 | 0 | (0,1) |
2 | 8120 | 1 | 33 | 753 | 0 | 0 | (0,2) |
3 | 8080 | 1 | 34 | 753 | 754 | 0 | (0,6) |
4 | 8040 | 1 | 35 | 753 | 0 | 0 | (0,4) |
5 | 8000 | 1 | 33 | 753 | 0 | 0 | (0,5) |
6 | 7960 | 1 | 34 | 754 | 0 | 0 | (0,6) |
lp | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+-------------+------------+--------+--------+-------+--------------------------
1 | 2 | 2306 | 24 | | | \x0100000007756e
2 | 2 | 2306 | 24 | | | \x020000000b64657578
3 | 16386 | 258 | 24 | | | \x030000000d74726f6973
4 | 2 | 2306 | 24 | | | \x040000000f717561747265
5 | 2 | 2306 | 24 | | | \x050000000b63696e71 6 | 32770 | 10242 | 24 | | | \x030000000d54524f4953
Tous les champs ne sont pas immédiatement compréhensibles, mais on peut lire facilement ceci :
t_ctid
de l’ancienne ligne ne contient plus
(0,3)
mais l’adresse de la nouvelle ligne (soit
(0,6)
).Mais encore :
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é) ;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 PostgreSQL.
- Lancer
VACUUM
surt2
.- Relancer la requête avec
pageinspect
.- Comment est réorganisé le bloc ?
VACUUM (VERBOSE) t2 ;
INFO: vacuuming "postgres.public.t2"
…
tuples: 1 removed, 5 remain, 0 are dead but not yet removable
… VACUUM
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)) ;
lp | lp_off | lp_flags | … | t_ctid |… | t_data
----+--------+----------+---+--------+--+--------------------------
1 | 8160 | 1 | | (0,1) | | \x0100000007756e
2 | 8120 | 1 | | (0,2) | | \x020000000b64657578
3 | 6 | 2 | | | |
4 | 8080 | 1 | | (0,4) | | \x040000000f717561747265
5 | 8040 | 1 | | (0,5) | | \x050000000b63696e71 6 | 8000 | 1 | | (0,6) | | \x030000000d54524f4953
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.
Pourquoi l’autovacuum n’a-t-il pas nettoyé encore la table ?
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.
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
Ouvrir une autre transaction, et tenter de supprimer la table
t1
.
DROP TABLE t1;
La suppression semble bloquée.
Lister les processus du serveur PostgreSQL. Que faut-il remarquer ?
En tant qu’utilisateur système postgres
:
$ ps -o pid,cmd fx
PID CMD
2657 -bash
2693 \_ psql
2431 -bash
2622 \_ psql
2728 \_ ps -o pid,cmd fx
2415 /usr/pgsql-15/bin/postmaster -D /var/lib/pgsql/15/data/
2417 \_ postgres: logger
2419 \_ postgres: checkpointer
2420 \_ postgres: background writer
2421 \_ postgres: walwriter
2422 \_ postgres: autovacuum launcher
2424 \_ postgres: logical replication launcher
2718 \_ postgres: postgres b2 [local] DROP TABLE waiting
2719 \_ postgres: postgres b2 [local] idle in transaction
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
Expanded display is on.
SELECT * FROM pg_stat_activity
WHERE application_name='psql' AND wait_event IS NOT NULL;
-[ 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
Récupérer la liste des verrous en attente pour la requête bloquée.
SELECT * FROM pg_locks WHERE pid = 2718 AND NOT granted;
-[ 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 |
Récupérer le nom de l’objet dont le verrou n’est pas récupéré.
SELECT relname FROM pg_class WHERE oid=16394;
-[ 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
?
SELECT * FROM pg_locks WHERE relation = 16394;
-[ 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.
Retrouver les informations sur la session bloquante.
On retrouve les informations déjà affichées :
SELECT * FROM pg_stat_activity WHERE pid = 2719;
-[ 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
Retrouver cette information avec la fonction
pg_blocking_pids
.
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}
Potentiellement, la session pourrait attendre la levée de plusieurs verrous de différentes sessions.
Détruire la session bloquant le
DROP TABLE
.
À partir de là, il est possible d’annuler l’exécution de l’ordre
bloqué, le DROP TABLE
, avec la fonction
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 transaction
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 ouverte.
LOCK TABLE t1;
Construire une vue
pg_show_locks
basée surpg_stat_activity
,pg_locks
,pg_class
qui permette de connaître à tout moment l’état des verrous en cours sur la base : processus, nom de l’utilisateur, âge de la transaction, table verrouillée, type de verrou.
Le code source de la vue pg_show_locks
est le
suivant :
CREATE VIEW pg_show_locks as
SELECT
a.pid,
usename,- query_start) as age,
(now()
c.relname,mode,
l.
l.grantedFROM
pg_stat_activity aLEFT OUTER JOIN pg_locks l
ON (a.pid = l.pid)
LEFT OUTER JOIN pg_class c
ON (l.relation = c.oid)
WHERE
'^pg_'
c.relname !~ ORDER BY
pid;
VACUUM est la contrepartie de la flexibilité du modèle MVCC. Derrière
les différentes options de VACUUM
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). Des
options permettent de réguler son activité. Son paramétrage n’est donc
pas toujours très clair.
L’outil vacuumdb
est un outil qui génère des ordres
VACUUM
après s’être connecté à PostgreSQL. Il reprend
simplement les options de VACUUM
(voir sa page de
manuel). Il est plutôt destiné aux appels depuis le système ou comme
tâche planifiée. Par rapport à un appel en SQL, vacuumdb
facilite les appels en masse, avec notamment ces options :
--all
pour nettoyer toutes les bases de données les
unes après les autres ;--jobs
pour paralléliser sur plusieurs sessions.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. Au contraire, il faudra parfois le rendre plus
agressif.
L’autovacuum ne gère pas toutes les variantes de VACUUM
(notamment pas le FULL
).
Un ordre VACUUM
vise d’abord à nettoyer les lignes
mortes.
Le traitement VACUUM
se déroule en trois passes. Cette
première passe parcourt la table à nettoyer, à la recherche
d’enregistrements morts. Un enregistrement est mort s’il possède un
xmax
qui correspond à une transaction validée, et que cet
enregistrement n’est plus visible dans l’instantané d’aucune transaction
en cours sur la base. D’autres lignes mortes portent un
xmin
d’une transaction annulée.
L’enregistrement mort ne peut pas être supprimé immédiatement : des
enregistrements d’index pointent vers lui et doivent aussi être
nettoyés. La session effectuant le VACUUM
garde en mémoire
la liste des adresses des enregistrements morts, à hauteur d’une
quantité indiquée par le paramètre maintenance_work_mem
. Si
cet espace est trop petit pour contenir tous les enregistrements morts,
VACUUM
effectue plusieurs séries de ces trois passes.
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).
Pour gagner du temps si le temps presse, cette phase peut être
ignorée de deux manières. La première est de désactiver l’option
INDEX_CLEANUP
:
off) nom_table ; VACUUM (VERBOSE, INDEX_CLEANUP
À 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 vacuum_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.
Cette phase de nettoyage des index peut être parallélisée (clause
PARALLEL
), chaque index pouvant être traité par un CPU.
Maintenant qu’il n’y a plus d’entrée d’index pointant sur les enregistrements morts identifiés, ceux-ci peuvent disparaître. C’est le rôle de cette passe. Quand un enregistrement est supprimé d’un bloc, ce bloc est complètement réorganisé afin de consolider l’espace libre. Cet espace est renseigné dans la Free Space Map (FSM).
Une fois cette passe terminée, si le parcours de la table n’a pas été terminé lors de la passe précédente, le travail reprend où il en était du parcours de la table.
Si les derniers blocs de la table sont vides, ils sont rendus au
système (si le verrou nécessaire peut être obtenu, et si l’option
TRUNCATE
n’est pas off
). C’est le seul cas où
VACUUM
réduit la taille de la table. Les espaces vides (et
réutilisables) au milieu de la table constituent le bloat
(littéralement « boursouflure » ou « gonflement », que l’on peut aussi
traduire par fragmentation).
Les statistiques d’activité sont aussi mises à jour.
VACUUM
Par défaut, VACUUM
procède principalement au nettoyage
des lignes mortes. Pour que cela soit efficace, 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 donné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.
Un ordre FREEZE
n’existe pas en tant que tel.
Préventivement, lors d’un VACUUM
simple, l’autovacuum
procède au gel de certaines des lignes rencontrées. De plus, il lancera
un VACUUM FREEZE
sur une table dont les plus vieilles
transactions dépassent un certain âge. Ce peut être très long, et très
lourd en écritures si une grosse table doit être entièrement gelée d’un
coup. Autrement, l’activité n’est qu’exceptionnellement gênée (voir plus
bas).
L’opération de gel sera détaillée plus loin.
VACUUM FULL
L’ordre VACUUM FULL
permet de reconstruire la table sans
les espaces vides. C’est une opération très lourde, risquant de bloquer
d’autres requêtes à cause du verrou exclusif qu’elle pose (on ne peut
même plus lire la table !), mais il s’agit de la seule option qui permet
de réduire la taille de la table au niveau du système de fichiers de
façon certaine.
Il faut prévoir l’espace disque (la table est reconstruite à côté de
l’ancienne, puis l’ancienne est supprimée). Les index sont reconstruits
au passage. Un VACUUM FULL
gèle agressivement les lignes,
et effectue donc au passage l’équivalent d’un FREEZE
.
L’autovacuum ne lancera jamais un VACUUM FULL
!
Il existe aussi un ordre CLUSTER
, qui permet en plus de
trier la table suivant un des index.
PARALLEL :
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 :
PARALLEL 4) matable ; VACUUM (VERBOSE,
INFO: vacuuming "public.matable" INFO: launched 3 parallel vacuum workers for index cleanup (planned: 3)
SKIP_DATABASE_STATS, ONLY_DATABASE_STATS :
En fin d’exécution d’un VACUUM
, même sur une seule
table, le champ pg_database.datfrozenxid
est mis à jour. Il
contient le numéro de la transaction la plus ancienne non encore gélée
dans toute la base de données. Cette opération impose de parcourir
pg_class
pour récupérer le plus ancien des numéros de
transaction de chaque table (relfrozenxid
), Or cette mise à
jour n’est utile que pour l’autovacuum et le VACUUM FREEZE
,
et a rarement un caractère d’urgence.
Depuis la version 16, l’option SKIP_DATABASE_STATS
demande au VACUUM
d’ignorer la mise à jour de l’identifiant
de transaction. Le principe est d’activer cette option pour les
nettoyages en masse. À l’inverse, l’option
ONLY_DATABASE_STATS
demande de ne faire que la mise à jour
du datfrozenxid
, ce qui peut être fait une seule fois en
fin de traitement.
L’outil vacuumdb
procède ainsi automatiquement si le
serveur est en version 16 minimum. Par exemple :
$ vacuumdb --echo --all
SELECT pg_catalog.set_config('search_path', '', false);
vacuumdb : exécution de VACUUM sur la base de données « pgbench »
RESET search_path;
SELECT c.relname, ns.nspname FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace ns ON c.relnamespace OPERATOR(pg_catalog.=) ns.oid
LEFT JOIN pg_catalog.pg_class t ON c.reltoastrelid OPERATOR(pg_catalog.=) t.oid
WHERE c.relkind OPERATOR(pg_catalog.=) ANY (array['r', 'm'])
ORDER BY c.relpages DESC;
SELECT pg_catalog.set_config('search_path', '', false);
VACUUM (SKIP_DATABASE_STATS) public.pgbench_accounts;
VACUUM (SKIP_DATABASE_STATS) pg_catalog.pg_proc;
VACUUM (SKIP_DATABASE_STATS) pg_catalog.pg_attribute;
VACUUM (SKIP_DATABASE_STATS) pg_catalog.pg_description;
VACUUM (SKIP_DATABASE_STATS) pg_catalog.pg_statistic;
…
…
VACUUM (ONLY_DATABASE_STATS);
SELECT pg_catalog.set_config('search_path', '', false);
vacuumdb : exécution de VACUUM sur la base de données « postgres »
…
… VACUUM (ONLY_DATABASE_STATS);
BUFFER_USAGE_LIMIT :
Cette option apparue en version 16 permet d’augmenter la taille de la
mémoire partagée que peuvent utiliser VACUUM
, et
ANALYZE
. Par défaut, cet espace nommé buffer ring
n’est que de 256 ko de mémoire partagée, une valeur assez basse, cela
pour protéger le cache (shared buffers). Si cette mémoire ne
suffit pas, PostgreSQL doit recycler certains de ces buffers, d’où une
écriture possible de journaux sur disque, avec un ralentissement à la
clé.
Monter la taille du buffer ring avec
BUFFER_USAGE_LIMIT
permet une exécution plus rapide de
VACUUM, et génére moins de journaux. De manière globale, on peut aussi
modifier le paramètre vacuum_buffer_usage_limit
. Les
valeurs vont de 128 ko à 16 Go ; 0 désactive le buffer ring, il
n’y a alors aucune limite en terme d’utilisation du cache. La taille par
défaut n’est que de 2 Mo (et même seulement 256 ko jusque PostgreSQL 16
inclus).
Les machines modernes permettent de monter sans problème ce paramètre
à quelques mégaoctets pour un
gain en vitesse d’écriture très appréciable. Il peut y avoir un
impact négatif sur les autres requêtes si le débit en lecture ou
écriture du VACUUM
augmente trop. La valeur 0 est
envisageable dans le cas d’une plage de maintenance où une purge du
cache de PostgreSQL n’aurait pas de gros impact.
Exemples d’utilisation :
ANALYZE (BUFFER_USAGE_LIMIT '8MB');
'8MB');
VACUUM (BUFFER_USAGE_LIMIT
ANALYZE, BUFFER_USAGE_LIMIT 0) ; # sans limite VACUUM (
vacuumdb --analyze --buffer-usage-limit=8MB --echo -d pgbench
…
VACUUM (SKIP_DATABASE_STATS, ANALYZE, BUFFER_USAGE_LIMIT '8MB')
public.pgbench_accounts; …
Les verrous, SKIP_LOCKED et lock_timeout :
L’option SKIP_LOCKED
permet d’ignorer toute table pour
laquelle la commande VACUUM
ne peut pas obtenir
immédiatement son verrou. Cela évite de bloquer le VACUUM
sur une table, et permet d’é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 :
FULL, SKIP_LOCKED) t_un_million_int, t_cent_mille_int ; VACUUM (
WARNING: skipping vacuum of "t_un_million_int" --- lock not available VACUUM
Une technique un peu différente est de paramétrer dans la session un petit délai avant abandon en cas de problème de verrou. Là encore, cela vise à limiter les empilements de verrou sur une base active. Par contre, comme l’ordre tombe immédiatement en erreur après le délai, il est plus adapté aux ordres ponctuels sur une table.
SET lock_timeout TO '100ms' ;
-- Un LOCK TABLE a été fait dans une autre session
VACUUM (verbose) pgbench_history,pgbench_tellers;
ERROR: canceling statement due to lock timeout
Durée : 1000,373 ms (00:01,000) RESET lock_timeout ;
Ces options sont surtout destinées à désactiver certaines étapes d’un
VACUUM
quand le temps presse vraiment.
INDEX_CLEANUP :
L’option INDEX_CLEANUP
(par défaut à on
jusque PostgreSQL 13 compris) déclenche systématiquement le nettoyage
des index. La commande VACUUM
va 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 :
off) unetable ; VACUUM (VERBOSE, INDEX_CLEANUP
Les index peuvent être nettoyés plus tard lors d’un autre
VACUUM
, ou reconstruits (REINDEX
).
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 décide 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 :
À partir de la version 14, cette option active ou non le traitement
de la partie TOAST associée à la table (parfois la partie la plus
volumineuse d’une table). Elle est activée par défaut. Son utilité est
la même que pour INDEX_CLEANUP
.
off) unetable ; VACUUM (VERBOSE, PROCESS_TOAST
TRUNCATE :
L’option TRUNCATE
(à on
par défaut) permet
de tronquer les derniers blocs vides d’une table.
TRUNCATE 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
).
BUFFER_USAGE_LIMIT :
Le ring buffer du VACUUM
étant par défaut très
réduit, une augmentation, même modeste, accélère les écritures. À la
limite, s’il n’y a pas d’autre activité, on peut lui octroyer tout le
cache de PostgreSQL (valeur 0
).
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 ;
INFO: vacuuming "public.pgbench_accounts_5"
INFO: scanned index "pgbench_accounts_5_pkey" to remove 9999999 row versions
DÉTAIL : CPU: user: 12.16 s, system: 0.87 s, elapsed: 18.15 s
INFO: "pgbench_accounts_5": removed 9999999 row versions in 163935 pages
DÉTAIL : CPU: user: 0.16 s, system: 0.00 s, elapsed: 0.20 s
INFO: index "pgbench_accounts_5_pkey" now contains 100000000 row versions in 301613 pages
DÉTAIL : 9999999 index row versions were removed.
0 index pages have been deleted, 0 are currently reusable.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO: "pgbench_accounts_5": found 10000001 removable,
10000051 nonremovable row versions in 327870 out of 1803279 pages
DÉTAIL : 0 dead row versions cannot be removed yet, oldest xmin: 1071186825
There were 1 unused item identifiers.
Skipped 0 pages due to buffer pins, 1475409 frozen pages.
0 pages are entirely empty.
CPU: user: 13.77 s, system: 0.89 s, elapsed: 19.81 s. VACUUM
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, qui est
généralement un tout petit fichier).
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.
Mélange des options :
Il est possible de mélanger toutes ces options presque à volonté, et de préciser plusieurs tables à nettoyer :
ANALYZE, INDEX_CLEANUP off, TRUNCATE off,
VACUUM (VERBOSE, DISABLE_PAGE_SKIPPING) bigtable, smalltable ;
Un VACUUM
, y compris lancé par l’autovacuum, apparaît
dans pg_stat_activity
et le processus est visible comme
processus système avec top
ou ps
:
$ ps faux
…
postgres 3470724 0.0 0.0 12985308 6544 ? Ss 13:58 0:02 \_ postgres: 13/main: autovacuum launcher
postgres 795432 7.8 0.0 14034140 13424 ? Rs 16:22 0:01 \_ postgres: 13/main: autovacuum worker
pgbench1000p10
…
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 informations.
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é modifié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
last_vacuum | 2020-01-06 18:42:50.237146+01
last_autovacuum | 2020-01-07 14:30:30.200728+01
last_analyze | 2020-01-06 18:42:50.504248+01
last_autoanalyze | 2020-01-07 14:30:39.839482+01
vacuum_count | 1
autovacuum_count | 1
analyze_count | 1 autoanalyze_count | 1
Activer le paramètre log_autovacuum_min_duration
avec
une valeur relativement faible (dépendant des tables visées de la taille
des logs générés), voire le mettre à 0, est également courant et
conseillé.
La vue pg_stat_progress_vacuum
contient une ligne par
VACUUM
(simple ou FREEZE
) en cours
d’exécution :
TABLE pg_stat_progress_vacuum \gx
-[ RECORD 1 ]------+--------------
pid | 2603780
datid | 1308955
datname | grossetable
relid | 1308962
phase | scanning heap
heap_blks_total | 163935
heap_blks_scanned | 3631
heap_blks_vacuumed | 0
index_vacuum_count | 0
max_dead_tuple_bytes | 67108864
dead_tuple_bytes | 0
num_dead_item_ids | 0
indexes_total | 0 indexes_processed | 0
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.
À noter que le suivi du VACUUM
dans les index (les deux
derniers champs) nécessite au moins PostgreSQL 17. C’est souvent la
partie la plus longue d’un VACUUM
.
Dans le cas d’un VACUUM ANALYZE
, la seconde partie de
recueil des statistiques pourra être suivie dans
pg_stat_progress_analyze
:
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
.
Cette vue est utilisable aussi avec l’ordre CLUSTER
, d’où
le nom. Exemple :
SELECT * FROM pg_stat_progress_cluster \gx
-[ 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
Ces vues n’affichent que les opérations en cours, elles n’historisent
rien. Si aucun VACUUM
n’est en cours, elles n’afficheront
rien.
VACUUM
est une opération de maintenance ne pouvant être
effectuée que par :
pg_maintain
ou par un
GRANT MAINTAIN
sur la table. (Ce droit de maintenance
inclut d’autres droits, comme les droits de ANALYZE
,
CLUSTER
, LOCK TABLE
,
REFRESH MATERIALIZED VIEW
et REINDEX
, mais pas
celui de lire les données.)Sans droit de maintenance, le VACUUM
ne fonctionne pas
pour un utilisateur non propriétaire de la table :
$ vacuumdb --username maintenance --table t1
vacuumdb: vacuuming database "b3"
WARNING: permission denied to vacuum "t1", skipping it
Si maintenance devient membre du rôle
pg_maintain
, tout fonctionne :
$ psql --username postgres --command 'GRANT pg_maintain TO maintenance'
GRANT ROLE
$ vacuumdb --username maintenance
vacuumdb: vacuuming database "b3"
Avant la version 17, il est toujours possible d’avoir un groupe propriétaire de l’objet et d’ajouter un rôle de maintenance comme membre de ce groupe. Il aura le droit de lire les données. Mais si jamais un objet est créé par quelqu’un sans transférer la propriété au groupe, le rôle de maintenance ne peut faire d’opération de maintenance sur cet objet.
Le principe est le suivant :
Le démon autovacuum launcher
s’occupe de lancer des
workers régulièrement sur les différentes bases. Ce nouveau
processus inspecte les statistiques sur les tables (vue
pg_stat_all_tables
) : nombres de lignes insérées, modifiées
et supprimées. Quand certains seuils sont dépassés sur un objet, le
worker effectue un VACUUM
, un
ANALYZE
, voire un VACUUM FREEZE
(mais jamais,
rappelons-le, un VACUUM FULL
).
Le nombre de ces workers est limité, afin de ne pas engendrer de charge trop élevée.
autovacuum
(on
par défaut) détermine si
l’autovacuum doit être activé.
Il est fortement conseillé de laisser autovacuum
à
on
!
S’il le faut vraiment, il est possible de désactiver l’autovacuum sur une table précise :
ALTER TABLE nom_table SET (autovacuum_enabled = off);
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é.
autovacuum_max_workers
est le nombre maximum de
workers que l’autovacuum pourra déclencher simultanément,
chacun s’occupant d’une table (ou partition de table). Chaque table ne
peut être traitée simultanément que par un unique worker. La
valeur par défaut (3) est généralement suffisante. Néanmoins, s’il y a
fréquemment trois autovacuum workers travaillant en même temps,
et surtout si cela dure, il peut être nécessaire d’augmenter ce
paramètre. Cela est fréquent quand il y a de nombreuses petites tables.
Noter qu’il faudra sans doute augmenter certaines ressources allouées au
nettoyage (paramètre autovacuum_vacuum_cost_limit
, voir
plus bas), car les workers se les partagent ; sauf
maintenance_work_mem
qui, lui, est une quantité de RAM
utilisable par chaque worker indépendamment.
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 officielle.)
Ces seuils pourront être adaptés table par table.
Pour le VACUUM
, si on considère les enregistrements
morts (supprimés ou anciennes versions de lignes), la condition de
déclenchement est :
nb_enregistrements_morts (pg_stat_all_tables.n_dead_tup) >=
autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × nb_enregs (pg_class.reltuples)
où, par défaut :
autovacuum_vacuum_threshold
vaut 50 lignes ;autovacuum_vacuum_scale_factor
vaut 0,2 soit 20 % de la
table.Donc, par exemple, dans une table d’un million de lignes, modifier
200 050 lignes provoquera le passage 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 durer longtemps et générer beaucoup d’écritures. Il est donc
fréquent de descendre la valeur de
autovacuum_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 autovacuum_vacuum_scale_factor
à 0, mais il
faut alors calculer le nombre de lignes qui déclenchera 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
où, par défaut :
autovacuum_analyze_threshold
vaut 50 lignes ;autovacuum_analyze_scale_factor
vaut 0,1, soit
10 %.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 statistiques 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 impossibles… Pour cette raison, à
partir de la version 13, les insertions sont aussi prises en compte pour
déclencher un VACUUM
.)
En fonction de la tâche exacte, de l’agressivité acceptable ou de l’urgence, plusieurs paramètres peuvent être mis en place.
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.
VACUUM manuel | 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 maintenance, 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 conservateurs.
maintenance_work_mem
est la quantité de mémoire qu’un
processus effectuant une opération de maintenance (c’est-à-dire
n’exécutant pas des requêtes classiques comme SELECT
,
INSERT
, UPDATE
…) est autorisé à allouer pour
sa tâche de maintenance.
Cette mémoire est utilisée lors de la construction d’index ou l’ajout
de clés étrangères. et, dans le contexte de VACUUM
, pour
stocker les adresses des enregistrements pouvant être recyclés. Cette
mémoire est remplie pendant la phase 1 du processus de
VACUUM
, tel qu’expliqué plus haut. Rappelons qu’une adresse
d’enregistrement (tid
, pour tuple id
) a une
taille de 6 octets et est composée du numéro dans la table, et du numéro
d’enregistrement dans le bloc, par exemple (0,1)
,
(3164,98)
ou (5351510,42)
.
Le défaut de 64 Mo est assez faible. Si tous les enregistrements
morts d’une table ne tiennent pas dans
maintenance_work_mem
, VACUUM
est obligé de
faire plusieurs passes de nettoyage, donc plusieurs parcours complets de
chaque index. Une valeur assez élevée de
maintenance_work_mem
est donc conseillée : s’il est déjà
possible de stocker plusieurs dizaines de millions d’enregistrements à
effacer dans 256 Mo, 1 Go peut être utile lors de très grosses
purges.
PostgreSQL 17 améliore beaucoup la consommation mémoire et la vitesse de nettoyage des index, et doit rendre rarissime les nettoyages d’index en plusieurs passes.
PostgreSQL 17 fait aussi disparaître une limite de 1 Go pour le
nettoyage des index : VACUUM
ne sait pas en utiliser plus
jusque PostgreSQL 16. Par contre, l’indexation de grosses tables pourra
toujours bénéficier d’une valeur supérieure à 1 Go.
Rappelons que plusieurs VACUUM
ou
autovacuum
peuvent fonctionner simultanément et consommer
chacun un maintenance_work_mem
! (Voir
autovacuum_max_workers
plus haut.)
autovacuum_work_mem
permet de surcharger
maintenance_work_mem
spécifiquement pour l’autovacuum. Par
défaut les deux sont identiques, et l’on conserve généralement cette
configuration. Au besoin, maintenance_work_mem
peut être
surchargé le temps d’une session.
Principe :
Les paramètres suivant permettent d’éviter qu’un VACUUM
ne gêne les autres sessions en saturant le disque. Le principe est de
provoquer une pause après qu’une certaine activité a été réalisée.
Paramètres de coûts :
Ces trois paramètres « quantifient » l’activité de
VACUUM
, affectant un coût arbitraire à chaque fois qu’une
opération est réalisée :
vacuum_cost_page_hit
: coût d’accès à chaque page
présente dans le cache de PostgreSQL (valeur : 1) ;vacuum_cost_page_miss
: coût d’accès à chaque page hors
de ce cache (valeur : 2 à partir de la PostgreSQL 14, 10
auparavant) ;vacuum_cost_page_dirty
: coût de modification d’une
page, et donc de son écriture (valeur : 20).Il est déconseillé de modifier ces paramètres de coût.
Pause :
Quand le coût cumulé atteint un seuil, l’opération de nettoyage marque une pause. Elle est gouvernée par deux paramètres :
vacuum_cost_limit
: coût cumulé à atteindre avant de
déclencher la 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 !
Mais c’est généralement ce que l’on veut dans un batch ou en urgence, et
il vaut mieux alors être le plus rapide possible.
(Pour les urgences, rappelons que l’option
INDEX_CLEANUP off
ou PROCESS_TOAST off
permettent aussi d’ignorer le nettoyage des index ou des TOAST.)
Paramétrage pour le VACUUM manuel :
Il est conseillé de ne pas toucher au paramétrage par défaut de
vacuum_cost_limit
et vacuum_cost_delay
.
Si on doit lancer un VACUUM
manuellement en limitant son
débit, procéder comme suit dans une session :
-- Reprise pour le VACUUM du paramétrage d'autovacuum
SET vacuum_cost_limit = 200 ;
SET vacuum_cost_delay = '2ms' ;
VACUUM (VERBOSE) matable ;
Avec vacuumdb
, il faudra passer par la variable
d’environnement PGOPTIONS
.
Paramétrage pour l’autovacuum :
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 vacuum_cost_limit
;autovacuum_vacuum_cost_delay
vaut par défaut 2 ms.Un (autovacuum_
)vacuum_cost_limit
à 200
consiste à traiter au plus 200 blocs lus en cache (car
vacuum_cost_page_hit
= 1), soit 1,6 Mo, avant de faire la
pause de 2 ms. Si ces blocs doivent être écrits, on descend en-dessous
de 10 blocs traités avant chaque pause
(vacuum_cost_page_dirty
= 20) avant la pause de 2 ms, d’où
un débit en écriture maximal de l’autovacuum de 40 Mo/s Cela s’observe
aisément par exemple avec iotop
.
Ce débit est partagé équitablement entre les différents workers lancés par l’autovacuum (sauf paramétrage spécifique au niveau de la table).
Pour rendre l’autovacuum
plus rapide, il est préférable
d’augmenter autovacuum_vacuum_cost_limit
au-delà de 200,
plutôt que de réduire autovacuum_vacuum_cost_delay
qui
n’est qu’à 2 ms, pour ne pas monopoliser le disque. (Exception : les
versions antérieures à la 12, car
autovacuum_vacuum_cost_delay
valait alors 20 ms et le débit
en écriture saturait à 4 Mo/s ! La valeur 2 ms tient mieux compte des
disques actuels.).
La prise en compte de la nouvelle valeur de la limite par les
workers en cours sur les tables est automatique à partir de
PostgreSQL 16. Dans les versions précédentes, il faut arrêter les
workers en cours (avec pg_cancel_backend()
) et
attendre que l’autovacuum
les relance. Quand
autovacuum_max_workers
est augmenté, prévoir aussi
d’augmenter la limite. Il faut pouvoir assumer le débit supplémentaire
pour les disques.
Sur le sujet, voir la conférence de Robert Haas à PGconf.EU 2023 à Prague.
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, l’opération
VACUUM FREEZE
« gèle » les vieux enregistrements, afin que
ceux-ci ne se retrouvent pas brusquement dans le futur.
Concrètement, il s’agit de positionner un hint bit dans les
entêtes des lignes concernées, indiquant qu’elle est plus vieille que
tous les numéros de transactions actuellement actifs. (Avant la 9.4, la
colonne système xmin
était simplement remplacée par un
FrozenXid
, ce qui revient au même).
Geler une ligne ancienne implique de réécrire le bloc et donc des écritures dans les journaux de transactions et les fichiers de données. 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.
Le numéro de transaction le plus ancien connu au sein d’une table est
porté par pgclass.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) ;
relname | relfrozenxid | age_Mtrx
---------------------+--------------+----------
pgbench_accounts_7 | 882324041 | 0.00
pgbench_accounts_8 | 882324041 | 0.00
pgbench_accounts_2 | 882324041 | 0.00
pgbench_history | 882324040 | 0.00
pgbench_accounts_5 | 848990708 | 33.33
pgbench_tellers | 832324041 | 50.00
pgbench_accounts_3 | 719860155 | 162.46
pgbench_accounts_9 | 719860155 | 162.46
pgbench_accounts_4 | 719860155 | 162.46
pgbench_accounts_6 | 719860155 | 162.46
pgbench_accounts_1 | 719860155 | 162.46
pgbench_branches | 719860155 | 162.46 pgbench_accounts_10 | 719860155 | 162.46
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 enregistrements. 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 ;"public.pgbench_tellers" INFO: aggressively vacuuming
C’est équivalent à l’option DISABLE_PAGE_SKIPPING
: les
blocs ne contenant que des lignes vivantes seront tout de même
parcourus. Les lignes non gelées qui s’y trouvent et plus vieilles que
vacuum_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 spécifiques
(vacuum_multixact_freeze_min_age
,
vacuum_multixact_freeze_table_age
) qui ont les mêmes
valeurs que leurs homologues.
Enfin, il faut traiter le cas de tables sur lesquelles un
VACUUM
complet ne s’est pas déclenché depuis très
longtemps. L’autovacuum y veille :
autovacuum_freeze_max_age
(par défaut 200 millions de
transactions) est l’âge maximum que doit avoir une table. S’il est
dépassé, un VACUUM
agressif est automatiquement lancé sur
cette table. Il est visible dans pg_stat_activity
avec la
mention caractéristique to prevent wraparound :
autovacuum: VACUUM public.pgbench_accounts (to prevent wraparound)
Ce traitement est lancé même si autovacuum
est désactivé
(c’est-à-dire à off
).
En fait, un VACUUM FREEZE
lancé manuellement équivaut à
un VACUUM
avec les paramètres
vacuum_freeze_table_age
(âge minimal de la table) et
vacuum_freeze_min_age
(âge minimal des lignes pour les
geler) à 0. Il va geler toutes les lignes qui le peuvent, même
« jeunes ».
Charge induite par le gel :
Le gel des lignes peut être très lourd s’il y a beaucoup de lignes à
geler, ou très rapide si l’essentiel du travail a été fait par les
nettoyages précédents. Si la table a déjà été entièrement gelée (parfois
depuis des centaines de millions de transactions), il peut juste s’agir
d’une mise à jour du relfrozenxid
.
Les blocs déjà entièrement gelés sont recensés dans la visibility
map (qui recense aussi les blocs sans ligne morte). ils ne seront
pas reparcourus s’ils ne sont plus modifiés. Cela accélère énormément le
FREEZE
sur les grosses tables (avant PostgreSQL 9.6, il y
avait forcément au moins un parcours complet de la table !) Si le
VACUUM
est interrompu, ce qu’il a déjà gelé n’est pas
perdu, il ne faut donc pas hésiter à l’interrompre au besoin.
L’âge de la table peut dépasser
autovacuum_freeze_max_age
si le nettoyage est laborieux, ce
qui explique la marge par rapport à la limite fatidique des 2 milliards
de transactions.
Quelques problèmes possibles sont évoqués plus bas.
Âge d’une base :
Nous avons vu que l’âge d’une base est en fait l’âge de la table la
plus ancienne, qui se calcule à partir de la colonne
pg_database.datfrozenxid
:
SELECT datname, datfrozenxid, age (datfrozenxid)
FROM pg_database ORDER BY 3 DESC ;
datname | datfrozenxid | age
-----------+--------------+-----------
pgbench | 1809610092 | 149835222
template0 | 1957896953 | 1548361
template1 | 1959012415 | 432899 postgres | 1959445305 | 9
Concrètement, on verra l’âge d’une base de données approcher peu à
peu des 200 millions de transactions, 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
réécrit 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 très grosse
table assez statique est une mauvaise surprise assez fréquente. La table
est réécrite, les entrées-sorties sont chargées, la sauvegarde PITR
enfle, évidemment à un moment où la base est chargée.
Une base chargée en bloc et peu modifiée peut même voir le
FREEZE
se déclencher sur toutes les tables en même temps.
Le délai avant le déclenchement du gel par l’autovacuum dépend de la
consommation des numéros de transaction sur l’instance migrée, et varie
de quelques semaines à des années. Sont concernés tous les imports
massifs et les migrations et restauration logiques
(pg_restore
, réplication logique, migration de bases depuis
d’autres bases de données), mais pas les migrations et restaurations
physiques.
Des ordres VACUUM FREEZE
sur les plus grosses tables à
des moments calmes permettent d’étaler ces écritures. Si ces ordres sont
interrompus, l’essentiel de ce qu’ils auront pu geler n’est plus à
re-geler plus tard.
Résumé :
Que retenir de ce paramétrage complexe ?
VACUUM
gèlera une partie des lignes un peu anciennes
lors de son fonctionnement habituel ;Nous avons vu que le paramétrage de l’autovacuum vise à limiter la charge sur la base. Le nettoyage d’une grosse table peut donc être parfois très long. Ce n’est pas forcément un problème majeur si l’opération arrive à terme dans un délai raisonnable, mais il vaut mieux savoir pourquoi. Il peut y avoir plusieurs causes, qui ne s’excluent pas mutuellement.
Il est fréquent que les grosses tables soient visitées trop peu
souvent. Rappelons que la propriété
autovacuum_vacuum_scale_factor
de chaque table est par
défaut à 20 % : lorsque l’autovacuum se déclenche, il doit donc traiter
une volumétrie importante. Il est conseillé de réduire la valeur de ce
paramètre (ou de jouer sur autovacuum_vacuum_threshold
)
pour un nettoyage plus fréquent.
Le débit en écriture peut être insuffisant (c’est fréquent sur les
anciennes versions), auquel cas, avec des disques corrects, on peut
baisser autovacuum_vacuum_cost_delay
ou monter
autovacuum_vacuum_cost_limit
. Sur le graphique ci-dessus,
issu d’un cas réel, les trois workers semblaient en permanence occupés.
Il risquait donc d’y avoir un retard pour nettoyer certaines tables, ou
calculer des statistiques. La réduction de
autovacuum_vacuum_cost_delay
de 20 à 2 ms a mené à une
réduction drastique de la durée de traitement de chaque worker.
Rappelons qu’un VACUUM
manuel (ou planifié) n’est soumis
à aucun bridage.
Le nombre de workers peut être trop bas, notamment s’il y a de
nombreuses tables. Auquel cas ils semblent tous activés en permanence
(comme ci-dessus). Monter autovacuum_max_workers
au-delà de
3 nécessite d’augmenter le débit autorisé avec les paramètres
ci-dessus.
Pour des grandes tables, le partitionnement permet de paralléliser l’activité de l’autovacuum. Les workers peuvent en effet travailler dans la même base, sur des tables différentes.
Un grand nombre de bases actives peut devenir un frein et augmenter
l’intervalle entre deux nettoyages d’une base, bien que
l’autovacuum launcher
ignore les bases inutilisées.
Exceptionnellement, l’autovacuum peut tomber en erreur (bloc corrompu, index fonctionnel avec une fonction boguée…) et ne jamais finir (surveiller les traces).
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 EXCLUSIVE). 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
autovacuum_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
manuel, 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).
La supervision peut se faire avec
pg_stat_progress_vacuum
et iotop
.
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. (Ces causes gênent aussi un VACUUM
simple,
mais les symptômes sont alors surtout un gonflement des tables
concernées).
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 fonctionnel 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 :
off, TRUNCATE off) ; VACUUM (FREEZE, VERBOSE, INDEX_CLEANUP
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 monoutilisateur de PostgreSQL.
Avec la sonde Nagios check_pgactivity,
et les services max_freeze_age et
oldest_xmin, il est possible 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 !
“Vacuuming is like exercising.
If it hurts, you’re not doing it enough!”(Robert Haas, PGConf.EU 2023, Prague, 13 décembre 2023)
Certains sont frileux à l’idée de passer un VACUUM
. En
général, ce n’est que reculer pour mieux sauter.
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é :
autovacuum_vacuum_cost_delay
), surtout avant la version
12 ; autovacuum_vacuum_cost_limit
) ;vacuum_buffer_usage_limit
(en version
16).Comme le déclenchement d’autovacuum
est très lié à
l’activité, il faut vérifier qu’il passe assez souvent sur les tables
sensibles en surveillant pg_stat_all_tables.last_autovacuum
et last_autoanalyze
.
Si les statistiques peinent à 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) ;
De même, si la fragmentation s’envole, descendre
autovacuum_vacuum_scale_factor
. (On peut préférer utiliser
les variantes en *_threshold
de ces paramètres, et mettre
les *_scale_factor
à 0).
Dans un modèle avec de très nombreuses tables actives, le nombre de workers doit parfois être augmenté.
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). De plus,
avant PostgreSQL 13, pour les tables où il n’y a que des insertions,
l’autovacuum ne lance spontanément que l’ANALYZE
: il
faudra effectuer un VACUUM
explicite pour profiter de
certaines optimisations.
Au final, un vacuumdb
planifié quotidien à un moment
calme est généralement une bonne idée : ce qu’il traite ne sera plus à
nettoyer en journée, et il apporte la garantie qu’aucune table ne sera
négligée. Cela ne dispense pas de contrôler l’activité de
l’autovacuum
sur les grosses tables très sollicitées.
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.
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/m5_solutions.
Créer une table
t3
avec une colonneid
de typeinteger
.
Désactiver l’autovacuum pour la table
t3
.
Insérer un million de lignes dans la table
t3
avec la fonctiongenerate_series
.
Récupérer la taille de la table
t3
.
Supprimer les 500 000 premières lignes de la table
t3
.
Récupérer la taille de la table
t3
. Que faut-il en déduire ?
Exécuter un
VACUUM VERBOSE
sur la tablet3
. Quelle est l’information la plus importante ?
Récupérer la taille de la table
t3
. Que faut-il en déduire ?
Exécuter un
VACUUM FULL VERBOSE
sur la tablet3
.
Récupérer la taille de la table
t3
. Que faut-il en déduire ?
Créer une table
t4
avec une colonneid
de typeinteger
.
Désactiver l’autovacuum pour la table
t4
.
Insérer un million de lignes dans la table
t4
avecgenerate_series
.
Récupérer la taille de la table
t4
.
Supprimer les 500 000 dernières lignes de la table
t4
.
Récupérer la taille de la table
t4
. Que faut-il en déduire ?
Exécuter un
VACUUM
sur la tablet4
.
Récupérer la taille de la table
t4
. Que faut-il en déduire ?
Créer une table
t5
avec deux colonnes :c1
de typeinteger
etc2
de typetext
.
Désactiver l’autovacuum pour la table
t5
.
Insérer un million de lignes dans la table
t5
avecgenerate_series
.
- Installer l’extension
pg_freespacemap
(documentation : https://docs.postgresql.fr/current/pgfreespacemap.html)- Que rapporte la fonction
pg_freespace()
quant à l’espace libre de la tablet5
?
- Modifier exactement 200 000 lignes de la table
t5
.
- Que rapporte
pg_freespace
quant à l’espace libre de la tablet5
?
Exécuter un
VACUUM
sur la tablet5
.
Que rapporte
pg_freespace
quant à l’espace libre de la tablet5
?
Récupérer la taille de la table
t5
.
Exécuter un
VACUUM (FULL, VERBOSE)
sur la tablet5
.
Récupérer la taille de la table
t5
et l’espace libre rapporté parpg_freespacemap
. Que faut-il en déduire ?
Créer une table
t6
avec une colonneid
de typeinteger
.
Insérer un million de lignes dans la table
t6
:INSERT INTO t6(id) SELECT generate_series (1, 1000000) ;
Que contient la vue
pg_stat_user_tables
pour la tablet6
? Il faudra peut-être attendre une minute. (Si la version de PostgreSQL est antérieure à la 13, il faudra lancer unVACUUM t6
.)
Vérifier le nombre de lignes dans
pg_class.reltuples
.
- Modifier 60 000 lignes supplémentaires de la table
t6
avec :UPDATE t6 SET id=1 WHERE id > 940000 ;
- Attendre une minute.
- Que contient la vue
pg_stat_user_tables
pour la tablet6
?- Que faut-il en déduire ?
- Modifier 60 000 lignes supplémentaires de la table
t6
avec :UPDATE t6 SET id=1 WHERE id > 940000 ;
- Attendre une minute.
- Que contient la vue
pg_stat_user_tables
pour la tablet6
?- Que faut-il en déduire ?
Descendre le facteur d’échelle de la table
t6
à 10 % pour leVACUUM
.
- Modifier encore 200 000 autres lignes de la table
t6
:UPDATE t6 SET id=1 WHERE id > 740000 ;
- Attendre une minute.
- Que contient la vue
pg_stat_user_tables
pour la tablet6
?- Que faut-il en déduire ?
Créer une table
t3
avec une colonneid
de typeinteger
.
CREATE TABLE t3(id integer);
CREATE TABLE
Désactiver l’autovacuum pour la table
t3
.
ALTER TABLE t3 SET (autovacuum_enabled = false);
ALTER TABLE
La désactivation de l’autovacuum ici a un but uniquement pédagogique. En production, c’est une très mauvaise idée !
Insérer un million de lignes dans la table
t3
avec la fonctiongenerate_series
.
INSERT INTO t3 SELECT generate_series(1, 1000000);
INSERT 0 1000000
Récupérer la taille de la table
t3
.
SELECT pg_size_pretty(pg_table_size('t3'));
pg_size_pretty
---------------- 35 MB
Supprimer les 500 000 premières lignes de la table
t3
.
DELETE FROM t3 WHERE id <= 500000;
DELETE 500000
Récupérer la taille de la table
t3
. Que faut-il en déduire ?
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 uniquement 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 tablet3
. Quelle est l’information la plus importante ?
VACUUM VERBOSE t3;
INFO: vacuuming "public.t3"
INFO: "t3": removed 500000 row versions in 2213 pages
INFO: "t3": found 500000 removable, 500000 nonremovable row versions
in 4425 out of 4425 pages
DÉTAIL : 0 dead row versions cannot be removed yet, oldest xmin: 3815272
There were 0 unused item pointers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.09 s, system: 0.00 s, elapsed: 0.10 s. VACUUM
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.
Récupérer la taille de la table
t3
. Que faut-il en déduire ?
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.
Exécuter un
VACUUM FULL VERBOSE
sur la tablet3
.
FULL t3; VACUUM
INFO: vacuuming "public.t3"
INFO: "t3": found 0 removable, 500000 nonremovable row versions in 4425 pages
DÉTAIL : 0 dead row versions cannot be removed yet.
CPU: user: 0.10 s, system: 0.01 s, elapsed: 0.21 s. VACUUM
Récupérer la taille de la table
t3
. Que faut-il en déduire ?
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 fragmentation
disparaît.
Créer une table
t4
avec une colonneid
de typeinteger
.
CREATE TABLE t4(id integer);
CREATE TABLE
Désactiver l’autovacuum pour la table
t4
.
ALTER TABLE t4 SET (autovacuum_enabled = false);
ALTER TABLE
Insérer un million de lignes dans la table
t4
avecgenerate_series
.
INSERT INTO t4(id) SELECT generate_series(1, 1000000);
INSERT 0 1000000
Récupérer la taille de la table
t4
.
SELECT pg_size_pretty(pg_table_size('t4'));
pg_size_pretty
---------------- 35 MB
Supprimer les 500 000 dernières lignes de la table
t4
.
DELETE FROM t4 WHERE id > 500000;
DELETE 500000
Récupérer la taille de la table
t4
. Que faut-il en déduire ?
SELECT pg_size_pretty(pg_table_size('t4'));
pg_size_pretty
---------------- 35 MB
Là aussi, nous n’avons rien perdu.
Exécuter un
VACUUM
sur la tablet4
.
VACUUM t4;
VACUUM
Récupérer la taille de la table
t4
. Que faut-il en déduire ?
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 exclusif 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 typeinteger
etc2
de typetext
.
CREATE TABLE t5 (c1 integer, c2 text);
CREATE TABLE
Désactiver l’autovacuum pour la table
t5
.
ALTER TABLE t5 SET (autovacuum_enabled=false);
ALTER TABLE
Insérer un million de lignes dans la table
t5
avecgenerate_series
.
INSERT INTO t5(c1, c2) SELECT i, 'Ligne '||i FROM generate_series(1, 1000000) AS i;
INSERT 0 1000000
- Installer l’extension
pg_freespacemap
(documentation : https://docs.postgresql.fr/current/pgfreespacemap.html)- Que rapporte la fonction
pg_freespace()
quant à l’espace libre de la tablet5
?
CREATE EXTENSION pg_freespacemap;
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.
- Modifier exactement 200 000 lignes de la table
t5
.- Que rapporte
pg_freespace
quant à l’espace libre de la tablet5
?
UPDATE t5 SET c2 = upper(c2) WHERE c1 <= 200000;
UPDATE 200000
SELECT count(blkno), sum(avail) FROM pg_freespace('t5'::regclass);
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 tablet5
.
VACUUM VERBOSE t5;
INFO: vacuuming "public.t5"
INFO: "t5": removed 200000 row versions in 1178 pages
INFO: "t5": found 200000 removable, 1000000 nonremovable row versions
in 7451 out of 7451 pages
DÉTAIL : 0 dead row versions cannot be removed yet, oldest xmin: 8685974
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.11 s, system: 0.03 s, elapsed: 0.33 s.
INFO: vacuuming "pg_toast.pg_toast_4160544"
INFO: index "pg_toast_4160544_index" now contains 0 row versions in 1 pages
DÉTAIL : 0 index row versions were removed.
0 index pages have been deleted, 0 are currently reusable.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO: "pg_toast_4160544": found 0 removable, 0 nonremovable row versions in 0 out of 0 pages
DÉTAIL : 0 dead row versions cannot be removed yet, oldest xmin: 8685974
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. VACUUM
Que rapporte
pg_freespace
quant à l’espace libre de la tablet5
?
SELECT count(blkno), sum(avail) FROM pg_freespace('t5'::regclass);
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.
Récupérer la taille de la table
t5
.
SELECT pg_size_pretty(pg_table_size('t5'));
pg_size_pretty
---------------- 58 MB
Exécuter un
VACUUM (FULL, VERBOSE)
sur la tablet5
.
FULL, VERBOSE) t5; VACUUM (
INFO: vacuuming "public.t5"
INFO: "t5": found 200000 removable, 1000000 nonremovable row versions in 7451 pages
DÉTAIL : 0 dead row versions cannot be removed yet.
CPU: user: 0.49 s, system: 0.19 s, elapsed: 1.46 s. VACUUM
Récupérer la taille de la table
t5
et l’espace libre rapporté parpg_freespacemap
. Que faut-il en déduire ?
SELECT count(blkno),sum(avail)FROM pg_freespace('t5'::regclass);
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.
Créer une table
t6
avec une colonneid
de typeinteger
.
CREATE TABLE t6 (id integer) ;
CREATE TABLE
Insérer un million de lignes dans la table
t6
:INSERT INTO t6(id) SELECT generate_series (1, 1000000) ;
INSERT INTO t6(id) SELECT generate_series (1, 1000000) ;
INSERT 0 1000000
Que contient la vue
pg_stat_user_tables
pour la tablet6
? Il faudra peut-être attendre une minute. (Si la version de PostgreSQL est antérieure à la 13, il faudra lancer unVACUUM t6
.)
\x
Expanded display is on.
SELECT * FROM pg_stat_user_tables WHERE relname = 't6' ;
-[ 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
Les deux dates last_autovacuum
et
last_autoanalyze
sont renseignées. Il faudra peut-être
attendre une minute que l’autovacuum passe sur la table (voire plus sur
une instance chargée par ailleurs).
Le seuil de déclenchement de l’autoanalyze est :
autovacuum_analyze_scale_factor
× nombre de lignes
+ autovacuum_analyze_threshold
soit par défaut 10 % × 0 + 50 = 50. Quand il n’y a que des insertions,
le seuil pour l’autovacuum est :
autovacuum_vacuum_insert_scale_factor
× nombre de
lignes
+ autovacuum_vacuum_insert_threshold
soit 20 % × 0 + 1000 = 1000.
Avec un million de nouvelles lignes, les deux seuils sont franchis.
Avec PostgreSQL 12 ou antérieur, seule la ligne
last_autoanalyze
sera remplie. S’il n’y a que des
insertions, le démon autovacuum ne lance un VACUUM
spontanément qu’à partir de PostgreSQL 13.
Jusqu’en PostgreSQL 12, il faut donc lancer manuellement :
ANALYZE t6 ;
Vérifier le nombre de lignes dans
pg_class.reltuples
.
Vérifions que le nombre de lignes est à jour dans
pg_class
:
SELECT * FROM pg_class WHERE relname = 't6' ;
-[ 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 ;
ce qui est d’ailleurs généralement conseillé après un gros chargement.
- Modifier 60 000 lignes supplémentaires de la table
t6
avec :UPDATE t6 SET id=1 WHERE id > 940000 ;
- Attendre une minute.
- Que contient la vue
pg_stat_user_tables
pour la tablet6
?- Que faut-il en déduire ?
UPDATE t6 SET id = 0 WHERE id <= 150000 ;
UPDATE 150000
Le démon autovacuum ne se déclenche pas instantanément après les écritures, attendons un peu :
SELECT pg_sleep(60) ;
SELECT * FROM pg_stat_user_tables WHERE relname = 't6' ;
-[ 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.
- Modifier 60 000 lignes supplémentaires de la table
t6
avec :UPDATE t6 SET id=1 WHERE id > 940000 ;
- Attendre une minute.
- Que contient la vue
pg_stat_user_tables
pour la tablet6
?- Que faut-il en déduire ?
UPDATE t6 SET id=1 WHERE id > 940000 ;
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 leVACUUM
.
ALTER TABLE t6 SET (autovacuum_vacuum_scale_factor=0.1);
ALTER TABLE
- Modifier encore 200 000 autres lignes de la table
t6
:UPDATE t6 SET id=1 WHERE id > 740000 ;
- Attendre une minute.
- Que contient la vue
pg_stat_user_tables
pour la tablet6
?- Que faut-il en déduire ?
UPDATE t6 SET id=1 WHERE id > 740000 ;
UPDATE 200000
SELECT pg_sleep(60);
SELECT * FROM pg_stat_user_tables WHERE relname='t6' ;
-[ 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 attendre 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 les limitations des versions précédentes (qui ne sont d’ailleurs plus supportées). Les améliorations à chaque nouvelle version de PostgreSQL doivent mener à favoriser une version récente de PostgreSQL.
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.
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 aberrant 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) manipulables 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 particulier, 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éliser 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 complexes, 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 supprimant 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 Nayrat : statistiques plus précises au niveau d’une partition, réduction plus simple de la fragmentation des index, jointure par rapprochement des partitions…
La principale difficulté d’un système de partitionnement consiste à partitionner avec un impact minimal sur la maintenance du code par rapport à une table classique.
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 partitions.
Une partition est une table à part entière, rattachée à une table partitionnée. Sa structure suit automatiquement 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 :
Les clés des différentes partitions ne doivent pas se recouvrir.
Une partition peut elle-même être partitionnée, sur une autre clé, ou la même.
Une table classique peut être attachée à une table partitionnée. Une partition peut être détachée et redevenir une table indépendante normale.
Même une table distante (utilisant foreign data wrapper) peut être définie comme partition, avec des restrictions. Pour les performances, préférer alors PostgreSQL 14 au moins.
Les clés étrangères entre tables partitionnées ne sont pas un problème dans les versions récentes de PostgreSQL.
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 purger 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 homogè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
-> Parallel Index Only Scan using pgbench_accounts_6_pkey on pgbench_accounts_6 pgbench_accounts_5
-> Partial Aggregate
-> Parallel Index Only Scan using pgbench_accounts_7_pkey on pgbench_accounts_7 pgbench_accounts_6
-> Partial Aggregate
-> Parallel Index Only Scan using pgbench_accounts_8_pkey on pgbench_accounts_8 pgbench_accounts_7
-> Partial Aggregate
-> Parallel Index Only Scan using pgbench_accounts_9_pkey on pgbench_accounts_9 pgbench_accounts_8
-> Partial Aggregate -> Parallel Index Only Scan using pgbench_accounts_10_pkey on pgbench_accounts_10 pgbench_accounts_9
Avec la clé, PostgreSQL se restreint à la (ou les) bonne(s) partition(s) :
EXPLAIN (COSTS OFF) SELECT * FROM pgbench_accounts WHERE aid = 599999 ;
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 :
EXPLAIN (COSTS OFF) SELECT * FROM pgbench_accounts_6 WHERE aid = 599999 ;
QUERY PLAN
QUERY PLAN
---------------------------------------------------------------------------------
Index Scan using pgbench_accounts_6_pkey on pgbench_accounts_6 Index Cond: (aid = 599999)
Cela allège la planification, surtout s’il y a beaucoup de partitions.
Exemples :
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, surtout
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 seraient 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.
Là aussi, l’opération est simple et rapide, mais demande un verrou exclusif.
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.
Une limitation sérieuse du partitionnement tient au temps de planification qui augmente très vite avec le nombre de partitions, même petites. En général, on considère qu’il faut éviter de dépasser 100 partitions.
Pour contourner cette limite, il reste possible de manipuler directement les partitions s’il est facile de trouver leur nom.
Avant PostgreSQL 13, de nombreuses limitations rendent l’utilisation moins pratique ou moins performante. Si le partitionnement vous intéresse, il est conseillé d’utiliser une version la plus récente possible.
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.
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).
Ce module fait le tour de la sauvegarde PITR, de la mise en place de
l’archivage (manuelle ou avec l’outil pg_receivewal
) à la
sauvegarde des fichiers (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.
Les journaux de transactions (appelés souvent WAL) sont une garantie contre les pertes de données. Il s’agit d’une technique standard de journalisation appliquée à toutes les transactions, pour garantir l’intégrité (la base reste cohérente quoiqu’il arrive) et la durabilité (ce qui est validé ne sera pas perdu).
Ainsi lors d’une modification de donnée, l’écriture au niveau du disque se fait généralement en deux temps :
COMMIT
;Ainsi en cas de crash :
Les écritures dans le journal se font de façon séquentielle, donc
sans grand déplacement de la tête d’écriture (sur un disque dur
classique, c’est l’opération la plus coûteuse). De plus, comme nous
n’écrivons que dans un seul fichier de transactions, la synchronisation
sur disque, lors d’un COMMIT
, peut se faire sur ce seul
fichier, si le système de fichiers le supporte. Concrètement, ces
journaux sont des fichiers de 16 Mo par défaut, avec des noms comme
0000000100000026000000AF
, dans le répertoire
pg_wal/
de l’instance PostgreSQL (répertoire souvent sur
une partition dédiée).
L’écriture définitive dans les fichiers de données est asynchrone, et généralement lissée, ce qui est meilleur pour les performances qu’une écriture immédiate. Cette opération est appelée « checkpoint » et périodique (5 minutes par défaut, ou plus).
Divers paramètres et fonctionnalités peuvent altérer ce comportement par défaut, par exemple pour des raisons de performances.
À côté de la sécurité et des performances, le mécanisme des journaux de transactions est aussi utilisé pour des fonctionnalités très intéressantes, comme le PITR et la réplication physique, basés sur le rejeu des informations stockées dans ces journaux.
Pour plus d’informations :
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.
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
survenant 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 (base backup). 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 stocké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 sauvegarde 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).
Comme les journaux redéroulent toute l’activité depuis le début de la sauvegarde PITR, ils doivent impérativement être rejoués dans l’ordre de leur écriture (et donc de leur nom), et leur contenu entier est appliqué.
Le rejeu s’arrête quand les journaux à rejouer sont épuisés, ou si le DBA a demandé à s’arrêter à un moment précis.
Il est critique de ne perdre aucun journal. S’il en manque un, ou s’il est inutilisable, la restauration n’ira pas plus loin, les journaux suivants ne seront pas rejoués. La base sera cohérente et utilisable uniquement si l’on a pu réappliquer au moins les journaux générés pendant la sauvegarde (point de cohérence).
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 application.
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 (RPO). 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, car les fichiers des journaux sont de taille fixe, et ils ne
sont archivés que complets. (À l’inverse, une sauvegarde logique avec
pg_dump
entraînera une perte de données bien plus
importante. Si elle est lancée à 3 h et restaurée après un souci à 12 h,
on perd 9 heures de données.)
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.
Le premier inconvénient vient directement du fait qu’on copie les fichiers : la sauvegarde et la restauration concernent l’instance complète. Il est impossible de ne restaurer qu’une seule base ou que quelques tables.
La restauration se fait impérativement sur la même architecture (x86/ARM, 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 ensuite (à cause d’une différence de définition des locales entre deux versions majeures d’une distribution).
Une sauvegarde PITR nécessite en plus un plus grand espace de
stockage. Non seulement il faut sauvegarder les fichiers, y compris les
index et la fragmentation, mais aussi tous les journaux de transactions
sur une certaine période, ce qui peut être volumineux (en tout cas
beaucoup plus que des pg_dump
). La volumétrie des journaux
dépend fortement de l’activité.
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 !
Si un journal est perdu ou corrompu, la restauration ne pourra jamais aller au-delà de ce journal. Le risque augmente avec le nombre de journaux conservés.
Pour réduire le risque de perte d’un journal, et aussi pour accélérer le temps de rejeu, il est conseillé de procéder à des base backups (complet, différentiels… selon l’outil) assez fréquemment.
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éhension 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
le rend immédiatement utilisable.
Il permet de réaliser toute la sauvegarde de l’instance, à 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
avant PostgreSQL 17. À partir de cette version, des sauvegardes
incrémentales faite avec pg_basebackup
peuvent se combiner
avec un outil nommé pg_combinebackup
, mais les
fonctionnalités sont encore un peu sommaire.
Il ne permet pas non plus de continuer à 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 (c’est le cas de barman, notamment).
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 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 équivalent.
Il ne reste plus qu’à :
pg_basebackup
, ici en lui demandant une archive
au format tar
;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/
644320/644320 kB (100%), 1/1 tablespace
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 :
Un slot de réplication temporaire sera créé par défaut pour garantir que le serveur gardera les journaux jusqu’à leur copie intégrale.
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).
Même avec un serveur un peu ancien, il est possible d’utiliser un
pg_basebackup
récent, en installant les outils clients de
la dernière version de PostgreSQL.
L’outil est développé plus en détail dans notre module I4.
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.
La méthode historique, et la plus utilisée toujours, utilise le
processus archiver
. Ce processus fonctionne sur l’instance
sauvegardée et fait partie des processus du serveur PostgreSQL. Seule sa
(bonne) configuration incombe au DBA, notamment le paramètre
archive_command
.
Une autre méthode existe : pg_receivewal
. Cet outil
livré aussi avec PostgreSQL se comporte comme un serveur secondaire,
tournant sur un autre serveur. Il reconstitue les journaux de
transactions à partir du flux de réplication.
Chaque solution a ses avantages et inconvénients.
Dans le cas de l’archivage historique, le serveur PostgreSQL va exécuter une commande, dont le rôle sera de copier les journaux à l’extérieur de son répertoire de travail :
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 système
postgres peut écrire dedans :
# mkdir /mnt/nfs1/archivage
# chown postgres:postgres /mnt/nfs1/archivage
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 réseau (NFS…).
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
(qui
est celui par défaut). Le niveau logical
, nécessaire à la
réplication logique, 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. Avec on
, l’instance n’archive les journaux que
si elle est primaire.) Le changement nécessite un redémarrage.
Enfin, une commande d’archivage doit être définie par le paramètre
archive_command
. archive_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.
(À noter qu’à partir de la version 15, il existe une alternative,
avec l’utilisation du paramètre archive_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_archive.
Notre blog présente un exemple
fonctionnel de module d’archivage utilisant une extension en C pour
compresser les journaux de transactions. En production, il vaudra mieux
utiliser une bibliothèque fournie par un outil PITR reconnu. Cependant,
à notre connaissance (en décembre 2024), aucun outil n’utilise encore
cette fonctionnalité, qui est sans doute plutôt utilisée par des
opérateurs cloud. L’utilisation simultanée de
archive_command
et archive_library
est
déconseillée, et interdite depuis PostgreSQL 16.)
PostgreSQL laisse le soin à l’administrateur de définir la méthode
d’archivage des journaux de transactions suivant son contexte. Si vous
utilisez un outil de sauvegarde, la commande vous sera probablement
fournie. Une simple commande de copie suffit dans la plupart des cas. La
directive archive_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, par exemple
pg_wal/00000001000000A900000065
. Le joker %f
correspond au nom du journal de transactions une fois archivé, par
exemple 00000001000000A900000065
. La commande réellement
exécutée ressemblera donc à ceci :
cp pg_wal/00000001000000A900000065 /mnt/nfs1/archivage/00000001000000A900000065
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 (ordre
sync
).
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 synchronisées sur disque. Et 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 fonctionnement 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 de placer dans archive_command
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, ou de la gestion d’erreur plus complexe. Il peut aussi
être intéressant de tracer l’action de l’archivage, ou de compresser le
journal avant archivage.
Dans vos commandes et scripts, il faut s’assurer d’une 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.
De plus si le code retour de la commande est supérieur à 125, le
processus archiver
redémarrera, et l’erreur ne sera pas
comptabilisée dans la vue pg_stat_archiver
!
Ce cas de figure inclut les erreurs de
type command not found
associées aux codes retours 126 et
127, ou le cas de rsync
, qui renvoie un code retour 255 en
cas d’erreur de syntaxe ou de configuration du ssh.
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 paramétrage de l’outil, droits insuffisants ou expirés, génération de journaux trop rapide…
À titre d’exemple encore, les commandes fournies par pgBackRest ou barman ressemblent à ceci :
# pgBackRest
archive_command='/usr/bin/pgbackrest --stanza=prod archive-push %p'
# 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 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'
Si l’activité en écriture est très réduite en volume, il peut se passer des heures entre deux archivages de journaux. Il est alors conseillé de forcer un archivage périodique, même si le journal n’a pas été rempli complètement, en indiquant un délai maximum entre deux archivages :
archive_timeout = '5min'
(La valeur par défaut, 0, désactive ce comportement.)
Ainsi, la perte de données maximale sera de cette durée.
Comme la taille d’un fichier journal, même incomplet, reste fixe (16 Mo par défaut), la consommation en terme d’espace disque sera plus importante (la compression par l’outil d’archivage peut compenser cela), et le temps de restauration plus long.
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ù il a fallu définir
wal_level = replica
ou archive_mode = on
, il
faudra relancer PostgreSQL.
PostgreSQL archive les journaux impérativement dans l’ordre où ils ont été générés.
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 !
La supervision se fait de quatre manières complémentaires.
Taille :
Si le répertoire pg_wal
commence à grossir fortement,
c’est que PostgreSQL n’arrive plus à recycler ses journaux de
transactions : c’est un indicateur d’une commande d’archivage n’arrivant
pas à faire son travail pour une raison ou une autre. Ce peut être
temporaire si l’archivage est juste lent. Les causes classiques sont un
réseau saturé, une compression des journaux trop lente, ou des écritures
trop intenses. Si l’archivage est complètement bloqué (à cause d’un
disque saturé par exemple), ce répertoire grossira indéfiniment.
Vue pg_stat_archiver :
La vue système pg_stat_archiver
indique les derniers
journaux archivés et les dernières erreurs. Dans l’exemple suivant, il y
a eu un problème pendant quelques secondes, d’où 6 échecs, avant que
l’archivage reprenne :
SELECT * FROM pg_stat_archiver \gx
-[ 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 !
L’ordre SELECT pg_switch_wal()
force un changement de
journal, et donc l’archivage du journal en cours, à condition qu’il y
ait eu une activité minimale. Cette commande est pratique pour
tester.
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_pgactivity se
base sur ce répertoire.
SELECT * FROM pg_ls_dir ('pg_wal/archive_status') ORDER BY 1;
pg_ls_dir
--------------------------------
0000000200000001000000DE.done
0000000200000001000000DF.done
0000000200000001000000E0.done
0000000200000001000000E1.ready
0000000200000001000000E2.ready
0000000200000001000000E3.ready
0000000200000001000000E4.ready
0000000200000001000000E5.ready
0000000200000001000000E6.ready 00000002.history.done
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 transactions. 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 induit 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
synchronous_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 sauvegarde permanente, il est nécessaire
de s’assurer qu’on ne puisse pas perdre des journaux de transactions. 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
déjà lors de sa création
-n, --no-loop ne boucle pas en cas de perte de la connexion
--no-sync n'attend pas que les modifications soient
proprement écrites sur disque
-s, --status-interval=SECS durée entre l'envoi de paquets de statut au
(par défaut 10)
-S, --slot=NOMREP slot de réplication à utiliser
--synchronous vide le journal de transactions immédiatement
après son écriture
-v, --verbose affiche des messages verbeux
-V, --version affiche la version puis quitte
-Z, --compress=METHOD[:DETAIL]
compresse comme indiqué
-?, --help affiche cette aide puis quitte
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)
Rapporter les bogues à <pgsql-bugs@lists.postgresql.org>. Page d'accueil de PostgreSQL : <https://www.postgresql.org/>
pg_receivewal
est utilisé par exemple par l’outil de
sauvegarde PITR barman. Les auteurs de pgBackRest préfèrent utiliser
archive_command
car ils peuvent ainsi mieux paralléliser
des débits élevés.
Le paramètre max_wal_senders
indique le nombre maximum
de connexions de réplication sur le serveur. Logiquement, une valeur de
1 serait suffisante, mais il faut compter sur quelques soucis réseau qui
pourraient faire perdre la connexion à pg_receivewal
sans
que le serveur primaire n’en soit mis au courant, et du fait que
certains autres outils peuvent utiliser la réplication.
max_replication_slots
indique le nombre maximum de slots de
réplication. Pour ces deux paramètres, le défaut est 10 et suffit dans
la plupart des cas.
Si l’on modifie un de ces paramètres, il est nécessaire de redémarrer le serveur PostgreSQL.
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é.
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
.
On peut alors lancer pg_receivewal
:
pg_receivewal -h 192.168.0.1 -U repli_user -D /data/archives -S archivage
Les journaux de transactions sont alors créés en temps réel dans le
répertoire indiqué (ici, /data/archives
) :
-rwx------ 1 postgres postgres 16MB juil. 27 00000001000000000000000E*
-rwx------ 1 postgres postgres 16MB juil. 27 00000001000000000000000F* -rwx------ 1 postgres postgres 16MB juil. 27 000000010000000000000010.partial*
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.
La méthode archiver est la méthode la plus simple à mettre en place.
Elle est gérée intégralement par 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 à un pg_backup_stop
),
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 sauvegarde 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 enregistrements 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 transactions complet.
Une fois l’archivage en place, une sauvegarde à chaud a lieu en trois temps :
La fonction de démarrage s’appelle pg_backup_start()
à
partir de la version 15 mais avait pour nom
pg_start_backup()
auparavant. De la même façon, la fonction
d’arrêt s’appelle pg_backup_stop()
à partir de la version
15, mais pg_stop_backup()
avant.
À cause de ces limites et de différents problèmes, , la sauvegarde exclusive est déclarée obsolète depuis la 9.6, et n’est plus disponible depuis la version 15. Même sur les versions antérieures, il est conseillé d’utiliser dès maintenant des scripts utilisant les sauvegardes concurrentes.
Tout ce qui suit ne concerne plus que la sauvegarde concurrente.
La sauvegarde concurrente peut être lancée plusieurs fois en parallèle. C’est utile pour créer des secondaires alors qu’une sauvegarde physique tourne, par exemple. Elle est nettement plus complexe à gérer par script. Elle peut être exécutée depuis un serveur secondaire, ce qui allège la charge sur le primaire.
Pendant la sauvegarde, l’utilisateur ne verra aucune différence (à part peut-être la conséquence d’I/O saturées pendant la copie). Aucun verrou n’est posé. Lectures, écritures, suppression et création de tables, archivage de journaux et autres opérations continuent comme si de rien n’était.
La description du mécanisme qui suit est essentiellement destinée à la compréhension et l’expérimentation. En production, un script maison reste une possibilité, mais préférez des outils dédiés et fiables : pg_basebackup, pgBackRest…
Les sauvegardes manuelles servent cependant encore quand on veut
utiliser une sauvegarde par snapshot de partition ou de baie, ou avec
rsync
(car pg_basebackup ne sait pas synchroniser vers une
sauvegarde interrompue ou ancienne), et quand les outils conseillés ne
sont pas utilisables ou disponibles sur le système.
L’exécution de pg_backup_start()
peut se faire depuis
n’importe quelle base de données de l’instance.
(Rappelons que pour les versions avant la 15, la fonction s’appelle
pg_start_backup()
. Pour effectuer une sauvegarde
non-exclusive avec ces versions, il faudra positionner un
troisième paramètre à false
.)
Le label (le texte en premier argument) n’a aucune importance pour PostgreSQL (il ne sert qu’à l’administrateur, pour reconnaître le backup).
Le deuxième argument est un booléen qui permet de demander un
checkpoint immédiat, si l’on est pressé et si un pic d’I/O
n’est pas gênant. Sinon il faudra attendre souvent plusieurs minutes
(selon la configuration du déclenchement du prochain checkpoint,
dépendant des paramètres checkpoint_timeout
et
max_wal_size
et de la rapidité d’écriture imposée par
checkpoint_completion_target
).
La session qui exécute la commande pg_backup_start()
doit être la même que celle qui exécutera plus tard
pg_backup_stop()
. Nous verrons que cette dernière fonction
fournira de quoi créer deux fichiers, qui devront être nommés
backup_label
et tablespace_map
. Si la
connexion est interrompue avant pg_backup_stop()
, alors la
sauvegarde doit être considérée comme invalide.
En plus de rester connectés à la base, les scripts qui gèrent la sauvegarde concurrente doivent donc récupérer et conserver les informations renvoyées par la commande de fin de sauvegarde.
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…).
La deuxième étape correspond à la sauvegarde des fichiers. Le choix de l’outil dépend de l’administrateur. Cela n’a aucune incidence au niveau de PostgreSQL.
La sauvegarde doit comprendre aussi les tablespaces si l’instance en dispose.
Snapshot :
Il est possible d’effectuer cette étape de copie des fichiers par snapshot au niveau de la baie, de l’hyperviseur, ou encore de l’OS (LVM, ZFS…).
Un snaphost cohérent, y compris entre les tablespaces, permet
théoriquement de réaliser une sauvegarde en se passant des étapes
pg_backup_start()
et pg_backup_stop()
. La
restauration de ce snapshot équivaudra pour PostgreSQL à un redémarrage
brutal.
Pour une sauvegarde PITR, il faudra cependant toujours encadrer le snapshot des appels aux fonctions de démarrage et d’arrêt ci-dessus, et c’est généralement ce que font les outils comme Veeam ou Tina. L’utilisation d’un tel outil implique de 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, ou un bug de la baie, peut donc emporter, corrompre ou bloquer la sauvegarde en même temps que les données originales. La sécurité de l’instance est donc reportée sur celle de l’infrastructure sous-jacente : il vaut mieux que celle-ci soit répliquée sur plusieurs sites. Une copie parallèle, hors infastructure, des données de manière plus classique reste conseillée pour éviter un désastre total, et pour parer à la malveillance.
Copie manuelle :
La sauvegarde se fait à chaud : il est donc possible que pendant ce temps des fichiers 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 interruption 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é (il existe différents émulateurs des
outils GNU sous Windows). Le plus sûr et simple est sans doute de
renoncer à une copie manuelle des fichiers et d’utiliser
pg_basebackup
.
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 officielle) :
postmaster.pid
, postmaster.opts
,
pg_internal.init
;pg_wal
, ainsi que les sous-répertoires (mais à archiver
séparément !) ;pg_replslot
: les slots de réplication seront au mieux
périmés, au pire gênants sur l’instance restaurée ;pg_dynshmem
, pg_notify
,
pg_serial
, pg_snapshots
,
pg_stat_tmp
et pg_subtrans
ne doivent pas être
copiés (ils contiennent des informations propres à l’instance, ou qui ne
survivent pas à un redémarrage) ;pgsql_tmp
(fichiers temporaires) ;On n’oubliera pas les fichiers de configuration s’ils ne sont pas dans le PGDATA.
La dernière étape correspond à l’exécution de la procédure stockée
SELECT * FROM pg_backup_stop()
.
N’oubliez pas d’exécuter pg_backup_stop()
, de vérifier
qu’il finit avec succès et de récupérer les informations qu’il renvoie
!
Cet oubli trop courant rend vos sauvegardes inutilisables !
PostgreSQL va alors :
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 :
backup_label
;tablespace_map
.SELECT * FROM pg_stop_backup() \gx
NOTICE: all required WAL segments have been archived
-[ RECORD 1 ]---------------------------------------------------------------
lsn | 22/2FE5C788
labelfile | START WAL LOCATION: 22/2B000028 (file 00000001000000220000002B)+
| CHECKPOINT LOCATION: 22/2B000060 +
| BACKUP METHOD: streamed +
| BACKUP FROM: master +
| START TIME: 2019-12-16 13:53:41 CET +
| LABEL: rr +
| START TIMELINE: 1 +
|
spcmapfile | 134141 /tbl/froid +
| 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
START WAL LOCATION: 22/2B000028 (file 00000001000000220000002B)
STOP WAL LOCATION: 22/2FE5C788 (file 00000001000000220000002F)
CHECKPOINT LOCATION: 22/2B000060
BACKUP METHOD: streamed
BACKUP FROM: master
START TIME: 2019-12-16 13:53:41 CET
LABEL: rr
START TIMELINE: 1
STOP TIME: 2019-12-16 13:54:04 CET
STOP TIMELINE: 1
Il faudra créer le fichier tablespace_map
avec le
contenu du champ spcmapfile
:
134141 /tbl/froid
134152 /tbl/quota
… ce qui n’est pas trivial à scripter.
Ces deux fichiers devront être placés dans la sauvegarde, pour être présent d’entrée dans le PGDATA du serveur restauré.
À partir du moment où pg_backup_stop()
rend la main, il
est possible de restaurer la sauvegarde obtenue puis de rejouer les
journaux de transactions suivants en cas de besoin, sur un autre serveur
ou sur ce même serveur.
Tous les journaux archivés avant celui précisé par le champ
START WAL LOCATION
dans le fichier
backup_label
ne sont plus nécessaires pour la récupération
de la sauvegarde du système de fichiers et peuvent donc être supprimés.
Attention, il y a plusieurs compteurs hexadécimaux différents dans le
nom du fichier journal, qui ne sont pas incrémentés de gauche à
droite.
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 interrompue ou à opérer une
sauvegarde différentielle ou incrémentale, du moins avant
PostgreSQL 17.
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.
La vue pg_stat_progress_basebackup
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.
Dans cet exemple, la sauvegarde a fini à 02 h du matin (le moment où
une fonction pg_backup_stop()
est appelée par un outil ou
un script). La sauvegarde des fichiers de données s’est effectuée en
parallèle de l’archivage des journaux, qui continue indéfiniment
ensuite.
À 06 h, le DBA a créé un « point de restauration », ainsi (le nom est arbitraire) :
SELECT pg_create_restore_point ('label');
pg_create_restore_point
------------------------- 26/9B000090
Ces points de restauration sont totalement optionnels, et peuvent être créés avant certaines opérations (par exemple un batch ou une mise en production), ou périodiquement.
Une catastrophe quelconque frappe à 13 h et il faut restaurer.
La ligne de temps de ce schéma correspond aux heures des transactions originales.
Pour restaurer, le DBA copie la sauvegarde de base, modifie la configuration et démarre l’instance qui commence à rejouer les journaux. Elle atteint le point de cohérence (correspondant à la fin de la sauvegarde), et est donc dans l’état correspondant à la fin de la sauvegarde, donc comme à 02 h.
Deux possibilités sont montrées ici :
label
, pour avoir une image de la base à
06 h ;Nous verrons qu’il lui suffira de choisir les bons paramètres (ici
recovery_target_name
ou
recovery_target_time
).
La restauration se déroule en trois, voire quatre étapes suivant qu’elle est effectuée sur le même serveur ou sur un autre serveur. Dans le cas où la restauration a lieu sur le même serveur, les étapes préliminaires suivantes sont à effectuer.
Il faut arrêter PostgreSQL s’il n’est pas arrêté. Cela arrive 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 la 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.
(L’exception est l’utilisation d’outils capable de trouver les
différence entre les fichiers à restaurer et ceux présents, pour gagner
du temps, comme rsync
ou
pgbackrest restore --delta
.) Cela est valable aussi pour
chaque tablespace.
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 fichier 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 supprimer. 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
archivées.
Quand PostgreSQL démarre après avoir subi un arrêt brutal, il ne
restaure que les journaux en place dans pg_wal
, puis il
s’ouvre en écriture. Pour une restauration, il faut lui indiquer qu’il
doit aller demander les journaux quelque part, et les rejouer tous
jusqu’à épuisement, avant de s’ouvrir.
Pour cela, il suffit de créer un fichier vide
recovery.signal
dans le répertoire des données.
Pour la récupération des journaux, le paramètre essentiel est
restore_command
. Il contient une commande symétrique des
paramètres archive_command
(ou
archive_library
) pour l’archivage. Il s’agit d’une commande
copiant un journal dans le pg_wal
. Cette commande est
souvent fournie par l’outil de sauvegarde PITR s’il y en a un. Si nous
poursuivons notre exemple, ce paramètre pourrait être :
restore_command = 'cp /mnt/nfs1/archivage/%f %p'
Cette commande sera appelée après la restauration de chaque journal pour récupérer le suivant, qui sera restauré et ainsi de suite. Il n’y a aucune paralléllisation prévue, mais des outils de sauvegarde PITR peuvent le faire au sein de la commande.
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’à ce que restore_command
tombe en erreur,
ce qui signifie l’épuisement de tous les journaux disponibles, et la fin
de la restauration.
Au cas où vous rencontreriez une instance en version 11 ou
antérieure, il faut savoir que la restauration se paramétrait dans un
fichier texte dans le PGDATA, contenant recovery_command
et
éventuellement les options de restauration.
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 :
recovery_target_name
(le nom correspond à un label
enregistré précédemment dans les journaux de transactions grâce à la
fonction pg_create_restore_point()
) ;recovery_target_time
;recovery_target_xid
, numéro de transaction qu’il est
possible de chercher dans les journaux eux-mêmes grâce à l’utilitaire
pg_waldump
(voir cet
article) ;recovery_target_lsn
, que là aussi on doit aller chercher
dans les journaux eux-mêmes.Évidemment, il ne faudra choisir qu’un paramètre parmi ceux-là.
Avec le paramètre recovery_target_inclusive
(par défaut
à true
), il est possible de préciser si la restauration se
fait en incluant les transactions au nom, à l’heure ou à l’identifiant
de transaction demandé, ou en les excluant.
Dans les cas complexes, nous verrons plus tard que choisir la
timeline peut être utile (avec
recovery_target_timeline
, en général à
latest
).
Exemples de paramétrage :
recovery_target_name = 'label';
recovery_target_time = '2022-12-31 12:45:00 UTC'
recovery_target_lsn = '0/2000060'
recovery_target_xid = '1100842'
Ces restaurations à un moment précis ne sont possibles que si elles
correspondent à un état cohérent d’après la fin du
base backup, soit après le moment du
pg_stop_backup
.
Si l’on a un historique de plusieurs sauvegardes, il faudra en choisir une antérieure au point de restauration voulu. Ce n’est pas forcément la dernière. Les outils ne sont pas forcément capables de deviner la bonne sauvegarde à restaurer.
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 (évidemment postérieur), il faut modifier la cible de restauration dans le fichier de configuration, et redémarrer PostgreSQL. C’est le seul moyen de rejouer d’autres journaux sans ouvrir l’instance en écriture.
Si l’on est arrivé au point de restauration voulu, un message de ce genre apparaît :
LOG: recovery stopping before commit of transaction 8693270, time 2021-09-02 11:46:35.394345+02
LOG: pausing at the end of recovery HINT: Execute pg_wal_replay_resume() to promote.
(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.
Attention : jusque PostgreSQL 12 inclus, si un
recovery_target
était spécifié mais n’est toujours
pas atteint à la fin du rejeu des archives, alors le mode
recovery se terminait et le serveur est promu sans erreur, et
ce, même si recovery_target_action
a la valeur
pause
! (À condition, bien sûr, que le point de cohérence
ait tout de même été dépassé.) Il faut donc être vigilant quant aux
messages dans le fichier de trace de PostgreSQL !
À partir de PostgreSQL 13, l’instance détecte le problème et s’arrête
avec un message FATAL
: la restauration ne s’est pas
déroulée comme attendu. S’il manque juste certains journaux de
transactions, cela permet de relancer PostgreSQL après correction de
l’oubli.
La documentation officielle complète sur le sujet est sur le site du projet.
La dernière étape est particulièrement simple. Il suffit de démarrer PostgreSQL. PostgreSQL va comprendre qu’il doit rejouer les journaux de transactions.
Les éventuels journaux présents sont rejoués, puis
restore_command
est appelé pour fournir d’autres journaux,
jusqu’à ce que la commande ne trouve plus rien dans les archives.
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 ce point, 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. 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"…
Si le message apparaît, le rejeu n’est pas terminé, mais on a au moins Au moment où ce message apparaît, le rejeu n’est pas terminé, mais il a atteint un stade où l’instance est cohérente et utilisable.
PostgreSQL continue ensuite jusqu’à arriver à la limite fixée,
jusqu’à ce qu’il ne trouve plus de journal à rejouer
(restore_command
tombe en erreur), ou que le bloc de
journal lu soit incohérent (ce qui indique qu’on est arrivé à la fin
d’un journal qui n’a pas été terminé, le journal courant au moment du
crash par exemple). PostgreSQL vérifie 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
Le fichier recovery.signal
est effacé pour ne pas poser
problème en cas de crash immédiat. (Ne l’effacez jamais
manuellement !)
Le fichier backup_label
d’une sauvegarde exclusive est
renommé en backup_label.old
.
La durée de la restauration est fortement dépendante du nombre de
journaux. Ils sont rejoués séquentiellement. 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 journaux, 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 nouvel 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éalisé des restaurations.
PostgreSQL permet de garder ces historiques grâce à la notion de
timeline. Une timeline est donc l’un de ces
historiques. Elle est identifiée par un numéro et se matérialise par un
ensemble de journaux de transactions. Le numéro de la timeline
est le premier nombre hexadécimal du nom des segments de journaux de
transactions, en 8ᵉ position (le second est le numéro du journal, et le
troisième, à la fin, le numéro du segment). Ainsi, lorsqu’une instance
s’ouvre après une restauration PITR, elle peut archiver immédiatement
ses 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/
000000010000000000000007
000000010000000000000008
000000010000000000000009
00000001000000000000000A
00000001000000000000000B
00000001000000000000000C
00000001000000000000000D
00000001000000000000000E
00000001000000000000000F
000000010000000000000010
000000010000000000000011
000000020000000000000009
00000002000000000000000A
00000002000000000000000B
00000002000000000000000C 00000002.history
Noter les timelines 1
et 2
en 8ᵉ position
des noms des fichiers. Il y a deux fichiers finissant par
09
: le premier 000000010000000000000009
contient des informations communes aux deux timelines mais sa
fin ne figure pas dans la timeline 2. Les fichiers
00000001000000000000000A
à
000000010000000000000011
contiennent des informations qui
ne figurent que dans la timeline 1. Les fichiers
00000002000000000000000A
jusque
00000002000000000000000C
sont uniquement dans la
timeline 2. Le fichier 00000002.history
contient
l’information sur la transition entre les deux timelines.
Ce fichier sert pendant le recovery, quand l’instance doit
choisir les timelines à suivre et les fichiers à restaurer. Les
timelines connues avec leur point de départ sont suivies grâce
aux fichiers d’historique, 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 journaux. En partant de la timeline
qu’elle quitte, l’instance restaure les fichiers historiques des
timelines suivantes pour choisir la première disponible. Une
fois la restauration finie, avant de s’ouvrir en écriture, l’instance
archive un nouveau fichier .history
pour la nouvelle
timeline sélectionnée. Il contient l’adresse du point de départ
dans la timeline qu’elle quitte, c’est-à-dire le point de
bifurcation entre la 1 et la 2 :
$ cat 00000002.history 1 0/9765A80 before 2015-10-20 16:59:30.103317+02
Puis l’instance continue normalement, et archive ses journaux
commençant par 00000002
.
Après une seconde restauration, repartant de la timeline 2, l’instance choisit la timeline 3 et écrit un nouveau fichier :
$ cat 00000003.history
1 0/9765A80 before 2015-10-20 16:59:30.103317+02 2 0/105AF7D0 before 2015-10-22 10:25:56.614316+02
Ce fichier reprend les timelines précédemment suivies par l’instance. En effet, l’enchaînement peut être complexe s’il y a eu plusieurs retours en arrière ou restauration.
À la restauration, on peut choisir la timeline cible en
configurant le paramètre recovery_target_timeline
. Il vaut
par défaut latest
, et la restauration suit donc les
changements de timeline depuis le moment de la sauvegarde.
Pour choisir une autre timeline que la dernière, il faut
donner le numéro de la timeline cible comme valeur du paramètre
recovery_target_timeline
. Les timelines permettent ainsi
d’effectuer plusieurs restaurations successives à partir du même
base backup, et d’archiver au même endroit sans mélanger les
journaux.
Bien sûr, 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.
Le changement de timeline ne se produit que lors d’une restauration explicite, et pas en cas de recovery après crash, notamment. (Cela arrive aussi quand un serveur secondaire est promu : il crée une nouvelle timeline.)
Il y a quelques pièges :
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 à recovery_target_timeline
(20
ou
'0x14'
). Attention, il n’y a pas de contrôle ! recovery_target_timeline
était current
: la
restauration se faisait donc dans la même timeline que le
base backup. Si entre-temps il y avait eu une bascule ou une
précédente restauration, la nouvelle timeline n’était pas
automatiquement suivie !Ce schéma illustre ce processus de plusieurs restaurations successives, et la création de différentes timelines qui en résulte.
On observe ici les éléments suivants avant la première restauration :
x12
;On décide d’arrêter l’instance alors qu’elle est arrivée à la
transaction x47
, par exemple parce qu’une nouvelle
livraison de l’application a introduit un bug qui provoque des pertes de
données. L’objectif est de restaurer l’instance avant l’apparition du
problème afin de récupérer les données dans un état cohérent, et de
relancer la production à partir de cet état. Pour cela, on restaure les
fichiers de l’instance à partir de la dernière sauvegarde, puis on
modifie le fichier de configuration pour que l’instance, lors de sa
phase de recovery :
x12
) ;x42
).On démarre ensuite l’instance et on l’ouvre en écriture, on constate
alors que celle-ci bascule sur la timeline 2, la bifurcation
s’effectuant à la transaction x42
. L’instance étant de
nouveau ouverte en é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 :
x12
) ;x42
) ;x55
).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 :
x12
) ;x20
).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 sauvegarde 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é archivé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 archivés.
Rappelons qu’un « trou » dans le flux des journaux dans le dépôt des archives empêchera la restauration 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 archive_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.
De plus, s’il y a eu plusieurs restaurations successives, qui ont
provoqué quelques archivages et des apparitions de timelines
dans le même dépôt d’archives, avant d’être abandonnées, il faut faire
attention au paramètre recovery_target_timeline
(latest
ne convient plus), ce qui complique une future
restauration.
Pour faciliter des restaurations ultérieures, il est recommandé de procéder au plus tôt à une sauvegarde complète du nouveau primaire.
Quant aux éventuelles instances secondaires, il est vraisemblable qu’elles doivent être reconstruites suite à la restauration de l’instance primaire. (Si elles ont appliqué des journaux qui ont été perdus et n’ont pas été repris par le primaire restauré, ces secondaires ne pourront se raccrocher. Consulter les traces.)
L’un des problèmes de la sauvegarde PITR est la place prise sur disque par les journaux de transactions. Si un journal de 16 Mo (par défaut) est généré toutes les minutes, le total est de 23 Go de journaux par jour, et parfois beaucoup plus. Il n’est pas forcément possible de conserver autant de journaux.
Un premier moyen est de reduire la volumétrie à la source en espaçant
les checkpoints. Le graphique ci-dessus représente la volumétrie générée
par un simple test avec pgbench
(OLTP classique donc) avec
checkpoint_timeout
variant entre 1 et 30 minutes : les
écarts sont énormes.
La raison est que, pour des raisons de fiabilité, un bloc modifié est
intégralement écrit (8 ko) dans les journaux à sa première modification
après un checkpoint. Par la suite, seules les modifications de ce bloc,
souvent beaucoup plus petites, sont journalisées. (Ce comportement
dépend du paramètre full_page_writes
, activé
par défaut et qu’il est impératif de laisser tel quel, sauf peut-être
sur ZFS.)
Espacer les checkpoints permet d’économiser des écritures de blocs complets, si l’activité s’y prête (en OLTP surtout). Il y a un intérêt en performances, mais surtout en place disque économisée quand les journaux sont archivés, aussi accessoirement en CPU s’ils sont compressés, et en trafic réseau s’ils sont répliqués. Un exemple figure dans ce billet du blog Dalibo.
Par cohérence, si l’on monte checkpoint_timeout
, il faut
penser à augmenter aussi max_wal_size
, et vice-versa. Des
valeurs courantes sont respectivement 15 minutes, parfois plus, et
plusieurs gigaoctets.
Il y a cependant un inconvénient : un écart plus grand entre checkpoints peut allonger la restauration après un arrêt brutal, car il y aura plus de journaux à rejouer, parfois des centaines ou des milliers.
PostgreSQL peut compresser les journaux à la source, si le paramètre
wal_compression
(désactivé par défaut) est passé à
on
. La compression est opérée par PostgreSQL au niveau de
la page, avec un gros gain en volumétrie (souvent plus de 50 % !). Les
journaux font toujours 16 Mo (par défaut), mais comme ils sont moins
nombreux, leur rejeu est plus rapide, ce qui accélère la réplication et
la reprise après un crash. Cette compression des journaux est totalement
transparente pour l’archivage ou la restauration. Le prix est une
augmentation de la consommation en CPU, souvent négligeable.
Depuis PostgreSQL 15, on peut même choisir l’algorithme de
compression : pglz
, lz4
ou zstd
.
on
est le synonyme de pglz
… qui est sans doute
le moins bon des trois (voir ce
petit test), surtout en terme de consommation CPU.
Une autre solution est la compression à la volée des journaux
archivés dans l’archive_command
. Les outils classiques
comme gzip
, bzip2
, lzma
,
xz
, etc. conviennent. Tous les outils PITR incluent
plusieurs de ces algorithmes. Un fichier de 16 Mo aura généralement une
taille compressée comprise entre 3 et 6 Mo.
Cependant, attention au temps de compression des journaux : en cas
d’écritures lourdes, une compression élevée mais lente peut mener à un
retard conséquent de l’archivage par rapport à l’écriture des journaux,
jusque saturation de pg_wal
, et arrêt de l’instance. Il est
courant de se contenter de gzip -1
ou lz4 -1
pour les journaux, et de ne compresser agressivement que les sauvegardes
des fichiers de la base.
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 :
archive_command
.Leur philosophie peut différer, notamment en terme de centralisation ou de compromis entre simplicité et fonctionnalités. Ces dernières s’enrichissent d’ailleurs au fil du temps.
Voir https://dali.bo/i4_html pour une description plus complète.
pgBackRest 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 complexité à la configuration :
pg_wal
;pgBackRest n’utilise pas pg_receivewal
pour garantir la
sauvegarde du dernier journal (non terminé) avant un sinistre. Les
auteurs considèrent que dans ce cas un secondaire synchrone est plus
adapté et plus fiable.
Le projet est très actif et considéré comme fiable, et les fonctionnalités proposées sont intéressantes.
Pour la supervision de l’outil, une sonde Nagios est fournie par un des développeurs : check_pgbackrest.
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 :
list-server
, pour connaître la liste des serveurs
configurés ;backup
, pour lancer une sauvegarde de base ;list-backup
, pour connaître la liste des sauvegardes de
base ;show-backup
, pour afficher des informations sur une
sauvegarde ;delete
, pour supprimer une sauvegarde ;recover
, pour restaurer une sauvegarde (la restauration
peut se faire à distance).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.
Cette méthode de sauvegarde est la seule utilisable dès que les besoins de performance de sauvegarde 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).
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/i2_solutions.
Configurer la réplication dans
postgresql.conf
etpg_hba.conf
:
- désactiver l’archivage s’il est actif
- autoriser des connexions de réplication en streaming en local.
Pour insérer des données :
- générer de l’activité avec
pgbench
en tant qu’utilisateur postgres :$ createdb bench $ /usr/pgsql-16/bin/pgbench -i -s 100 bench $ /usr/pgsql-16/bin/pgbench bench -n -P 5 -R 20 -T 720
- laisser tourner en arrière-plan
- surveiller l’évolution de l’activité sur la table
pgbench_history
, par exemple ainsi :$ watch -n 1 "psql -d bench -c 'SELECT max(mtime) FROM pgbench_history ;'"
En parallèle, sauvegarder l’instance avec :
pg_basebackup
au format tar, compressé avec gzip ;- sans oublier les journaux ;
- avec l’option
--max-rate=16M
pour ralentir la sauvegarde ;- le répertoire de sauvegarde sera
/var/lib/pgsql/16/backups/basebackup
;- surveillez la progression dans une autre session avec la vue système adéquate.
Une fois la sauvegarde terminée :
- regarder les fichiers générés ;
- arrêter la session
pgbench
; Afficher la date de dernière modification danspgbench_history
.
- Arrêter l’instance.
- Faire une copie à froid des données (par exemple avec
cp -rfp
) vers/var/lib/pgsql/16/data.old
(cette copie resservira plus tard).
- Vider le répertoire des données.
- Restaurer la sauvegarde
pg_basebackup
en décompressant ses deux archives.- Redémarrer l’instance.
Une fois l’instance restaurée et démarrée, vérifier les traces : la base doit accepter les connexions.
Quelle est la dernière donnée restaurée ?
Tenter une nouvelle restauration depuis l’archive
pg_basebackup
sans restaurer les journaux de transaction. Que se passe-t-il ?
Remettre en place la copie à froid de l’instance prise précédemment dans
/var/lib/pgsql/16/data.old
. Configurer l’archivage vers un répertoire/archives
, par exemple avecrsync
. Configurer la commande de restauration inverse. Démarrer PostgreSQL.
Générer à nouveau de l’activité avec
pgbench
. Vérifier que l’archivage fonctionne.
En parallèle, lancer une nouvelle sauvegarde avec
pg_basebackup
au format plain.
Utiliser
pg_verify_backup
pour contrôler l’intégrité de la sauvegarde.
À quoi correspond le fichier finissant par
.backup
dans les archives ?
Arrêter pgbench et noter la date des dernières données insérées.
Effacer le PGDATA. Restaurer la sauvegarde précédente sans les journaux. Configurer la
restore_command
. Créer le fichierrecovery.signal
. Démarrer PostgreSQL.
Vérifier les traces, ainsi que les données restaurées une fois le service démarré.
Vérifier quelles données ont été restaurées.
Dans ce qui suit, la plupart des commandes seront à lancer en tant
que postgres, les ordres sudo
nécessitant
un utilisateur privilégié.
Configurer la réplication dans
postgresql.conf
etpg_hba.conf
:
- désactiver l’archivage s’il est actif
- autoriser des connexions de réplication en streaming en local.
On n’aura ici pas besoin de l’archivage. S’il est déjà actif, on peut se contenter d’inhiber ainsi la commande d’archivage :
archive_mode = on
archive_command = '/bin/true'
(Cela permet d’épargner le redémarrage à chaque modification de
archive_mode
.)
Vérifier la configurer de l’autorisation de connexion en réplication
dans pg_hba.conf
. Si besoin, mettre à jour la ligne en fin
de fichier :
local replication all peer
Cela va ouvrir l’accès sans mot de passe depuis l’utilisateur système postgres.
Redémarrer PostgreSQL :
sudo systemctl restart postgresql-16
Pour insérer des données :
- générer de l’activité avec
pgbench
en tant qu’utilisateur postgres :$ createdb bench $ /usr/pgsql-16/bin/pgbench -i -s 100 bench $ /usr/pgsql-16/bin/pgbench bench -n -P 5 -R 20 -T 720
- laisser tourner en arrière-plan
- surveiller l’évolution de l’activité sur la table
pgbench_history
, par exemple ainsi :$ watch -n 1 "psql -d bench -c 'SELECT max(mtime) FROM pgbench_history ;'"
En parallèle, sauvegarder l’instance avec :
pg_basebackup
au format tar, compressé avec gzip ;- sans oublier les journaux ;
- avec l’option
--max-rate=16M
pour ralentir la sauvegarde ;- le répertoire de sauvegarde sera
/var/lib/pgsql/16/backups/basebackup
;- surveillez la progression dans une autre session avec la vue système adéquate.
En tant que postgres :
pg_basebackup -D /var/lib/pgsql/16/backups/basebackup -Ft \
--checkpoint=fast --gzip --progress --max-rate=16M
1583675/1583675 kB (100%), 1/1 tablespace
La progression peut se suivre depuis psql avec :
on
\x SELECT * FROM pg_stat_progress_basebackup ;
\watch
Thu Nov 11 16:58:05 2023 (every 2s)
-[ RECORD 1 ]--------+---------------------------------
pid | 19763
phase | waiting for checkpoint to finish
backup_total |
backup_streamed | 0
tablespaces_total | 0
tablespaces_streamed | 0
Thu Nov 11 16:58:07 2023 (every 2s)
-[ RECORD 1 ]--------+-------------------------
pid | 19763
phase | streaming database files
backup_total | 1611215360
backup_streamed | 29354496
tablespaces_total | 1
tablespaces_streamed | 0 …
Évidemment, en production, il ne faut pas sauvegarder en local.
Une fois la sauvegarde terminée :
- regarder les fichiers générés ;
- arrêter la session
pgbench
; Afficher la date de dernière modification danspgbench_history
.
$ ls -lha /var/lib/pgsql/16/backups/basebackup
…
-rw-------. 1 postgres postgres 180K Nov 11 17:00 backup_manifest
-rw-------. 1 postgres postgres 91M Nov 11 17:00 base.tar.gz -rw-------. 1 postgres postgres 23M Nov 11 17:00 pg_wal.tar.gz
On obtient donc :
pgbench
s’arrête avec un simple Ctrl-C.
L’heure de dernière modification est :
-d bench -c 'SELECT max(mtime) FROM pgbench_history;' psql
max
---------------------------- 2023-11-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/16/data.old
(cette copie resservira plus tard).
En tant qu’utilisateur privilégié :
sudo systemctl stop postgresql-16
En tant que postgres :
cp -rfp /var/lib/pgsql/16/data /var/lib/pgsql/16/data.old
- Vider le répertoire des données.
- Restaurer la sauvegarde
pg_basebackup
en décompressant ses deux archives.- Redémarrer l’instance.
On restaure dans le répertoire de données l’archive de base, puis les journaux dans leur sous-répertoire. La suppression des traces est optionnelle, mais elle nous permettra de ne pas mélanger celles d’avant et d’après la restauration.
En tant que postgres :
rm -rf /var/lib/pgsql/16/data/*
tar -C /var/lib/pgsql/16/data \
-xzf /var/lib/pgsql/16/backups/basebackup/base.tar.gz
tar -C /var/lib/pgsql/16/data/pg_wal \
-xzf /var/lib/pgsql/16/backups/basebackup/pg_wal.tar.gz
rm -rf /var/lib/pgsql/16/data/log/*
sudo systemctl start postgresql-16
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/16/data/log/postgresql-*.log
…
… LOG: database system was interrupted; last known up at 2023-11-05 16:59:03 UTC
… LOG: redo starts at 0/830000B0
… LOG: consistent recovery state reached at 0/8E8450F0
… LOG: redo done at 0/8E8450F0 system usage: CPU: user: 0.28 s, system: 0.24 s, elapsed: 0.59 s
… LOG: checkpoint starting: end-of-recovery immediate wait
… LOG: checkpoint complete: wrote 16008 buffers (97.7%); … … 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.
Quelle est la dernière donnée restaurée ?
-d bench -c 'SELECT max(mtime) FROM pgbench_history;' psql
max
---------------------------- 2023-11-05 17:00:40.936925
Grâce aux journaux (pg_wal
) restaurés, l’ensemble des
modifications survenues pendant la sauvegarde 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 ?
sudo systemctl stop postgresql-16
rm -rf /var/lib/pgsql/16/data/*
tar -C /var/lib/pgsql/16/data \
-xzf /var/lib/pgsql/16/backups/basebackup/base.tar.gz
rm -rf /var/lib/pgsql/16/data/log/*
systemctl start postgresql-16
sudo systemctl start postgresql-16
Résultat :
Job for postgresql-16.service failed because the control process exited with error code. See "systemctl status postgresql-16.service" and "journalctl -xe" for details.
Pour trouver la cause :
tail -F /var/lib/pgsql/16/data/log/postgresql-*.log
…
… LOG: database system was interrupted; last known up at 2023-11-05 16:59:03 UTC
… LOG: invalid checkpoint record
2023-11-05 17:16:52.134 UTC [20177] FATAL: could not locate required checkpoint record
2023-11-05 17:16:52.134 UTC [20177] HINT: If you are restoring from a backup, touch "/var/lib/pgsql/16/data/recovery.signal" and add required recovery options.
If you are not restoring from a backup, try removing the file "/var/lib/pgsql/16/data/backup_label". Be careful: removing "/var/lib/pgsql/16/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.
Quant au fichier backup_label
, le supprimer permettrait
peut-être de démarrer l’instance mais celle-ci serait alors dans un état
incohérent ! Il y a de bonnes chances que le démarrage s’achève
par :
PANIC: could not locate a valid checkpoint record
En résumé : la restauration des journaux n’est pas optionnelle !
Remettre en place la copie à froid de l’instance prise précédemment dans
/var/lib/pgsql/16/data.old
. Configurer l’archivage vers un répertoire/archives
, par exemple avecrsync
. Configurer la commande de restauration inverse. Démarrer PostgreSQL.
sudo systemctl stop postgresql-16 # si nécessaire
rm -rf /var/lib/pgsql/16/data
cp -rfp /var/lib/pgsql/16/data.old /var/lib/pgsql/16/data
Créer le répertoire d’archivage s’il n’existe pas déjà, et avec les bons droits pour postgres :
sudo mkdir /archives
sudo chown postgres: /archives
sudo chmod 700 /archives
Là encore, en production, ce sera plutôt un partage distant. L’utilisateur système postgres doit avoir le droit d’y écrire.
La commande d’archivage se définit dans
postgresql.conf
:
archive_mode = on
archive_command = 'rsync %p /archives/%f'
et on peut y définir aussi tout de suite la commande de restauration :
restore_command = 'rsync /archives/%f %p'
sudo systemctl start postgresql-16
Générer à nouveau de l’activité avec
pgbench
. Vérifier que l’archivage fonctionne.
/usr/pgsql-16/bin/pgbench bench -n -P 5 -R 20 -T 720
ls -lha /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 …
En parallèle, lancer une nouvelle sauvegarde avec
pg_basebackup
au format plain.
rm -rf /var/lib/pgsql/16/backups/basebackup
pg_basebackup -D /var/lib/pgsql/16/backups/basebackup -Fp \
--checkpoint=fast --progress --max-rate=16M
1586078/1586078 kB (100%), 1/1 tablespace
Le répertoire cible devra avoir été vidé.
La taille de la sauvegarde sera bien sûr nettement plus grosse qu’en tar compressé.
Utiliser
pg_verify_backup
pour contrôler l’intégrité de la sauvegarde.
Si tout va bien, le message sera lapidaire :
/usr/pgsql-16/bin/pg_verifybackup /var/lib/pgsql/16/backups/basebackup
backup successfully verified
S’il y a un problème, des messages de ce genre apparaîtront :
pg_verifybackup: error: "global/TEST" is present on disk but not in the manifest
pg_verifybackup: error: "global/2671" is present in the manifest but not on disk
pg_verifybackup: error: "postgresql.conf" has size 29507 on disk but size 29506 in the manifest
À quoi correspond le fichier finissant par
.backup
dans les archives ?
En effet, parmi les journaux archivés, figure ce fichier :
ls -1 /archives
…
0000000100000000000000BE
0000000100000000000000BE.00003E00.backup
0000000100000000000000BF …
Son contenu correspond au futur backup_label
:
START WAL LOCATION: 0/BE003E00 (file 0000000100000000000000BE)
STOP WAL LOCATION: 0/C864D0F8 (file 0000000100000000000000C8)
CHECKPOINT LOCATION: 0/BE0AB340
BACKUP METHOD: streamed
BACKUP FROM: primary
START TIME: 2023-11-05 18:32:52 UTC
LABEL: pg_basebackup base backup
START TIMELINE: 1
STOP TIME: 2023-11-05 18:34:29 UTC
STOP TIMELINE: 1
Arrêter pgbench et noter la date des dernières données insérées.
psql -d bench -c 'SELECT max(mtime) FROM pgbench_history;'
max
---------------------------- 2023-11-05 18:41:23.068948
Effacer le PGDATA. Restaurer la sauvegarde précédente sans les journaux. Configurer la
restore_command
. Créer le fichierrecovery.signal
. Démarrer PostgreSQL.
sudo systemctl stop postgresql-16
La sauvegarde étant au format plain, il s’agit d’une simple copie de fichiers :
rm -rf /var/lib/pgsql/16/data/*
rsync -a --exclude 'pg_wal/*' --exclude 'log/*' \
\
/var/lib/pgsql/16/backups/basebackup/ /var/lib/pgsql/16/data/
Créer le fichier recovery.signal
:
touch /var/lib/pgsql/16/data/recovery.signal
Démarrer le service :
sudo systemctl start postgresql-16
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/16/data/log/postgresql-*.log
…
… LOG: database system was interrupted; last known up at 2023-11-05 18:32:52 UTC
rsync: link_stat "/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]
… LOG: starting archive recovery
… LOG: restored log file "0000000100000000000000BE" from archive
… LOG: redo starts at 0/BE003E00
… LOG: restored log file "0000000100000000000000BF" from archive
… LOG: restored log file "0000000100000000000000C0" from archive
… LOG: restored log file "0000000100000000000000C1" from archive
…
… LOG: restored log file "0000000100000000000000C8" from archive
… LOG: restored log file "0000000100000000000000C9" from archive
… LOG: consistent recovery state reached at 0/C864D0F8
… LOG: database system is ready to accept read-only connections
… LOG: restored log file "0000000100000000000000CA" from archive
… LOG: restored log file "0000000100000000000000CB" from archive
…
… LOG: restored log file "0000000100000000000000E0" from archive
… LOG: restored log file "0000000100000000000000E1" from archive
… LOG: redo in progress, elapsed time: 10.25 s, current LSN: 0/E0FF3438
… LOG: restored log file "0000000100000000000000E2" from archive
… LOG: restored log file "0000000100000000000000E3" from archive
…
… LOG: restored log file "0000000100000000000000EF" from archive
… LOG: restored log file "0000000100000000000000F0" from archive
rsync: link_stat "/archives/0000000100000000000000F1" failed: No such file or directory (2)
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1189) …
rsync: link_stat "/archives/0000000100000000000000F1" failed: No such file or directory (2)
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1189) …
… LOG: redo done at 0/F0A6C9E0 system usage:
CPU: user: 2.51 s, system: 2.28 s, elapsed: 15.77 s
… LOG: last completed transaction
was at log time 2023-11-05 18:41:23.077219+00
… LOG: restored log file "0000000100000000000000F0" from archive
rsync: link_stat "/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) …
… LOG: selected new timeline ID: 2
rsync: link_stat "/archives/00000001.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) …
… LOG: archive recovery complete
… LOG: checkpoint starting: end-of-recovery immediate wait
… LOG: checkpoint complete: wrote 16012 buffers (97.7%); … … LOG: database system is ready to accept connections
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.
Les erreurs sur les fichiers 00000001.history
et
00000002.history
sont normales. PostgreSQL cherche à tout
hasrd ces fichiers pour voir quel est l’enchaînement des
timelines et quelle est la dernière.
La progression de la restauration peut être suivie grâce aux différents messages, repris ci-dessous, de démarrage, 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-11-05 18:41:23.077219+00
LOG: selected new timeline ID: 2
LOG: archive recovery complete
LOG: checkpoint complete:
Noter que les journaux portent une nouvelle timeline numérotée 2 :
ls -l /var/lib/pgsql/16/data/pg_wal/
…
-rw-------. 1 postgres postgres 16777216 Jan 5 18:43 000000020000000100000023
-rw-------. 1 postgres postgres 16777216 Jan 5 18:43 000000020000000100000024
-rw-------. 1 postgres postgres 42 Jan 5 18:43 00000002.history drwx------. 2 postgres postgres 35 Jan 5 18:43 archive_status
Vérifier quelles données ont été restaurées.
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-11-05 18:41:23.068948
Ce module se propose de faire une description des bonnes et mauvaises pratiques en cas de coup dur :
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 2014
Il est impossible de parer à tous les cas de désastres imaginables.
Le matériel peut subir des pannes, une faille logicielle non connue peut être exploitée, une modification d’infrastructure ou de configuration peut avoir des conséquences imprévues à long terme, une erreur humaine est toujours possible.
Les principes de base de la haute disponibilité (redondance, surveillance…) permettent de mitiger le problème, mais jamais de l’éliminer complètement.
Il est donc extrêmement important de se préparer au mieux, de procéder à des simulations, de remettre en question chaque brique de l’infrastructure pour être capable de détecter une défaillance et d’y réagir rapidement.
Par nature, les désastres arrivent de façon inattendue.
Il faut donc se préparer à devoir agir en urgence, sans préparation, dans un environnement perturbé et stressant — par exemple, en pleine nuit, la veille d’un jour particulièrement critique pour l’activité de la production.
Un des premiers points d’importance est donc de s’assurer de la présence d’une documentation claire, précise et à jour, afin de minimiser le risque d’erreurs humaines.
Cette documentation devrait détailler l’architecture dans son ensemble, et particulièrement la politique de sauvegarde choisie, l’emplacement de celles-ci, les procédures de restauration et éventuellement 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 environnements 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 !
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.
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 :
PANIC
ou FATAL
, mais
les messages ERROR
et WARNING
sont également
très significatifs, particuliè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 manipulations.
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érativement 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 !
Pour plus d’information, voir sur le wiki PostgreSQL.
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 — notamment 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 PostgreSQL) 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 corrompus.
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 environnement 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.
La communication est très importante dans la gestion d’un désastre.
Il est préférable de minimiser le nombre de canaux de communication plutôt que de les multiplier (téléphone, e-mail, chat, ticket…), ce qui pourrait amener à une perte d’informations et à des délais indésirables.
Il est primordial de rapidement cerner l’ampleur du problème, et pour cela il est généralement nécessaire de demander l’expertise d’autres administrateurs / équipes (applicatif, système, réseau, virtualisation, SAN…). Il ne faut pas rester isolé et risquer que la vision étroite que l’on a des symptômes (notamment en terme de supervision / accès aux traces) empêche l’identification de la nature réelle du 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 rapporteraient 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 possible 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 rencontré. 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èlement 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éfinies dans la documentation.
Notamment (liste non exhaustive) :
log_error_verbosity
) ;Pour les problèmes relevant du domaine de la sécurité (découverte d’une faille), la liste adéquate est security@postgresql.org.
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 problè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 physique 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 (et uniquement les
tables), sans nécessiter de place sur disque supplémentaire :
$ pg_dumpall > /dev/null
Sous Windows Powershell, la commande est :
PS C:\ pg_dumpall > $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
pg_dump: erreur : La commande était : COPY public.corrompue (i) TO stdout; pg_dumpall: erreur : échec de pg_dump sur la base de données « corruption », quitte
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 l’intégrité fonctionnelle des données : les corruptions
peuvent très bien être silencieuses ou concerner les index. Une
vérification exhaustive implique d’autres outils comme
pg_checksums
ou pg_basebackup
.
Même si la lecture des données par pg_dumpall
ou
pg_dump
ne renvoie aucune erreur, il est toujours possible
que des problèmes subsistent, par exemple des corruptions silencieuses,
des index incohé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 normalement le cas depuis la détection de l’incident
si les conseils précédents ont été suivis) pour exporter 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 corruptions 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.
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 situation 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 remplissage 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 provient d’un document mentionnant les mêmes messages d’erreur que ceux rencontrés (et tout particuliè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 documentation, «
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 » (documentation).
Nous reviendrons 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.
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 documenter l’origine du problème (rapport « post-mortem ») a une valeur inestimable pour prendre les mesures afin d’éviter que le problème ne se reproduise, et retrouver rapidement les informations utiles s’il venait à se reproduire malgré tout.
Après s’être assuré d’avoir bien compris le problème rencontré, il est tout aussi important de le documenter 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.
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.
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 :
fsync
?
(SAN ? virtualisation ?) ;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 ressources, et de la séparation des compétences et des informations de supervision entre différentes équipes.
Tout comme pour les problèmes au niveau du matériel, les problèmes au niveau du système de virtualisation peuvent être complexes à détecter et à diagnostiquer correctement.
Le principal facteur de problème avec la virtualisation est lié à une mutualisation excessive des ressources.
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 systè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 modifications 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 :
dmesg
, et dans les fichiers traces du système,
généralement situés sous /var/log
;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 :
PGDATA
,
sous Linux :$ touch $PGDATA/test_write
PGDATA
, sous
Linux :$ cat $PGDATA/PGVERSION
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 diagnostic.
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 rapidement.
Dans le cas d’une consommation mémoire d’un serveur Linux qui menacerait de dépasser la quantité 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 processus Out Of Memory Killer (OOM Killer) qui va se charger de tuer les processus les plus consommateurs.
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.
Pour un serveur dédié à PostgreSQL, la recommendation est habituellement de désactiver la sur-allocation de la mémoire, empêchant ainsi le déclenchement de ce phénomène.
Voir pour cela les paramètres kernel
vm.overcommit_memory
et vm.overcommit_ratio
(référence : https://kb.dalibo.com/overcommit_memory).
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 parvient 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écutant
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 également 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êmement 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).
Le paramètre full_page_write
indique à PostgreSQL
d’effectuer une écriture complète d’une page chaque fois qu’elle reçoit
une nouvelle écriture après un checkpoint, pour éviter un éventuel
mélange entre des anciennes et nouvelles données en cas d’écriture
partielle.
La désactivation de full_page_write
peut avoir le même
type de conséquences catastrophiques que celle de
fsync
!
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. Hélas, elles ne
sont pas actives par défaut (cela pourrait bientôt changer). Leur mise
en place est donc fortement recommandée sur une nouvelle instance.
L’activation se fait différemment suivant les environnements (voir les
procédures pour les paquets RPM et Debian du PGDG mais revient
toujours à transmettre le paramètre --data-checksums
à
initdb
.
Il est possible de mettre en place les checksums sur une
instance existante avec l’utilitaire pg_checksums
. Les
gros inconvénients d’une activation tardive sont :
À 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
Insérons une valeur de test, sur chacun des deux clusters :
CREATE TABLE test (name text);
INSERT INTO test (name) VALUES ('toto');
On récupère le chemin du fichier de la table pour aller le corrompre à la main (seul celui sans checksums 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 remplaç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
(On remarquera au passage que la ligne est à la fin du bloc de 8 ko.) Enfin, on peut ensuite exécuter des requêtes sur ces deux clusters.
Sans checksums :
TABLE test;
name
------ qoto
Avec checksums :
TABLE test;
WARNING: page verification failed, calculated checksum 16321
but expected 21348 ERROR: invalid page in block 0 of relation base/12036/16387
Les sommes de contrôle sont vérifiées lors des lectures par les
requêtes, mais aussi lors des sauvegardes, notamment lors d’un appel à
pg_dump
(données de la base concernée uniquement),
pg_basebackup
, pgbackrest
… En cas de
corruption des données, l’opération sera interrompue.
pg_checksums --check
peut vérifier complètement une
instance arrêtée.
En pratique, avec PostgreSQL 9.5 au moins et des processeurs
supportant les instructions SSE 4.2 (voir dans
/proc/cpuinfo
, mais cela concerne au moins tous les
processeurs Intel depuis 15 ans), il n’y aura pas d’impact notable en
performances. Par contre, il y a aura un peu plus de journaux générés,
pour des raisons techniques.
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.
L’erreur humaine fait également partie des principales causes de désastre.
Une commande de suppression tapée trop rapidement, un oubli de clause
WHERE
dans une requête de mise à jour, nombreuses sont les
opérations qui peuvent provoquer des pertes de données ou un crash de
l’instance.
Il convient donc de revoir les dernières opérations effectuées sur le serveur, en commençant par les interventions planifiées, et si possible récupérer l’historique des commandes passées.
Des exemples de commandes particulièrement dangereuses :
kill -9
rm -rf
rsync
find
(souvent couplé avec des commandes destructices
comme rm
, mv
, gzip
…)L’outil pg_controldata
est livré avec PostgreSQL. Il 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 :
Quelques informations de paramétrage sont également renvoyées, comme la configuration du niveau de WAL, ou le nombre maximal de connexions autorisées.
En complément, le dernier état connu de l’instance est également affiché. Les états potentiels sont :
in production
: l’instance est démarrée et est ouverte
en écriture ;shut down
: l’instance est arrêtée ;in archive recovery
: l’instance est démarrée et est en
mode recovery
(restauration, Warm
ou
Hot Standby
) ;shut down in recovery
: l’instance s’est arrêtée alors
qu’elle était en mode recovery
;shutting down
: état transitoire, l’instance est en
cours d’arrêt ;in crash recovery
: état transitoire, l’instance est en
cours de démarrage suite à un crash ;starting up
: état transitoire, concrètement jamais
utilisé.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_control version number: 1002
Catalog version number: 201707211
Database system identifier: 6451139765284827825
Database cluster state: in production
pg_control last modified: Mon 28 Aug 2017 03:40:30 PM CEST
Latest checkpoint location: 1/2B04EC0
Prior checkpoint location: 1/2B04DE8
Latest checkpoint's REDO location: 1/2B04E88
Latest checkpoint's REDO WAL file: 000000010000000100000002
Latest checkpoint's TimeLineID: 1
Latest checkpoint's PrevTimeLineID: 1
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID: 0:1023
Latest checkpoint's NextOID: 41064
Latest checkpoint's NextMultiXactId: 1
Latest checkpoint's NextMultiOffset: 0
Latest checkpoint's oldestXID: 548
Latest checkpoint's oldestXID's DB: 1
Latest checkpoint's oldestActiveXID: 1022
Latest checkpoint's oldestMultiXid: 1
Latest checkpoint's oldestMulti's DB: 1
Latest checkpoint's oldestCommitTsXid:0
Latest checkpoint's newestCommitTsXid:0
Time of latest checkpoint: Mon 28 Aug 2017 03:40:30 PM CEST
Fake LSN counter for unlogged rels: 0/1
Minimum recovery ending location: 0/0
Min recovery ending loc's timeline: 0
Backup start location: 0/0
Backup end location: 0/0
End-of-backup record required: no
wal_level setting: replica
wal_log_hints setting: off
max_connections setting: 100
max_worker_processes setting: 8
max_prepared_xacts setting: 0
max_locks_per_xact setting: 64
track_commit_timestamp setting: off
Maximum data alignment: 8
Database block size: 8192
Blocks per segment of large relation: 131072
WAL block size: 8192
Bytes per WAL segment: 16777216
Maximum length of identifiers: 64
Maximum columns in an index: 32
Maximum size of a TOAST chunk: 1996
Size of a large-object chunk: 2048
Date/time type storage: 64-bit integers
Float4 argument passing: by value
Float8 argument passing: by value
Data page checksum version: 0
Mock authentication nonce: 7fb23aca2465c69b2c0f54ccf03e0ece 3c0933c5f0e5f2c096516099c9688173
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 sont lisibles est de forcer leur lecture complète, notamment grâce à la commande suivante :
$ pg_dumpall > /dev/null
Sous Windows Powershell :
pg_dumpall > $null
Rappelons que les fichiers associés aux index et vues matérialisées ne sont pas parcourus pendant cette opération.
Si pg_dumpall
ou pg_dump
renvoient des
messages d’erreur et ne parviennent pas à exporter certaines tables, il
est possible de contourner le problème à l’aide de la commande
COPY
, en sélectionnant exclusivement les données lisibles
autour du bloc corrompu. Exemples :
-- si accès par index possible
COPY (SELECT * FROM pgbench_accounts WHERE aid BETWEEN 50000 AND 60000)
TO 'backuppartiel.dmp';
-- accès par références de lignes
COPY (SELECT * FROM pgbench_accounts WHERE ctid BETWEEN '(0,1)' AND '(1639,20)')
TO 'backuppartiel.dmp';
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 :
$ pg_restore -1 --section=pre-data --verbose -d cible base.dump
$ pg_restore -1 --section=data --verbose -d cible base.dump
$ pg_restore -1 --section=post-data --exit-on-error --verbose -d cible base.dump
En cas de problème, on verra les contraintes posant problème.
Il peut être utile de générer les scripts en pur SQL avant de les appliquer, éventuellement par étape :
$ pg_restore --section=post-data -f postdata.sql base.dump
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 valider l’intégrité fonctionnelle des données.
Voici quelques exemples.
Contenu d’une page d’une table :
SELECT * FROM heap_page_items(get_raw_page('dspam_token_data',0)) LIMIT 2;
-[ 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
Méta-données d’un index (contenu dans la première page) :
SELECT * FROM bt_metap('dspam_token_data_uid_key');
-[ RECORD 1 ]-----
magic | 340322
version | 2
root | 243
level | 2
fastroot | 243 fastlevel | 2
La page racine est la 243. Allons la voir :
SELECT * FROM bt_page_items('dspam_token_data_uid_key',243) LIMIT 10;
offset | ctid | len |nulls|vars| data
--------+-----------+-----+-----+----+-------------------------------------
1 | (3,1) | 8 | f | f |
2 | (44565,1) | 20 | f | f | f3 4b 2e 8c 39 a3 cb 80 0f 00 00 00
3 | (242,1) | 20 | f | f | 77 c6 0d 6f a6 92 db 81 28 00 00 00
4 | (43569,1) | 20 | f | f | 47 a6 aa be 29 e3 13 83 18 00 00 00
5 | (481,1) | 20 | f | f | 30 17 dd 8e d9 72 7d 84 0a 00 00 00
6 | (43077,1) | 20 | f | f | 5c 3c 7b c5 5b 7a 4e 85 0a 00 00 00
7 | (719,1) | 20 | f | f | 0d 91 d5 78 a9 72 88 86 26 00 00 00
8 | (41209,1) | 20 | f | f | a7 8a da 17 95 17 cd 87 0a 00 00 00
9 | (957,1) | 20 | f | f | 78 e9 64 e9 64 a9 52 89 26 00 00 00 10 | (40849,1) | 20 | f | f | 53 11 e9 64 e9 1b c3 8a 26 00 00 00
La première entrée de la page 243, correspondant à la donnée
f3 4b 2e 8c 39 a3 cb 80 0f 00 00 00
est stockée dans la
page 3 de notre index :
SELECT * FROM bt_page_stats('dspam_token_data_uid_key',3);
-[ 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
SELECT * FROM bt_page_items('dspam_token_data_uid_key',3) LIMIT 10;
offset | ctid | len |nulls|vars| data
--------+-----------+-----+-----+----+-------------------------------------
1 | (38065,1) | 20 | f | f | f3 4b 2e 8c 39 a3 cb 80 0f 00 00 00
2 | (1,1) | 8 | f | f |
3 | (37361,1) | 20 | f | f | 30 fd 30 b8 70 c9 01 80 26 00 00 00
4 | (2,1) | 20 | f | f | 18 2c 37 36 27 03 03 80 27 00 00 00
5 | (4,1) | 20 | f | f | 36 61 f3 b6 c5 1b 03 80 0f 00 00 00
6 | (43997,1) | 20 | f | f | 30 4a 32 58 c8 44 03 80 27 00 00 00
7 | (5,1) | 20 | f | f | 88 fe 97 6f 7e 5a 03 80 27 00 00 00
8 | (51136,1) | 20 | f | f | 74 a8 5a 9b 15 5d 03 80 28 00 00 00
9 | (6,1) | 20 | f | f | 44 41 3c ee c8 fe 03 80 0a 00 00 00 10 | (45317,1) | 20 | f | f | d4 b0 7c fd 5d 8d 05 80 26 00 00 00
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 :
SELECT * FROM bt_page_stats('dspam_token_data_uid_key',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
SELECT * FROM bt_page_items('dspam_token_data_uid_key',38065) LIMIT 10;
offset | ctid | len |nulls|vars| data
--------+-------------+-----+-----+----+-------------------------------------
1 | (11128,118) | 20 | f | f | 33 37 89 95 b9 23 cc 80 0a 00 00 00
2 | (45713,181) | 20 | f | f | f3 4b 2e 8c 39 a3 cb 80 0f 00 00 00
3 | (45424,97) | 20 | f | f | f3 4b 2e 8c 39 a3 cb 80 26 00 00 00
4 | (45255,28) | 20 | f | f | f3 4b 2e 8c 39 a3 cb 80 27 00 00 00
5 | (15672,172) | 20 | f | f | f3 4b 2e 8c 39 a3 cb 80 28 00 00 00
6 | (5456,118) | 20 | f | f | f3 bf 29 a2 39 a3 cb 80 0f 00 00 00
7 | (8356,206) | 20 | f | f | f3 bf 29 a2 39 a3 cb 80 28 00 00 00
8 | (33895,272) | 20 | f | f | f3 4b 8e 37 99 a3 cb 80 0a 00 00 00
9 | (5176,108) | 20 | f | f | f3 4b 8e 37 99 a3 cb 80 0f 00 00 00 10 | (5466,41) | 20 | f | f | f3 4b 8e 37 99 a3 cb 80 26 00 00 00
Nous avons trouvé une feuille (type l
). Les ctid pointés
sont maintenant les adresses dans la table :
SELECT * FROM dspam_token_data WHERE ctid = '(11128,118)';
uid | token | spam_hits | innocent_hits | last_hit
-----+----------------------+-----------+---------------+------------ 40 | -6317261189288392210 | 0 | 3 | 2014-11-10
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 :
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 pas, 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.
Il faut donc bien retenir les points suivants :
pg_resetwal
n’est pas magique ;pg_resetwal
rend les données incohérentes (ce qui est
souvent pire qu’une simple perte d’une partie des données, comme on
aurait eu en restaurant une sauvegarde) ;pg_resetwal
n’est à utiliser qu’en dernier recours s’il
n’y a aucun autre moyen de faire autrement pour récupérer les
données ;À partir de PostgreSQL 14, cette extension regroupe des fonctions qui permettent de modifier le statut d’un tuple dans une relation. Il est par exemple possible de rendre une ligne morte ou de rendre visible des tuples qui sont invisibles à cause des informations de visibilité.
Ces fonctions sont dangereuses et peuvent provoquer ou aggraver des corruptions. Elles peuvent par exemple rendre une table incohérente par rapport à ses index, ou provoquer une violation de contrainte d’unicité ou de clé étrangère. Il ne faut donc les utiliser qu’en dernier recours, sur une copie de votre instance.
Référence : Documentation officielle
Une fois les sommes de contrôle en place, elles sont revérifiées à chaque lecture du bloc, mais il n’y a pas d’automatisme pour une vérification globale.
pg_checksums :
pg_checksums --check
est la commande dédiée pour
vérifier les sommes de contrôles existantes sur les bases de données
à froid : l’instance doit être arrêtée proprement
auparavant.
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
pg_checksums: error: checksum verification failed in file
"/var/lib/pgsql/12/data/base/14187/16389", block 0:
calculated checksum 5BF9 but block contains C55D
pg_checksums: error: checksum verification failed in file
"/var/lib/pgsql/12/data/base/14187/16389", block 4438:
calculated checksum A3 but block contains B8AE
Checksum operation completed
Files scanned: 1282
Blocks scanned: 28484
Bad checksums: 2
Data checksum version: 1
Rappelons que pg_checksums
sait aussi ajouter ou
supprimer les sommes de contrôle sur une instance existante arrêtée
(donc après le initdb
).
Sauvegarde logique :
pg_dumpall > /dev/null
vérifie certaines sommes de
contrôles car il lit tous les fichiers de données, mais pas les index.
Si une sauvegarde logique fonctionne (et se restaure sans souci…), on a
au moins sauvé l’essentiel.
pg_basebackup :
La meilleure alternative est d’effectuer une sauvegarde physique avec
pg_basebackup
. Par défaut, toutes les sommes de contrôle
sont alors vérifiées. C’est plus lourd, mais cela n’oblige pas à arrêter
la base. De plus, si l’outil est utilisé pour faire des sauvegardes, on
obtient une vérification gratuite.
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.
Une sauvegarde PITR est un bon moyen de vérifier les sommes de
contrôle régulièrement, tout en permettant de parer aux soucis qui
pourraient survenir. L’outil barman, selon sa configuration, peut
utiliser pg_basebackup
. pgBackRest vérifie aussi les sommes
de contrôle lors de ses sauvegardes.
amcheck :
Plus finement, amcheck
permet de vérifier la cohérence
des tables et index et de leur structure interne, et ainsi détecter des
corruptions. Il définit plusieurs fonctions :
verify_heapam
(PostgreSQL 14 et suivants) vérifie que
chaque bloc est bien lisible, correctement construit ; cette fonction
peut couvrir les TOAST et peut se limiter à certains blocs ;bt_index_check
est destinée aux vérifications de
routine des index, et ne pose qu’un verrou AccessShareLock 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.Avec le paramètre heapallindex
à true
, les
deux dernières fonctions effectuent une vérification supplémentaire en
recréant temporairement une structure d’index et en la comparant avec
l’index original. bt_index_check
vérifie alors que chaque
entrée de la table possède une entrée dans l’index.
bt_index_parent_check
vérifie en plus qu’à chaque entrée de
l’index correspond 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.
À partir de PostgreSQL 17, le paramètre checkunique
à
true
permet de détecter des violations de contrainte
d’unicité.
amcheck
ne fournit aucun moyen de corriger une erreur,
puisqu’il détecte des choses qui ne devraient jamais arriver.
REINDEX
sera souvent la solution la plus sûre, simple et
facile, mais tout dépend de la cause du problème.
Exemple :
Soit unetable_pkey
, un index de 10 Go sur un
entier :
CREATE EXTENSION amcheck ;
SELECT bt_index_check('unetable_pkey');
63753,257 ms (01:03,753)
Durée :
SELECT bt_index_check('unetable_pkey', true);
234200,678 ms (03:54,201) Durée :
Ici, la vérification exhaustive multiplie le temps de vérification par un facteur 4.
Voir la documentation d’amcheck pour tous les détails et les options disponibles.
pg_amcheck :
À partir de la version 14, PostgreSQL dispose d’un nouvel outil en
ligne de commande appelé pg_amcheck
, basé sur
amcheck
.
Ses options de ligne de commande reprennent celles de l’extension, tout en permettant de lancer les vérifications en masse (choix des bases, des tables, parallélisation).
Voir sa documentation.
Exemple de recherche de corruption d’unicité :
Cet exemple, dérivé d’un autre de la liste pgsql-hackers, modifie les tables systèmes pour provoquer la corrution d’un index unique (à ne jamais faire en production, bien sûr !) :
CREATE TABLE junk (t text UNIQUE);
-- données
INSERT INTO junk (t) VALUES ('ba'), ('be'), ('bo'), ('by');
UPDATE pg_catalog.pg_index
SET indisunique = false
WHERE indrelid = 'junk'::regclass;
-- nouvelles données avec doublons
INSERT INTO junk (t) VALUES ('ba'), ('bi'), ('bu'), ('by');
-- remise en place de l'unicité
UPDATE pg_catalog.pg_index
SET indisunique = true
WHERE indrelid = 'junk'::regclass;
SELECT * FROM junk ORDER BY 1;
Tout semble fonctionner ensuite, si ce n’est que que l’unicité des données déjà présentes n’est pas là.
amcheck
:CREATE EXTENSION amcheck ;
SELECT bt_index_check('junk_t_key', false, true);
ERROR: index uniqueness is violated for index "junk_t_key" DÉTAIL : Index tid=(1,1) and tid=(1,2) (point to heap tid=(0,1) and tid=(0,5)) page lsn=65/EB31C810.
pg_amcheck
(l’extension
amcheck
doit être installée) :pg_amcheck --database=postgres --relation=junk --checkunique
vérification de l'index btree« postgres public.junk_t_key » :
ERROR: index uniqueness is violated for index "junk_t_key" DÉTAIL : Index tid=(1,1) and tid=(1,2) (point to heap tid=(0,1) and tid=(0,5)) page lsn=65/EB31C810.
Les données responsables peuvent être trouvées dans la table
(heap) grâce aux tid
indiqués :
SELECT * FROM junk WHERE ctid='(0,1)' or ctid='(0,5)';
t
----
ba ba
Malheureusement, l’outil ne fournit que la première erreur rencontrée. Il faudra faire une requête pour les trouver :
SELECT t, count(*) FROM junk
GROUP BY 1 HAVING count(*) > 1 ORDER BY t ;
Sachant que cette requête risque de se baser sur l’index
problématique, il faudra se méfier, ou plutôt agréger avec
t||''
.
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.
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 corruptions.
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
.
Une corruption d’index se corrige donc facilement, mais le plus
inquiétant est la présence même d’une corruption. Le contrôle complet de
la base est fortement conseillé.
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.
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 :
SET zero_damaged_pages = true ;
FULL tablecorrompue ; VACUUM
WARNING: invalid page header in block 32570 of relation base/16390/2663; zeroing out page
Si l’opération continue et 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 respectées…).
Par ailleurs, sans les checksums, PostgreSQL ne détecte pas les corruptions logiques, c’est-à-dire n’affectant pas la structure des données mais corrompant 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, utiliser la
fonction pg_relation_filepath()
:
SELECT pg_relation_filepath('test_corruptindex') ;
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écupérables de grandes portions de données. Il est donc fortement déconseillé de se lancer dans ce genre de manipulations à moins d’être absolument certain que c’est indispensable.
Encore une fois, ne pas oublier de travailler sur une copie, et pas directement sur l’instance de production.
Les fichiers WAL sont les journaux de transactions de PostgreSQL. Leur fonction est d’assurer que les transactions qui ont été effectuées depuis le dernier checkpoint ne seront pas perdues en cas de crash de l’instance.
Si certains sont corrompus ou manquants, alors PostgreSQL ne pourra pas redémarrer.
Il ne faut jamais supprimer les fichiers WAL, même si le système de fichiers est plein ! (Dans ce dernier cas, chercher la source : il s’agit souvent d’un archivage qui traîne ou d’un slot de réplication bloqué.)
Si l’archivage était activé et que les fichiers WAL perdus ont été
archivés, alors il est possible de les restaurer avant de tenter un
nouveau démarrage (copie manuelle, utilisation de
restore_command
…).
Si ce n’est pas possible, ou si les archives ont également été corrompues ou supprimées, l’instance ne pourra pas redémarrer.
Dans cette situation, comme dans la plupart des autres évoquées ici, la seule solution permettant de s’assurer que les données ne seront pas corrompues est de procéder à une restauration de l’instance depuis la dernière sauvegarde complète disponible.
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ètement 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 sauvegarde 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.
Le fichier de contrôle de l’instance contient de nombreuses informations liées à l’activité et au statut de l’instance, notamment l’instant du dernier checkpoint, la position correspondante dans les WAL, le numéro de transaction courant et le prochain à venir…
Ce fichier est le premier lu par l’instance. S’il est corrompu ou supprimé, l’instance ne pourra pas démarrer.
Il est possible de forcer la réinitialisation de ce fichier à l’aide
de la commande pg_resetwal
, qui va se baser par défaut sur
les informations contenues dans les fichiers WAL présents pour tenter de
« deviner » le contenu du fichier de contrôle.
Ces informations seront très certainement erronées, potentiellement à tel point que même l’accès aux bases de données par leur nom ne sera pas possible :
$ pg_isready
/var/run/postgresql:5432 - accepting connections
$ 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
transactions, 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 annulées soient désormais considérées comme valides, et donc que les modifications de données correspondantes 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 correspondance 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.
Cette situation est très délicate, et appelle là encore une restauration.
Si le catalogue était complètement inaccessible, sans sauvegarde la seule solution restante serait de tenter d’extraire les données directement des fichiers data de l’instance, en oubliant toute notion de cohérence, de type de données, de relation… Personne ne veut faire ça.
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/i5_solutions.
Vérifier que l’instance utilise bien les checksums. Au besoin les ajouter avec
pg_checksums
.
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
Voir la taille de
pgbench_accounts
, les valeurs que prend sa clé primaire.
Retrouver le fichier associé à la table
pgbench_accounts
(par exemple avecpg_file_relationpath
).
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.
Redémarrer PostgreSQL et lire le contenu de
pgbench_accounts
.
Tenter un
pg_dumpall > /dev/null
.
- Arrêter PostgreSQL.
- Voir ce que donne
pg_checksums
(pg_verify_checksums
en v11).
- Faire une copie de travail à froid du PGDATA.
- Protéger en écriture le PGDATA original.
- Dans la copie, supprimer la possibilité d’accès depuis l’extérieur.
Avant de redémarrer PostgreSQL, supprimer les sommes de contrôle dans la copie (en désespoir de cause).
Démarrer le cluster sur la copie avec
pg_ctl
.
Que renvoie ceci ?
SELECT * FROM pgbench_accounts LIMIT 100 ;
Tenter une récupération avec
SET zero_damaged_pages
. Quelles données ont pu être perdues ?
Nous continuons sur la copie de la base de travail, où les sommes de contrôle ont été désactivées.
Consulter le format et le contenu de la table
pgbench_branches
.
Retrouver les fichiers des tables
pgbench_branches
(par exemple avecpg_file_relationpath
).
Pour corrompre la table :
- Arrêter PostgreSQL.
- Avec hexedit, dans le premier bloc en tête de fichier, remplacer les derniers caractères non nuls (
C0 9E 40
) parFF FF FF
.- En toute fin de fichier, remplacer le dernier
01
par unFF
.- Redémarrer PostgreSQL.
- Compter le nombre de lignes dans
pgbench_branches
.- Recompter après
SET enable_seqscan TO off ;
.- Quelle est la bonne réponse ? Vérifier le contenu de la table.
Qu’affiche
pageinspect
pour cette table ?
Avec l’extension
amcheck
, essayer de voir si le problème peut être détecté. Si non, pourquoi ?
Pour voir ce que donnerait une restauration :
- Exporter
pgbench_accounts
, définition des index comprise.- Supprimer la table (il faudra supprimer
pgbench_history
aussi).- Tenter de la réimporter.
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
$ dropdb --if-exists pgbench ;
$ createdb pgbench ;
$ /usr/pgsql-15/bin/pgbench -i -s 10 -d pgbench --foreign-keys
...
creating tables...
generating data...
100000 of 1000000 tuples (10%) done (elapsed 0.15 s, remaining 1.31 s)
200000 of 1000000 tuples (20%) done (elapsed 0.35 s, remaining 1.39 s)
..
1000000 of 1000000 tuples (100%) done (elapsed 2.16 s, remaining 0.00 s)
vacuuming...
creating primary keys...
creating foreign keys...
done.
Voir la taille de
pgbench_accounts
, les valeurs que prend sa clé primaire.
La table fait 128 Mo selon un \d+
.
La clé aid
va de 1 à 100000 :
SELECT min(aid), max(aid) FROM pgbench_accounts ; #
min | max
-----+--------- 1 | 1000000
Un SELECT
montre que les valeurs sont triées mais c’est
dû à l’initialisation.
Retrouver le fichier associé à la table
pgbench_accounts
(par exemple avecpg_file_relationpath
).
SELECT pg_relation_filepath('pgbench_accounts') ;
pg_relation_filepath
---------------------- base/16454/16489
Arrêter PostgreSQL.
# systemctl stop postgresql-15
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.
# dnf install hexedit
postgres$ hexedit /var/lib/pgsql/15/data/base/16454/16489
Aller par exemple sur la 2è ligne, modifier 80 9F
en
FF FF
. Sortir avec Ctrl-X, confirmer la sauvegarde.
Redémarrer PostgreSQL et lire le contenu de
pgbench_accounts
.
# systemctl start postgresql-15
SELECT * FROM pgbench_accounts ; #
WARNING: page verification failed, calculated checksum 62947 but expected 57715 ERROR: invalid page in block 0 of relation base/16454/16489
Tenter un
pg_dumpall > /dev/null
.
$ pg_dumpall > /dev/null
pg_dump: WARNING: page verification failed, calculated checksum 62947 but expected 57715
pg_dump: error: Dumping the contents of table "pgbench_accounts" failed: PQgetResult() failed.
pg_dump: error: Error message from server:
ERROR: invalid page in block 0 of relation base/16454/16489
pg_dump: error: The command was:
COPY public.pgbench_accounts (aid, bid, abalance, filler) TO stdout;
pg_dumpall: error: pg_dump failed on database "pgbench", exiting
- Arrêter PostgreSQL.
- Voir ce que donne
pg_checksums
(pg_verify_checksums
en v11).
# systemctl stop postgresql-15
$ /usr/pgsql-15/bin/pg_checksums -D /var/lib/pgsql/15/data/ --check --progress
pg_checksums: error: checksum verification failed in file
"/var/lib/pgsql/15/data//base/16454/16489", block 0:
calculated checksum F5E3 but block contains E173
216/216 MB (100%) computed
Checksum operation completed
Files scanned: 1280
Blocks scanned: 27699
Bad checksums: 1
Data checksum version: 1
- Faire une copie de travail à froid du PGDATA.
- Protéger en écriture le PGDATA original.
- Dans la copie, supprimer la possibilité d’accès depuis l’extérieur.
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/
Dans /var/lib/pgsql/15/data.BACKUP/pg_hba.conf
ne doit
plus subsister que :
local all all trust
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
Démarrer le cluster sur la copie avec
pg_ctl
.
/usr/pgsql-15/bin/pg_ctl -D /var/lib/pgsql/15/data.BACKUP/ start
Que renvoie ceci ?
SELECT * FROM pgbench_accounts LIMIT 100 ;
SELECT * FROM pgbench_accounts LIMIT 10; #
ERROR: out of memory DÉTAIL : Failed on request of size 536888061 in memory context "printtup".
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 perdues ?
=# SET zero_damaged_pages TO on ;
pgbenchSET
=# VACUUM FULL pgbench_accounts ;
pgbench VACUUM
=# SELECT * FROM pgbench_accounts LIMIT 100; pgbench
aid | bid | abalance | filler
-----+-----+----------+--------------------------------------------------------------------------------------
2 | 1 | 0 |
3 | 1 | 0 |
4 | 1 | 0 | [...]
=# SELECT min(aid), max(aid), count(aid) FROM pgbench_accounts ; pgbench
min | max | count
-----+---------+-------- 2 | 1000000 | 999999
Apparemment une ligne a disparu, celle portant la valeur 1 pour la clé. Il est rare que la perte soit aussi évidente !
Consulter le format et le contenu de la table
pgbench_branches
.
Cette petite table ne contient que 10 valeurs :
SELECT * FROM pgbench_branches ; #
bid | bbalance | filler
-----+----------+--------
255 | 0 |
2 | 0 |
3 | 0 |
4 | 0 |
5 | 0 |
6 | 0 |
7 | 0 |
8 | 0 |
9 | 0 |
10 | 0 | (10 lignes)
Retrouver les fichiers des tables
pgbench_branches
(par exemple avecpg_file_relationpath
).
SELECT pg_relation_filepath('pgbench_branches') ; #
pg_relation_filepath
---------------------- base/16454/16490
Pour corrompre la table :
- Arrêter PostgreSQL.
- Avec hexedit, dans le premier bloc en tête de fichier, remplacer les derniers caractères non nuls (
C0 9E 40
) parFF FF FF
.- En toute fin de fichier, remplacer le dernier
01
par unFF
.- Redémarrer PostgreSQL.
$ /usr/pgsql-15/bin/pg_ctl -D /var/lib/pgsql/15/data.BACKUP/ stop
$ hexedit /var/lib/pgsql/15/data.BACKUP/base/16454/16490
$ /usr/pgsql-15/bin/pg_ctl -D /var/lib/pgsql/15/data.BACKUP/ start
- Compter le nombre de lignes dans
pgbench_branches
.- Recompter après
SET enable_seqscan TO off ;
.- Quelle est la bonne réponse ? Vérifier le contenu de la table.
Les deux décomptes sont contradictoires :
=# SELECT count(*) FROM pgbench_branches ;
pgbenchcount
-------
9
=# SET enable_seqscan TO off ;
pgbenchSET
=# SELECT count(*) FROM pgbench_branches ;
pgbenchcount
-------
10
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 ; #
bid | bbalance | filler
-----+----------+--------
255 | 0 |
2 | 0 |
3 | 0 |
4 | 0 |
5 | 0 |
6 | 0 |
7 | 0 |
8 | 0 |
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 :
=# SET enable_seqscan TO off;
pgbenchSET
=# SELECT * FROM pgbench_branches
pgbenchWHERE bid = 1 ;
bid | bbalance | filler
-----+----------+-------- 255 | 0 |
Qu’affiche
pageinspect
pour cette table ?
=# CREATE EXTENSION pageinspect ;
pgbench
=# SELECT t_ctid, lp_off, lp_len, t_xmin, t_xmax, t_data
pgbenchFROM heap_page_items(get_raw_page('pgbench_branches',0));
t_ctid | lp_off | lp_len | t_xmin | t_xmax | t_data
--------+--------+--------+--------+--------+--------------------
(0,1) | 8160 | 32 | 63726 | 0 | \xff00000000000000
(0,2) | 8128 | 32 | 63726 | 0 | \x0200000000000000
(0,3) | 8096 | 32 | 63726 | 0 | \x0300000000000000
(0,4) | 8064 | 32 | 63726 | 0 | \x0400000000000000
(0,5) | 8032 | 32 | 63726 | 0 | \x0500000000000000
(0,6) | 8000 | 32 | 63726 | 0 | \x0600000000000000
(0,7) | 7968 | 32 | 63726 | 0 | \x0700000000000000
(0,8) | 7936 | 32 | 63726 | 0 | \x0800000000000000
(0,9) | 7904 | 32 | 63726 | 0 | \x0900000000000000
| 32767 | 127 | | | (10 lignes)
La première ligne indique bien que le 1 est devenu un 255.
La dernière ligne porte sur la première modification, qui a détruit
les informations sur le ctid
. Celle-ci est à présent
inaccessible.
Avec l’extension
amcheck
, essayer de voir si le problème peut être détecté. Si non, pourquoi ?
La documentation est sur https://docs.postgresql.fr/current/amcheck.html.
Une vérification complète se fait ainsi :
=# CREATE EXTENSTION amcheck ;
pgbench
=# SELECT bt_index_check (index => 'pgbench_branches_pkey',
pgbench=> true);
heapallindexed
bt_index_check----------------
1 ligne)
(
=# SELECT bt_index_parent_check (index => 'pgbench_branches_pkey',
pgbench=> true, rootdescend => true);
heapallindexed heap tuple (0,1) from table "pgbench_branches"
ERROR: index tuple within index "pgbench_branches_pkey" lacks matching
Un seul des problèmes a été repéré.
Un REINDEX
serait ici une mauvaise idée : c’est la table
qui est corrompue ! Les sommes de contrôle, là encore, auraient permis
de cibler le problème très tôt.
Pour voir ce que donnerait une restauration :
- Exporter
pgbench_accounts
, définition des index comprise.- Supprimer la table (il faudra supprimer
pgbench_history
aussi).- Tenter de la réimporter.
$ pg_dump -d pgbench -t pgbench_accounts -f /tmp/pgbench_accounts.dmp
$ psql pgbench -c 'DROP TABLE pgbench_accounts CASCADE'
NOTICE: drop cascades to constraint pgbench_history_aid_fkey on table pgbench_history
DROP TABLE
$ psql pgbench < /tmp/pgbench_accounts.dmp
SET
SET
SET
SET
SET
set_config
------------
(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 nombreuses 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.