Formation DBAADM
Dalibo SCOP
24.09
29 août 2024
Formation | Formation DBAADM |
Titre | PostgreSQL pour DBA expérimentés |
Révision | 24.09 |
ISBN | N/A |
https://dali.bo/dbaadm_pdf | |
EPUB | https://dali.bo/dbaadm_epub |
HTML | https://dali.bo/dbaadm_html |
Slides | https://dali.bo/dbaadm_slides |
Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :
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 12 à 16.
Quelle version utiliser ?
De M.m à M.m+n :
jsonb
pg_stat_progress_copy
, pg_stat_wal
,
pg_lock.waitstart
, query_id
…MERGE
DISTINCT
parallélisablepublic
n’est plus accessible en écriture à tousDISTINCT
…)pg_hba.conf
pg_stat_io
…Entre de nombreux autres :
PostgreSQL n’est que le moteur ! Besoin d’outils pour :
Entre autres, dédiés ou pas :
N’hésitez pas, c’est le moment !
Fonctionnalités du moteur
Objets SQL
Connaître les différentes fonctionnalités et possibilités
Découvrir des exemples concrets
Gestion transactionnelle : la force des bases de données relationnelles :
BEGIN
obligatoire pour grouper des modificationsCOMMIT
pour valider
ROLLBACK
/ perte de la connexion / arrêt (brutal ou
non) du serveurSAVEPOINT
pour sauvegarde des modifications d’une
transaction à un instant t
BEGIN ISOLATION LEVEL xxx;
read commited
(défaut)repeatable read
serializable
pg_dump
, pg_dumpall
,
pg_restore
pg_basebackup
CREATE EXTENSION monextension ;
pg_hba.conf
search_path
pg_catalog
, information_schema
Par défaut, une table est :
int
, float
numeric
, char
,
varchar
, date
, time
,
timestamp
, bool
jsonb
), XMLCHECK
prix > 0
NOT NULL
id_client NOT NULL
id_client UNIQUE
UNIQUE NOT NULL
==>
PRIMARY KEY (id_client)
produit_id REFERENCES produits(id_produit)
EXCLUDE
EXCLUDE USING gist (room WITH =, during WITH &&)
DEFAULT
GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY
GENERATED ALWAYS AS ( generation_expr ) STORED
SETOF
ou TABLE
pour plusieurs lignesINSERT
, UPDATE
,
DELETE
, TRUNCATE
FOR STATEMENT
)FOR EACH ROW
)N’hésitez pas, c’est le moment !
Étapes :
postgresql-<version>.tar.bz2
apt install postgresql-<version majeure>
initdb
pg_lsclusters
pg_ctlcluster
systemctl stop|start postgresql-15@main
pg_createcluster
/etc/postgresql/
/etc/apt/sources.list.d/pgdg.list
sudo dnf install -y \
https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf -qy module disable postgresql
sudo dnf install -y postgresql16-server
# dont : utilisateur postgres
sudo /usr/pgsql-16/bin/postgresql-16-setup initdb
sudo systemctl enable postgresql-16
sudo systemctl start postgresql-16
/usr/pgsql-XX/bin
: binairespostgresql-15-setup initdb
)
/var/lib/pgsql/XX/data
docker-compose
systemd
ou
rsyslog
pg_hba.conf
postgresql.conf
listen_addresses = '*'
(systématique)port = 5432
password_encryption
= scram-sha-256
(v10+)max_connections = 100
shared_buffers = (?)GB
work_mem
× hash_mem_multiplier
maintenance_work_mem
Pas de limite stricte à la consommation mémoire des sessions
fsync
= on
(si vous tenez à vos
données)
log_destination
logging_collector
postgresql-????.log
log_line_prefix
à compléter :
log_line_prefix = '%t [%p]: user=%u,db=%d,app=%a,client=%h '
lc_messages
= C
(anglais)Laisser ces deux paramètres à on
:
autovacuum
track_counts
pg_upgrade
pg_dumpall -g
puis pg_dump
psql
puis pg_restore
pg_upgrade
: fourni avec PostgreSQLSi vous migrez aussi l’OS ou déplacez les fichiers d’une instance :
Installation
N’hésitez pas, c’est le moment !
Les outils graphiques et console :
createdb
: ajouter une nouvelle base de donnéescreateuser
: ajouter un nouveau compte utilisateurdropdb
: supprimer une base de donnéesdropuser
: supprimer un compte utilisateurpg_dumpall
: sauvegarder l’instance PostgreSQLpg_dump
: sauvegarder une base de donnéespg_restore
: restaurer une base de données
PostgreSQLpg_basebackup
pg_verifybackup
vacuumdb
: récupérer l’espace inutilisé,
statistiquesclusterdb
: réorganiser une table en fonction d’un
indexreindexdb
: réindexerinitdb
: création d’instancepg_ctl
: lancer, arrêter, relancer, promouvoir
l’instancepg_upgrade
: migrations majeurespg_config
, pg_controldata
:
configurationpgbench
pour des testsPour se connecter à une base :
Option | Variable | Valeur par défaut |
---|---|---|
-h HÔTE |
$PGHOST | /tmp ,
/var/run/postgresql |
-p PORT |
$PGPORT | 5432 |
-U NOM |
$PGUSER | nom de l’utilisateur OS |
-d base |
$PGDATABASE | nom de l’utilisateur PG |
$PGOPTIONS | options de connexions |
psql
)
-W | --password
-w | --no-password
$PGPASSWORD
.pgpass
chmod 600 .pgpass
nom_hote:port:database:nomutilisateur:motdepasse
psql
\?
\h <COMMANDE>
\q
ou ctrl-D
quit
ou exit
(v11)\password nomutilisateur
\conninfo
SELECT current_user,session_user,system_user;
\c ma base
\c mabase utilisateur serveur 5432
Lister :
\l
, \l+
\d
, \d+
, \dt
,
\dt+
\di
, \di+
\dn
\df[+]
\du[+]
\dp
\ddp
ALTER DEFAULT PRIVILEGES
\drds
\dv
, \df
\sv
\sf
\dconfig
(v15+)\x
pour afficher un champ par ligneless
:
set PAGER='less -S'
\setenv PAGER 'pspg'
\gdesc
\gexec
\e
\ev nom_vue
\ef nom_fonction
\s
\i fichier.sql
\o resultat.out
\echo "Texte…"
\timing on
\! ls -l
(sur le client !)\cd /tmp
\getenv toto PATH
\set NOMVAR nouvelle_valeur
ON_ERROR_STOP
: on
/ off
ON_ERROR_ROLLBACK
: on
/ off
/ interactive
ROW_COUNT
: nombre de lignes renvoyées par la dernière
requête (v11)ERROR
: true
si dernière requête en erreur
(v11)SET
(au niveau du serveur) !\if
\elif
\else
\endif
~/.psqlrc
~/.psqlrc-X.Y
ou ~/.psqlrc-X
-X
.psqlrc
:psql
est en mode auto-commit par défaut
AUTOCOMMIT
BEGIN;
COMMIT;
ou ROLLBACK;
-1
(--single-transaction
)\encoding
SET client_encoding
DO $$
DECLARE r record;
BEGIN
FOR r IN (SELECT schemaname, relname
FROM pg_stat_user_tables
WHERE coalesce(last_analyze, last_autoanalyze) IS NULL
) LOOP
RAISE NOTICE 'Analyze %.%', r.schemaname, r.relname ;
EXECUTE 'ANALYZE ' || quote_ident(r.schemaname)
|| '.' || quote_ident(r.relname) ;
END LOOP;
END$$;
ON_ERROR_ROLLBACK
ON_ERROR_STOP
-XAt
-t
(--tuples-only
)-A
(--no-align
)-X
(--no-psqlrc
)-F
(--field-separator
) et
-R
(--record-separator
)-H | --html
--csv
(à partir de la version 12)\crosstabview [colV [colH [colD [colonnedetriH]]]]
\pset title 'Résultat de la requête
’\pset format html
(ou csv
…)cron
#!/bin/bash
# Paramètre : la base
t=$(mktemp) # fichier temporaire
pg_dump -Fc "$1" > $t # sauvegarde
d=$(eval date +%d%m%y-%H%M%S) # date
mv $t /backup/"${1}_${d}.dump" # déplacement
exit 0
N’hésitez pas, c’est le moment !
pg_database
psql
:
\l
\l+
template1
CREATE DATABASE
SUPERUSER
ou
CREATEDB
createdb
DROP DATABASE
SUPERUSER
ou propriétaire de la
basedropdb
ALTER DATABASE
pg_db_role_setting
CREATE/DROP/ALTER USER
CREATE/DROP/ALTER GROUP
pg_roles
psql
: \du
CREATE ROLE
SUPERUSER
ou
CREATEROLE
createuser
LOGIN
par défautDROP ROLE
SUPERUSER
ou
CREATEROLE
dropuser
ALTER ROLE
pg_db_role_setting
Le mot de passe ne concerne pas toutes les méthodes d’authentification
\password
ou createuser
GRANT USAGE ON SCHEMA unschema TO utilisateur ;
GRANT SELECT,DELETE,INSERT ON TABLE matable TO utilisateur ;
public
public
lisible par tous
(≤ 14) !*acl
sur les tables systèmes
datacl
pour pg_database
relacl
pour pg_class
role1=xxxx/role2
(format aclitem
)
role1
: rôle concerné par les droitsxxxx
: droits parmi xxxx
role2
: rôle qui a donné les droitspsql
:
\dp
et \z
df
, \dconfig+
etc..REASSIGN OWNER
/ DROP OWNED
pg_signal_backend
pg_database_owner
(14+)pg_checkpoint
(15+)pg_reserved_connections
(16+)pg_read_all_stats
pg_read_all_settings
pg_stat_scan_tables
pg_monitor
pg_read_server_files
pg_write_server_files
pg_execute_server_program
pg_read_all_data
pg_write_all_data
pg_create_subscription
(16+)SET ROLE
Pour se connecter à une base, il faut :
PostgreSQL utilise les informations de connexion pour choisir la méthode de connexion
pg_hba.conf
local DATABASE USER METHOD [OPTIONS]
host DATABASE USER ADDRESS METHOD [OPTIONS]
hostssl DATABASE USER ADDRESS METHOD [OPTIONS]
hostnossl DATABASE USER ADDRESS METHOD [OPTIONS]
include
, include_if_exists
,
include_dir
(v16)pg_hba_file_rules
# TYPE DATABASE USER ADDRESS METHOD
# accès direct par la socket
local all all peer
# accès en local via réseau (localhost IPv6 et IPv4)
host all all ::1/128 scram-sha-256
host all all 127.0.0.0/24 scram-sha-256
# accès distants
hostssl erp all 192.168.0.0/16 scram-sha-256
hostssl logistique all 192.168.30.0/24 scram-sha-256
hostnossl test demo 192.168.74.150/32 scram-sha-256
# vieux client
hostssl compta durand 192.168.10.99/32 md5
# Connexions de réplication
local replication all peer
host replication all 192.168.74.5/32 scram-sha-256
host replication all 127.0.0.1/32 scram-sha-256
host replication all ::1/128 scram-sha-256
local
host
hostssl
(ssl=on
prérequis)hostnossl
À quelle(s) base(s) autoriser la connexion ?
mabase
base1,base2,base3
sameuser
/ samerole
all
@fichier.txt
(fichier avec plusieurs bases)/db_[1-6]
, /erp.*
(regex, v16+)replication
(pseudo-base pour réplication
physique)À quel(s) utilisateur(s) permettre la connexion ?
unrole
role1,role2,role3
all
+groupe
(membres d’un rôle-groupe)@fichier.txt
(fichier avec plusieurs utilisateurs)/user_.*
(regex, v16+)host
/ hostssl
/
hostnossl
192.168.1.0/24
192.168.1.1/32
192.168.1.0 255.255.255.0
Quelle méthode d’authentification utiliser ?
map
trust
: dangereux !reject
: pour interdirepassword
: en clair, à éviter !md5
: déconseilléescram-sha-256
ldap
, radius
gss
, sspi
cert
peer
, pam
, ident
,
bsd
VACUUM
ANALYZE
REINDEX
VACUUM ANALYZE table
(batchs, gros
imports…)VACUUM nomtable ;
pg_stat_progress_vacuum
vacuumdb --echo
VACUUM FULL nomtable ;
pg_stat_progress_cluster
(v12)VACUUM
VACUUM FULL
ALTER TABLE
, tables
temporaires…default_statistics_target
(défaut 100)ALTER TABLE ma_table ALTER ma_colonne SET STATISTICS 500;
pg_stat_progress_analyze
(v13)REINDEX
régulièrement permet
VACUUM
ne provoque pas de réindexationVACUUM FULL
réindexeCONCURRENTLY
(v12+)TABLESPACE
(v14+)CLUSTER
VACUUM FULL
CLUSTER
nécessite près du double de l’espace
disque utilisé pour stocker la table et ses indexpg_stat_progress_cluster
VACUUM
/ANALYZE
si nécessaireREINDEX
CREATE INDEX CONCURRENTLY
Un utilisateur standard peut :
CONNECT
: accéder à toutes les bases de donnéesCREATE
:
public
de
toute base de donnéesSELECT
: voir les données de ses tablesINSERT
,UPDATE
,DELETE
,TRUNCATE
: les modifierTEMP
: créer des objets temporairesCREATE
, USAGE ON LANGUAGE
: créer des
fonctionsEXECUTE
: exécuter des fonctions définies par d’autres
dans le schéma public
Un utilisateur standard peut aussi :
pg_settings
psql
: \dconfig
pg_hba.conf
GRANT
/ REVOKE
SECURITY LABEL
SECURITY DEFINER
LEAKPROOF
security_barrier
WITH CHECK OPTION
pg_cancel_backend (pid int)
pg_terminate_backend(pid int, timeout bigint)
kill -SIGTERM pid
, kill -15 pid
(éviter)kill -9
!!ANALYZE
(voire VACUUM
)initdb --data-checksums
N’hésitez pas, c’est le moment !
La politique de sauvegarde découle du :
pg_dump
pg_dumpall
Format | Dump | Restore |
---|---|---|
plain (SQL) | pg_dump -Fp ou pg_dumpall |
psql |
tar | pg_dump -Ft |
pg_restore |
custom | pg_dump -Fc |
pg_restore |
directory | pg_dump -Fd |
pg_restore |
-f
: fichier où stocker la sauvegarde--schema-only
/ -s
: uniquement la
structure--data-only
/ -a
: uniquement les
données--section
pre-data
: définition des objets (hors contraintes et
index)data
: les donnéespost-data
: définition des contraintes et index-n <schema>
: uniquement ce schéma-N <schema>
: tous les schémas sauf celui-là-t <table>
: uniquement cette relation (sans
dépendances !)-T <table>
: toutes les tables sauf celle-là--exclude-table-data=<table>
--strict-names
--jobs <nombre_de_threads>
-Fd
) uniquement--create
(-C
) : recréer la base
plain
)--no-owner
: ignorer le propriétaire
--no-privileges
: ignorer les droits
--no-tablespaces
: ignorer les tablespaces
--inserts
: remplacer COPY
par
INSERT
--rows-per-insert
,
--on-conflict-do-nothing
divers paramètres pour les tables partitionnées
-v
: progression
-f nomfichier.dmp
-f -
-g
: tous les objets globaux-r
: uniquement les rôles-t
: uniquement les tablespaces--no-role-passwords
: sans les mots de passe
--exclude-database
(v12+)pg_dump
-h
/ $PGHOST
/ socket Unix-p
/ $PGPORT
/ 5432-U
/ $PGUSER
/ utilisateur du système-W
/ $PGPASSWORD
.pgpass
pg_read_all_data
)pg_dump | bzip2
plain
,tar
,custom
pbzip2
, pigz
xz
, zstd
…-b
-n
/-N
et/ou
-t
/-T
--no-blobs
bytea_output
escape
hex
--extension
(-e
)
-n
/-N
et/ou -t
/-T
)psql
-Fp
) :pg_restore
-Ft
/-Fc
/-Fd
)-f
-f
: lit l’entrée standard-1
(--single-transaction
)
-e
ON_ERROR_ROLLBACK
/ON_ERROR_STOP
-F
inutile)-d
: base de données de connexion-C
(--create
) :
-d
) et CREATE DATABASE
-f
: fichier SQL ou liste (-l
)-f -
--schema-only
: uniquement la structure--data-only
: uniquement les donnéesou :
--section
pre-data
data
post-data
-n <schema>
: uniquement ce schéma-N <schema>
: tous les schémas sauf ce
schéma-t <table>
: cette relation-T <trigger>
: ce trigger-I <index>
: cet index-P <fonction>
: cette fonction--strict-names
, pour avoir une erreur si l’objet est
inconnu-l
: récupération de la liste des objets-L <liste_objets>
: restauration uniquement des
objets listés dans ce fichier-j <nombre_de_threads>
custom
ou directory
-O
(--no-owner
) : ignorer le
propriétaire-x
(--no-privileges
) : ignorer les
droits--no-comments
: ignorer les commentaires--no-tablespaces
: ignorer le tablespace-1
(--single-transaction
) : pour tout
restaurer en une seule transaction-c
(--clean
) : pour détruire un objet
avant de le restaurerpg_dump
: reconnaît les versions de PG antérieurespg_restore
pg_dump
utilisépg_dump
)pg_dump -Fp | psql
pg_dump -Ft | pg_restore
pg_dump -Fc | pg_restore
-h
, -p
,
-d
ANALYZE
impérativement après une restauration !VACUUM
(ou VACUUM ANALYZE
)VACUUM FREEZE
-v
COPY
pg_stat_progress_copy
(v14+)ANALYZE
, VACUUM ANALYZE
,
VACUUM FREEZE
après importcp
, tar
…rsync
en 2 étapes : à chaud puis à froidpg_basebackup
Simplicité | Coupure | Restauration | Fragmentation | |
---|---|---|---|---|
copie à froid | facile | longue | rapide | conservée |
snapshot FS | facile | aucune | rapide | conservée |
pg_dump | facile | aucune | lente | perdue |
rsync + copie à froid | moyen | courte | rapide | conservée |
PITR | difficile | aucune | rapide | conservée |
N’hésitez pas, c’est le moment !
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.
PostgreSQL est :
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.
# ps f -e --format=pid,command | grep -E "postgres|postmaster"
96122 /usr/pgsql-15/bin/postmaster -D /var/lib/pgsql/15/data/
96123 \_ postgres: logger
96125 \_ postgres: checkpointer
96126 \_ postgres: background writer
96127 \_ postgres: walwriter
96128 \_ postgres: autovacuum launcher
96131 \_ postgres: logical replication launcher
(sous Rocky Linux 8)
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.
postmaster
background writer
checkpointer
walwriter
autovacuum launcher
stats collector
(avant v15)logical replication launcher
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.
max_connections
(défaut : 100) - connexions réservées
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).
Structure de la mémoire sous PostgreSQL
work_mem
)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),
postgres$ ls $PGDATA
base pg_ident.conf pg_stat pg_xact
current_logfiles pg_logical pg_stat_tmp postgresql.auto.conf
global pg_multixact pg_subtrans postgresql.conf
log pg_notify pg_tblspc postmaster.opts
pg_commit_ts pg_replslot pg_twophase postmaster.pid
pg_dynshmem pg_serial PG_VERSION
pg_hba.conf pg_snapshots pg_wal
Le répertoire de données est souvent appelé PGDATA, du nom de la
variable d’environnement que l’on peut faire pointer vers lui pour
simplifier l’utilisation de nombreux utilitaires PostgreSQL. Il est
possible aussi de le connaître, une fois connecté à une base de
l’instance, en interrogeant le paramètre
data_directory
.
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.
postgresql.conf
( + fichiers inclus)postgresql.auto.conf
pg_hba.conf
( + fichiers inclus (v16))pg_ident.conf
(idem)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
: fichier contenant la version majeure de
l’instancepostmaster.pid
external_pid_file
postmaster.opts
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 :
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 :
base/
: contient les fichiers de données
pgsql_tmp
: fichiers temporairesglobal/
: contient les objets globaux à toute
l’instancebase/
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');
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 :
alors nous pourrons suivre les fichiers temporaires depuis une autre session :
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.
pg_wal/
: journaux de transactions
pg_xlog/
avant la v10archive_status
00000002 00000142 000000FF
pg_xact/
: état des transactions
pg_clog/
avant la v10pg_commit_ts/
, pg_multixact/
,
pg_serial/
pg_snapshots/
, pg_subtrans/
,
pg_twophase/
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()
:
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/
pg_repslot/
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.
pg_tblspc/
: tablespaces
Dans PGDATA, le sous-répertoire pg_tblspc
contient les
tablespaces, c’est-à-dire des espaces de stockage.
Sous Linux, ce sont des liens symboliques vers un simple répertoire
extérieur à PGDATA. Chaque lien symbolique a comme nom l’OID du
tablespace (table système pg_tablespace
). PostgreSQL y crée
un répertoire lié aux versions de PostgreSQL et du catalogue, et y place
les fichiers de données.
postgres=# \db+
Liste des tablespaces
Nom | Propriétaire | Emplacement | … | Taille |…
------------+--------------+-----------------------+---+---------+-
froid | postgres | /HDD/tbl/froid | | 3576 kB |
pg_default | postgres | | | 6536 MB |
pg_global | postgres | | | 587 kB |
/HDD/tbl/froid:
PG_15_202209061
/HDD/tbl/froid/PG_15_202209061:
5
/HDD/tbl/froid/PG_15_202209061/5:
142532 142532_fsm 142532_vm
Sous Windows, les liens sont à proprement parler des Reparse Points (ou Junction Points) :
postgres=# \db
Liste des tablespaces
Nom | Propriétaire | Emplacement
------------+--------------+-------------
pg_default | postgres |
pg_global | postgres |
tbl1 | postgres | T:\TBL1
PS P:\PGDATA13> dir 'pg_tblspc/*' | ?{$_.LinkType} | select FullName,LinkType,Target
FullName LinkType Target
-------- -------- ------
P:\PGDATA13\pg_tblspc\105921 Junction {T:\TBL1}
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.
Statistiques d’activité :
stats collector
(≤v14) & extensionspg_stat_tmp/
: temporairespg_stat/
: définitifpg_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
:
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/
pg_notify/
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 :
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.
PostgreSQL est complexe, avec de nombreux composants
Une bonne compréhension de cette architecture est la clé d’une bonne administration.
Pour aller (beaucoup) plus loin :
N’hésitez pas, c’est le moment !
But : voir quelques processus de PostgreSQL
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 :
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 :
Se connecter une seizième fois à l’instance PostgreSQL. Qu’observe-t-on ?
Configurer la valeur du paramètre
max_connections
à sa valeur initiale.
But : voir les fichiers de PostgreSQL
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 :
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.
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 :
Alternativement, depuis la session déjà ouverte dans
psql
:
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 :
ou depuis la session psql
:
Création de la table :
Insérer 10 millions de lignes dans la table
t1
avec :
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
:
La seconde méthode se fait directement depuis psql en tant que superutilisateur :
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 :
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 :
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 ?
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 :
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 :
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 :
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.
$ 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
: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
?
$ 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
.
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 :
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
?
$ 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
?
$ 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 :
b1=# SELECT CASE WHEN attnum<0 THEN 'systeme' ELSE 'utilisateur' END AS type,
attname, attnum, typname, typlen,
sum(typlen) OVER (PARTITION BY attnum<0) AS longueur_tot
FROM pg_attribute a
JOIN pg_type t ON t.oid=a.atttypid
WHERE attrelid ='t1'::regclass
ORDER BY attnum;
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 :
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).
initdb
block_size
: 8 kowal_block_size
: 8 kosegment_size
: 1 Gowal_segment_size
: 16 Mo (option
--wal-segsize
d’initdb
en v11)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.)
postgresql.conf
postgresql.auto.conf
pg_hba.conf
pg_ident.conf
Les fichiers de configuration sont habituellement les 4 suivants :
postgresql.conf
: il contient une liste de paramètres,
sous la forme paramètre=valeur
. Tous les paramètres énoncés
précédemment sont modifiables (et présents) dans ce fichier ;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
.Fichier principal de configuration :
/var/lib/…
)/etc/postgresql/<version>/<nom>/postgresql.conf
clé = valeur
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 :
config_file
---------------------------------------------
/var/lib/postgresql/15/data/postgresql.conf
Ce fichier contient un paramètre par ligne, sous le format :
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.
include
,
include_if_exists
ALTER SYSTEM SET …
( renseigne
postgresql.auto.conf
)pg_ctl
ALTER DATABASE | ROLE … SET paramètre = …
SET
/ SET LOCAL
SHOW
pg_settings
pg_file_settings
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 :
Ce fichier est principalement utilisé par les administrateurs et les outils qui n’ont pas accès au système de fichiers.
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 :
ou même juste au sein d’une transaction :
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)
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 | notice | 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 debug
. Nous pouvons aussi voir le
contexte dans lequel le paramètre est modifiable : le
client_min_messages
est modifiable par l’utilisateur dans
sa session. Le log_checkpoints
seulement par
sighup
, c’est-à-dire par un pg_ctl 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() ;
-[ 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.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.
pg_hba.conf
(Host Based Authentication)pg_ident.conf
: si mécanisme externe
d’authentificationhba_file
et ident_file
L’authentification est paramétrée au moyen du fichier
pg_hba.conf
. Dans ce fichier, pour une tentative de
connexion à une base donnée, pour un utilisateur donné, pour un
transport (IP, IPV6, Socket Unix, SSL ou non), et pour une source
donnée, ce fichier permet de spécifier le mécanisme d’authentification
attendu.
Si le mécanisme d’authentification s’appuie sur un système externe
(LDAP, Kerberos, Radius…), des tables de correspondances entre
utilisateur de la base et utilisateur demandant la connexion peuvent
être spécifiées dans pg_ident.conf
.
Ces noms de fichiers ne sont que les noms par défaut. Ils peuvent
tout à fait être remplacés en spécifiant de nouvelles valeurs de
hba_file
et ident_file
dans
postgresql.conf
(les installations Red Hat et Debian
utilisent là aussi des emplacements différents, comme pour
postgresql.conf
).
Leur utilisation est décrite dans notre première 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 :
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, voire deux niveaux en-dessous du point de montage.
Par exemple, déclarez votre PGDATA dans
/<point de montage>/<version majeure>/<nom instance>
plutôt que directement dans /<point de montage>
.
Un tablespace ira dans
/<autre point de montage>/<nom répertoire>/
plutôt que directement dans
/<autre point de montage>/
.
(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 :
Il est aussi déconseillé de mettre le numéro de version de PostgreSQL
dans le chemin du tablespace. PostgreSQL le gère à l’intérieur du
tablespace, et en tient notamment compte dans les migrations avec
pg_upgrade
.
CREATE TABLESPACE chaud LOCATION '/SSD/tbl/chaud';
CREATE DATABASE nom TABLESPACE 'chaud';
ALTER DATABASE nom SET default_tablespace TO 'chaud';
GRANT CREATE ON TABLESPACE chaud TO un_utilisateur ;
CREATE TABLE une_table (…) TABLESPACE chaud ;
ALTER TABLE une_table SET TABLESPACE chaud ; -- verrou !
ALTER INDEX une_table_i_idx SET TABLESPACE chaud ; -- pas automatique
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) :
Les ordres SQL plus haut permettent de :
Quelques choses à savoir :
La table ou l’index est totalement verrouillé le temps du déplacement.
Les index existants ne « suivent » pas automatiquement une table déplacée, il faut les déplacer séparément.
Par défaut, les nouveaux index ne sont pas créés
automatiquement dans le même tablespace que la table, mais en fonction
de default_tablespace
.
Les tablespaces des tables sont visibles dans la vue système
pg_tables
, dans \d+
sous psql, et dans
pg_indexes
pour les index :
default_tablespace
temp_tablespaces
seq_page_cost
, random_page_cost
effective_io_concurrency
,
maintenance_io_concurrency
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 critique SET default_tablespace TO 'chaud' ; -- base
ALTER ROLE etl SET default_tablespace TO 'chaud' ; -- rôle
SET default_tablespace TO 'chaud' ; -- 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 :
Si plusieurs tablespaces de tri sont paramétrés, chaque transaction en choisira un de façon aléatoire à la création d’un objet temporaire, puis utilisera alternativement chaque tablespace. Un gros tri sera donc étalé sur plusieurs de ces tablespaces. afin de répartir la charge.
Paramètres de performances :
Dans le cas de disques de performances différentes, il faut adapter les paramètres concernés aux 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) ;random_page_cost
(coût d’accès à un bloc isolé) ;effective_io_concurrency
(nombre d’I/O simultanées) et
maintenance_io_concurrency
(idem, pour une opération de
maintenance).Notamment : effective_io_concurrency
a pour but
d’indiquer le nombre d’opérations disques possibles en même temps pour
un client (prefetch). Seuls les parcours Bitmap Scan
sont impactés par ce paramètre. Selon la 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. À l’inverse, il faut tenir
compte du nombre de requêtes simultanées qui utiliseront ce nœud. Le
défaut est seulement de 1, et la valeur maximale est 1000. Attention, à
partir de la version 13, le principe reste le même, mais la valeur
exacte de ce paramètre doit être 2 à 5 fois plus élevée qu’auparavant,
selon la formule des notes de
version.
Toujours en version 13 apparaît
maintenance_io_concurrency
. 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. Le défaut est de 10, et il faut penser à le
monter aussi si on adapte effective_io_concurrency
.
Par exemple, sur un système paramétré pour des disques classiques, un tablespace sur un SSD peut porter ces paramètres :
L’accès à la base se fait par un protocole réseau clairement défini :
Les demandes de connexion sont gérées par le postmaster.
Paramètres : port
, listen_adresses
,
unix_socket_directories
, unix_socket_group
et
unix_socket_permissions
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.
tcp_keepalives_idle
tcp_keepalives_interval
tcp_keepalives_count
client_connection_check_interval
(v14)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.
ssl
, ssl_ciphers
,
ssl_renegotiation_limit
Il existe des options pour activer SSL et le paramétrer.
ssl
vaut on
ou off
,
ssl_ciphers
est la liste des algorithmes de chiffrement
autorisés, et ssl_renegotiation_limit
le volume maximum de
données échangées sur une session avant renégociation entre le client et
le serveur. Le paramétrage SSL impose aussi la présence d’un certificat.
Pour plus de détails, consultez la documentation
officielle.
track_activities
,
track_activity_query_size
track_counts
, track_io_timing
et
track_functions
update_process_title
stats_temp_directory
(< v15)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.
INSERT
, SELECT
…) par table
et indexSupervision / métrologie
Diagnostiquer
Vues système :
pg_stat_user_*
pg_statio_user_*
pg_stat_activity
: requêtespg_stat_bgwriter
pg_locks
PostgreSQL propose de nombreuses vues, accessibles en SQL, pour obtenir des informations sur son fonctionnement interne. Il est possible d’avoir des informations sur le fonctionnement des bases, des processus d’arrière-plan, des tables, les requêtes en cours…
Pour les statistiques aux objets, le système fournit à chaque fois trois vues différentes :
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
.
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).
Des statistiques globales par base sont aussi disponibles, dans
pg_stat_database
: le nombre de transactions validées et
annulées, quelques statistiques sur les sessions, et quelques
statistiques sur les accès physiques et en cache, ainsi que sur les
opérations logiques.
pg_stat_bgwriter
stocke les statistiques d’écriture des
buffers des Background Writer, Checkpointer et des sessions
elles-mêmes.
pg_stat_activity
est une des vues les plus utilisées et
est souvent le point de départ d’une recherche : elle donne des
informations sur les processus en cours sur l’instance, que ce soit des
processus en tâche de fond ou des processus backends associés aux
clients : numéro de processus, adresse et port, date de début d’ordre,
de transaction, de session, requête en cours, état, ordre SQL et nom de
l’application si elle l’a renseigné. (Noter qu’avant la version 10,
cette vue n’affichait que les processus backend ; à partir de la version
10 apparaissent des workers, le checkpointer, le walwriter… ; à partir
de la version 14 apparaît le processus d’archivage).
=# SELECT datname, pid, usename, application_name, backend_start, state, backend_type, query
FROM pg_stat_activity \gx
-[ RECORD 1 ]----+-------------------------------------------------------------
datname | ¤
pid | 26378
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.236776+02
state | ¤
backend_type | autovacuum launcher
query |
-[ RECORD 2 ]----+-------------------------------------------------------------
datname | ¤
pid | 26380
usename | postgres
application_name |
backend_start | 2019-10-24 18:25:28.238157+02
state | ¤
backend_type | logical replication launcher
query |
-[ RECORD 3 ]----+-------------------------------------------------------------
datname | pgbench
pid | 22324
usename | test_performance
application_name | pgbench
backend_start | 2019-10-28 10:26:51.167611+01
state | active
backend_type | client backend
query | UPDATE pgbench_accounts SET abalance = abalance + -3810 WHERE…
-[ RECORD 4 ]----+-------------------------------------------------------------
datname | postgres
pid | 22429
usename | postgres
application_name | psql
backend_start | 2019-10-28 10:27:09.599426+01
state | active
backend_type | client backend
query | select datname, pid, usename, application_name, backend_start…
-[ RECORD 5 ]----+-------------------------------------------------------------
datname | pgbench
pid | 22325
usename | test_performance
application_name | pgbench
backend_start | 2019-10-28 10:26:51.172585+01
state | active
backend_type | client backend
query | UPDATE pgbench_accounts SET abalance = abalance + 4360 WHERE…
-[ RECORD 6 ]----+-------------------------------------------------------------
datname | pgbench
pid | 22326
usename | test_performance
application_name | pgbench
backend_start | 2019-10-28 10:26:51.178514+01
state | active
backend_type | client backend
query | UPDATE pgbench_accounts SET abalance = abalance + 2865 WHERE…
-[ RECORD 7 ]----+-------------------------------------------------------------
datname | ¤
pid | 26376
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.235574+02
state | ¤
backend_type | background writer
query |
-[ RECORD 8 ]----+-------------------------------------------------------------
datname | ¤
pid | 26375
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.235064+02
state | ¤
backend_type | checkpointer
query |
-[ RECORD 9 ]----+-------------------------------------------------------------
datname | ¤
pid | 26377
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.236239+02
state | ¤
backend_type | walwriter
query |
Cette vue fournit aussi des informations sur ce que chaque session
attend. Pour les détails sur wait_event_type
(type
d’événement en attente) et wait_event
(nom de l’événement
en attente), voir le tableau des événements
d’attente.
# SELECT datname, pid, wait_event_type, wait_event, query FROM pg_stat_activity
WHERE backend_type='client backend' AND wait_event IS NOT NULL \gx
-[ RECORD 1 ]---+--------------------------------------------------------------
datname | pgbench
pid | 1590
state | idle in transaction
wait_event_type | Client
wait_event | ClientRead
query | UPDATE pgbench_accounts SET abalance = abalance + 1438 WHERE…
-[ RECORD 2 ]---+--------------------------------------------------------------
datname | pgbench
pid | 1591
state | idle
wait_event_type | Client
wait_event | ClientRead
query | END;
-[ RECORD 3 ]---+--------------------------------------------------------------
datname | pgbench
pid | 1593
state | idle in transaction
wait_event_type | Client
wait_event | ClientRead
query | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES…
-[ RECORD 4 ]---+--------------------------------------------------------------
datname | postgres
pid | 1018
state | idle in transaction
wait_event_type | Client
wait_event | ClientRead
query | delete from t1 ;
-[ RECORD 5 ]---+--------------------------------------------------------------
datname | postgres
pid | 1457
state | active
wait_event_type | Lock
wait_event | transactionid
query | delete from t1 ;
Des vues plus spécialisées existent :
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
.
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_locks
permet de voir les verrous posés sur les objets
(principalement les relations comme les tables et les index).
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_archiver
donne des informations sur l’archivage
des wals et notamment sur les erreurs d’archivage.
pg_stats
default_statistics_target
)ANALYZE table
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, nombre de valeurs distinctes,
si négatif, fraction de valeurs distinctes pour cette colonne dans la
table. Il est possible de forcer le nombre de valeurs distinctes, s’il
est constaté que la collecte des statistiques n’y arrive pas :
ALTER TABLE xxx ALTER COLUMN yyy SET (n_distinct = -0.5) ; ANALYZE xxx;
par exemple indique à l’optimiseur que chaque valeur apparaît
statistiquement deux fois ;most_common_vals
et most_common_freqs
:
les valeurs les plus fréquentes de la table, et leur fréquence. Le
nombre de valeurs collectées est au maximum celui indiqué par le
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é par
ALTER TABLE matable ALTER COLUMN macolonne SET statistics 300 ;
(avec une évolution proportionnelle du nombre de lignes consultées)
sachant que le temps de planification augmente exponentiellement et
qu’il vaut mieux ne pas dépasser la valeur 1000 ;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
).
SQL est un langage déclaratif :
Le langage SQL décrit le résultat souhaité. Par exemple :
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).
seq_page_cost
, random_page_cost
,
cpu_tuple_cost
, cpu_index_tuple_cost
,
cpu_operator_cost
parallel_setup_cost
,
parallel_tuple_cost
effective_cache_size
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. 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
intéressante.
effective_io_concurrency
a pour but d’indiquer le nombre
d’opérations disques possibles en même temps pour un client
(prefetch). Seuls les parcours Bitmap Scan sont
impactés par ce paramètre. Selon la 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. À l’inverse, il faut tenir
compte du nombre de requêtes simultanées qui utiliseront ce nœud. Le
défaut est seulement de 1, et la valeur maximale est 1000. Attention, à
partir de la version 13, le principe reste le même, mais la valeur
exacte de ce paramètre doit être 2 à 5 fois plus élevée qu’auparavant,
selon la formule des notes de
version.
Toujours à partir de la version 13, un nouveau paramètre apparaît :
maintenance_io_concurrency
. Il a le même sens que
effective_io_concurrency
, mais pour les opérations de
maintenance, non les requêtes. Celles-ci peuvent ainsi se voir accorder
plus de ressources qu’une simple requête. Le défaut est de 10, et il
faut penser à le monter aussi si nous adaptons
effective_io_concurrency
.
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.
constraint_exclusion
enable_partition_pruning
from_collapse_limit
&
join_collapse_limit
(défaut : 8)plan_cache_mode
cursor_tuple_fraction
synchronize_seqscans
Tous les paramètres suivants peuvent être modifiés par session.
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 nouveau partitionnement déclaratif,
enable_partition_pruning
, activé par défaut, est le
paramètre équivalent.
Pour limiter la complexité des plans d’exécution à étudier, il est
possible de limiter la quantité de réécriture autorisée par l’optimiseur
via les paramètres from_collapse_limit
et
join_collapse_limit
. Le premier interdit que plus de 8 (par
défaut) tables provenant d’une sous-requête ne soient déplacées dans la
requête principale. Le second interdit que plus de 8 (par défaut) tables
provenant de clauses JOIN
ne soient déplacées vers la
clause FROM
. Ceci réduit la qualité du plan d’exécution
généré, mais permet qu’il soit généré dans un temps raisonnable. Il est
fréquent de monter les valeurs à 10 ou un peu au-delà si de longues
requêtes impliquent beaucoup de tables.
Pour les requêtes préparées, l’optimiseur génère des plans
personnalisés pour les cinq premières exécutions d’une requête préparée,
puis il bascule sur un plan générique dès que celui-ci devient plus
intéressant que la moyenne des plans personnalisés précédents. Ceci
décrit le mode auto
en place depuis de nombreuses versions.
Depuis la version 12, il est possible de modifier ce comportement grâce
au paramètre de configuration plan_cache_mode
:
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, et l’on conserve 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.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.
Quand plusieurs requêtes souhaitent accéder séquentiellement à la
même table, les processus se rattachent à ceux déjà en cours de
parcours, afin de profiter des entrées-sorties que ces processus
effectuent, le but étant que le système se comporte comme si un seul
parcours de la table était en cours, et réduise donc fortement la charge
disque. Le seul problème de ce mécanisme est que les processus se
rattachant ne parcourent pas la table dans son ordre physique : elles
commencent leur parcours de la table à l’endroit où se trouve le
processus auquel elles se rattachent, puis rebouclent sur le début de la
table. Les résultats n’arrivent donc pas forcément toujours dans le même
ordre, ce qui n’est normalement pas un problème (on est censé utiliser
ORDER BY
dans ce cas). Mais il est toujours possible de
désactiver ce mécanisme en passant synchronize_seqscans
à
off
.
geqo
& geqo_threshold
(12
tables)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 évoqués ci-dessous, il est déconseillé de modifier les paramètres 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.Malgré l’introduction de ces mutations aléatoires, le moteur arrive
tout de même à conserver un fonctionnement déterministe. Tant que le
paramètre geqo_seed
ainsi que les autres paramètres
contrôlant GEQO restent inchangés, le plan obtenu pour une requête
donnée restera inchangé. Il est donc possible de faire varier la valeur
de geqo_seed
pour chercher d’autres plans (voir
la documentation officielle).
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
.N’hésitez pas, c’est le moment !
But : Ajouter un tablespace
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 ?
But : Consulter les statistiques d’activité
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 ?
But : Consulter les statistiques sur les données
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 :
En tant qu’utilisateur 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
.
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 :
b1=# DROP TABLESPACE ts1 ;
ERROR: tablespace "ts1" is not empty
b1=# ALTER TABLE t_dans_ts1 SET TABLESPACE pg_default ;
ALTER TABLE
b1=# DROP TABLESPACE ts1 ;
DROP TABLESPACE
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
.
-[ 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.
b1=# CREATE ROLE pgbench LOGIN ;
CREATE ROLE
b1=# CREATE DATABASE pgbench OWNER pgbench ;
CREATE 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
:
Une jointure est possible avec la table pg_roles
pour
déterminer le propriétaire des bases :
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",
pg_catalog.pg_get_userbyid(d.datdba) as "Owner",
pg_catalog.pg_encoding_to_char(d.encoding) as "Encoding",
d.datcollate as "Collate",
d.datctype as "Ctype",
pg_catalog.array_to_string(d.datacl, E'\n') AS "Access privileges"
FROM pg_catalog.pg_database d
ORDER BY 1;
**************************
L’encodage se retrouve donc en appelant la fonction
pg_encoding_to_char
:
b1=# SELECT db.datname, r.rolname, db.encoding, pg_catalog.pg_encoding_to_char(db.encoding)
FROM 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 ?
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 :
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
.
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
.
Rechercher la ligne ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
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
.
Rechercher la ligne ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
QUERY PLAN
--------------------------------------------------------------------
Gather (cost=1000.00..10633.43 rows=1 width=4)
Workers Planned: 2
-> Parallel Seq Scan on t4 (cost=0.00..9633.33 rows=1 width=4)
Filter: (id = 100000)
Les statistiques sont beaucoup plus précises. PostgreSQL sait maintenant qu’il ne va récupérer qu’une seule ligne, sur le million de lignes dans la table. C’est le cas typique où un index serait intéressant.
Ajouter un index sur la colonne
id
de la tablet4
.
Rechercher la ligne ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
QUERY PLAN
-------------------------------------------------------------------------
Index Only Scan using t4_id_idx on t4 (cost=0.42..8.44 rows=1 width=4)
Index Cond: (id = 100000)
Après création de l’index, nous constatons que PostgreSQL choisit un autre plan qui permet d’utiliser cet index.
Modifier le contenu de la table
t4
avecUPDATE t4 SET id = 100000;
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.
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
.
Rechercher les lignes ayant comme valeur
100000
dans la colonneid
et afficher le plan d’exécution.
QUERY PLAN
----------------------------------------------------------
Seq Scan on t4
(cost=0.00..21350.00 rows=1000000 width=4)
(actual time=75.185..186.019 rows=1000000 loops=1)
Filter: (id = 100000)
Planning time: 0.122 ms
Execution time: 223.357 ms
Avec des statistiques à jour et malgré la présence de l’index, PostgreSQL va utiliser un parcours séquentiel qui, au final, sera plus performant.
Si l’autovacuum avait été activé, les modifications massives dans la table auraient provoqué assez rapidement la mise à jour des statistiques.
La mémoire & PostgreSQL :
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 généralement un paramétrage du noyau Linux. Sous Windows, le
type est windows
.
shared_buffers
wal_buffers
max_connections
track_activity_query_size
max_connections
,
max_locks_per_transaction
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 ;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).
work_mem
hash_mem_multiplier
(v 13)maintenance_work_mem
autovacuum_work_mem
temp_buffers
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 sur
les machines récentes, car il concerne des opérations lourdes. 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.
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, 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 :
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 :
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 :
hash_mem_multiplier :
À partir de PostgreSQL 13, un paramètre multiplicateur peut
s’appliquer à certaines opérations particulières (le hachage, lors de
jointures ou agrégations). Nommé hash_mem_multiplier
, il
vaut 1 par défaut en versions 13 et 14, et 2 à partir de la 15.
hash_mem_multiplier
permet de donner plus de RAM à ces
opérations sans augmenter globalement work_mem
.
Tables temporaires
Les tables temporaires (et leurs index) sont locales à chaque session, et disparaîtront avec elle. Elles sont tout de même écrites sur disque dans le répertoire de la base.
Le cache dédié à ces tables pour minimiser les accès est séparé des
shared buffers, parce qu’il est propre à la session. Sa taille
dépend du paramètre temp_buffers
. La valeur par défaut
(8 Mo) peut être insuffisante dans certains cas pour éviter les accès
aux fichiers de la table. Elle doit être augmentée avant la création de
la table temporaire.
effective_cache_size
)shared_buffers
= 25 % RAM généralementmax_wal_size
…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. Les deux caches risquent donc de stocker les mêmes informations. Les algorithmes d’éviction sont différents entre le système et PostgreSQL, PostgreSQL disposant de davantage d’informations sur l’utilisation des données, et le type d’accès qui y est fait. La redondance est donc habituellement limitée.
Dimensionner correctement ce cache est important pour de nombreuses raisons.
Un cache trop petit :
Un cache trop grand :
shared_buffers
a un coût de traitement ;Pour dimensionner shared_buffers
sur un serveur dédié à
PostgreSQL, la documentation
officielle donne 25 % de la mémoire vive totale comme un bon point
de départ, et déconseille de 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 :
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
:
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.
But : ne pas purger le cache à cause :
VACUUM
(écritures)COPY
, CREATE TABLE AS SELECT…
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
, la
même technique est utilisée, mais les écritures se font dans le ring
buffer. Pour les écritures en masse (notamment COPY
ou
CREATE TABLE AS SELECT
), une technique similaire utilise un
ring buffer de 16 Mo.
Le site The Internals of PostgreSQL et un README dans le code de PostgreSQL entrent plus en détail sur tous ces sujets tout en restant lisibles.
2 extensions en « contrib » :
pg_buffercache
pg_prewarm
Deux extensions sont livrées dans les contribs de PostgreSQL qui impactent le cache.
pg_buffercache
permet de consulter le contenu du cache
(à utiliser de manière très ponctuelle). La requête suivante indique les
objets non système de la base en cours, présents dans le cache et s’ils
sont dirty ou pas :
pgbench=# CREATE EXTENSION pg_buffercache ;
pgbench=# SELECT
relname,
isdirty,
count(bufferid) AS blocs,
pg_size_pretty(count(bufferid) * current_setting ('block_size')::int) AS taille
FROM pg_buffercache b
INNER JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE relname NOT LIKE 'pg\_%'
GROUP BY
relname,
isdirty
ORDER BY 1, 2 ;
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) :
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.
Pour synchroniser les blocs « dirty » :
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. Un arrêt
d’urgence ne va pas la faire disparaître.Pour cela, le mécanisme est relativement simple : toute modification
affectant un fichier sera d’abord écrite dans le journal. Les
modifications affectant les vrais fichiers de données ne sont écrites
qu’en mémoire, dans les shared buffers. Elles seront écrites de
façon asynchrone, soit par un processus recherchant un buffer libre,
soit par le background writer
, soit par le
checkpointer
.
Les écritures dans le journal, bien que synchrones, sont relativement performantes, car elles sont séquentielles (moins de déplacement de têtes pour les disques).
Essentiellement :
pg_wal/
: journaux de transactions
archive_status
00000002 00000142 000000FF
pg_xact/
: état des transactionsRappelons que les journaux de transaction sont des fichiers de 16 Mo
par défaut, stockés dans PGDATA/pg_wal
(pg_xlog
avant la version 10), dont les noms comportent le
numéro de timeline, un numéro de journal de 4 Go et un numéro
de segment, en hexadécimal.
$ ls -l
total 2359320
...
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007C
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007D
...
-rw------- 1 postgres postgres 33554432 Mar 26 16:25 000000020000014300000023
-rw------- 1 postgres postgres 33554432 Mar 26 16:25 000000020000014300000024
drwx------ 2 postgres postgres 16384 Mar 26 16:28 archive_status
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 !
checkpointer
PostgreSQL trace les modifications de données dans les journaux WAL. Ceux-ci sont générés au fur et à mesure des écritures.
Si le système ou l’instance sont arrêtés brutalement, il faut que PostgreSQL puisse appliquer le contenu des journaux non traités sur les fichiers de données. Il a donc besoin de savoir à partir d’où rejouer ces données. Ce point est ce qu’on appelle un checkpoint, ou « point de reprise ».
Les principes sont les suivants :
Toute entrée dans les journaux est idempotente, c’est-à-dire qu’elle peut être appliquée plusieurs fois, sans que le résultat final ne soit changé. C’est nécessaire, au cas où la récupération serait 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.
checkpoint_timeout
max_wal_size
(pas un plafond !)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…).
checkpoint_completion_target
× durée moy. entre 2
checkpointscheckpoint_warning
log_checkpoints
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 !
Nettoyage selon l’activité, en plus du
checkpointer
:
bgwriter_delay
bgwriter_lru_maxpages
bgwriter_lru_multiplier
bgwriter_flush_after
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.
walwriter
wal_buffers
wal_writer_flush_after
fsync
= on
full_page_writes
= on
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
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.
synchronous_commit
commit_delay
/ commit_siblings
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.
wal_level
, archive_mode
archive_command
ou archive_library
Les journaux permettent de rejouer, suite à un arrêt brutal de la base, toutes les modifications depuis le dernier checkpoint. Les journaux devenus obsolète depuis le dernier checkpoint (l’avant-dernier avant la version 11) sont à terme recyclés ou supprimés, car ils ne sont plus nécessaires à la réparation de la base.
Le but de l’archivage est de stocker ces journaux, afin de pouvoir
rejouer leur contenu, non plus depuis le dernier checkpoint, mais
depuis une sauvegarde. Le mécanisme d’archivage permet
de repartir d’une sauvegarde binaire de la base (c’est-à-dire des
fichiers, pas un pg_dump
), et de réappliquer le contenu des
journaux archivés.
Il suffit de rejouer tous les journaux depuis le checkpoint précédent la sauvegarde jusqu’à la fin de la sauvegarde, ou même à un point précis dans le temps. L’application de ces journaux permet de rendre à nouveau cohérents les fichiers de données, même si ils ont été sauvegardés en cours de 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
.
Mémoire et journalisation :
N’hésitez pas, c’est le moment !
But : constater l’effet du cache sur les accès.
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 ?
But : constater l’influence de la mémoire de tri
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 ?
But : constater l’effet du cache de PostgreSQL
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 :
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 :Que contient le cache de PostgreSQL ?
Exécuter un checkpoint. Que contient le cache de PostgreSQL ?
But : Observer la génération de journaux
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
.
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
Cette fonction attend un OID comme paramètre :
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
:
Il y a cependant un raccourci à connaître :
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 ?
-[ 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 ?
-[ 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 ?
-[ 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
:
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 :
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
.
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 ?
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).
Lire de nouveau les données de la table
t2
en triant par la colonneid
. Qu’observe-t-on ?
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
.
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 :
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.)
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 ?
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 :
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 ?
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 :
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 ?
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 :Que contient le cache de PostgreSQL ?
b1=# SELECT
relname,
isdirty,
count(bufferid) AS blocs,
pg_size_pretty(count(bufferid) * current_setting ('block_size')::int) AS taille
FROM pg_buffercache b
INNER JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE relname NOT LIKE 'pg\_%'
GROUP BY
relname,
isdirty
ORDER BY 1, 2 ;
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 ?
b1=# SELECT
relname,
isdirty,
count(bufferid) AS blocs,
pg_size_pretty(count(bufferid) * current_setting ('block_size')::int) AS taille
FROM pg_buffercache b
INNER JOIN pg_class c ON c.relfilenode = b.relfilenode
WHERE relname NOT LIKE 'pg\_%'
GROUP BY
relname,
isdirty
ORDER BY 1, 2 ;
Les blocs dirty ont tous été écrits sur le disque et sont devenus « propres ».
Insérer 10 millions de lignes dans la table
t2
avecgenerate_series
. Que se passe-t-il au niveau du répertoirepg_wal
?
$ ls -al $PGDATA/pg_wal
total 131076
$ ls -al $PGDATA/pg_wal
total 638984
drwx------ 3 postgres postgres 4096 Apr 16 17:55 .
drwx------ 20 postgres postgres 4096 Apr 16 17:48 ..
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000033
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000034
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000035
…
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000054
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000055
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000056
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000057
-rw------- 1 postgres postgres 16777216 Apr 16 17:55 000000010000000000000058
-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
?
$ 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 utilise un modèle appelé MVCC (Multi-Version Concurrency Control).
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é.ROLLBACK
instantané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 :
BEGIN ISOLATION LEVEL xxx
;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.
READ COMMITTED
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 :
Il faut donc que l’application soit capable de la rejouer au besoin.
PostgreSQL fournit un mode d’isolation appelé
SERIALIZABLE
:
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) ;ctid
= (bloc, item dans le bloc)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
# SELECT ctid, i,j FROM deuxblocs
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 :
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 :
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 ;COMMIT
ou ROLLBACK
très rapideLa CLOG est stockée dans une série de fichiers de 256 ko, stockés
dans le répertoire pg_xact/
de PGDATA (répertoire racine de
l’instance PostgreSQL).
Chaque transaction est créée dans ce fichier dès son démarrage et est encodée sur deux bits puisqu’une transaction peut avoir quatre états :
TRANSACTION_STATUS_IN_PROGRESS
signifie que la
transaction 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.
Annuler une transaction (ROLLBACK
) est quasiment
instantané sous PostgreSQL : il suffit d’écrire
TRANSACTION_STATUS_ABORTED
dans l’entrée de CLOG
correspondant à la transaction.
Toute modification dans la CLOG, comme toute modification d’un
fichier de données (table, index, séquence, vue matérialisée), est bien
sûr enregistrée tout d’abord dans les journaux de transactions (dans le
répertoire pg_wal/
).
ROLLBACK
instantané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.)
VACUUM
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.
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
.
Les index n’ont pas d’information de visibilité. Il est donc
nécessaire d’aller vérifier dans la table associée que l’enregistrement
trouvé dans l’index est bien visible. Cela a un impact sur le temps
d’exécution de requêtes comme SELECT count(*)
sur une
table : dans le cas le plus défavorable, il est nécessaire d’aller
visiter tous les enregistrements pour s’assurer qu’ils sont bien
visibles. La visibility map permet de limiter cette
vérification aux données les plus récentes.
Un VACUUM
ne s’occupe pas de l’espace libéré par des
colonnes supprimées (fragmentation verticale). Un
VACUUM FULL
est nécessaire pour reconstruire la table.
xmin
/xmax
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 :
-[ 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.
Après 4 milliards de transactions :
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 :
datname | datfrozenxid | age
-----------+--------------+-----------
pgbench | 1809610092 | 149835222
template0 | 1957896953 | 1548361
template1 | 1959012415 | 432899
postgres | 1959445305 | 9
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 ?
VACUUM FREEZE
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.
MVCC a été affiné au fil des versions :
Les améliorations suivantes ont été ajoutées au fil des versions :
Heap-Only Tuples (HOT) s’agit de pouvoir stocker, sous
condition, plusieurs versions du même enregistrement dans le même bloc.
Ceci permet au fur et à mesure des mises à jour de supprimer
automatiquement les anciennes versions, sans besoin de
VACUUM
. Cela permet aussi de ne pas toucher aux index, qui
pointent donc grâce à cela sur plusieurs versions du même
enregistrement. Les conditions sont les suivantes :
fillfactor
pour une table donnée (cf documentation
officielle) ;Chaque table possède une Free Space Map avec une liste
des espaces libres de chaque table. Elle est stockée dans les fichiers
*_fsm
associés à chaque table.
La Visibility Map permet de savoir si l’ensemble des
enregistrements d’un bloc est visible. En cas de doute, ou
d’enregistrement non visible, le bloc n’est pas marqué comme totalement
visible. Cela permet à la phase 1 du traitement de VACUUM
de ne plus parcourir toute la table, mais uniquement les enregistrements
pour lesquels la Visibility Map est à faux (des
données sont potentiellement obsolètes dans le bloc). À l’inverse, les
parcours d’index seuls utilisent cette Visibility Map pour
savoir s’il faut aller voir les informations de visibilité dans la
table. VACUUM
repositionne la Visibility Map à
vrai après nettoyage d’un bloc, si tous les enregistrements
sont visibles pour toutes les sessions. Enfin, depuis la 9.6, elle
repère aussi les bloc entièrement gelés pour accélérer les
VACUUM FREEZE
.
Toutes ces optimisations visent le même but : rendre
VACUUM
le moins pénalisant possible, et simplifier la
maintenance.
La gestion des verrous est liée à l’implémentation de MVCC
PostgreSQL possède un gestionnaire de verrous
pg_locks
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
.
xmax
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é.
pg_locks
:
C’est une vue globale à l’instance.
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
).
max_locks_per_transaction
(+ paramètres pour la
sérialisation)lock_timeout
(éviter l’empilement des verrous)deadlock_timeout
(défaut 1 s)log_lock_waits
Nombre de verrous :
max_locks_per_transaction
sert à dimensionner un espace
en mémoire partagée destinée aux verrous sur des objets (notamment les
tables). Le nombre de verrous est :
ou plutôt, si les transactions préparées sont activées (et
max_prepared_transactions
monté au-delà de 0) :
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)
:
postgres=# SET lock_timeout TO '3s' ;
SET
postgres=# VACUUM FULL t_grosse_table ;
ERROR: canceling statement due to lock timeout
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 :
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é :
TOAST : The Oversized-Attribute Storage Technique
Que faire si une ligne dépasse d’un bloc ?
Une ligne ne peut déborder d’un bloc, et un bloc fait 8 ko (par
défaut). Cela ne suffit pas pour certains champs beaucoup plus longs,
comme certains textes, mais aussi des types composés (json
,
jsonb
, hstore
), ou binaires
(bytea
), et même numeric
. PostgreSQL sait
compresser alors les champs, mais ça ne suffit pas forcément non
plus.
pg_toast_XXX
PLAIN
/MAIN
/EXTERNAL
ou
EXTENDED
Principe du TOAST :
Le mécanisme TOAST consiste à déporter le contenu de certains champs d’un enregistrement vers une autre table associée à la table principale, gérée de manière transparente pour l’utilisateur. Ce mécanisme permet d’éviter qu’un enregistrement ne dépasse la taille d’un bloc.
Le mécanisme TOAST a d’autres intérêts :
UPDATE
ne modifie pas un de ces champs
« toastés », la table TOAST n’est pas mise à jour : le pointeur vers
l’enregistrement de cette table est juste « cloné » dans la nouvelle
version de l’enregistrement.Politiques de stockage :
Chaque champ possède une propriété de stockage :
# \d+ unetable
Table « public.unetable »
Colonne | Type | Col... | NULL-able | Par défaut | Stockage | …
---------+---------+--------+-----------+------------+----------+--
i | integer | | | | plain |
t | text | | | | extended |
b | bytea | | | | extended |
j2 | jsonb | | | | extended |
Méthode d’accès : heap
Les différentes politiques de stockage sont :
PLAIN
permettant de stocker uniquement dans la table,
sans compression (champs numériques ou dates notamment) ;MAIN
permettant de stocker dans la table tant que
possible, éventuellement compressé (politique rarement utilisée) ;EXTERNAL
permettant de stocker éventuellement dans la
table TOAST, sans compression ;EXTENDED
permettant de stocker éventuellement 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 PDF…) se compressent si mal qu’il ne vaut pas la peine de gaspiller
du CPU dans cette tâche. Dans le cas extrême où le champ compressé est
plus grand que l’original, PostgreSQL revient à la valeur originale,
mais 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 :
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.
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.
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 !
La présence de ces tables n’apparaît guère que dans
pg_class
, par exemple ainsi :
SELECT * FROM pg_class c
WHERE c.relname = 'longs_textes'
OR c.oid = (SELECT reltoastrelid FROM pg_class
WHERE relname = 'longs_textes');
-[ RECORD 1 ]-------+---------------
oid | 16614
relname | longs_textes
relnamespace | 2200
reltype | 16616
reloftype | 0
relowner | 10
relam | 2
relfilenode | 16614
reltablespace | 0
relpages | 35
reltuples | 2421
relallvisible | 35
reltoastrelid | 16617
…
-[ RECORD 2 ]-------+---------------
oid | 16617
relname | pg_toast_16614
relnamespace | 99
reltype | 16618
reloftype | 0
relowner | 10
relam | 2
relfilenode | 16617
reltablespace | 0
relpages | 73161
reltuples | 293188
relallvisible | 73161
reltoastrelid | 0
…
La partie TOAST est une table à part entière, avec une clé primaire. On ne peut ni ne doit y toucher !
\d+ pg_toast.pg_toast_16614
Table TOAST « pg_toast.pg_toast_16614 »
Colonne | Type | Stockage
------------+---------+----------
chunk_id | oid | plain
chunk_seq | integer | plain
chunk_data | bytea | plain
Table propriétaire : « public.textes_comp »
Index :
"pg_toast_16614_index" PRIMARY KEY, btree (chunk_id, chunk_seq)
Méthode d'accès : heap
L’index 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,
c.relnamespace::regnamespace || '.' || relname AS TABLE,
reltoastrelid,
reltoastrelid::regclass::text AS table_toast,
reltuples AS nb_lignes_estimees,
pg_size_pretty(pg_table_size(c.oid)) AS " Table (dont TOAST)",
pg_size_pretty(pg_relation_size(c.oid)) AS " Heap",
pg_size_pretty(pg_relation_size(reltoastrelid)) AS " Toast",
pg_size_pretty(pg_indexes_size(reltoastrelid)) AS " Toast (PK)",
pg_size_pretty(pg_indexes_size(c.oid)) AS " Index",
pg_size_pretty(pg_total_relation_size(c.oid)) AS "Total"
FROM pg_class c
WHERE relkind = 'r'
AND relname = 'longs_textes'
\gx
-[ RECORD 1 ]------+------------------------
table_oid | 16614
table | public.longs_textes
reltoastrelid | 16617
table_toast | pg_toast.pg_toast_16614
nb_lignes_estimees | 2421
Table (dont TOAST) | 578 MB
Heap | 280 kB
Toast | 572 MB
Toast (PK) | 6448 kB
Index | 560 kB
Total | 579 MB
La taille des index sur les champs susceptibles d’être toastés est comptabilisée avec tous les index de la table (la clé primaire de la table TOAST est à part).
Les tables TOAST restent forcément dans le même tablespace que la
table principale. Leur maintenance (notamment le nettoyage par
autovacuum
) s’effectue en même temps que la table
principale, comme le montre un VACUUM VERBOSE
.
Détails du mécanisme TOAST :
Les détails techniques du mécanisme TOAST sont dans la documentation 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 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 :
mais cela est rarement utile.
pglz
(zlib
) : défautslz4
à préférer
ou :
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 :
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
: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 :
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
.VACUUM
et le processus
d’arrière-plan autovacuum.N’hésitez pas, c’est le moment !
But : Découvrir les niveaux d’isolation
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é ?
But : Découvrir le niveau d’isolation serializable
Une table de comptes bancaires contient 1000 clients, chacun avec 3 lignes de crédit et 600 € au total :
CREATE TABLE mouvements_comptes
(client int,
mouvement numeric NOT NULL DEFAULT 0
);
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 plpgsql
AS $$
DECLARE
solde numeric ;
BEGIN
SELECT round(sum (mouvement), 0)
INTO solde
FROM mouvements_comptes
WHERE client = p_client ;
IF solde < 0 THEN
-- Erreur fatale
RAISE EXCEPTION 'Client % - Solde négatif : % !', p_client, solde ;
ELSE
-- Simple message
RAISE NOTICE 'Client % - Solde positif : %', p_client, solde ;
END IF ;
END ;
$$ ;
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
:
But : Voir l’effet du MVCC dans les lignes
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
:Que faut-il remarquer ?
VACUUM
sur
t2
.pageinspect
.Pourquoi l’autovacuum n’a-t-il pas nettoyé encore la table ?
But : Trouver des verrous
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.
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
.
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
.
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é ?
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
(client int,
mouvement numeric NOT NULL DEFAULT 0
);
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 plpgsql
AS $$
DECLARE
solde numeric ;
BEGIN
SELECT round(sum (mouvement), 0)
INTO solde
FROM mouvements_comptes
WHERE client = p_client ;
IF solde < 0 THEN
-- Erreur fatale
RAISE EXCEPTION 'Client % - Solde négatif : % !', p_client, solde ;
ELSE
-- Simple message
RAISE NOTICE 'Client % - Solde positif : %', p_client, solde ;
END IF ;
END ;
$$ ;
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 € :
Première transaction :
BEGIN ;
INSERT INTO mouvements_comptes(client, mouvement) VALUES (1, -300) ;
CALL verifie_solde_positif (1) ;
Lors d’une seconde transaction : les mêmes ordres renvoient :
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) ;
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) ;
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 :
Au final, le solde est négatif, ce qui est pourtant strictement interdit !
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
:
Première session :
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
INSERT INTO mouvements_comptes(client, mouvement) VALUES (3, -500) ;
CALL verifie_solde_positif (3) ;
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) ;
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 :
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 :
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.
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 ?
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 ?
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 :
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 :
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 :
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 :
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.
Installer l’extension
pageinspect
.
La documentation indique comment décoder le contenu du bloc 0 de la table
t2
: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) :
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.
VACUUM
sur
t2
.pageinspect
.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 :
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.
Ouvrir une autre transaction, et tenter de supprimer la 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
.
-[ 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.
-[ 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é.
Noter que l’objet n’est visible dans pg_class
que si
l’on est dans la même base de données que lui. D’autre part, la colonne
oid
des tables systèmes n’est pas visible par défaut dans
les versions antérieures à la 12, il faut demander explicitement son
affichage pour la voir.
Récupérer la liste des verrous sur cet objet. Quel processus a verrouillé la table
t1
?
-[ RECORD 1 ]------+--------------------
locktype | relation
database | 16387
relation | 16394
page |
tuple |
virtualxid |
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 4/10
pid | 2719
mode | AccessShareLock
granted | t
fastpath | f
waitstart |
-[ RECORD 2 ]------+--------------------
locktype | relation
database | 16387
relation | 16394
page |
tuple |
virtualxid |
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 5/7
pid | 2718
mode | AccessExclusiveLock
granted | f
fastpath | f
waitstart |
Le processus de PID 2718 (le DROP TABLE
) demande un
verrou exclusif sur t1
, mais ce verrou n’est pas encore
accordé (granted
est à false
). La session
idle in transaction
a acquis un verrou
Access Share
, normalement peu gênant, qui n’entre en
conflit qu’avec les verrous exclusifs.
Retrouver les informations sur la session bloquante.
On retrouve les informations déjà affichées :
-[ 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 :
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()
:
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.
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 :
VACUUM
VACUUM
seul, ANALYZE
,
FULL
, FREEZE
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
: nettoie d’abord les lignes mortesautovacuum
(seuils)VACUUM
est né du besoin de nettoyer les lignes mortes.
Au fil du temps il a été couplé à d’autres ordres (ANALYZE
,
VACUUM FREEZE
) et s’est occupé d’autres opérations de
maintenance (création de la visibility map par exemple).
autovacuum
est un processus de l’instance PostgreSQL. Il
est activé par défaut, et il fortement conseillé de le conserver ainsi.
Dans le cas général, son fonctionnement convient et il ne gênera pas les
utilisateurs.
L’autovacuum ne gère pas toutes les variantes de VACUUM
(notamment pas le FULL
).
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
(1
Go au plus). 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
:
À 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.
À partir de la version 13, cette phase de nettoyage des index peut
être parallélisée (clause PARALLEL
), chaque index pouvant
être traité par un CPU.
NB : L’espace est rarement rendu à l’OS !
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.
Ne pas confondre :
VACUUM
seul
ANALYZE
VACUUM (ANALYZE)
VACUUM (FREEZE)
VACUUM FULL
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
(v13+)BUFFER_USAGE_LIMIT
vacuum_buffer_usage_limit
(256 ko)SKIP_DATABASE_STATS
, ONLY_DATABASE_STATS
(v16+)SKIP_LOCKED
SET lock_timeout = '1s'
PARALLEL :
Apparue avec PostgreSQL 13, l’option PARALLEL
permet le
traitement parallélisé des index. Le nombre indiqué après
PARALLEL
précise le niveau de parallélisation souhaité. Par
exemple :
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 :
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érer moins de journaux, mais il peut y avoir un impact
négatif sur les autres requêtes si la mémoire partagée disponible se
réduit, ou si le débit en lecture ou écriture du VACUUM
augmente trop. Les valeurs vont de 128 ko à 16 Go ; 0 désactive le
buffer ring, il n’y a alors aucune limite en terme d’accès aux
buffers.
Exemples d’utilisation :
ANALYZE (BUFFER_USAGE_LIMIT '4MB');
VACUUM (BUFFER_USAGE_LIMIT '256kB');
VACUUM (ANALYZE, BUFFER_USAGE_LIMIT 0);
…
VACUUM (SKIP_DATABASE_STATS, BUFFER_USAGE_LIMIT '1GB') public.pgbench_accounts;
VACUUM (SKIP_DATABASE_STATS, BUFFER_USAGE_LIMIT '1GB') pg_catalog.pg_proc;
…
De manière globale, on peut aussi modifier le paramètre
vacuum_buffer_usage_limit
, à 256 ko par défaut. Les
machines modernes permettent de monter sans problème ce paramètre à
quelques mégaoctets pour un gain très appréciable. 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.
Les verrous, SKIP_LOCKED et lock_timeout :
À partir de la version 12, 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 :
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.
INDEX_CLEANUP off
PROCESS_TOAST off
(v14+)TRUNCATE off
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 :
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
.
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
).
VERBOSE
Ponctuellement :
DISABLE_PAGE_SKIPPING
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 :
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 :
pg_stat_activity
ou top
pg_stat_user_tables
last_vacuum
/ last_autovacuum
last_analyze
/ last_autoanalyze
log_autovacuum_min_duration
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).
-[ 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é.
Pour VACUUM
simple / VACUUM FREEZE
pg_stat_progress_vacuum
Partie ANALYZE
pg_stat_progress_analyze
(v13)Manuel ou via autovacuum
Pour VACUUM FULL
pg_stat_progress_cluster
(v12)La vue pg_stat_progress_vacuum
contient une ligne par
VACUUM
(simple ou FREEZE
) en cours
d’exécution.
Voici un exemple :
-[ RECORD 1 ]------+--------------
pid | 4299
datid | 13356
datname | postgres
relid | 16384
phase | scanning heap
heap_blks_total | 127293
heap_blks_scanned | 86665
heap_blks_vacuumed | 86664
index_vacuum_count | 0
max_dead_tuples | 291
num_dead_tuples | 53
Dans cet exemple, le VACUUM
exécuté par le PID 4299 a
parcouru 86 665 blocs (soit 68 % de la table), et en a traité
86 664.
Dans le cas d’un VACUUM ANALYZE
, la seconde partie de
recueil des statistiques pourra être suivie dans
pg_stat_progress_analyze
(à partir de PostgreSQL 13) :
-[ 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
,
qui renvoie par exemple :
-[ RECORD 1 ]-------+------------------
pid | 21157
datid | 13444
datname | postgres
relid | 16384
command | VACUUM FULL
phase | seq scanning heap
cluster_index_relid | 0
heap_tuples_scanned | 13749388
heap_tuples_written | 13749388
heap_blks_total | 199105
heap_blks_scanned | 60839
index_rebuild_count | 0
Cette vue est utilisable aussi avec l’ordre CLUSTER
,
d’où le nom.
VACUUM
VACUUM
, ANALYZE
,
FREEZE
FULL
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 !)autovacuum_naptime
(1 min)autovacuum_max_workers
(3)
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 :
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.
Seuil de déclenchement =
threshold + scale factor × nb lignes de la table
L’autovacuum déclenche un VACUUM
ou un
ANALYZE
à partir de seuils calculés sur le principe d’un
nombre de lignes minimal (threshold) et d’une proportion de la
table existante (scale factor) de lignes modifiées, insérées ou
effacées. (Pour les détails précis sur ce qui suit, voir la
documentation officielle.)
Ces seuils pourront être adaptés table par table.
VACUUM
autovacuum_vacuum_scale_factor
(20 %)autovacuum_vacuum_threshold
(50)autovacuum_vacuum_insert_threshold
(1000)autovacuum_vacuum_insert_scale_factor
(20 %)ANALYZE
autovacuum_analyze_scale_factor
(10 %)autovacuum_analyze_threshold
(50)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
:
À 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
/
autovacuum_work_mem
VACUUM
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 grosses purges. Un
maintenance_work_mem
à plus de 1 Go est inutile pour le
VACUUM
car il ne sait pas en utiliser plus. Par contre, une
valeur supérieure à 1 Go sera prise en compte lors de l’indexation de
grosses tables.
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.
vacuum_cost_page_hit
/_miss
/_dirty
(1/ 10 ou 2 /20)vacuum_cost_limit
(200)vacuum_cost_delay
(en manuel : 0 ms !)autovacuum_vacuum_cost_limit
(identique)autovacuum_vacuum_cost_delay
(2 ms)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.
Le but est de geler les numéros de transaction assez vite :
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).
Quand le VACUUM
gèle-t-il les lignes ?
age ( pgclass.relfrozenxid )
vacuum_freeze_min_age
(50 Mtrx)
vacuum_freeze_table_age
(150 Mtrx)
autovacuum_freeze_max_age
(200 Mtrx)VACUUM FREEZE
préventif en période de maintenanceGeler 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 » :
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 :
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
:
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).
pg_cancel_backend
+ VACUUM FREEZE
manuelLe 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
.
VACUUM
check_pg_activity
: xmin
,
max_freeze_age
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, à partir de PostgreSQL 12 :
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.
last_(auto)analyze
/
last_(auto)vacuum
ALTER TABLE table_name SET (autovacuum_analyze_scale_factor = 0.01) ;
ALTER TABLE table_name SET (autovacuum_vacuum_threshold = 1000000) ;
vacuumdb
quotidienL’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é.
FREEZE
brutal après migration logique ou gros
import
VACUUM FULL
: dernière extrémitéL’autovacuum n’est pas toujours assez rapide à se déclencher, par
exemple entre les différentes étapes d’un batch : on intercalera des
VACUUM ANALYZE
manuels. Il faudra le faire systématiquement
pour les tables temporaires (que l’autovacuum ne voit pas). Pour les
tables où il n’y a que des insertions, avant PostgreSQL 13, l’autovacuum
ne lance spontanément que l’ANALYZE
: il faudra effectuer
un VACUUM
explicite pour profiter de certaines
optimisations.
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.
VACUUM
fait de plus en plus de choses au fil des
versionsN’hésitez pas, c’est le moment !
But : Traiter la fragmentation
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 ?
But : Détecter la fragmentation
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
.
pg_freespacemap
(documentation : https://docs.postgresql.fr/current/pgfreespacemap.html)pg_freespace()
quant à l’espace libre de la table
t5
?t5
.pg_freespace
quant à
l’espace libre de la table t5
?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 ?
But : Voir fonctionner l’autovacuum
Créer une table
t6
avec une colonneid
de typeinteger
.
Insérer un million de lignes dans la table
t6
:
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 :
- 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 :
- 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
:
- 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
.
Désactiver l’autovacuum pour la table
t3
.
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
.
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 ?
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 ?
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 :
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 :
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 ?
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
.
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 ?
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
.
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 ?
Là aussi, nous n’avons rien perdu.
Exécuter un
VACUUM
sur la tablet4
.
Récupérer la taille de la table
t4
. Que faut-il en déduire ?
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
.
Désactiver l’autovacuum pour la table
t5
.
Insérer un million de lignes dans la table
t5
avecgenerate_series
.
pg_freespacemap
(documentation : https://docs.postgresql.fr/current/pgfreespacemap.html)pg_freespace()
quant à l’espace libre de la table
t5
?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.
et donc 6274 blocs (soit 51,4 Mo) sans aucun espace vide.
t5
.pg_freespace
quant à
l’espace libre de la table t5
?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
.
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
?
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
.
Exécuter un
VACUUM (FULL, VERBOSE)
sur la tablet5
.
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 ?
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
.
Insérer un million de lignes dans la table
t6
:
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
.)
-[ 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 :
Vérifier le nombre de lignes dans
pg_class.reltuples
.
Vérifions que le nombre de lignes est à jour dans
pg_class
:
-[ 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 :
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 :
- Attendre une minute.
- Que contient la vue
pg_stat_user_tables
pour la tablet6
?- Que faut-il en déduire ?
Le démon autovacuum ne se déclenche pas instantanément après les écritures, attendons un peu :
-[ 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 :
- Attendre une minute.
- Que contient la vue
pg_stat_user_tables
pour la tablet6
?- Que faut-il en déduire ?
L’autovacuum ne passe pas tout de suite, les 210 000 lignes mortes au total sont bien visibles :
-[ 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
.
- Modifier encore 200 000 autres lignes de la table
t6
:
- Attendre une minute.
- Que contient la vue
pg_stat_user_tables
pour la tablet6
?- Que faut-il en déduire ?
-[ 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
.
Le partitionnement déclaratif apparaît avec PostgreSQL 10
Préférer PostgreSQL 13 ou plus récent
Ne plus utiliser l’ancien partitionnement par héritage.
Ce module introduit le partitionnement déclaratif introduit avec PostgreSQL 10, et amélioré dans les versions suivantes. PostgreSQL 13 au minimum est conseillé pour ne pas être gêné par une des limites levées dans les versions précédentes (et non développées ici).
Le partitionnement par héritage, au fonctionnement totalement différent, reste utilisable, mais ne doit plus servir aux nouveaux développements, du moins pour les cas décrits ici.
VACUUM
(FULL
), réindexation, déplacements,
sauvegarde logique…pg_dump
parallélisableMaintenir 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…
CREATE TABLE logs ( d timestamptz, contenu text) PARTITION BY RANGE (d) ;
CREATE TABLE logs_201901 PARTITION OF logs
FOR VALUES FROM ('2019-01-01') TO ('2019-02-01');
CREATE TABLE logs_201902 PARTITION OF logs
FOR VALUES FROM ('2019-02-01') TO ('2019-03-01');
…
CREATE TABLE logs_201912 PARTITION OF logs
FOR VALUES FROM ('2019-12-01') TO ('2020-01-01');
…
CREATE TABLE logs_autres PARTITION OF logs
DEFAULT ; -- pour ne rien perdre
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 :
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.
DROP
ou DETACH
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 :
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) :
QUERY PLAN
---------------------------------------------------------------------------------
Index Scan using pgbench_accounts_1_pkey on pgbench_accounts_1 pgbench_accounts
Index Cond: (aid = 599999)
Si l’on connaît la clé et que le développeur sait en déduire la table, il est aussi possible d’accéder directement à la partition :
QUERY PLAN
QUERY PLAN
---------------------------------------------------------------------------------
Index Scan using pgbench_accounts_6_pkey on pgbench_accounts_6
Index Cond: (aid = 599999)
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 ne faut pas 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.
pg_dump
à chaudLa 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).
pg_basebackup
pg_receivewal
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.
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. 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). Ils doivent impérativement être rejoués dans l’ordre de leur écriture (et donc de leur nom).
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. Généralement, une sauvegarde
pg_dump
s’exécute toutes les nuits, disons à 3 h du matin.
Supposons qu’un gros problème survienne à midi. S’il faut restaurer la
dernière sauvegarde, la perte de données sera de 9 h. Le volume maximum
de données perdu correspond à l’espacement des sauvegardes. Avec
l’archivage continu des journaux de transactions, la fenêtre de perte de
données va être fortement réduite. Plus l’activité est intense, plus la
fenêtre de temps sera petite : il faut changer de fichier de journal
pour que le journal précédent soit archivé et les fichiers de journaux
sont de taille fixe.
Pour les systèmes n’ayant pas une grosse activité, il est aussi possible de forcer un changement de journal à intervalle régulier, ce qui a pour effet de forcer son archivage, et donc dans les faits de pouvoir s’assurer une perte correspondant au maximum à cet intervalle.
pg_wal
en cas d’échec d’archivage… avec arrêt si
il est plein !Certains inconvénients viennent directement du fait qu’on copie les fichiers : sauvegarde et restauration complète (impossible de ne restaurer qu’une seule base ou que quelques tables), restauration sur la même architecture (32/64 bits, little/big endian). Il est même fortement conseillé de restaurer dans la même version du même système d’exploitation, sous peine de devoir réindexer l’instance (différence de définition des locales notamment).
Elle nécessite en plus un plus grand espace de stockage car il faut
sauvegarder les fichiers (dont les index) ainsi que les journaux de
transactions sur une certaine période, ce qui peut être volumineux (en
tout cas beaucoup plus que des pg_dump
).
En cas de problème dans l’archivage et selon la méthode choisie,
l’instance ne voudra pas effacer les journaux non archivés. Il y a donc
un risque d’accumulation de ceux-ci. Il faudra donc surveiller la taille
du pg_wal
. En cas de saturation, PostgreSQL s’arrête !
Enfin, la sauvegarde PITR est plus complexe à mettre en place qu’une
sauvegarde pg_dump
. Elle nécessite plus d’étapes, une
réflexion sur l’architecture à mettre en œuvre et une meilleure
compré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, et ne permet pas 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.
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 :
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é :
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 :
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 temporaire sera créé par défaut pour garantir que le serveur gardera les journaux jusqu’à leur copie intégrale.
À partir de la version 13, la commande pg_basebackup
crée un fichier manifeste contenant la liste des fichiers sauvegardés,
leur taille et une somme de contrôle. Cela permet de vérifier la
sauvegarde avec l’outil pg_verifybackup
(ce dernier ne
fonctionne hélas que sur une sauvegarde au format plain
, ou
décompressée).
Même avec un serveur un peu ancien, il est possible d’installer 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.
2 étapes :
pg_receivewal
pg_basebackup
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.
archiver
pg_receivewal
(flux de réplication)La méthode historique est la méthode utilisant le processus
archiver
. Ce processus fonctionne sur le serveur à
sauvegarder et est de la responsabilité du serveur PostgreSQL. Seule sa
(bonne) configuration incombe au DBA.
Une autre méthode existe : pg_receivewal
. Cet outil
livré aussi avec PostgreSQL se comporte comme un serveur secondaire. Il
reconstitue les journaux de transactions à partir du flux de
réplication.
Chaque solution a ses avantages et inconvénients qu’on étudiera après avoir détaillé leur mise en place.
Dans le cas de l’archivage historique, le serveur PostgreSQL va exécuter une commande qui va copier les journaux à l’extérieur de son répertoire de travail :
Dans le cas de l’archivage avec pg_receivewal
, c’est cet
outil qui va écrire les journaux dans un répertoire de travail. Cette
écriture ne peut se faire qu’en local. Cependant, le répertoire peut se
trouver dans un montage NFS.
L’exemple pris ici utilise le répertoire
/mnt/nfs1/archivage
comme répertoire de copie. Ce
répertoire est en fait un montage NFS. Il faut donc commencer par créer
ce répertoire et s’assurer que l’utilisateur Unix (ou Windows)
postgres
peut écrire dedans :
postgresql.conf
)
wal_level = replica
archive_mode = on
(ou always
)archive_command = '… une commande …'
archive_library = '… une bibliothèque …'
(v15+)archive_timeout = '… min'
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 :
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 :
(La valeur always
permet d’archiver depuis un
secondaire). Le changement nécessite un redémarrage !
Commande d’archivage :
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. C’est la technique encore la plus utilisée.
(Noter qu’à partir de la version 15, il existe une alternative, avec
l’utilisation du paramètre 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. Mais en production, il vaudra
mieux utiliser une bibliothèque fournie par un outil PITR reconnu.
Cependant, à notre connaissance (en décembre 2023), aucun n’utilise
encore cette fonctionnalité. L’utilisation simultanée de
archive_command
et archive_library
est
déconseillée, et interdite depuis PostgreSQL 16.)
Exemples d’archive_command :
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 :
Le joker %p
est remplacé par le chemin complet vers le
journal de transactions à archiver, alors que le joker %f
correspond au nom du journal de transactions une fois archivé.
En toute rigueur, une copie du fichier ne suffit pas. Par exemple,
dans le cas de la commande cp
, le nouveau fichier n’est pas
immédiatement écrit sur disque. La copie est effectuée dans le cache
disque du système d’exploitation. En cas de crash juste après la copie,
il est tout à fait possible de perdre l’archive. Il est donc essentiel
d’ajouter une étape de synchronisation du cache sur disque.
La commande d’archivage suivante est donnée dans la documentation officielle à titre d’exemple :
Cette commande a deux inconvénients. Elle ne garantit pas que les données seront synchronisées sur disque. De plus si le fichier existe ou a été copié partiellement à cause d’une erreur précédente, la copie ne s’effectuera pas.
Cette protection est une bonne chose. Cependant, il faut être vigilant lorsque l’on rétablit le 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. Il peut aussi être intéressant de tracer l’action de
l’archivage par exemple, ou encore de compresser le journal avant
archivage.
Il faut s’assurer d’une seule chose : la commande d’archivage doit retourner 0 en cas de réussite et surtout une valeur différente de 0 en cas d’échec.
Si le code retour de la commande est compris entre 1 et 125, PostgreSQL va tenter périodiquement d’archiver le fichier jusqu’à ce que la commande réussisse (autrement dit, renvoie 0).
Tant qu’un fichier journal n’est pas considéré comme archivé avec succès, PostgreSQL ne le supprimera ou recyclera pas !
Il ne cherchera pas non plus à archiver les fichiers suivants.
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 :
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
.
Période minimale entre deux archivages :
Si l’activité en écriture est très réduite, 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 :
(La valeur par défaut, 0, désactive ce comportement.) 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.
wal_level
et/ou
archive_mode
Il ne reste plus qu’à indiquer à PostgreSQL de recharger sa
configuration pour que l’archivage soit en place (avec
SELECT pg_reload_conf();
ou la commande reload
adaptée au système). Dans le cas où l’un des paramètres
wal_level
et archive_mode
a été modifié, il
faudra relancer PostgreSQL.
pg_stat_archiver
pg_wal/archive_status/
pg_wal
PostgreSQL archive les journaux impérativement dans l’ordre.
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. Si l’archivage est bloqué, 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 :
-[ RECORD 1 ]------+------------------------------
archived_count | 156
last_archived_wal | 0000000200000001000000D9
last_archived_time | 2020-01-17 18:26:03.715946+00
failed_count | 6
last_failed_wal | 0000000200000001000000D7
last_failed_time | 2020-01-17 18:24:24.463038+00
stats_reset | 2020-01-17 16:08:37.980214+00
Comme dit plus haut, pour que cette supervision soit fiable, la commande exécutée doit renvoyer un code retour inférieur ou égal à 125. Dans le cas contraire, le processus archiver redémarre et l’erreur n’apparaît pas dans la vue !
Traces :
On trouvera la sortie et surtout les messages d’erreurs du script d’archivage dans les traces (qui dépendent bien sûr du script utilisé) :
2020-01-17 18:24:18.427 UTC [15431] LOG: archive command failed with exit code 3
2020-01-17 18:24:18.427 UTC [15431] DETAIL: The failed archive command was:
rsync pg_wal/0000000200000001000000D7 /opt/pgsql/archives/0000000200000001000000D7
rsync: change_dir#3 "/opt/pgsql/archives" failed: No such file or directory (2)
rsync error: errors selecting input/output files, dirs (code 3) at main.c(695)
[Receiver=3.1.2]
2020-01-17 18:24:19.456 UTC [15431] LOG: archive command failed with exit code 3
2020-01-17 18:24:19.456 UTC [15431] DETAIL: The failed archive command was:
rsync pg_wal/0000000200000001000000D7 /opt/pgsql/archives/0000000200000001000000D7
rsync: change_dir#3 "/opt/pgsql/archives" failed: No such file or directory (2)
rsync error: errors selecting input/output files, dirs (code 3) at main.c(695)
[Receiver=3.1.2]
2020-01-17 18:24:20.463 UTC [15431] LOG: archive command failed with exit code 3
C’est donc le premier endroit à regarder en cas de souci ou lors de la mise en place de l’archivage.
pg_wal/archive_status :
Enfin, on peut monitorer les fichiers présents dans
pg_wal/archive_status
. Les fichiers .ready
, de
taille nulle, indiquent en permanence quels sont les journaux prêts à
être archivés. Théoriquement, leur nombre doit donc rester faible et
retomber rapidement à 0 ou 1. Le service ready_archives
de
la sonde Nagios check_pgactivity se
base sur ce répertoire.
pg_ls_dir
--------------------------------
0000000200000001000000DE.done
0000000200000001000000DF.done
0000000200000001000000E0.done
0000000200000001000000E1.ready
0000000200000001000000E2.ready
0000000200000001000000E3.ready
0000000200000001000000E4.ready
0000000200000001000000E5.ready
0000000200000001000000E6.ready
00000002.history.done
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/>
postgresql.conf
:pg_hba.conf
: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
.
pg_receivewal
s’arrêtepg_receivewal
tente de se
reconnecterOn peut alors lancer pg_receivewal
:
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.
pg_receivewal
La méthode archiver est la méthode la plus simple à mettre en place.
Elle se lance au lancement du serveur 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, apparue avec PostgreSQL 9.6, 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, 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.
SELECT pg_backup_start (
un_label
: textefast
: forcer un checkpoint ?)
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…).
rsync
et autres outilspostmaster.pid
, log
, pg_wal
,
pg_replslot
et quelques autresLa 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 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. Une copie parallèle des données de manière plus classique est conseillée pour éviter un désastre total.
Copie manuelle :
La sauvegarde se fait à chaud : il est donc possible que pendant ce temps des fichiers 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.
Ne pas oublier !!
SELECT * FROM pg_backup_stop (
true
: attente de l’archivage)
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
:
… 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.
Outil de sauvegarde pouvant aussi servir au sauvegarde basique
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.
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.
pg_stat_progress_basebackup
La version 13 permet de suivre la progression de la sauvegarde de base, quelque soit l’outil utilisé à condition qu’il passe par le protocole de réplication.
Cela permet ainsi de savoir à quelle phase la sauvegarde se trouve, quelle volumétrie a été envoyée, celle à envoyer, etc.
La restauration se déroule en trois voire quatre étapes suivant qu’elle est effectuée sur le même serveur ou sur un autre serveur.
Dans le cas où la restauration a lieu sur le même serveur, quelques étapes préliminaires sont à effectuer.
Il faut arrêter PostgreSQL s’il n’est pas arrêté. Cela arrivera quand la restauration a pour but, par exemple, de récupérer des données qui ont été supprimées par erreur.
Ensuite, il faut supprimer (ou archiver) l’ancien répertoire des données pour pouvoir y placer 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.
pg_wal
restauré
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 :
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.
Indiquer qu’on est en restauration
Commande de restauration
restore_command = '… une commande …'
postgresql.[auto.]conf
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 :
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.
recovery_target_name
,
recovery_target_time
recovery_target_xid
,
recovery_target_lsn
recovery_target_inclusive
recovery_target_timeline
: latest
?recovery_target_action
: pause
pg_wal_replay_resume
pour ouvrir immédiatementSi 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
;recovery_target_lsn
, que là aussi on doit aller chercher
dans les journaux eux-mêmes.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
).
Ces restaurations ponctuelles 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 :
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.
.history
recovery_target_timeline
latest
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 :
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.)
checkpoint_timeout
max_wal_size
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.
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.
wal_compression
gzip
,
bzip2
, lzma
…
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 coût en CPU à l’écriture des journaux, très minime, et
un gros gain en volumétrie (souvent plus de 50 % !). 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.
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.
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
)list-server
, backup
,
list-backup
, recover
…pg_receivewal
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.
archive_wal
pitrery
restore_wal
pitrery a été créé par la société Dalibo. Il mettait l’accent sur la simplicité de sauvegarde et la restauration de la base.
Après 10 ans de développement actif, le projet Pitrery est désormais placé en maintenance LTS (Long Term Support) jusqu’en novembre 2026. Plus aucune nouvelle fonctionnalité n’y sera ajoutée, les mises à jour concerneront les correctifs de sécurité uniquement. Il est désormais conseillé de lui préférer pgBackRest. Il n’est plus compatible avec PostgreSQL 15 et supérieur.
pg_dump
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).
N’hésitez pas, c’est le moment !
But : Créer une sauvegarde physique à chaud à un moment précis de la
base avec pg_basebackup
, et la restaurer.
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 :
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
.
cp -rfp
) vers /var/lib/pgsql/16/data.old
(cette copie resservira plus tard).pg_basebackup
en décompressant ses deux archives.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 ?
But : Coupler une sauvegarde à chaud avec pg_basebackup
et l’archivage
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/var/lib/pgsql/16/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 :
(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 :
Cela va ouvrir l’accès sans mot de passe depuis l’utilisateur système postgres.
Redémarrer PostgreSQL :
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 :
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
La progression peut se suivre depuis psql avec :
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
.
…
-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 :
cp -rfp
) vers /var/lib/pgsql/16/data.old
(cette copie resservira plus tard).En tant qu’utilisateur privilégié :
En tant que postgres :
pg_basebackup
en décompressant ses deux archives.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/*
Une fois l’instance restaurée et démarrée, vérifier les traces : la base doit accepter les connexions.
…
… 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 ?
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 ?
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
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 :
…
… 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 :
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/var/lib/pgsql/16/archives
, par exemple avecrsync
. Configurer la commande de restauration inverse. Démarrer PostgreSQL.
Créer le répertoire d’archivage s’il n’existe pas déjà :
Là encore, en production, ce sera pluôt un partage distant. L’utilisateur système postgres doit avoir le droit d’y écrire.
L’archivage se définit dans postgresql.conf
:
et on peut y définir aussi tout de suite la commande de restauration :
Générer à nouveau de l’activité avec
pgbench
. Vérifier que l’archivage fonctionne.
…
-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.
pg_basebackup -D /var/lib/pgsql/16/backups/basebackup -Fp \
--checkpoint=fast --progress --max-rate=16M
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 :
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 :
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.
Effacer le PGDATA. Restaurer la sauvegarde précédente sans les journaux. Configurer la
restore_command
. Créer le fichierrecovery.signal
. Démarrer PostgreSQL.
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
:
Démarrer le service :
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 :
…
… LOG: database system was interrupted; last known up at 2023-11-05 18:32:52 UTC
rsync: link_stat "/var/lib/pgsql/16/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 "/var/lib/pgsql/16/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 "/var/lib/pgsql/16/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 "/var/lib/pgsql/16/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 "/var/lib/pgsql/16/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 :
…
-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 :
Superviser un serveur de bases de données consiste à superviser le SGBD lui-même, mais aussi le système d’exploitation et le matériel. Ces deux derniers sont importants pour connaître la charge système, l’utilisation des disques ou du réseau, qui pourraient expliquer des lenteurs au niveau du SGBD. PostgreSQL propose lui aussi des informations qu’il est important de surveiller pour détecter des problèmes au niveau de l’utilisation du SGBD ou de sa configuration.
Ce module a pour but de montrer comment effectuer une supervision occasionnelle (au cas où un problème survient, savoir comment interpréter les informations fournies par le système et par PostgreSQL) et comment mettre en place une supervision automatique (pour des alertes ou la supervision à long terme).
Il n’existe pas qu’une seule supervision. Suivant la personne concernée par la supervision et son objectif, les critères de la supervision seront différents.
Lors de la mise en place de la supervision, il est important de se demander l’objectif de cette supervision, à qui elle va servir, les critères qui importent à cette personne.
Répondre à ces questions permettra de mieux choisir l’outil de supervision à mettre en place, ainsi que sa configuration.
Généralement, les administrateurs mettant en place la supervision veulent pouvoir anticiper les problèmes, qu’ils soient matériels, de performance, de qualité de service, etc.
Améliorer les performances du SGBD sans connaître les performances globales du système est très difficile. Si un utilisateur se plaint d’une perte de performance, pouvoir corroborer ses dires avec des informations provenant du système de supervision aide à s’assurer qu’il y a bien un problème de performances et peut fréquemment aider à résoudre ce problème. De plus, il est important de pouvoir mesurer les gains de performances.
Une supervision des traces de PostgreSQL permet aussi d’améliorer les applications qui utilisent une base de données. Toute requête en erreur est tracée dans les journaux applicatifs, ce qui permet de trouver rapidement les problèmes que les utilisateurs rencontrent.
Un suivi régulier de la volumétrie ou du nombre de connexions permet de prévoir les évolutions nécessaires du matériel ou de la configuration : achat de matériel, création d’index, amélioration de la configuration.
Prévenir les incidents peut se faire en ayant une sonde de
supervision des erreurs disques par exemple. La supervision permet aussi
d’anticiper les problèmes de configuration. Par exemple, surveiller le
nombre de sessions ouvertes sur PostgreSQL permet de s’assurer que ce
nombre n’approche pas trop du nombre maximum de sessions configuré avec
le paramètre max_connections
dans le fichier
postgresql.conf
.
Enfin, une bonne configuration de la supervision implique d’avoir configuré finement la gestion des traces de PostgreSQL. Avoir un bon niveau de trace (autrement dit : ni trop, ni pas assez) permet de réagir rapidement après un crash.
Il y a trois types d’acteurs concernés par la supervision.
Le développeur doit pouvoir visualiser l’activité de la base de données. Il peut ainsi comprendre l’impact du code applicatif sur la base. De plus, le développeur est intéressé par la qualité des requêtes que son code exécute. Donc des traces qui ramènent les requêtes en erreur et celles qui ne sont pas performantes sont essentielles pour ce profil.
L’administrateur de bases de données a besoin de surveiller les bases pour s’assurer de la qualité de service, pour garantir les performances et pour réagir rapidement en cas de problème. Il doit aussi faire les mises à jours mineures dès qu’elles sont disponibles.
Enfin, l’administrateur système doit s’assurer de la présence du service. Il doit aussi s’assurer que le service dispose des ressources nécessaires, en terme de processeur (donc de puissance de calcul), de mémoire et de disque (notamment pour la place disponible).
top
, atop
, free
,
df
, vmstat
, sar
,
iotop
Voici quelques exemples d’indicateurs intéressants à superviser pour la partie du système d’exploitation.
La charge CPU (processeur) est importante. Elle peut expliquer pourquoi des requêtes, auparavant rapides, deviennent lentes. Cependant, la suractivité comme la non-activité sont un problème. En fait, si le service est tombé, le serveur sera en sous-activité, ce qui est un excellent indice.
Les entrées/sorties disque permettent de montrer un souci au niveau du système disque. Par exemple, PostgreSQL peut écrire trop à cause d’une mauvaise configuration des journaux de transactions, ou des fichiers temporaires issus de requêtes lourdes ou mal écrites ; ou il peut lire trop de données par manque de cache en RAM, ou de requêtes mal écrites.
L’espace disque est essentiel à surveiller. PostgreSQL ne propose rien pour cela, il faut donc le faire au niveau système. L’espace disque peut poser problème s’il manque, surtout si cela concerne la partition des journaux de transactions.
Unix possède de nombreux outils pour surveiller les différents
éléments du système. Les grands classiques sont top
et ses
innombrables clones comme atop
pour le CPU,
free
pour la RAM, df
pour l’espace disque,
vmstat
pour la mémoire virtuelle, iotop
pour
les entrées/sorties, ou sar
(généraliste). Sous Windows,
les premiers outils sont bien sûr le Gestionnaire des tâches, et Process
Monitor des outils Sysinternals.
Il est conseillé d’avoir une requête étalon dont la durée d’exécution sera testée de temps à autre pour détecter les moments problématiques sur le serveur.
Il existe de nombreux indicateurs intéressant sur les bases : nombre de connexions (en faisant par exemple la différence entre connexions inactives, actives, en attente de verrous), nombre de requêtes lentes et/ou fréquentes, volumétrie (en taille, en nombre de lignes), des ratios (utilisation du cache par exemple)…
La supervision occasionnelle est la conséquence d’une plainte d’un utilisateur : on se contente de réagir à un problème. C’est généralement insuffisant.
Il est important de mettre en place une solution de supervision automatique. Le but est de récupérer périodiquement des données statistiques sur les objets et sur l’utilisation du serveur pour avoir des graphes de tendance, et recevoir des alertes quand des seuils sont dépassés.
PostgreSQL propose deux canaux d’informations : les statistiques
d’activité (à ne pas confondre avec les statistiques sur les données, à
destination de l’optimiseur de requêtes) et les traces applicatives (ou
« logs »), souvent dans un fichier comme postgresql.log
(le
nom exact varie avec la distribution et l’installation).
PostgreSQL stocke un ensemble d’informations (métadonnées des schémas, informations sur les tables et les colonnes, données de suivi interne, etc.) dans des tables systèmes qui peuvent être consultées par les administrateurs. PostgreSQL fournit également des vues combinant des informations puisées dans différentes tables systèmes. Ces vues simplifient le suivi de l’activité de la base.
PostgreSQL est aussi capable de tracer un grand nombre d’informations qui peuvent être exploitées pour surveiller l’activité de la base de données.
Pour pouvoir mettre en place un système de supervision automatique, il est essentiel de s’assurer que les statistiques d’activité et les traces applicatives sont bien configurées et il faut aussi leur associer un outil permettant de sauvegarder les données, les alertes et de les historiser.
Pour récupérer et enregistrer les informations statistiques, les historiser, envoyer des alertes, il faut faire appel à un outil externe. Cela peut être un outil très simple comme munin ou un outil très complexe comme Nagios ou Zabbix.
Le script de monitoring check_pgactivity
permet
d’intégrer la supervision de bases de données PostgreSQL dans un système
de supervision piloté par Nagios (entre autres).
Au départ, Dalibo utilisait une sonde nommmée
check_postgres
et participait activement à son
développement, avec même un commiter dans le projet. Cependant,
rapidement, nous nous sommes aperçus que nous ne pouvions pas aller
aussi loin que nous le souhaitions. C’est à ce moment que, dans le cadre
de sa R&D, Dalibo a conçu check_pgactivity
.
La plupart des sondes de check_postgres
y ont été
réimplémentées. Le script corrige certaines sondes existantes et en
fournit de nouvelles répondant mieux aux besoins de notre supervision,
avec notamment un nombre plus important de métriques de performances. Il
renvoie directement des ratios par rapport à la valeur précédente plutôt
que des valeurs fixes. Pour les besoins les plus simples, le script peut
être utilisé de façon autonome, sans nécessité d’installer toute
l’infrastructure d’un outil comme Nagios, Icinga ou Grafana.
La supervision d’un serveur PostgreSQL passe par la surveillance de
sa disponibilité, des indicateurs sur son activité, l’identification des
besoins de maintenance, et le suivi de la réplication le cas échéant.
Ci-dessous figurent les sondes check_pgactivity
à mettre en
place sur ces différents aspects. Le site du projet contient toute la
documentation de chaque sonde.
Disponibilité :
connection
: réalise un test de connexion pour vérifier
que le serveur est accessible ;backends
: compte le nombre de connexions au serveur
comparé au paramètre max_connections
;backends_status
: permet d’obtenir des statistiques
plus précises sur l’état des connexions clientes et d’être alerté
lorsqu’un certain nombre de connexions clientes sont dans un état donné
(waiting, idle in transaction…) ;uptime
: détecte un redémarrage du serveur ou du
rechargement de la configuration.Vacuum :
autovacuum
: suit le fonctionnement de l’autovacuum et
des tâches en cours (VACUUM
, ANALYZE
,
FREEZE
…) ;table_bloat
: vérifie le volume de données « mortes »
et la fragmentation des tables ;btree_bloat
: vérifie le volume de données « mortes »
et la fragmentation des index - par rapport à
check_postgres
, le calcul est séparé entre tables et
index ;last_analyze
: vérifie si le dernier analyze (relevé
des statistiques relatives aux calculs des plans d’exécution) est trop
ancien ;last_vacuum
: vérifie si le dernier vacuum (relevé des
espaces réutilisables dans les tables) est trop ancien.Activité :
locks
: permet d’obtenir des statistiques plus
détaillées sur les verrous obtenus et tient notamment compte des
spécificités des predicate locks du niveau d’isolation
SERIALIZABLE
;wal_files
: compte le nombre de segments du journal de
transaction présents dans le répertoire pg_wal
;longest_query
: permet d’être alerté si une requête est
en cours d’exécution depuis plus d’un certain temps ;oldest_xact
: permet d’être alerté si une transaction
est ouverte depuis un certain temps sans être utilisée ;oldest_2pc
: calcule l’âge de la plus ancienne
transaction préparée (two-phase commit transaction) ;oldest_xmin
: repère la plus ancienne transaction de
chaque base, et ce à quoi elle est liée (requête, slot…) ;bgwriter
: permet de collecter des données de
performance des différents processus d’écritures de PostgreSQL ;hit_ratio
: calcule le hit ratio (utilisation
du cache de PostgreSQL) ;commit_ratio
: calcule la proportion de
COMMIT
et ROLLBACK
;checksum_errors
: détecte l’apparition d’erreurs de
sommes de contrôle (à partir de PostgreSQL 12) ;database_size
: suit la volumétrie des bases et leurs
variations ;max_freeze_age
: calcule l’âge des plus vieilles lignes
stockées dans chaque base pour suivre le bon passage des
VACUUM FREEZE
;stat_snapshot_age
: calcule l’âge des statistiques
d’activité pour repérer un blocage du collecteur ;temp_files
: suivi des fichiers temporaires.Configuration :
configuration
: permet de vérifier que les principaux
paramètres mémoire n’ont pas leur valeur par défaut ;minor_version
: détecte les instances n’ayant pas la
dernière version mineure ;settings
: repère un changement des paramètres ;invalid_indexes
: repérer tout index invalide ;pgdata_permission
: vérifie les droits sur
PGDATA
pour éviter un blocage au redémarrage ;table_unlogged
: remonte le nombre de tables
unlogged ;extensions_versions
: détecte les extensions à mettre à
jour.Réplication & archivage :
archiver
: compte le nombre de segments du journal de
transaction en attente d’archivage ;archive_folder
: vérifie qu’il n’y a pas de journal
manquant dans les archives de sauvegarde PITR ;hot_standby_delta
: calcule le délai de réplication
entre un serveur primaire et un serveur secondaire ;is_master
/ is_hot_standby
: vérifie que
l’instance est bien démarrée en lecture/écriture, ou une instance
secondaire ;is_replay_paused
: vérifie si la réplication est en
pause ;replication_slots
: calcule la volumétrie conservée
pour chaque slot de réplication.Sauvegarde physique et logique :
backup_label_age
: calcule l’âge du fichier
backup_label
(sauvegardes PITR exclusives) ;pg_dump_backup
: contrôle l’âge et la variation de
taille des sauvegardes logiques.Le script de monitoring check_postgres
est la première
sonde à avoir été écrite pour PostgreSQL. Comme vu précédemment,
check_pgactivity
est un remplaçant plus fonctionnel,
donnant plus de métriques, sur un nombre plus limité de sondes.
check_postgres
évolue cependant toujours et est
intéressant dans certains cas : alerte pour les triggers désactivés,
taille des relations, estimation du retard en réplication logique
(native et Slony), comparaison de schémas, détection de proximité du
wraparound. Elle intègre aussi beaucoup de sondes pour l’outil de
pooling pgBouncer.
La première information que fournit PostgreSQL sur son activité sort dans les traces. Chaque requête en erreur génère une trace indiquant la requête erronée et l’erreur. Chaque problème de lecture ou d’écriture de fichier génère une trace. En fait, tout problème détecté par PostgreSQL fait l’objet d’un message dans les traces. PostgreSQL peut aussi y envoyer d’autres messages suivant certains événements, comme les connexions, l’activité de processus système en tâche de fond, etc.
Nous allons donc aborder la configuration des traces (où tracer, quoi tracer, quel niveau d’informations). Nous verrons au passage nombre d’informations intéressantes à récupérer. Enfin, nous verrons quelques outils permettant de traiter automatiquement les fichiers de trace.
Il est essentiel de bien configurer PostgreSQL pour que les traces ne soient pas à la fois trop lourdes (pour ne pas être submergé par les informations) et incomplètes (il manque des informations). Un bon dosage du niveau des traces est important. Savoir où envoyer les traces est tout aussi important.
Suivant la configuration réalisée, les journaux applicatifs peuvent contenir quantité d’informations importantes. La plus fréquemment recherchée est la durée d’exécution des requêtes. L’intérêt principal est de récupérer les requêtes les plus lentes. L’autre information importante concerne les messages d’erreur, de niveau PANIC en premier lieu. Ces messages indiquent un état anormal du serveur qui s’est soldé par un arrêt brutal. Ce genre de problème est anormal et doit être surveillé.
Les messages PANIC
sont très importants. Généralement,
vous ne les verrez pas au moment où ils se produisent. Un crash va
survenir et vous allez chercher à comprendre ce qui s’est passé. Il est
possible à ce moment-là que vous trouviez dans les traces des messages
PANIC
, comme celui-ci :
Là, le problème est très simple : PostgreSQL n’arrive pas à créer un journal de transactions à cause d’un manque d’espace sur le disque. Du coup, le système ne peut plus fonctionner, il panique et s’arrête.
Un outil comme tail_n_mail peut aider à détecter automatiquement ce genre de problème et à envoyer un mail à la personne d’astreinte. Il n’est d’ailleurs pas si rare que PostgreSQL, après un problème grave, redémarre si vite qu’il n’y a aucune conséquence visible sérieuse, et que ce genre de détection automatique soit le seul symptôme d’un problème.
Un autre événement à suivre est le changement de la configuration du
serveur, et surtout la valeur des paramètres modifiés. PostgreSQL envoie
un message niveau LOG
lorsque la configuration est relue.
Il indique aussi les nouvelles valeurs des paramètres, ainsi que les
paramètres modifiés qu’il n’a pas pu prendre en compte (cela peut
arriver pour tous les paramètres exigeant un redémarrage du
serveur).
Là-aussi, tail_n_mail est l’outil adapté pour être prévenu dès que la configuration du serveur est relue.
—
log_destination
:
stderr
/ csvlog
/ jsonlog
(v15)syslog
/ eventlog
logging_collector
: géré par PostgreSQL (Red Hat)
log_directory
, log_filename
,
log_file_mode
log_rotation_age
, log_rotation_size
,
log_truncate_on_rotation
off
, penser à logrotate
(Debian)syslog
(Unix)
syslog_facility
, syslog_ident
syslog_sequence_numbers
,
syslog_split_messages
eventlog
(Windows) : event_source
PostgreSQL peut envoyer les traces sur plusieurs destinations selon
la valeur de log_destination
:
stderr, csvlog et jsonlog
stderr
(valeur par défaut, y compris sur Debian &
Red Hat), csvlog
et jsonlog
, disponibles sur
toutes les plateformes, correspondent à la sortie des erreurs.
La différence entre les trois réside dans le format des traces (respectivement texte simple ou CSV ou JSON). La sortie JSON n’existe que depuis la version 15 mais il est à noter qu’il existe une extension jsonlog, par Michaël Paquier, qui offre en plus le format JSON pour les versions antérieures.
Les paramètres suivants sont spécifiques à stderr
,
csvlog
et jsonlog
.
logging_collector
indique ensuite si PostgreSQL doit
s’occuper lui-même de la gestion des fichiers de logs.
S’il est à on
(défaut sous Red Hat/CentOS/Rocky
Linux) :
log_directory
désigne l’emplacement des journaux
applicatifs. Par défaut, il est dans le répertoire de l’instance
(sous-répertoire log
, ou pg_log
jusqu’en
version 9.6 comprise) ;
log_filename
indique le nom des fichiers. Sa valeur
varie suivant la distribution :
postgresql-%Y-%m-%d_%H%M%S.log
est le défaut des
versions compilées (avec rotation quotidienne) ;postgresql-%a.log
est le défaut sur
Red Hat/CentOS/Rocky Linux : on obtient une rotation quotidienne sur une
semaine ( postgresql-Mon.log
,
postgresql-Tue.log
, etc.) ;log_file_mode
précise les droits sur les fichiers
(0600
par défaut, les réservant à l’utilisateur sous lequel
l’instance tourne). Une rotation est configurable suivant la taille
(log_rotation_size
) et la durée de vie
(log_rotation_age
, souvent 1d
, à garder en
cohérence avec log_filename
) ;
log_truncate_on_rotation
à on
entraîne
l’effacement de tout fichier dont le nom serait réutilisé, ce qui est en
général une bonne idée si les mêmes noms de fichiers sont
réutilisés.
Par contre, sous Debian, logging_collector
est par
défaut à off
, les paramètres ci-dessus sont ignorés et la
gestion des logs est gérée par le système d’exploitation :
/var/log/postgresql/
, donc hors du
PGDATA ;pg_ctlcluster
(donc
pour l’instance installée par défaut, en version 14, le fichier sera
postgresql-14-main.log
), sauf à modifier le
pg_ctl.conf
de l’instance ;logrotate
(comme
les autres fichiers de log), et paramétrée dans
/etc/logrotate.d/postgresql-common
avec par défaut une
rotation sur 10 jours.Une instance compilée n’utilise pas non plus le logging collector.
syslog
syslog
fonctionne uniquement sur un serveur Unix et est
intéressant pour centraliser la configuration des traces.
Dans cette configuration, il reste à définir le niveau avec
syslog_facility
, et l’identification du programme avec
syslog_ident
. Les valeurs par défaut de ces deux paramètres
sont généralement bonnes. Il est intéressant de modifier ces valeurs
surtout si plusieurs instances de PostgreSQL sont installées sur le même
serveur car cela permet de différencier leur traces.
syslog_sequence_numbers
préfixe chaque message d’un
numéro de séquence incrémenté automatiquement, pour éviter le message
--- last message repeated N times ---
, utilisé par un grand
nombre d’implémentations de syslog. Ce comportement est activé par
défaut. Quant à syslog_split_messages
, s’il est activé, les
messages envoyés à syslog sont divisés par lignes, elles-mêmes divisées
pour tenir sur 1024 octets. Attention à ce que syslog
soit
configuré pour accepter des débits élevés quand on tient à tout
tracer.
eventlog
eventlog
est disponible uniquement sur Windows et
alimente le journal des événements. Pour identifier les messages de
PostgreSQL, il faut aussi renseigner event_source
,
qui par défaut est à « PostgreSQL ».
log_min_messages
panic
/ fatal
/ log
/ error
/ warning
log_min_error_statement
error
(ou warning
)log_error_verbosity
default
/ terse
/
verbose
log_min_messages
est le paramètre à configurer pour
avoir plus ou moins de traces. Par défaut, PostgreSQL enregistre tous
les messages de niveau panic
, fatal
,
log
, error
et warning
. Cela peut
sembler beaucoup mais, dans les faits, c’est assez discret. Cependant,
il est possible de descendre le niveau ou de l’augmenter.
log_min_error_statement
indique à partir de quel niveau
la requête est elle-aussi tracée. Par défaut, la requête n’est tracée
que si une erreur est détectée. Généralement, ce paramètre n’est pas
modifié, sauf dans un cas précis. Les messages d’avertissement (niveau
warning
) n’indiquent pas la requête qui a généré
l’affichage du message. Cela est assez important, notamment dans le
cadre de l’utilisation d’antislash dans les chaînes de caractères. On
verra donc parfois un abaissement au niveau warning
pour
cette raison.
log_error_verbosity
vaut default
, qui
convient généralement. Si une requête est tracée, plusieurs lignes
peuvent apparaître, chacune de niveau DETAIL
,
HINT
, QUERY
ou CONTEXT
. La valeur
terse
les masque. À l’inverse, verbose
rajoute
encore d’autres informations sur le code source d’origine.
log_min_duration_statement
(ex : 1s
)log_statement
+ log_duration
log_transaction_sample_rate
log_statement_sample_rate
+
log_min_duration_sample
Pour répérer les problèmes de performances, il est intéressant de pouvoir tracer les requêtes et leur durée d’exécution. PostgreSQL propose deux solutions à cela.
log_statement & log_duration :
La première solution disponible concerne les paramètres
log_statement
et log_duration
. Le premier
permet de tracer toute requête exécutée si la requête correspond au
filtre indiqué par le paramètre :
none
: aucune requête n’est tracée ;ddl
: seules les requêtes DDL (autrement dit de
changement de structure) sont tracées ;mod
: seules les requêtes de changement de structure et
de données sont tracées ;all
: toutes les requêtes sont tracées.Le paramètre log_duration
est un simple booléen. S’il
vaut true
ou on
, chaque requête exécutée
envoie en plus un message dans les traces indiquant la durée d’exécution
de la requête. Évidemment, il vaut mieux alors configurer
log_statement
à all
, ou il sera impossible de
dire à quelles requêtes les temps correspondent.
Donc pour tracer toutes les requêtes et leur durée d’exécution, une solution serait de réaliser la configuration suivante :
Une requête générera deux entrées dans les traces, de cette façon :
2019-01-28 15:43:27.993 CET [25575] LOG: statement: SELECT * FROM pg_stat_activity;
2019-01-28 15:43:27.999 CET [25575] LOG: duration: 7.093 ms
log_min_duration_statement :
Il est préférable de désactiver ces deux paramètres et de préférer
log_min_duration_statement
. Son but est d’abord de cibler
les requêtes lentes, par exemple celles qui prennent plus de deux
secondes à s’exécuter :
La requête et la durée d’exécution seront alors tracées dans le même message :
2019-01-28 15:49:56.193 CET [32067] LOG:
duration: 2906.270 ms
statement: insert into t1 select i, i from generate_series(1, 200000) as i;
En plus de la trace par log_min_duration_statement
, rien
n’interdit de tracer des requêtes sensibles, notamment le DDL :
Échantillonnage :
Quelle que soit la méthode, tracer toutes les requêtes peut poser problème pour de simples raisons de volumétrie du fichier de traces. Même s’il est possible de configurer finement la durée à partir de laquelle une requête est tracée, il faut bien comprendre que plus la durée minimale est importante, plus la vision des performances est partielle. Passeront ainsi « sous le radar » des requêtes relativement rapides mais très nombreuses qui, ensemble, peuvent représenter l’essentiel de la charge.
Cela étant dit, laisser 0 en permanence n’est pas recommandé. Il est préférable de configurer ce paramètre à une valeur plus importante en temps normal pour détecter seulement les requêtes longues et, lorsqu’un audit de la plateforme est nécessaire, passer temporairement ce paramètre à une valeur très basse (0 étant le mieux).
Une nouvelle fonctionnalité a donc été ajoutée : tracer une certaine proportion des requêtes ou des transactions.
log_transaction_sample_rate
, à partir de PostgreSQL 12,
indique une proportion de transactions à tracer. Par
exemple, en le configurant à 0.01
, toutes les requêtes d’un
centième des transactions, choisies au hasard, seront tracées.
De manière similaire, à partir de PostgreSQL 13,
log_statement_sample_rate
indique la proportion de
requêtes à tracer, parmi celles durant plus d’une
certaine durée, à indiquer dans
log_min_duration_sample
:
Évidemment, une requête dépassant la durée de
log_min_duration_statement
sera toujours tracée.
log_connections
, log_disconnections
log_autovacuum_min_duration
log_checkpoints
log_lock_waits
(mini 1s)log_recovery_conflict_waits
(v14+)En dehors des erreurs et des durées des requêtes, il est aussi possible de tracer certaines activités ou comportements. Le paramétrage par défaut est peu bavard, et il est généralement conseillé d’activer tous les paramètres qui suivent.
log_connections
et son pendant
log_disconnections
, à on
, permettent de suivre
qui se (dé)connecte, depuis où, et durant combien de temps :
2019-01-28 13:34:32 CEST LOG: connection received: host=[local]
2019-01-28 13:34:32 CEST LOG: connection authorized: user=u1 database=b1
…
2016-09-01 13:34:35 CEST LOG: disconnection: session time: 0:01:04.634
user=u1 database=b1 host=[local]
Il est possible de récupérer cette durée de session pour calculer leur durée moyenne. Cette information est importante pour savoir si un outil de pooling de connexions a un intérêt.
log_autovacuum_min_duration
équivaut à
log_min_duration_statement
, mais pour l’autovacuum. Le but
est de tracer son activité, au-delà d’une certaine durée, de vérifier
qu’il passe suffisamment fréquemment et rapidement.
log_checkpoints
à on
ajoute un message dans
les traces pour indiquer qu’un checkpoint commence ou se termine, auquel
cas s’ajoutent des statistiques :
2019-01-28 13:34:17 CEST LOG: checkpoint starting: xlog
2019-01-28 13:34:20 CEST LOG: checkpoint complete:
wrote 13115 buffers (80.0%);
0 WAL file(s) added, 0 removed, 0 recycled;
write=3.007 s, sync=0.324 s, total=3.400 s;
sync files=16, longest=0.285 s, average=0.020 s;
distance=404207 kB, estimate=404207 kB
Le message indique donc en plus le nombre de blocs écrits sur disque, le nombre de journaux de transactions ajoutés, supprimés et recyclés. Il est rare que des journaux soient ajoutés, ils sont plutôt recyclés. Des journaux sont supprimés quand il y a eu une très grosse activité qui a généré plus de journaux que d’habitude. Les statistiques incluent aussi la durée des écritures, de la synchronisation sur disque, la durée totale, etc. Le plus important est de pouvoir vérifier que l’écriture des checkpoints est bien régulière (essentiellement périodique).
log_lock_waits
à on
permet de tracer les
attentes de verrous (par exemple, un UPDATE
bloqué par un
autre UPDATE
, un SELECT
bloqué par
TRUNCATE
ou un VACUUM FULL
, etc…) Lorsque
l’attente dépasse la durée indiquée par le paramètre
deadlock_timeout
(1 seconde par défaut), un message est
enregistré, comme dans cet exemple :
2019-01-28 13:38:40 CEST LOG: process 15976 still waiting for
AccessExclusiveLock on relation 26160 of
database 16384 after 1000.123 ms
2019-01-28 13:38:40 CEST STATEMENT: DROP TABLE t1;
Ici, un DROP TABLE
attend depuis 1 seconde de pouvoir
poser un verrou exclusif sur une relation.
Plus ce type de message apparaît dans les traces, plus des contentions ont lieu sur certains objets, ce qui peut diminuer fortement les performances. Ces messages peuvent permettre d’analyser la cause première d’une accumulation de verrous, à condition que les requêtes soient tracées.
En version 14 apparaît le paramètre
log_recovery_conflict_waits
. Ce dernier, une fois activé,
permet de tracer toute attente due à un conflit de réplication. Il n’est
donc valable et pris en compte que sur un serveur secondaire.
log_temp_files
à activer !Quand PostgreSQL ne peut effectuer un tri en mémoire, il le fait sur
disque dans un fichier temporaire, ce qui est beaucoup plus lent qu’en
mémoire, même avec un SSD. Typiquement, cela concerne le tri de données
et le hachage, quand la valeur du paramètre work_mem
ne
permet pas de tout faire en mémoire. Cela ne sera pas forcément gênant
pour une grosse requête ponctuelle, mais, répétés, ces fichiers peuvent
avoir un impact sur la performance du système. Ils sont parfois
inévitables quand on brasse beaucoup de données.
Être averti lors de la création de ce type de fichiers peut être
intéressant, mais ils sont parfois trop fréquents pour que ce soit
réaliste. Il est préférable de faire analyser après coup un fichier de
traces pour savoir combien de fichiers temporaires ont été créés, et de
quelles tailles. Cela peut mener à vérifier les requêtes exécutées, les
optimiser, vérifier la configuration, réviser la valeur de
work_mem
…
Le paramètre log_temp_files
à 0 permet de tracer toutes
les créations de fichiers temporaires, comme ici :
Pour le même tri, il peut y avoir de nombreux fichiers temporaires. De plus, la requête est aussi tracée, et si elle est longue et fréquente, le volume de traces peut être conséquent.
log_line_prefix
%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h
lc_messages
= C
log_timezone
= 'Europe/Paris'
Le paramètre log_line_prefix
permet d’ajouter un préfixe
à une trace. Le défaut ('%m [%p] '
, soit horodatage et
numéro de processus), est insuffisant :
2021-02-05 14:12:12.343 UTC [2917] LOG: duration: 3.276 ms
statement: SELECT count(*) FROM pgbench_branches ;
Il est conseillé de rajouter le nom de l’application cliente, le nom
de l’utilisateur, le nom de la base, etc. Une valeur
habituellement conseillée pour pgBadger, pour une sortie vers
stderr
, est :
ce qui nous donnera ce genre de traces :
2021-02-05 14:30:01 UTC [3006]: user=durand,db=bench,app=test,client=[local]
LOG: duration: 0.184 ms statement: SELECT count(*) FROM pgbench_branches ;
Pour une sortie vers syslog
, l’horodatage est
inutile :
Par défaut, les traces sont enregistrées dans la locale par défaut du serveur. Des traces en français peuvent présenter certains intérêts pour des débutants, mais ont plusieurs gros inconvénients : un moteur de recherche renverra beaucoup moins de résultats avec des traces en français qu’en anglais, et les outils d’analyse automatique des traces se basent principalement sur des traces en anglais. Donc, il vaut mieux préciser systématiquement :
Quant à log_timezone
, il permet de choisir le fuseau
horaire pour l’horodatage des traces. C’est inestimable quand on
administre différents serveurs dispersés sur la planète.
Il existe de nombreux programmes qui analysent les traces. On peut distinguer deux catégories :
Mais également :
L’analyse en temps réel des traces permet de réagir rapidement à certains messages. Par exemple, il est important d’avoir une réaction rapide à l’archivage échoué d’un journal de transactions, ainsi qu’en cas de manque d’espace disque. Dans cette catégorie, il existe des outils généralistes comme logwatch, et des outils spécifiques pour PostgreSQL comme tail_n_mail.
L’analyse après coup permet une analyse plus fine, se terminant généralement par un rapport en HTML, parfois avec des graphes. Cette analyse plus fine nécessite des outils spécialisés. Il en a existé plusieurs qui ne sont plus maintenus. La référence dans le domaine est pgBadger.
VACUUM
, des connexions, des
checkpointsGilles Darold a créé pgBadger, un analyseur des journaux applicatifs de PostgreSQL. Il permet de générer des rapports détaillés depuis ceux-ci. pgBadger est très souvent utilisé pour déterminer les requêtes à améliorer en priorité pour accélérer son application basée sur PostgreSQL. C’est certainement le meilleur outil actuel de rétro-analyse d’un fichier de traces PostgreSQL, au point qu’il est cité dans le manuel de PostgreSQL.
pgBadger est écrit en Perl et est facilement extensible si vous avez besoin de rapports spécifiques.
Il est conçu pour traiter rapidement de gros fichiers de traces avec une mémoire réduite, mais permet d’exploiter plusieurs CPU pour accélérer considérablement l’analyse.
pgBadger s’utilise en ligne de commande : il suffit de lui fournir le ou les fichiers de trace à analyser et il rend un rapport HTML sur les requêtes exécutées, sur les connexions, sur les bases, etc. Le rapport est très complet. Les graphes sont zoomables. Les requêtes sont reformatées pour plus de lisibilité.
log_destination
log_line_prefix
lc_messages
=C
log_connections
, log_disconnections
log_checkpoints
log_lock_waits
log_temp_files
log_autovacuum_min_duration
log_min_duration_statement = 0
(attention !)pgBadger a besoin d’un minimum d’informations dans les traces :
timestamp (%t
), pid (%p
) et numéro de ligne
dans la session (%l
). Il n’y a pas de conseil particulier
sur la destination des traces (en dehors de eventlog
que
pgBadger ne sait pas traiter). De même, le préfixe des traces est laissé
au choix de l’utilisateur. Dans certains cas, il faudra le préciser à
pgBadger avec l’option --prefix
(par exemple si la valeur
de log_line_prefix
a changé entre le début et la fin du
fichier). Une configuration courante et vraiment informative est par
exemple :
Noter que même sans cela, pgBadger essaie de récupérer les
informations sur les adresses IP, les utilisateurs connectés, les bases
de données à partir des traces sur les connexions. Ajouter le joker
%e
(code SQLState) permet d’obtenir un tableau sur les
erreurs.
La langue des traces doit être l’anglais (lc_messages
à
C
), ce qui de toute manière est la valeur conseillée.
Pour tracer les requêtes, il est préférable de passer par
log_min_duration_statement
plutôt que
log_statement
et log_duration
: pgBadger fera
plus facilement l’association entre chaque requête et sa durée :
Comme déjà dit plus haut, log_min_duration_statement = 0
peut générer un énorme volume de traces et n’est souvent configuré ainsi
que le temps d’un audit. Avec une valeur supérieure, pgBadger ne verra
absolument pas les requêtes les plus rapides.
Il est aussi conseillé de tirer parti d’autres informations dans les traces :
log_checkpoints
pour des statistiques sur les
checkpoints ;log_connections
et log_disconnections
pour
des informations sur les connexions et déconnexions ;log_lock_waits
pour des statistiques sur les verrous en
attente ;log_temp_files
pour des statistiques sur les fichiers
temporaires ;log_autovacuum_min_duration
pour des statistiques sur
l’activité de l’autovacuum.--outfile
--prefix
--begin
/ --end
--dbname
, --dbuser
,
--dbclient
, --appname
--jobs
Pour l’utilisation la plus simple, il suffit de fournir au script en paramètre les noms des différents fichiers de traces, éventuellement compressés, pour obtenir le rapport sur tous les événements qu’il y trouvera.
Il existe énormément d’options, et Gilles Darold en rajoute fréquemment. L’aide fournie sur le site web officiel les cite intégralement.
Parmi les plus utiles, --outfile
permet d’indiquer le
nom du rapport (par défaut out.html
).
--prefix
permet de préciser une valeur de
log_line_prefix
si la détection automatique échoue.
Le rapport peut être restreint à une tranche horaire précise avec
--begin
et --end
(par exemple
--begin '2019-04-01 16:00:00
).
Pour mieux cibler le rapport, il est possible de restreindre
l’analyse à un utilisateur (--dbuser
), une base de données
(--dbname
), une machine cliente (--dbclient
),
ou une application (--appname
, si elle définit bien
application_name
à la connexion).
--jobs
permet de paralléliser la lecture des traces sur
plusieurs processeurs. Attention, de très gros fichiers les satureront
probablement plusieurs minutes.
Au tout début du rapport, pgBadger donne des statistiques générales sur les fichiers de traces.
Dans les informations importantes se trouve le nombre de requêtes normalisées. En fait, des requêtes telles que :
et
sont différentes car elles ne vont pas récupérer la même fiche utilisateur. Cependant, en enlevant la partie constante, les requêtes sont identiques. La seule différence est la ligne récupérée mais pas la requête. pgBadger est capable de faire cette différence. Toute constante, qu’elle soit de type numérique, textuelle, horodatage ou booléenne, peut être supprimée de la requête. Dans l’exemple ci-dessus, pgBadger a comptabilisé environ 19 millions de requêtes, mais seulement 19 069 requêtes différentes après normalisation. Ceci est important dans le fait où nous n’allons pas devoir travailler sur plusieurs millions de requêtes mais « seulement » sur 19 000.
Autre information intéressante, la durée d’exécution totale des requêtes. Ici, nous avons 9 heures d’exécution de requêtes. Cependant, les traces ne couvrent que 11 h à 12 h, soit une heure. Cela indique que le serveur est assez sollicité. Il est fréquent que la durée d’exécution sérielle des requêtes soit plusieurs fois plus importantes que la durée des traces.
Ce graphe indique le nombre de requêtes par seconde : en fait, environ 33 requêtes/s en pointe.
Ce graphe affiche en vert le nombre de fichiers temporaires créés sur la durée des traces. La ligne bleue correspond à la taille des fichiers. Nous remarquons ainsi la création à peu près régulière de fichiers temporaires, à raison d’environ 200 Mo par tranche de 5 minutes.
De grosses écritures peuvent mener à un nombre important de journaux. Cette courbe montre une création relativement régulière de journaux. En fait, il s’agit uniquement de recyclage de journaux existants : PostgreSQL n’a pas à en créer et en initialiser en masse. Le pic n’est que de 5 journaux de 16 Mo à la minute.
Plus bas dans l’onglet Checkpoints figurent des informations sur l’écart (en octets) entre deux checkpoints, la durée d’écriture, la durée de synchronisation sur le disque, et le nombre d’avertissements sur des checkpoints non périodiques.
Le plus important est certainement l’onglet Top/Time consuming queries. Il liste les requêtes qui ont pris le plus de temps, que ce soit parce que les requêtes en question sont vraiment très lentes ou parce qu’elles sont exécutées un très grand nombre de fois.
Ci-dessus n’est affichée que la première requête de la liste. Le bouton Details permet d’afficher la courbe indiquant les heures d’occurrences et les durées.
Nous remarquons d’ailleurs dans cet exemple qu’avec la seule requête affichée, nous arrivons à un total de 2 h 43. Le premier exemple nous indique que l’exécution sérielle des requêtes aurait pris 9 heures. En ne travaillant que sur elle, nous travaillons en fait sur presque le tiers du temps total d’exécution des requêtes comprises dans les traces.
Les autres requêtes les plus consommatrices ne sont pas listées ici, mais il est peu probable que l’on ait à travailler sur les 19 000 requêtes normalisées. Il est fréquent que l’essentiel de la charge soit contenu dans les quelques premières requêtes du tableau. Ce sont évidemment les premières cibles pour une tentative d’optimisation : réécriture, modification du paramétrage, index dédiés…
Surveiller ses journaux applicatifs (ou fichiers de trace) est une activité nécessaire mais bien souvent rébarbative. De nombreux programmes existent pour nous faciliter la tâche, mais le propre de ces programmes est d’être exhaustif. De plus, ils demandent une action de la part de l’administrateur, à savoir : penser à aller les regarder.
logwatch est une petite application permettant d’analyser les journaux de nombreux services et de produire un rapport synthétique. Le nombre de services connus est impressionnant et il est simple d’en ajouter de nouveaux. logwatch est le plus souvent intégré à votre distribution Linux. Depuis la version 7.4.2 il sait analyser les traces de PostgreSQL.
Après son installation, il se lancera tous les jours grâce au fichier
/etc/cron.daily/00logwatch
. Vous recevrez tous les jours
par mail les rapports des événements importants détectés dans les
fichiers de traces. Vous pouvez aussi le lancer manuellement ainsi :
--range All
indique que l’on veut un rapport sur tous
les fichiers de traces existants. La valeur par défaut est
Yesterday
. Pour n’avoir un rapport que sur la journée,
choisir Today
.
Avec le niveau de détail minimal (--detail
à
Low
), seuls les messages de type FATAL
,
PANIC
et ERROR
seront remontés. Avec la valeur
Med
, apparaîtront aussi les messages de type
WARNING
et HINTS
.
Exemple de résultat :
################### Logwatch 7.3.6 (05/19/07) ####################
Processing Initiated: Tue Dec 13 12:28:46 2011
Date Range Processed: all
Detail Level of Output: 5
Type of Output/Format: stdout / text
Logfiles for Host: devel
##################################################################
--------------------- PostgreSQL Begin ------------------------
Fatals:
-------
9 times:
[2011-12-04 04:28:46 +/-9 day(s)] password authentication failed for
user "postgres"
8 times:
[2011-11-21 10:15:01 +/-11 day(s)] terminating connection due to
administrator command
…
Errors:
-------
7 times:
[2011-11-14 12:20:44 +/-58 minute(s)] relation "COUNTRIES" does not exist
5 times:
[2011-11-14 12:21:24 +/-59 minute(s)] syntax error at or near
"role_permission_view"
…
Warnings:
---------
55 times:
[2011-11-18 12:26:39 +/-6 day(s)] terminating connection because of
crash of another server process
Hints:
------
55 times:
[2011-11-18 12:26:39 +/-6 day(s)] In a moment you should be able to
reconnect to the database and repeat
you command.
14 times:
[2011-12-08 09:47:16 +/-19 day(s)] No function matches the given name and
argument types. You might need to add
explicit type casts.
---------------------- PostgreSQL End -------------------------
###################### Logwatch End #########################
logwatch dispose de nombreuses options, et la page de manuel est certainement la meilleure documentation à l’heure actuelle.
tail_n_mail est un outil écrit par la société EndPointCorporation.
Son but est d’analyser périodiquement le contenu des fichiers de traces
et d’envoyer un mail en cas de la détection d’un motif d’intérêt. Par
exemple, il peut envoyer un mail lorsqu’il rencontre un message de
niveau PANIC
ou FATAL
dans les traces. Ainsi
une personne d’astreinte sera prévenue rapidement et pourra agir en
conséquence.
Les clés INCLUDE
et EXCLUDE
permettent
d’indiquer les motifs à inclure et à exclure respectivement. Tout motif
non inclus ne sera pas pris en compte. Cette configuration permet donc
d’envoyer un mail à l’adresse astreinte@dalibo.com
à chaque
fois qu’un message contenant les mots PANIC
,
FATAL
, temporary file
ou
reloading configuration files
sont enregistrés dans les
traces. Par contre, tous les messages contenant la phrase
database ... does not exist
ne sont pas pris en compte.
Exemple:
Voici un exemple de mail envoyé:
Matches from /var/log/postgresql/postgresql-10-main.log: 42
Date: Fri Jan 1 10:34:00 2010
Host: pollo
[1] Between lines 123005 and 147976, occurs 39 times.
First: Jan 1 00:00:01 rojogrande postgres[4306]
Last: Jan 1 10:30:00 rojogrande postgres[16854]
Statement: user=root,db=rojogrande FATAL: password authentication failed
for user "root"
[2] Between lines 147999 and 148213, occurs 2 times.
First: Jan 1 10:31:01 rojogrande postgres[3561]
Last: Jan 1 10:31:10 rojogrande postgres[15312]
Statement: FATAL main: write to worker pipe failed -(9) Bad file descriptor
[3] (from line 152341)
PANIC: could not locate a valid checkpoint record
Les statistiques sont certainement les informations les plus simples à récupérer par un système de supervision. Il faut dans un premier temps s’assurer que la configuration est adéquate. Ceci fait, il est possible de lire les statistiques disponibles dans les vues proposées par défaut. Enfin, il existe quelques outils capables de récupérer des informations provenant des tables statistiques de PostgreSQL. Leur mise en place permettra une supervision facilitée.
track_activities
= on
track_activity_query_size
= 10000
ou
+compute_query_id
= onIl est important d’avoir des informations sur les sessions en cours
d’exécution sur le serveur. Cela se fait grâce au paramètre
track_activities
. Il est à on
par défaut.
Ainsi, dans la vue pg_stat_activity
qui liste les sessions,
les colonnes xact_start
, query_start
,
state_change
, wait_event_type
,
wait_event
, state
et query
sont
renseignées.
-[ RECORD 1 ]----+---------------------------------------
datid | 16425
datname | bench
pid | 3006
leader_pid |
usesysid | 10
usename | postgres
application_name | test
client_addr |
client_hostname |
client_port | -1
backend_start | 2021-02-05 14:29:42.833102+00
xact_start | 2021-02-05 17:28:12.157115+00
query_start | 2021-02-05 17:28:12.157115+00
state_change | 2021-02-05 17:28:12.157117+00
wait_event_type |
wait_event |
state | active
backend_xid |
backend_xmin | 186938
query_id |
query | select * from pg_stat_activity where backend_type = 'client backend'
backend_type | client backend
Il est à noter que la requête indiquée dans la colonne
query
est tronquée à 1024 caractères par défaut. En
pratique, cette limite est vite atteinte par de longues requêtes. Il est
conseillé de l’augmenter à quelques kilooctets (paramètre
track_activity_query_size
).
Depuis la version 14, il est possible d’avoir en plus l’identifiant
de la requête. Pour cela, il faut activer le paramètre
compute_query_id
.
track_counts
= on
track_io_timing
= on
track_functions
= off
/ pl
/
all
Par défaut, le paramètre track_counts
est à
on
. Le collecteur de statistiques est alors capable de
récupérer des informations sur des nombres de lignes lues, insérées,
mises à jour, supprimées, vivantes, mortes, etc., et de blocs lus dans
le cache de PostgreSQL (hit) ou en dehors (read).
track_io_timing
réalise un chronométrage des opérations
de lecture et écriture disque. Il complète les champs
blk_read_time
et blk_write_time
dans les
tables pg_stat_database
et pg_stat_statements
(si cette
extension est installée). Il ajoute des traces suite à un
VACUUM
ou un ANALYZE
exécutés par le processus
autovacuum
Dans les plans d’exécutions (avec
EXPLAIN (ANALYZE,BUFFERS)
), il permet l’affichage du temps
passé à lire hors du cache de PostgreSQL(sur disque ou dans le cache de
l’OS) :
Avant d’activer track_io_timing
sur une machine peu
performante, vérifiez avec l’outil pg_test_timing
que la
quasi-totalité des appels dure moins d’une nanoseconde.
PostgreSQL sait aussi récupérer des statistiques sur les routines
stockées. Il faut activer le paramètre track_functions
qui
a trois valeurs : off
pour ne rien récupérer,
pl
pour récupérer les statistiques sur les procédures
stockées en PL/*
et all
pour récupérer les
statistiques des fonctions quelque soit leur langage (notamment celles
en C). Les résultats (nombre et durée d’exécution) peuvent se suivre
dans la vue pg_stat_user_functions
.
stats_temp_directory
(<v15)
pg_stat
lors d’un arrêt propreAvant la version 15, il existe un processus collecteur de statistiques, qui fonctionne ainsi :
stats_temp_directory
;$PGDATA/pg_stat
.L’intérêt de ce fonctionnement est de pouvoir copier le fichier de
statistiques sur un disque très rapide (comme un disque SSD), voire dans
de la mémoire montée en disque comme tmpfs
(c’est
d’ailleurs le défaut sur Debian).
À partir de la version 15, les statistiques sont stockés en mémoire partagée et il n’y a plus de collecteur.
ANALYZE
(voire VACUUM
)En cas d’arrêt brutal de PostgreSQL ou de restauration physique, les
statisiques d’activité sont perdues (pas les statistiques sur les
données). Lancer un ANALYZE
est préférable pour éviter que
l’autovacuum tarde à nettoyer les tables ou rafraîchir les statistiques
sur les données.
Sur :
Au niveau des statistiques, il existe un grand nombre d’informations intéressantes à récupérer. Cela va des informations sur l’activité en cours (nombre de connexions, noms des utilisateurs connectés, requêtes en cours d’exécution), à celles sur les bases de données (nombre d’écritures, nombre de lectures dans le cache), à celles sur les tables, index et fonctions.
Les connaître toutes présente peu d’intérêt car seulement certaines sont vraiment intéressantes à superviser. Nous allons voir ici quelques exemples.
Il est souvent intéressant de connaître le nombre de personnes
connectées. Cela permet notamment de s’assurer que la configuration du
paramètre max_connections
est suffisamment élevée pour ne
pas voir des demandes de connexion être refusées.
Il est aussi intéressant de pouvoir dénombrer le nombre de connexions par bases. Cela permet d’avoir une idée de la charge sur chaque base. Une base ayant de plus en plus d’utilisateurs et souffrant de performances devra peut-être être placée seule sur un serveur.
Il est aussi possible d’avoir le nombre de connexions par :
Le graphe affiché ci-dessus montre l’utilisation des connexions sur différentes bases. Les bases systèmes ne sont pas du tout utilisées. Seule la base utilisateur reçoit un grand nombre de connexions. Il y a même eu deux pics, un à environ 400 connexions et un autre à 450 connexions.
La volumétrie des bases est une autre information fréquemment demandée. L’exécution de cette requête toutes les cinq minutes permettra de suivre l’évolution de la taille des bases.
Le graphe ci-dessus montre une montée en volumétrie assez importante, la base ayant doublé en un an.
Autre demande fréquente : pouvoir suivre le nombre de verrous. La requête ci-dessus récupère le nombre de verrous posés par base. Il est aussi possible de récupérer les types de verrous, ou de ne prendre en compte que certains types de verrous.
Le graphe montre une utilisation très limitée des verrous. En fait, on observe surtout une grosse utilisation des verrous entre 20h et 21h30. Si le graphe montrait plusieurs jours d’affilé, on pourrait s’apercevoir que le pic est présent tous les jours à ce moment-là. En fait, il s’agit de la sauvegarde. La sauvegarde pose un grand nombre de verrous, des verrous très légers qui vont bloquer très peu de monde, mais néanmoins, ils sont présents.
Les trois exemples proposés ci-dessus ne sont que les exemples les plus marquants. Beaucoup d’autres informations sont récupérables. On peut citer par exemple :
Il existe de nombreux outils utilisables avec les statistiques. Les
plus connus sont Munin (sondes déjà intégrées), Nagios et Zabbix. Pour
ces deux derniers, il est nécessaire d’ajouter des sondes comme
check_postgres
et check_pgactivity
évoquées
plus haut.
pg_stat_statements
est un module contrib de PostgreSQL,
donc non installé par défaut. Il collecte des statistiques sur toutes
les requêtes exécutées. Son contenu est souvent extrêmement instructif.
Pour plus de détails sur les métriques relevées, voir https://dali.bo/h2_html#pg_stat_statements, et pour
l’installation et des exemples de requêtes, voir https://dali.bo/x2_html#pg_stat_statements, ou encore la
documentation
officielle.
PoWA (PostgreSQL Workload Analyzer) est un outil
communautaire historisant les informations collectées par
pg_stat_statements
, et fournissant une interface graphique
permettant d’observer en temps réel les requêtes normalisées les plus
consommatrices d’une instance selon plusieurs critères.
Munin est à la base un outil de métrologie utilisé par les administrateurs systèmes. Il comprend un certain nombre de sondes capables de récupérer des informations telles que la charge système, l’utilisation de la mémoire et des interfaces réseau, l’espace libre d’un disque, etc. Des sondes lui ont été ajoutées pour pouvoir surveiller certains services comme Sendmail, Postfix, Apache, et même PostgreSQL.
Munin est composé de deux parties : les sondes et le générateur de rapports. Les sondes sont exécutées toutes les cinq minutes. Elles sont généralement écrites en Perl. Elles récupèrent les informations et les stockent dans des bases BerkeleyDB. Ensuite, le générateur de rapports lit les bases en question pour générer des graphes au format PNG. Des pages HTML sont créées pour faciliter l’accès aux graphes.
Munin est inclus dans les distributions habituelles.
check_postgres
et
check_pgactivity
Nagios est un outil très connu de supervision. Il dispose par défaut de quelques sondes, principalement système. Il existe aussi des outils concurrents (Naemon, Icinga 2…) ou des surcouches compatibles au niveau des sondes.
Pour PostgreSQL, il est possible de le coupler à des sondes dédiées,
comme check_postgres
ou check_pgactivity
, déjà
évoquées, pour qu’il puisse récupérer un certain nombre d’informations
statistiques de PostgreSQL. Ne pas oublier de compléter par des sondes
plus classiques pour les I/O, le CPU, la RAM, etc.
check_postgres.pl
Zabbix est certainement le deuxième outil opensource le plus utilisé pour la supervision. Son avantage par rapport à Nagios est qu’il est capable de faire des graphes directement et qu’il dispose d’une interface plus simple d’approche.
Là-aussi, la sonde check_postgres.pl
lui permet de
récupérer des informations sur un serveur PostgreSQL.
pg_stat_statements
est une extension issue des
« contrib » de PostgreSQL, donc livrée avec lui, mais non installée par
défaut. Sa mise en place nécessite le préchargement de bibliothèques
dans la mémoire partagée (paramètre
shared_preload_libraries
), et donc le redémarrage de
l’instance.
Une fois installé et configuré, des mesures (nombre de blocs lus dans
le cache, hors cache, …) sont collectées sur toutes les requêtes
exécutées, et elles sont stockées avec les requêtes normalisées. Ces
données sont ensuite exploitables en interrogeant la vue
pg_stat_statements
. À noter que ces statistiques sont
cumulées sans être historisées, il est donc souvent difficile
d’identifier quelle requête est la plus consommatrice à un instant
donné, à moins de réinitialiser les statistiques.
Voir aussi la documentation officielle : https://docs.postgresql.fr/current/pgstatstatements.html
pg_stat_statements
PoWA (PostgreSQL Workload Analyzer) est un outil communautaire, sous licence PostgreSQL.
Tout comme pour l’extension standard pg_stat_statements
,
sa mise en place nécessite la modification du paramètre
shared_preload_libraries
, et donc le redémarrage de
l’instance. Il faut également créer une nouvelle base de données dans
l’instance. Par ailleurs, PoWA repose sur les statistiques collectées
par pg_stat_statements
, celui-ci doit donc être également
installé.
Une fois installé et configuré, l’outil va récupérer à intervalle
régulier les statistiques collectées par
pg_stat_statements
, les stocker et les historiser.
Il tire aussi partie d’autres extensions comme HypoPG, pg_qualstats, pg_stat_kcache…
L’outil fournit également une interface graphique permettant d’exploiter ces données, et donc d’observer en temps réel l’activité de l’instance. Cette activité est présentée sous forme de graphiques interactifs et de tableaux permettant de trier selon divers critères (nombre d’exécution, blocs lus hors cache…) les différentes requêtes normalisées sur l’intervalle de temps sélectionné.
Une bonne politique de supervision est la clef de voûte d’un système pérenne. Pour cela, il faut tout d’abord s’assurer que les traces et les statistiques soient bien configurées. Ensuite, l’installation d’un outil d’historisation, de création de graphes et de génération d’alertes, est obligatoire pour pouvoir tirer profit des informations fournies par PostgreSQL.
N’hésitez pas, c’est le moment !
Analyse de traces avec pgBadger
But : Analyser un journal de traces avec pgBadger
S’assurer que la configuration suivante est en place, au moins le temps de l’audit :
log_min_duration_statement = 0
log_temp_files = 0
log_autovacuum_min_duration = 0
lc_messages='C'
log_line_prefix = '%t [%p]: db=%d,user=%u,app=%a,client=%h '
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on
Si cela n’a pas déjà été fait aujourd’hui, lancer une session
pgbench
de 10 minutes dans la basepgbench
.
Installer pgBadger, soit depuis les dépôts du PGDG, soit depuis le site de l’auteur https://pgbadger.darold.net/.
Repérer où sont vos fichiers de traces, et notamment ceux d’aujourd’hui.
Générer dans
/tmp
un rapport pgBadger sur toute la journée en cours. La documentation est entre autres sur le site de l’auteur : https://pgbadger.darold.net/documentation.html. Combien de requêtes a-t-il détecté ?
Ouvrir le rapport dans un navigateur. Dans l’onglet Overview, à quoi correspondent les pics de trafic en nombre de requêtes ?
Dans l’onglet Top, chercher l’histogramme de la répartition des durées des requêtes.
Quelles sont les requêtes les plus lentes, prises une à une ?
Quelles sont les requêtes qui ont consommé le plus de temps au total ?
Analyse de traces avec pgBadger
S’assurer que la configuration suivante est en place, au moins le temps de l’audit :
log_min_duration_statement = 0
log_temp_files = 0
log_autovacuum_min_duration = 0
lc_messages='C'
log_line_prefix = '%t [%p]: db=%d,user=%u,app=%a,client=%h '
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on
On vérifie que la configuration est bien active avec :
Au besoin, le paramétrage peut soit se faire dans
postgresql.conf
(ne pas oublier de recharger la
configuration), soit au niveau de la base de données
(ALTER DATABASE pgbench SET log_min_duration_statement = 0
).
Si cela n’a pas déjà été fait aujourd’hui, lancer une session
pgbench
de 10 minutes dans la basepgbench
.
Pour créer la base si elle n’existe pas déjà :
Pour lancer un traitement de 10 minutes avec 20 clients répartis sur 2 processeurs :
Installer pgBadger, soit depuis les dépôts du PGDG, soit depuis le site de l’auteur https://pgbadger.darold.net/.
Le plus simple reste le dépôt de la distribution :
Pour la version la plus à jour, comme Gilles Darold fait évoluer le produit régulièrement, il n’est pas rare que le dépôt Github soit plus à jour et l’on peut préférer cette source. La release 12.4 est la dernière au moment où ceci est écrit.
Dans le répertoire pgbadger-12.4
, il n’y a guère que le
script pgbadger
dont on ait besoin, et que l’on placera par
exemple dans /usr/local/bin
.
Repérer où sont vos fichiers de traces, et notamment ceux d’aujourd’hui.
Par défaut, les traces sur Red Hat/CentOS/Rocky Linux sont dans
/var/lib/pgsql/14/data/log
.
Elles ont pu être déplacées dans /var/lib/pgsql/traces
au fil de ces TP.
Générer dans
/tmp
un rapport pgBadger sur toute la journée en cours. La documentation est entre autres sur le site de l’auteur : https://pgbadger.darold.net/documentation.html. Combien de requêtes a-t-il détecté ?
Dans sa plus simple expression, la commande est la suivante, à exécuter avec un utilisateur ayant accès aux traces :
$ cd /var/lib/pgsql/14/data/log
$ pgbadger -o /tmp/pgabdger-aujourdhui.html postgresql-2022-05-02*.log
Il peut arriver que la détection du format échoue, auquel cas il faut
préciser la valeur du paramètre log_line_prefix
(%m [%p]
par défaut sur RHEL, mais l’auteur préconise
'%t [%p]: db=%d,user=%u,app=%a,client=%h '
que l’on a
configuré initialement) avec l’option -p
.
Le paramètre -j
permet de paralléliser le travail sur
plusieurs processeurs.
On peut vouloir des statistiques plus fines que la moyenne par défaut
de 5 minutes (-a
), ou cibler sur une page de dates précises
(-b
et -e
). Enfin, préciser le fuseau horaire
du serveur (-Z
) est souvent utile pour éviter des décalages
dans les heures.
$ pgbadger -o /tmp/pgabdger-aujourdhui.html -p '%m [%p] ' -j 2 -a 1 \
-b '2022-05-02 09:00:00' -e '2022-05-02 18:00:00' -Z '+02' \
postgresql-2022-05-02*.log
...
[========================>] Parsed 170877924 bytes of 170877924 (100.00%),
queries: 503580, events: 51
LOG: Ok, generating html report...
En l’occurence, pgBadger a détecté ici un demi-million de requêtes.
Ouvrir le rapport dans un navigateur. Dans l’onglet Overview, à quoi correspondent les pics de trafic en nombre de requêtes ?
Il s’agit probablement des tests avec pgbench
.
Dans l’onglet Top, chercher l’histogramme de la répartition des durées des requêtes.
Le graphique indique que l’essentiel des requêtes doit figurer dans la tranche « 0ms-1ms ». La version tableau de ce graphique liste une poignée de requête autour de la seconde ou plus, que l’on voit également dans les slowest queries en-dessous.
Quelles sont les requêtes les plus lentes, prises une à une ?
On les trouve dans Top/Slowest individual queries.
Ce sont probablement les ordres COPY
des différents essais
avec pg_restore
, et les ordres
CREATE DATABASE
.
Quelles sont les requêtes qui ont consommé le plus de temps au total ?
On les trouve dans Top/Time consuming queries. Cet onglet est très intéressant pour repérer les requêtes relativement rapides, mais qui représentent la véritable charge de l’instance au quotidien.
Il s’agit très probablement des requêtes de
pgbench
:
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 avec
pg_basebackup
(voir plus loin).
Alternativement, la commande suivante, exécutée avec l’utilisateur
système propriétaire de l’instance (généralement postgres
)
effectue une lecture complète de toutes les tables (mais sans les index
ni les vues matérialisées), sans nécessiter de place sur disque
supplémentaire :
Sous Windows Powershell, la commande est :
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
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.
pg_wal
pg_resetwal
pg_surgery
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.
fsync
est-il bien honoré de l’OS au disque ?
(batteries !)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 :PGDATA
, sous
Linux :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 :
sar
, atop
…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.
fsync
full_page_write
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
!
À partir de la version 9.5, le bloc peut être compressé avant d’être
écrit dans le journal de transaction. Comme il n’y avait qu’un seul
algorithme de compression, le paramètre wal_compression
était un booléen pour activer ou non la compression. À partir de la
version 15, d’autres algorithmes sont disponibles et il faut donc
configurer le paramètre wal_compression
avec le nom de
l’algorithme de compression utilisable (parmi pglz
,
lz4
, zstd
).
initdb --data-checksums
pg_checksums --enable
(à posteriori, v12)pg_basebackup
(v11)PostgreSQL ne verrouille pas tous les fichiers dès son ouverture. Sans mécanisme de sécurité, il est donc possible de modifier un fichier sans que PostgreSQL s’en rende compte, ce qui aboutit à une corruption silencieuse.
Les sommes de contrôles (checksums) permettent de se
prémunir contre des corruptions silencieuses de données. Leur mise en
place est fortement recommandée sur une nouvelle instance.
Malheureusement, jusqu’en version 11 comprise, on ne peut le faire qu’à
l’initialisation de l’instance. La version 12 permet de les mettre en
place, base arrêtée, avec l’utilitaire
pg_checksums
.
À titre d’exemple, créons une instance sans utiliser les checksums, et une autre qui les utilisera :
Insérons une valeur de test, sur chacun des deux clusters :
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).
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 :
Enfin, on peut ensuite exécuter des requêtes sur ces deux clusters.
Sans checksums :
Avec checksums :
WARNING: page verification failed, calculated checksum 16321
but expected 21348
ERROR: invalid page in block 0 of relation base/12036/16387
Depuis la version 11, les sommes de contrôles, si elles sont là, sont
vérifiées par défaut lors d’un pg_basebackup
. En cas de
corruption des données, l’opération sera interrompue. Il est possible de
désactiver cette vérification avec l’option
--no-verify-checksums
pour obtenir une copie, aussi
corrompue que l’original, mais pouvant servir de base de travail.
En pratique, si vous utilisez PostgreSQL 9.5 au moins et si votre
processeur supporte les instructions SSE 4.2 (voir dans
/proc/cpuinfo
), il n’y aura pas d’impact notable en
performances. Par contre vous générerez un peu plus de journaux.
L’activation ou non des sommes de contrôle peut se faire indépendamment sur un serveur primaire et son secondaire, mais il est fortement conseillé de les activer simultanément des deux côtés pour éviter de gros problèmes dans certains scénarios de restauration.
kill -9
, rm -rf
,
rsync
, find … -exec
…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
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 :
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
pg_dump
pg_dumpall
COPY
psql
/ pg_restore
--section=pre-data
/ data
/
post-data
Les outils pg_dump
et pg_dumpall
permettent
d’exporter des données à partir d’une instance démarrée.
Dans le cadre d’un incident grave, il est possible de les utiliser pour :
Par exemple, un moyen rapide de s’assurer que tous les fichiers des tables de l’instance sont lisibles est de forcer leur lecture complète, notamment grâce à la commande suivante :
Sous Windows Powershell :
Attention, les fichiers associés aux index ne sont pas parcourus
pendant cette opération. Par ailleurs, ne pas avoir d’erreur ne garantit
en aucun cas pas l’intégrité fonctionnelle des données : les corruptions
peuvent très bien être silencieuses ou concerner les index. Une
vérification exhaustive implique d’autres outils comme
pg_checksums
ou pg_basebackup
(voir plus
loin).
Si pg_dumpall
ou pg_dump
renvoient des
messages d’erreur et ne parviennent pas à exporter 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.
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 :
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 :
-[ 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 :
-[ 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) :
La page racine est la 243. Allons la voir :
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 :
-[ 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
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 :
-[ 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
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 :
pg_resetwal
est un outil fourni avec PostgreSQL. Son
objectif est de pouvoir démarrer une instance après un crash si des
corruptions de fichiers (typiquement WAL ou fichier de contrôle)
empêchent ce démarrage.
Cette action n’est pas une action de réparation ! La réinitialisation des journaux de transactions implique que des transactions qui n’étaient que partiellement validées ne seront pas détectées comme telles, et ne seront donc pas annulées lors du recovery.
La conséquence est que les données de l’instance ne sont plus cohérentes. Il est fort possible d’y trouver des violations de contraintes diverses (notamment clés étrangères), ou d’autres cas d’incohérences plus difficiles à détecter.
Il s’utilise manuellement, en ligne de commande. Sa fonctionnalité principale est d’effacer les fichiers WAL courants, et il se charge également de réinitialiser les informations correspondantes du fichier de contrôle.
Il est possible de lui spécifier les valeurs à initialiser dans le
fichier de contrôle si l’outil ne parvient pas à les déterminer (par
exemple, si tous les WAL dans le répertoire pg_wal
ont été
supprimés).
Attention, pg_resetwal
ne doit jamais
être utilisé sur une instance démarrée. Avant d’exécuter l’outil, il
faut toujours vérifier qu’il ne reste aucun processus de l’instance.
Après la réinitialisation des WAL, une fois que l’instance a démarré, il ne faut surtout pas ouvrir les accès à l’application ! Comme indiqué, les données présentent sans aucun doute des incohérences, et toute action en écriture à ce point ne ferait qu’aggraver le problème.
L’étape suivante est donc de faire un export immédiat des données, de les restaurer dans une nouvelle instance initialisée à cet effet (de préférence sur un nouveau serveur, surtout si l’origine de la corruption n’a pas été clairement identifiée), et ensuite de procéder à une validation méthodique des données.
Il est probable que certaines données incohérentes puissent être identifiées à l’import, lors de la phase de recréation des contraintes : celles-ci échoueront si les données ne les respectent, ce qui permettra de les identifier.
En ce qui concerne les incohérences qui passeront au travers de ces tests, il faudra les trouver et les corriger manuellement, en procédant à une validation fonctionnelle des données.
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 en restaurant une sauvegarde) ;pg_resetwal
que s’il n’y a aucun autre moyen
de faire autrement pour récupérer les données ;Cette extension regroupe des fonctions qui permettent de modifier le satut 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 pas 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.
pg_checksums
(à froid, v11)pg_basebackup
(v11)amcheck
: pure vérification
pg_amcheck
Depuis la version 11, pg_checksums
permet de vérifier
les sommes de contrôles existantes sur les bases de données à
froid : l’instance doit être arrêtée proprement auparavant. (En
version 11 l’outil s’appelait pg_verify_checksums
.)
Par exemple, suite à une modification de deux blocs dans une table
avec l’outil hexedit
, on peut rencontrer ceci :
$ /usr/pgsql-12/bin/pg_checksums -D /var/lib/pgsql/12/data -c
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
À partir de PostgreSQL 12, l’outil pg_checksums
peut
aussi ajouter ou supprimer les sommes de contrôle sur une instance
existante arrêtée (donc après le initdb
), ce qui n’était
pas possible dans les versions antérieures.
Une alternative, toujours à partir de la version 11, est d’effectuer
une sauvegarde physique avec pg_basebackup
, ce qui est plus
lourd, mais n’oblige pas à arrêter la base.
Le module amcheck
était apparu en version 10 pour
vérifier la cohérence des index et de leur structure interne, et ainsi
détecter des bugs, des corruptions dues au système de fichier voire à la
mémoire. Il définit deux fonctions :
bt_index_check
est destinée aux vérifications de
routine, et ne pose qu’un verrou 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.En v11 apparaît le nouveau paramètre heapallindex
. S’il
vaut true
, chaque fonction effectue une vérification
supplémentaire en recréant temporairement une structure d’index et en la
comparant avec l’index original. bt_index_check
vérifiera
que chaque entrée de la table possède une entrée dans l’index.
bt_index_parent_check
vérifiera en plus qu’à chaque entrée
de l’index 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.
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 simple et facile,
mais tout dépend de la cause du problème.
Soit unetable_pkey
, un index de 10 Go sur un
entier :
CREATE EXTENSION amcheck ;
SELECT bt_index_check('unetable_pkey');
Durée : 63753,257 ms (01:03,753)
SELECT bt_index_check('unetable_pkey', true);
Durée : 234200,678 ms (03:54,201)
Ici, la vérification exhaustive multiplie le temps de vérification par un facteur 4.
En version 14, PostgreSQL dispose d’un nouvel outil appelé
pg_amcheck
. Ce dernier facilite l’utilisation de
l’extension amcheck
.
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.
REINDEX
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 :
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
.
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
Les corruptions de blocs vont généralement déclencher des erreurs du type suivant :
ERROR: invalid page header in block 32570 of relation base/16390/2663
ERROR: could not read block 32570 of relation base/16390/2663:
read only 0 of 8192 bytes
Si la relation concernée est une table, tout ou partie des données contenues dans ces blocs est perdu.
L’apparition de ce type d’erreur est un signal fort qu’une restauration est certainement nécessaire.
Néanmoins, s’il est nécessaire de lire le maximum de données
possibles de la table, il est possible d’utiliser l’option de PostgreSQL
zero_damaged_pages
pour demander au moteur de réinitialiser
les blocs invalides à zéro lorsqu’ils sont lus au lieu de tomber en
erreur. Il s’agit d’un des très rares paramètres absents de
postgresql.conf
.
Par exemple :
SET zero_damaged_pages = true ;
SET
VACUUM FULL tablecorrompue ;
WARNING: invalid page header in block 32570 of relation base/16390/2663; zeroing out page
VACUUM
Si cela se termine sans erreur, les blocs invalides ont été réinitialisés.
Les données qu’ils contenaient sont évidemment perdues, mais la table peut désormais être accédée dans son intégralité en lecture, permettant ainsi par exemple de réaliser un export des données pour récupérer ce qui peut l’être.
Attention, du fait des données perdues, le résultat peut être incohérent (contraintes non respectées…).
Par ailleurs, par défaut PostgreSQL ne détecte pas les corruptions logiques, c’est-à-dire n’affectant pas la structure des données mais uniquement le contenu.
Il ne faut donc pas penser que la procédure d’export complet de données suivie d’un import sans erreur garantit l’absence de corruption.
pg_relation_filepath()
ctid
/
pageinspect
dd
Dans certains cas, il arrive que la corruption soit suffisamment importante pour que le simple accès au bloc fasse crasher l’instance.
Dans ce cas, le seul moyen de réinitialiser le bloc est de le faire
manuellement au niveau du fichier, instance arrêtée, par exemple avec la
commande dd
.
Pour identifier le fichier associé à la table corrompue, il est
possible d’utiliser la fonction
pg_relation_filepath()
:
> 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.
pg_wal
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 (rappel : il ne faut JAMAIS supprimer les fichiers WAL, même si le système de fichiers est plein !), alors PostgreSQL ne pourra pas redémarrer.
Si l’archivage était activé et que les fichiers WAL affectés ont bien été archivés, alors il est possible de les restaurer avant de tenter un nouveau démarrage.
Si ce n’est pas possible ou des fichiers WAL archivés ont également été corrompus ou supprimés, 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.
pg_resetwal
permet de forcer le démarrageL’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.
global/pg_control
pg_resetwal
… parfoisLe 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.
pg_xact
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.
But : Corrompre un bloc et voir certains impacts possibles.
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 :
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
.
pg_checksums
(pg_verify_checksums
en v11).Avant de redémarrer PostgreSQL, supprimer les sommes de contrôle dans la copie (en désespoir de cause).
Démarrer le cluster sur la copie avec
pg_ctl
.
Que renvoie ceci ?
Tenter une récupération avec
SET zero_damaged_pages
. Quelles données ont pu être perdues ?
But : Corrompre une table portant une clé étrangère.
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.
pgbench_branches
.SET enable_seqscan TO off ;
.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
.
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 :
$ 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 :
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
).
Arrêter PostgreSQL.
Cela permet d’être sûr qu’il ne va pas écraser nos modifications lors d’un checkpoint.
Avec un outil
hexedit
(à installer au besoin, l’aide s’obtient par F1), modifier une ligne dans le PREMIER bloc de la table.
Aller par exemple sur la 2è ligne, modifier 80 9F
en
FF FF
. Sortir avec Ctrl-X, confirmer la sauvegarde.
Redémarrer PostgreSQL et lire le contenu de
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
pg_checksums
(pg_verify_checksums
en v11).$ /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
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 :
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
.
Que renvoie ceci ?
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 ?
aid | bid | abalance | filler
-----+-----+----------+--------------------------------------------------------------------------------------
2 | 1 | 0 |
3 | 1 | 0 |
4 | 1 | 0 |
[...]
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 :
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
).
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
pgbench_branches
.SET enable_seqscan TO off ;
.Les deux décomptes sont contradictoires :
pgbench=# SELECT count(*) FROM pgbench_branches ;
count
-------
9
pgbench=# SET enable_seqscan TO off ;
SET
pgbench=# SELECT count(*) FROM pgbench_branches ;
count
-------
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 :
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 :
Qu’affiche
pageinspect
pour cette table ?
pgbench=# CREATE EXTENSION pageinspect ;
pgbench=# SELECT t_ctid, lp_off, lp_len, t_xmin, t_xmax, t_data
FROM 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 :
pgbench=# CREATE EXTENSTION amcheck ;
pgbench=# SELECT bt_index_check (index => 'pgbench_branches_pkey',
heapallindexed => true);
bt_index_check
----------------
(1 ligne)
pgbench=# SELECT bt_index_parent_check (index => 'pgbench_branches_pkey',
heapallindexed => true, rootdescend => true);
ERROR: heap tuple (0,1) from table "pgbench_branches"
lacks matching index tuple within index "pgbench_branches_pkey"
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.
$ 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.
La trace se retrouve encore dans le nom de la librairie C pour les clients, la libpq.↩︎