Strasbourg, le 24 février 2023

Un DBA PostgreSQL vérifie régulièrement que l’autovacuum nettoie ses tables. Il lance occasionnellement un VACUUM, et reconstruit parfois une table avec VACUUM FULL. Le VACUUM FREEZE est moins connu, et souvent négligé. Souvent, sans conséquence. Mais quand un problème survient, la surprise peut être très désagréable pour le DBA non averti.

Principe du gel

Le rôle du FREEZE (« gel ») est de recycler les numéros de transaction de PostgreSQL. Ces numéros de transaction (txid) sont stockés dans chaque ligne. PostgreSQL les utilise pour savoir si une transaction peut voir une ligne créée par une autre transaction. Les txid stockés dans les lignes sont codés sur 32 bits : il y aura donc rebouclage (wraparound) de ce compteur au bout de 4 milliards de transactions. (Les débats sur comment lever cette limite durent depuis des années.)

Sur les machines actuelles, et avec une activité intense, 4 milliards de transactions peuvent être atteintes en quelques mois, voire semaines. Il est donc important qu’avant le rebouclage les lignes aient été « gelées », c’est-à-dire marquées comme assez vieilles pour être visibles par toute transaction.

Le paramétrage par défaut de PostgreSQL prévoit de chercher à geler toutes les lignes d’une table quand les plus vieilles lignes arrivent à un « âge » de 200 millions de transactions écoulées. Cela laisse une marge appréciable au cas où ce gel prendrait du temps. Monter le seuil de 200 millions est généralement vain et ne fait que repousser l’inéluctable.

Déclenchement

C’est le démon autovacuum qui décide généralement de lancer le FREEZE, et un processus système avec un libellé explicite apparaît (visible également dans pg_stat_activity) :

autovacuum: VACUUM public.pgbench_accounts (to prevent wraparound)

Si l’autovacuum est désactivé (une très mauvaise idée, en général), le FREEZE se déclenchera tout de même à l’approche de l’âge fatidique. Si on interrompt l’autovacuum en plein FREEZE, il recommencera dans la minute. Au fil du temps, on voit donc l’âge des bases (en fait celui des plus vieilles lignes) monter progressivement, approcher de la limite des 200 millions, puis retomber un peu quand la table la plus vieille a été gelée, puis remonter, etc. :

img/evolutions_freeze

Le gel peut se déclencher manuellement :

VACUUM (FREEZE, VERBOSE) nomtable ;

Charge

En pratique, le gel réécrit les lignes concernées. Si elles sont nombreuses, l’impact en I/O peut être lourd. À part cela, un FREEZE ne gêne pas l’activité habituelle, sauf si l’application utilise des verrous très lourds (TRUNCATE, LOCK TABLE…) sur les tables en cours de gel.

Le gel a été optimisé au fil du temps. Une partie se fait « en passant », lorsqu’un VACUUM classique rencontre des lignes assez anciennes dans le bloc qu’il traite.

Avant PostgreSQL 9.6, chaque FREEZE reparcourait intégralement la table, parfois inutilement. Depuis, PostgreSQL sait quels blocs ont déjà été gelés et n’ont pas été modifiés depuis. En version 12, l’autovacuum a été rendu plus agressif (augmentation des débits en lecture et écriture), et cela concerne aussi le FREEZE. À partir de la version 14, un mode failsafe encore plus agressif peut se déclencher.

Quand les tables sont de petite taille, ou que les données ont des âges très divers, ou que les lignes sont régulièrement mises à jour, le FREEZE ne posera pas de problème, car il ne traitera que des petits paquets de données à la fois.

Problèmes potentiels

Ce n’est pas toujours aussi rose.

Avant la version 12, l’autovacuum trop peu agressif prend beaucoup trop de temps à geler les lignes, surtout quand il doit commencer par lire toute la table (jusqu’en 9.5), même s’il y a très peu de lignes à nettoyer. Si l’applicatif demande alors un verrou très contraignant, l’attente peut durer des heures… et tout bloquer !

