MVCC dans PostgreSQL-8. Congélation

Nous avons commencé par des problèmes liés à l' isolement , avons fait une digression sur l' organisation des données à un bas niveau et avons parlé en détail des versions de ligne et de la façon dont les instantanés sont obtenus à partir des versions.

Ensuite, nous avons examiné différents types de nettoyage: intra-page (avec mises à jour HOT), régulier et automatique .

Et je suis arrivé au dernier sujet de ce cycle. Aujourd'hui, nous allons parler du problème de l'habillage et du gel des ID de transaction.

Débordement du compteur de transactions


PostgreSQL a 32 bits alloués pour le numéro de transaction. C'est un nombre assez important (environ 4 milliards), mais avec le fonctionnement actif du serveur, il pourrait bien être épuisé. Par exemple, à une charge de 1000 transactions par seconde, cela se produira après seulement un mois et demi de fonctionnement continu.

Mais nous avons parlé du fait que le mécanisme de multi-versions repose sur la séquence de numérotation - alors sur deux transactions, une transaction avec un nombre inférieur peut être considérée comme ayant commencé plus tôt. Par conséquent, il est clair que vous ne pouvez pas simplement réinitialiser le compteur et continuer à nouveau la numérotation.



Pourquoi est-ce que 64 bits ne sont pas alloués pour le numéro de transaction - car cela éliminerait complètement le problème? Le fait est que (comme discuté précédemment ) dans l'en-tête de chaque version de la ligne sont stockés deux numéros de transaction - xmin et xmax. L'en-tête est déjà assez volumineux, au moins 23 octets, et une augmentation de la profondeur de bits entraînerait son augmentation de 8 octets supplémentaires. Ce n'est absolument pas possible.

Les numéros de transaction 64 bits sont implémentés dans le produit de notre société, Postgres Pro Enterprise, mais ils ne sont pas tout à fait honnêtes non plus: xmin et xmax restent 32 bits, et l'en-tête de la page contient un "début d'une ère" commun à l'échelle de la page.

Que faire? Au lieu d'un diagramme linéaire, tous les numéros de transaction sont bouclés. Pour toute transaction, la moitié des chiffres «dans le sens antihoraire» sont considérés comme appartenant au passé, et la moitié «dans le sens horaire» à l'avenir.

L'âge d'une transaction est le nombre de transactions passées depuis son apparition dans le système (que le compteur soit passé par zéro ou non). Lorsque nous voulons comprendre si une transaction est plus ancienne qu'une autre ou non, nous comparons leur âge, pas leurs chiffres. (Par conséquent, soit dit en passant, les opérations «supérieur» et «moins» ne sont pas définies pour le type de données xid.)



Mais dans un tel circuit en boucle, une situation désagréable se présente. Une transaction qui était dans un passé lointain (transaction 1 sur la figure), après un certain temps sera dans la moitié du cercle qui se rapporte à l'avenir. Cela, bien sûr, viole les règles de visibilité et entraînerait des problèmes - les modifications apportées par la transaction 1 disparaîtraient tout simplement de la vue.



Règles de gel et de visibilité des versions


Afin d'éviter de tels «voyages» du passé vers le futur, le processus de nettoyage (en plus de libérer de l'espace dans les pages) effectue une autre tâche. Il trouve des versions assez anciennes et «froides» des lignes (qui sont visibles dans toutes les images et dont le changement est déjà peu probable) et les marque d'une manière spéciale - les «gèle». La version figée de la ligne est considérée comme plus ancienne que toutes les données normales et est toujours visible dans tous les instantanés de données. De plus, il n'est plus nécessaire de regarder le numéro de transaction xmin, et ce numéro peut être réutilisé en toute sécurité. Ainsi, les versions figées des chaînes restent toujours dans le passé.



Afin de marquer le numéro de transaction xmin comme figé, les deux bits de conseil sont définis en même temps - le bit de validation et le bit d'annulation.

Notez que la transaction xmax n'a pas besoin d'être gelée. Sa présence signifie que cette version de la chaîne n'est plus pertinente. Une fois qu'elle ne sera plus visible dans les instantanés de données, cette version de la ligne sera effacée.

Pour les expériences, créez un tableau. Nous avons défini le facteur de remplissage minimum pour que seules deux lignes tiennent sur chaque page - il sera donc plus pratique pour nous d'observer ce qui se passe. Et désactivez l'automatisation pour contrôler vous-même le temps de nettoyage.

=> CREATE TABLE tfreeze( id integer, s char(300) ) WITH (fillfactor = 10, autovacuum_enabled = off); 

Nous avons déjà créé plusieurs variantes de la fonction, qui, en utilisant l'extension pageinspect, a montré la version des lignes qui sont sur la page. Nous allons maintenant créer une autre version de la même fonction: maintenant, elle affichera plusieurs pages à la fois et affichera l'âge de la transaction xmin (l'âge de la fonction système est utilisé pour cela):

 => CREATE FUNCTION heap_page(relname text, pageno_from integer, pageno_to integer) RETURNS TABLE(ctid tid, state text, xmin text, xmin_age integer, xmax text, t_ctid tid) AS $$ SELECT (pageno,lp)::text::tid AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin || CASE WHEN (t_infomask & 256+512) = 256+512 THEN ' (f)' WHEN (t_infomask & 256) > 0 THEN ' (c)' WHEN (t_infomask & 512) > 0 THEN ' (a)' ELSE '' END AS xmin, age(t_xmin) xmin_age, t_xmax || CASE WHEN (t_infomask & 1024) > 0 THEN ' (c)' WHEN (t_infomask & 2048) > 0 THEN ' (a)' ELSE '' END AS xmax, t_ctid FROM generate_series(pageno_from, pageno_to) p(pageno), heap_page_items(get_raw_page(relname, pageno)) ORDER BY pageno, lp; $$ LANGUAGE SQL; 

Veuillez noter que le signe de gel (que nous montrons avec la lettre f entre parenthèses) est déterminé par l'installation simultanée d'invites validées et abandonnées. De nombreuses sources (y compris la documentation) mentionnent le nombre spécial FrozenTransactionId = 2, qui marque les transactions gelées. Un tel système fonctionnait jusqu'à la version 9.4, mais il a maintenant été remplacé par des bits d'info-bulle - cela vous permet d'enregistrer le numéro de transaction d'origine dans la version en ligne, ce qui est pratique à des fins de support et de débogage. Cependant, les transactions avec le numéro 2 peuvent toujours se produire dans les anciens systèmes, même mis à niveau vers les dernières versions.

Nous avons également besoin de l'extension pg_visibility, qui vous permet de consulter la carte de visibilité:

 => CREATE EXTENSION pg_visibility; 

Avant PostgreSQL 9.6, la carte de visibilité contenait un bit par page; il a marqué des pages contenant uniquement des versions "assez anciennes" de chaînes qui sont déjà garanties d'être visibles sur toutes les images. L'idée ici est que si la page est marquée dans la carte de visibilité, alors pour sa version des lignes, vous n'avez pas besoin de vérifier les règles de visibilité.

À partir de la version 9.6, une carte figée a été ajoutée à la même couche - un bit de plus par page. La carte de gel marque les pages sur lesquelles toutes les versions des lignes sont figées.

Nous insérons plusieurs lignes dans le tableau et effectuons immédiatement le nettoyage pour créer une carte de visibilité:

 => INSERT INTO tfreeze(id, s) SELECT g.id, 'FOO' FROM generate_series(1,100) g(id); => VACUUM tfreeze; 

Et nous voyons que les deux pages sont maintenant marquées dans la carte de visibilité (all_visible), mais pas encore figées (all_frozen):

 => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno; 
  blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | f (2 rows) 

L'âge de la transaction qui a créé les lignes (xmin_age) est 1 - il s'agit de la dernière transaction effectuée sur le système:

 => SELECT * FROM heap_page('tfreeze',0,1); 
  ctid | state | xmin | xmin_age | xmax | t_ctid -------+--------+---------+----------+-------+-------- (0,1) | normal | 697 (c) | 1 | 0 (a) | (0,1) (0,2) | normal | 697 (c) | 1 | 0 (a) | (0,2) (1,1) | normal | 697 (c) | 1 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 1 | 0 (a) | (1,2) (4 rows) 

Âge minimum pour congeler


Trois paramètres principaux contrôlent le gel, et nous les examinerons tour à tour.

Commençons par vacuum_freeze_min_age , qui définit l'âge minimum de transaction xmin auquel la version de chaîne peut être gelée. Plus cette valeur est faible, plus les frais généraux inutiles peuvent s'avérer être: si nous avons affaire à des données «chaudes» et en changement actif, le gel de plus en plus de nouvelles versions sera inutile. Dans ce cas, il vaut mieux attendre.

La valeur par défaut de ce paramètre définit que les transactions commencent à geler après que 50 millions d'autres transactions se soient écoulées depuis leur apparition:

 => SHOW vacuum_freeze_min_age; 
  vacuum_freeze_min_age ----------------------- 50000000 (1 row) 

Afin de voir comment le gel se produit, nous réduisons la valeur de ce paramètre à l'unité.

 => ALTER SYSTEM SET vacuum_freeze_min_age = 1; => SELECT pg_reload_conf(); 

Et nous mettrons à jour une ligne sur la page zéro. La nouvelle version arrivera à la même page en raison de la petite valeur fillfactor.

 => UPDATE tfreeze SET s = 'BAR' WHERE id = 1; 

Voici ce que nous voyons maintenant dans les pages de données:

 => SELECT * FROM heap_page('tfreeze',0,1); 
  ctid | state | xmin | xmin_age | xmax | t_ctid -------+--------+---------+----------+-------+-------- (0,1) | normal | 697 (c) | 2 | 698 | (0,3) (0,2) | normal | 697 (c) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (c) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 2 | 0 (a) | (1,2) (5 rows) 

Maintenant, les lignes plus anciennes que vacuum_freeze_min_age = 1 doivent être gelées. Mais notez que la ligne zéro n'est pas marquée dans la carte de visibilité (le bit a été réinitialisé par la commande UPDATE, qui a changé la page), et la première reste vérifiée:

 => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno; 
  blkno | all_visible | all_frozen -------+-------------+------------ 0 | f | f 1 | t | f (2 rows) 

Nous avons déjà dit que le nettoyage ne numérise que les pages qui ne sont pas marquées dans la carte de visibilité. Et il s'avère donc:

 => VACUUM tfreeze; => SELECT * FROM heap_page('tfreeze',0,1); 
  ctid | state | xmin | xmin_age | xmax | t_ctid -------+---------------+---------+----------+-------+-------- (0,1) | redirect to 3 | | | | (0,2) | normal | 697 (f) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 (c) | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (c) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 2 | 0 (a) | (1,2) (5 rows) 

Sur la page zéro, une version est figée, mais la première page ne considérait pas du tout le nettoyage. Ainsi, si seules les versions actuelles sont laissées sur la page, le nettoyage ne viendra pas sur une telle page et ne les gèlera pas.

 => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno; 
  blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | f (2 rows) 

Âge pour geler toute la table


Pour figer toujours la version des lignes laissées dans les pages que le nettoyage ne regarde tout simplement pas, un deuxième paramètre est fourni: vacuum_freeze_table_age . Il détermine l'âge de la transaction, auquel le nettoyage ignore la carte de visibilité et parcourt toutes les pages de la table pour se figer.

Chaque table stocke un numéro de transaction, pour lequel il est connu que toutes les transactions plus anciennes sont garanties d'être gelées (pg_class.relfrozenxid). Avec l'âge de cette transaction mémorisée, la valeur du paramètre vacuum_freeze_table_age est comparée .

 => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze'; 
  relfrozenxid | age --------------+----- 694 | 5 (1 row) 

Avant PostgreSQL 9.6, le nettoyage effectuait une analyse complète de la table pour s'assurer que toutes les pages étaient analysées. Pour les grandes tables, cette opération a été longue et triste. L'affaire était aggravée par le fait que si le nettoyage n'arrivait pas à son terme (par exemple, un administrateur impatient interrompait l'exécution d'une commande), il fallait recommencer depuis le début.

À partir de la version 9.6, grâce à la carte figée (que nous voyons dans la colonne all_frozen de la sortie pg_visibility_map), la suppression contourne uniquement les pages qui ne sont pas déjà marquées dans la carte. Ce n'est pas seulement une quantité de travail beaucoup plus petite, mais aussi une résistance aux interruptions: si le processus de nettoyage est arrêté et recommencé, il n'aura pas à revoir les pages qu'il a déjà réussi à marquer dans la carte de congélation la dernière fois.

D'une manière ou d'une autre, toutes les pages du tableau sont figées une fois dans les transactions ( vacuum_freeze_table_age - vacuum_freeze_min_age ). Avec les valeurs par défaut, cela se produit une fois par million de transactions:

 => SHOW vacuum_freeze_table_age; 
  vacuum_freeze_table_age ------------------------- 150000000 (1 row) 

Ainsi, il est clair que trop de vacuum_freeze_min_age ne doit pas être défini, car au lieu de réduire les frais généraux, cela commencera à les augmenter.

Voyons comment la table entière est gelée, et pour ce faire, réduisez vacuum_freeze_table_age à 5 afin que la condition de gel soit remplie.

 => ALTER SYSTEM SET vacuum_freeze_table_age = 5; => SELECT pg_reload_conf(); 

Nettoyons:

 => VACUUM tfreeze; 

Maintenant, étant donné que la table entière a été garantie pour être vérifiée, le nombre de la transaction gelée peut être augmenté - nous sommes sûrs que les pages n'ont pas une transaction plus ancienne et non gelée.

 => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze'; 
  relfrozenxid | age --------------+----- 698 | 1 (1 row) 

Maintenant, toutes les versions des lignes de la première page sont figées:

 => SELECT * FROM heap_page('tfreeze',0,1); 
  ctid | state | xmin | xmin_age | xmax | t_ctid -------+---------------+---------+----------+-------+-------- (0,1) | redirect to 3 | | | | (0,2) | normal | 697 (f) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 (c) | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (f) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (f) | 2 | 0 (a) | (1,2) (5 rows) 

De plus, la première page est marquée dans la carte de gel:

 => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno; 
  blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | t (2 rows) 

Âge pour une réponse «agressive»


Il est important que les versions de ligne se figent à temps. Si une situation se présente où une transaction qui n'a pas encore été gelée risque d'entrer dans le futur, PostgreSQL se bloquera pour éviter des problèmes potentiels.

Quelle pourrait en être la raison? Il y a plusieurs raisons.

  • Le nettoyage automatique peut être désactivé et le nettoyage régulier ne démarre pas non plus. Nous avons déjà dit que ce n'est pas nécessaire, mais techniquement c'est possible.
  • Même le nettoyage automatique inclus ne vient pas aux bases de données qui ne sont pas utilisées (rappelez-vous le paramètre track_counts et la base de données template0).
  • Comme nous l'avons vu la dernière fois , le nettoyage ignore les tables dans lesquelles les données sont uniquement ajoutées, mais pas supprimées ou modifiées.

Dans de tels cas, une opération de nettoyage automatique «agressif» est fournie, et elle est régulée par le paramètre autovacuum_freeze_max_age . Si dans n'importe quelle table d'une base de données, il est possible qu'une transaction non gelée soit plus ancienne que l'âge spécifié dans le paramètre, le nettoyage automatique démarre de force (même s'il est désactivé) et tôt ou tard, il atteindra la table des problèmes (malgré les critères habituels).

La valeur par défaut est assez conservatrice:

 => SHOW autovacuum_freeze_max_age; 
  autovacuum_freeze_max_age --------------------------- 200000000 (1 row) 

La limite pour autovacuum_freeze_max_age est de 2 milliards de transactions, et une valeur 10 fois plus petite est utilisée. Cela a du sens: en augmentant la valeur, nous augmentons le risque que l'auto-nettoyage n'ait tout simplement pas le temps de geler toutes les versions nécessaires des lignes.

De plus, la valeur de ce paramètre détermine la taille de la structure XACT: comme il ne devrait pas y avoir de transactions plus anciennes dans le système dont vous pourriez avoir besoin de connaître l'état, le nettoyage automatique supprime les fichiers de segments XACT inutiles, libérant ainsi de l'espace.

Voyons comment le nettoyage gère les tables avec ajout uniquement, en utilisant tfreeze comme exemple. Pour ce tableau, l'autonettoyage est généralement désactivé, mais ce ne sera pas un obstacle.

La modification du paramètre autovacuum_freeze_max_age nécessite un redémarrage du serveur. Mais tous les paramètres décrits ci-dessus peuvent également être définis au niveau des tables individuelles à l'aide des paramètres de stockage. Habituellement, cela n'a de sens que dans des cas particuliers, lorsque la table nécessite vraiment un soin particulier.

Donc, nous allons définir autovacuum_freeze_max_age au niveau de la table (et en même temps retourner le facteur de remplissage normal également). Malheureusement, la valeur minimale possible est de 100 000:

 => ALTER TABLE tfreeze SET (autovacuum_freeze_max_age = 100000, fillfactor = 100); 

Malheureusement, car nous devons effectuer 100 000 transactions afin de reproduire la situation qui nous intéresse. Mais, bien sûr, à des fins pratiques, il s'agit d'une valeur très, très faible.

Puisque nous allons ajouter des données, nous allons insérer 100 000 lignes dans le tableau - chacune dans notre transaction. Et encore une fois, je dois faire une réserve que, dans la pratique, cela ne devrait pas être fait. Mais maintenant, nous explorons, nous le pouvons.

 => CREATE PROCEDURE foo(id integer) AS $$ BEGIN INSERT INTO tfreeze VALUES (id, 'FOO'); COMMIT; END; $$ LANGUAGE plpgsql; => DO $$ BEGIN FOR i IN 101 .. 100100 LOOP CALL foo(i); END LOOP; END; $$; 

Comme nous pouvons le voir, l'âge de la dernière transaction gelée dans le tableau a dépassé la valeur seuil:

 => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze'; 
  relfrozenxid | age --------------+-------- 698 | 100006 (1 row) 

Mais si vous attendez un peu maintenant, dans le journal des messages du serveur, il y aura une entrée sur le vide agressif automatique de la table "test.public.tfreeze", le numéro de la transaction gelée changera et son âge reviendra à la décence:

 => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze'; 
  relfrozenxid | age --------------+----- 100703 | 3 (1 row) 

Il y a aussi le gel des transactions multiples, mais nous n'en parlerons pas encore - nous le reporterons jusqu'à ce que nous parlions de verrous afin de ne pas prendre de l'avance sur nous-mêmes.

Congélation manuelle


Parfois, il est pratique de contrôler manuellement le gel plutôt que d'attendre l'arrivée de l'auto-nettoyage.

Vous pouvez figer manuellement une commande à l'aide de la commande VACUUM FREEZE - toutes les versions de ligne seront gelées, quel que soit l'âge des transactions (comme si le paramètre autovacuum_freeze_min_age = 0). Lorsqu'une table est reconstruite avec les commandes VACUUM FULL ou CLUSTER, toutes les lignes sont également figées.

Pour figer toutes les bases de données, vous pouvez utiliser l'utilitaire:

 vacuumdb --all --freeze 

Les données peuvent également être gelées lors du chargement initial à l'aide de la commande COPY en spécifiant le paramètre FREEZE. Pour ce faire, la table doit être créée (ou vidée avec la commande TRUNCATE) dans le même
transactions comme COPY.

Étant donné qu'il existe des règles de visibilité distinctes pour les lignes figées, ces lignes seront visibles dans les instantanés des données d'autres transactions en violation des règles d'isolement habituelles (cela s'applique aux transactions avec le niveau Lecture répétable ou Sérialisable).

Pour vérifier cela, dans une autre session, démarrez une transaction avec le niveau d'isolement Lecture répétable:

 | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT txid_current(); 

Notez que cette transaction a généré un instantané des données, mais n'a pas accédé à la table tfreeze. Nous allons maintenant vider la table tfreeze et y charger de nouvelles lignes en une seule transaction. Si une transaction parallèle lit le contenu de tfreeze, la commande TRUNCATE sera verrouillée jusqu'à la fin de la transaction.

 => BEGIN; => TRUNCATE tfreeze; => COPY tfreeze FROM stdin WITH FREEZE; 
 1 FOO 2 BAR 3 BAZ \. 
 => COMMIT; 

Maintenant, une transaction parallèle voit de nouvelles données, bien que cela brise l'isolement:

 | => SELECT count(*) FROM tfreeze; 
 | count | ------- | 3 | (1 row) 
 | => COMMIT; 

Mais, comme il est peu probable qu'un tel chargement de données se produise régulièrement, ce n'est généralement pas un problème.

Pire encore, COPY WITH FREEZE ne fonctionne pas avec la carte de visibilité - les pages chargées ne sont pas marquées comme ne contenant que les versions des lignes visibles par tout le monde. Par conséquent, lorsque vous accédez pour la première fois à la table, le nettoyage est obligé de tout retraiter et de créer une carte de visibilité. Pour aggraver les choses, les pages de données ont un signe de pleine visibilité dans leur propre en-tête, donc le nettoyage non seulement lit la table entière, mais aussi la réécrit complètement, en posant le bit souhaité. Malheureusement, la solution à ce problème ne doit pas attendre plus tôt que la version 13 ( discussion ).

Conclusion


Ceci conclut ma série d'articles sur l'isolement et le multiversion de PostgreSQL. Merci pour votre attention et surtout pour les commentaires - ils améliorent le matériel et soulignent souvent des domaines qui nécessitent une attention plus attentive de ma part.

Restez avec nous, pour continuer!

Source: https://habr.com/ru/post/fr455590/


All Articles