Un autre problème concerne même les versions plus récentes. Si de nombreuses lignes ont été importées en bloc, par exemple lors d’une migration, et que la plupart n’ont pas été modifiées par la suite, toutes ces lignes et tables atteindront l’âge fatidique des 200 millions en même temps. C’est une volumétrie énorme qui va devoir être réécrite d’un coup, en saturant peut-être les IO et surtout en chargeant lourdement les journaux, avec un impact possible sur l’archivage ou la réplication. Loi de Murphy oblige, ce sera au moment le plus défavorable (celui où, justement, vous générerez de nombreuses transactions). La version 13 tente de mitiger le problème en activant aussi l’autovacuum lors des insertions.

De manière proactive, il est possible de lancer des VACUUM FREEZE manuellement à des moments où cela ne gêne pas. Il faut savoir que l’ordre entré manuellement n’est pas soumis au bridage en débit de l’autovacuum, ce qui raccourcit l’opération (mais écrit autant de journaux). Même si l’ordre est interrompu avant la fin, ce qui sera gelé à cette occasion ne sera plus à regeler plus tard.

Le plus problématique arrive quand le processus de gel est bloqué. Celui-ci ne peut geler une ligne que si toutes les transactions peuvent la voir, c’est-à-dire si les transactions en cours sont toutes plus récentes que cette ligne. Classiquement, des sessions en statut idle in transaction (figées avant d’avoir fait un COMMIT ou un ROLLBACK) de très longue durée vont bloquer le nettoyage de l’autovacuum, dont le gel. Il faudra les tuer. Les gens qui redémarrent « à tout hasard » verront ainsi disparaître le problème. Par contre, une transaction préparée (ne pas confondre avec une requête préparée) peut, elle, survivre à ce redémarrage… Un slot de réplication très en retard, ou oublié après destruction d’un secondaire, peut provoquer le même phénomène si hot_standby_feedback a été activé. L’autovacuum et le VACUUM FREEZE peuvent aussi échouer de manière répétée à cause d’un bloc corrompu, d’un bug dans un index fonctionnel…

Si l’on ne surveille pas le fichier de traces de PostgreSQL et que le problème persiste, l’autovacuum déclenchera des FREEZE de manière répétée, vainement. Puis, à quelques dizaines de millions de transactions du seuil fatidique de 2 milliards de transactions, PostgreSQL inondera les traces d’avertissements ; puis, à quelques millions de transactions de la catastrophe, refusera toute nouvelle transaction : pour PostgreSQL l’intégrité des données est menacée, et le DBA doit corriger le problème. Pour l’utilisateur, cela devient un problème de disponibilité.

Dépannage

En cas de problème, le plus rapide, à partir de PostgreSQL 12, est d’exécuter :

VACUUM (FREEZE, VERBOSE, INDEX_CLEANUP off, TRUNCATE off) ;

Et ce, sur toutes les bases de l’instance.

Sans cette action, on ne pourra plus se connecter, sauf en arrêtant tout et en utilisant le préhistorique et très austère mode single (ce qui est une punition en soi) :

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.

N’exagérons pas le danger : nos clients qui ont su arriver à ce dernier stade se comptent sur un pouce d’une main en une décennie. Les générations brutales et inattendues de journaux sont le problème le plus fréquent de nos jours.

Surveillez l’âge de vos bases, les sessions en idle_in_transaction, les transactions préparées, les sessions les plus anciennes, le retard des slots de réplication (par exemple avec les sondes adéquates dans check_pgactivity) ; cherchez les erreurs dans vos traces ; par précaution, lancez un VACUUM FREEZE à l’occasion sur vos plus grosses tables (surtout si elles ont toutes été importées en même temps !) ; et le gel d’une base ne devrait jamais poser problème.

Pour plus de détails, voir ces extraits de notre formation DBA2 :


Des questions, des commentaires ? Écrivez-nous !


DALIBO

DALIBO est le spécialiste français de PostgreSQL®. Nous proposons du support, de la formation et du conseil depuis 2005.