WAL dans PostgreSQL: 2. Journal de pré-enregistrement

La dernière fois, nous avons rencontré l'appareil de l'un des objets importants de la mémoire partagée, le cache tampon. La possibilité de perdre des informations de la RAM est la principale raison de la nécessité de récupérer après une panne. Aujourd'hui, nous allons parler de ces outils.

Magazine


Hélas, les miracles ne se produisent pas: pour survivre à la perte d'informations dans la RAM, tout ce qui est nécessaire doit être écrit sur un disque (ou un autre appareil non volatile) en temps opportun.

C'est donc ce qui a été fait. Parallèlement aux modifications des données, un journal de ces modifications est également conservé. Lorsque nous modifions quelque chose sur une page du cache de tampon, nous créons un enregistrement dans le journal de cette modification. L'enregistrement contient les informations minimales suffisantes pour que, si nécessaire, la modification puisse être répétée.

Pour que cela fonctionne, l'entrée de journal doit nécessairement aller sur le disque avant que la page modifiée n'y arrive. D'où le nom: journal d'écriture anticipée.

En cas d'échec, les données du disque sont dans un état incohérent: certaines pages ont été écrites plus tôt, d'autres plus tard. Mais il reste un journal qui peut être lu et relancé par les opérations qui ont déjà été effectuées avant l'échec, mais dont le résultat n'a pas atteint le disque.

Pourquoi ne pas forcer l'écriture des pages de données elles-mêmes sur le disque, pourquoi effectuer des doubles travaux à la place? Cela s'avère si efficace.
Tout d'abord, un journal est un flux séquentiel de données à écrire. Même les disques durs fonctionnent assez bien avec l'enregistrement séquentiel. Mais l'enregistrement des données lui-même est aléatoire, car les pages sont dispersées sur le disque de manière plus ou moins aléatoire.
Deuxièmement, une entrée de journal peut être beaucoup plus petite qu'une page.
Troisièmement, lors de l'enregistrement, vous n'avez pas à vous soucier de garantir la cohérence des données sur le disque à tout moment arbitraire (cette exigence complique considérablement la vie).
Et quatrièmement, comme nous le verrons plus loin, le journal (puisqu'il existe) peut être utilisé non seulement pour la restauration, mais aussi pour la sauvegarde et la réplication.

Vous devez enregistrer toutes les opérations, au cours desquelles il existe un risque d'incohérence sur le disque en cas de panne. En particulier, les actions suivantes sont enregistrées:

  • changer les pages dans le cache de tampon (en règle générale, ce sont des tables et des pages d'index) - puisque la page modifiée ne va pas immédiatement sur le disque;
  • validation et annulation de transactions - le changement d'état se produit dans les tampons XACT et n'atteint pas immédiatement le disque;
  • opérations sur les fichiers (création et suppression de fichiers et de répertoires, par exemple, création de fichiers lors de la création d'une table) - car ces opérations doivent se produire simultanément avec les modifications des données.

Non connecté:

  • opérations avec des tables non journalisées (non enregistrées) - leur nom parle de lui-même;
  • opérations avec des tables temporaires - cela n'a aucun sens, car la durée de vie de ces tables ne dépasse pas la durée de vie de la session qui les a créées.

Avant PostgreSQL 10, les index de hachage n'étaient pas enregistrés (ils ne servaient qu'à mapper les fonctions de hachage à différents types de données), mais maintenant cela a été corrigé.

Dispositif logique




Logiquement, un journal peut être considéré comme une séquence d'enregistrements de différentes longueurs. Chaque enregistrement contient des données sur une certaine opération, précédées d'un en- tête standard. Le titre, entre autres, indique:

  • Numéro de transaction auquel appartient l'enregistrement.
  • gestionnaire de ressources - le composant du système responsable de l'enregistrement;
  • somme de contrôle (CRC) - vous permet de déterminer la corruption des données;
  • longueur d'enregistrement et lien vers l'enregistrement précédent.

Les données elles-mêmes ont un format et une signification différents. Par exemple, ils peuvent représenter un fragment d'une page qui doit être écrit sur son contenu avec un certain décalage. Le gestionnaire de ressources spécifié «comprend» comment interpréter les données de son enregistrement. Il existe des gestionnaires distincts pour les tables, pour chaque type d'index, pour l'état des transactions, etc. Une liste complète d'entre eux peut être obtenue si vous le souhaitez par la commande

pg_waldump -r list 

Appareil physique


Sur le disque, le journal est stocké sous forme de fichiers dans le répertoire $ PGDATA / pg_wal. La valeur par défaut de chaque fichier est de 16 Mo. La taille peut être augmentée pour éviter un grand nombre de fichiers dans un répertoire. Avant PostgreSQL 11, cela ne pouvait être fait que lors de la compilation du code source, mais maintenant vous pouvez spécifier la taille lors de l'initialisation du cluster (la --wal-segsize ).

Les entrées de journal tombent dans le fichier en cours d'utilisation; quand il se termine, le suivant commence à être utilisé.

Des tampons spéciaux sont alloués au journal dans la mémoire partagée du serveur. La taille du cache de journal est définie par le paramètre wal_buffers (la valeur par défaut implique une configuration automatique: 1/32 du cache de tampon est alloué).

Le cache du journal est organisé comme un cache tampon, mais il fonctionne principalement en mode tampon en anneau: les entrées sont ajoutées à la "tête" et écrites sur le disque à partir de la "queue".

Les positions d'enregistrement («queue») et d'insertion («tête») montrent respectivement les fonctions pg_current_wal_lsn et pg_current_wal_insert lsn:

 => SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn(); 
  pg_current_wal_lsn | pg_current_wal_insert_lsn --------------------+--------------------------- 0/331E4E64 | 0/331E4EA0 (1 row) 

Afin de faire référence à un enregistrement spécifique, le type de données pg_lsn (LSN = numéro de séquence de journal) est utilisé - il s'agit d'un nombre de 64 bits représentant le décalage d'octet avant l'enregistrement par rapport au début du journal. LSN est émis sous forme de deux nombres de 32 bits en notation hexadécimale.

Vous pouvez savoir dans quel fichier nous trouverons la position souhaitée, et avec quel décalage par rapport au début du fichier:

 => SELECT file_name, upper(to_hex(file_offset)) file_offset FROM pg_walfile_name_offset('0/331E4E64'); 
  file_name | file_offset --------------------------+------------- 000000010000000000000033 | 1E4E64 \ /\ /  0/331E4E64  

Le nom de fichier se compose de deux parties. Les 8 chiffres hexadécimaux supérieurs indiquent le numéro de la branche horaire (il est utilisé lors de la restauration à partir de la sauvegarde), le reste correspond aux chiffres LSN les plus élevés (et les chiffres LSN inférieurs restants indiquent le décalage).

Les fichiers journaux peuvent être consultés sur le système de fichiers dans le répertoire $ PGDATA / pg_wal /, mais à partir de PostgreSQL 10, ils peuvent également être vus avec une fonction spéciale:

 => SELECT * FROM pg_ls_waldir() WHERE name = '000000010000000000000033'; 
  name | size | modification --------------------------+----------+------------------------ 000000010000000000000033 | 16777216 | 2019-07-08 20:24:13+03 (1 row) 

Écriture avant


Voyons comment se déroule la journalisation et comment un enregistrement proactif est fourni. Créez une table:

 => CREATE TABLE wal(id integer); => INSERT INTO wal VALUES (1); 

Nous allons regarder l'en-tête de la page du tableau. Pour ce faire, nous avons besoin d'une extension déjà familière:

 => CREATE EXTENSION pageinspect; 

Commençons la transaction et rappelons la position d'insertion dans le journal:

 => BEGIN; => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/331F377C (1 row) 

Maintenant, faisons une opération, par exemple, mettons à jour la ligne:

 => UPDATE wal set id = id + 1; 

Cette modification a été enregistrée dans le journal, la position d'insertion a changé:

 => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/331F37C4 (1 row) 

Pour garantir que la page de données modifiée n'est pas poussée sur le disque avant l'entrée de journal, le LSN de la dernière entrée de journal associée à cette page est stocké dans l'en-tête de page:

 => SELECT lsn FROM page_header(get_raw_page('wal',0)); 
  lsn ------------ 0/331F37C4 (1 row) 

Gardez à l'esprit que le journal est commun à l'ensemble du cluster et que de nouvelles entrées y tombent tout le temps. Par conséquent, le LSN sur la page peut être inférieur à la valeur que la fonction pg_current_wal_insert_lsn vient de renvoyer. Mais rien ne se passe dans notre système, donc les chiffres sont les mêmes.

Terminez maintenant la transaction.

 => COMMIT; 

L'enregistrement de validation est également enregistré dans le journal et la position change à nouveau:

 => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/331F37E8 (1 row) 

Commit change le statut d'une transaction dans une structure appelée XACT (nous en avons déjà parlé ). Les statuts sont stockés dans des fichiers, mais ils utilisent également leur propre cache, qui occupe 128 pages en mémoire partagée. Par conséquent, pour les pages XACT, le LSN de la dernière entrée de journal doit être suivi. Mais ces informations ne sont pas stockées dans la page elle-même, mais dans la RAM.

À un moment donné, les entrées de journal créées seront écrites sur le disque. Dans lequel - nous parlerons une autre fois, mais dans notre cas, cela s'est déjà produit:

 => SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn(); 
  pg_current_wal_lsn | pg_current_wal_insert_lsn --------------------+--------------------------- 0/331F37E8 | 0/331F37E8 (1 row) 

Après ce point, les données et les pages XACT peuvent être poussées hors du cache. Mais s'il était nécessaire de les expulser plus tôt, il serait détecté et les entrées de journal seraient obligées d'être enregistrées en premier.

Connaissant les deux positions LSN, vous pouvez obtenir la taille des entrées de journal entre elles (en octets) en soustrayant simplement une position de l'autre. Vous avez juste besoin de convertir les positions en type pg_lsn:

 => SELECT '0/331F37E8'::pg_lsn - '0/331F377C'::pg_lsn; 
  ?column? ---------- 108 (1 row) 

Dans ce cas, la mise à jour et la validation de la ligne ont nécessité 108 octets dans le journal.

De la même manière, vous pouvez estimer la quantité d'entrées de journal générées par le serveur par unité de temps à une certaine charge. Ce sont des informations importantes qui seront nécessaires lors de la configuration (dont nous parlerons la prochaine fois).

Nous allons maintenant utiliser l'utilitaire pg_waldump pour examiner les entrées de journal créées.

L'utilitaire peut fonctionner avec la plage LSN (comme dans cet exemple) et sélectionner des enregistrements pour la transaction spécifiée. Il doit être exécuté au nom de l'utilisateur du système d'exploitation postgres, car il a besoin d'accéder aux fichiers journaux sur le disque.

 postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/331F377C -e 0/331F37E8 000000010000000000000033 
 rmgr: Heap len (rec/tot): 69/ 69, tx: 101085, lsn: 0/331F377C, prev 0/331F3014, desc: HOT_UPDATE off 1 xmax 101085 ; new off 2 xmax 0, blkref #0: rel 1663/16386/33081 blk 0 
 rmgr: Transaction len (rec/tot): 34/ 34, tx: 101085, lsn: 0/331F37C4, prev 0/331F377C, desc: COMMIT 2019-07-08 20:24:13.945435 MSK 

Ici, nous voyons les en-têtes des deux entrées.

La première est l'opération HOT_UPDATE , liée au gestionnaire de ressources du tas. Le nom de fichier et le numéro de page sont indiqués dans le champ blkref et correspondent à la page de table mise à jour:

 => SELECT pg_relation_filepath('wal'); 
  pg_relation_filepath ---------------------- base/16386/33081 (1 row) 

La deuxième entrée est COMMIT, liée au gestionnaire de ressources de transaction.

Ce n'est pas le format le plus lisible, mais vous pouvez le découvrir si nécessaire.

Récupération


Lorsque nous démarrons le serveur, le processus postmaster démarre en premier et, à son tour, démarre le processus de démarrage, dont la tâche est d'assurer la récupération en cas de défaillance.

Pour déterminer si la récupération est requise, le démarrage examine le fichier de contrôle spécial $ PGDATA / global / pg_control et examine l'état du cluster. Nous pouvons vérifier le statut nous-mêmes en utilisant l'utilitaire pg_controldata:

 postgres$ /usr/lib/postgresql/11/bin/pg_controldata -D /var/lib/postgresql/11/main | grep state 
 Database cluster state: in production 

Un serveur correctement arrêté aura le statut «arrêté». Si le serveur ne fonctionne pas et que le statut reste «en production», cela signifie que le SGBD est tombé et que la récupération sera automatiquement effectuée.

Pour la récupération, le processus de démarrage lira séquentiellement le journal et appliquera des entrées aux pages, si nécessaire. Vous pouvez vérifier le besoin en comparant le LSN de la page sur le disque avec le LSN de l'entrée de journal. Si le LSN de la page est plus grand, l'enregistrement n'est pas nécessaire. Mais en fait - ce n'est même pas possible, car les enregistrements sont conçus pour une application strictement cohérente.

Il y a des exceptions. Certains enregistrements sont formés comme une image pleine page (FPI, image pleine page), et il est clair qu'une telle image peut être appliquée à une page dans n'importe quel état - elle effacera toujours tout ce qui s'y trouvait. Une autre modification du statut de la transaction peut être appliquée à n'importe quelle version de la page XACT - par conséquent, à l'intérieur de ces pages, il n'est pas nécessaire de stocker LSN.

La modification des pages pendant la récupération se produit dans le cache de tampon, comme pendant le travail normal - pour ce maître de poste, démarre les processus d'arrière-plan nécessaires.

De même, les entrées de journal s'appliquent aux fichiers: par exemple, si un enregistrement indique que le fichier doit exister, mais qu'il n'existe pas, le fichier est créé.

Eh bien, à la toute fin du processus de récupération, toutes les tables non journalisées sont remplacées par des "nuls" de leurs couches init .

Il s'agit d'une présentation très simplifiée de l'algorithme. En particulier, nous n'avons rien dit par où commencer la lecture des écritures de journal (cette conversation devra être reportée jusqu'à ce que le point de contrôle soit considéré).

Et la dernière précision. Le processus de récupération "classique" comprend deux phases. Dans la première phase (roll forward), les écritures de journal sont restaurées et le serveur répète tout le travail perdu lors de l'échec. Dans la seconde (annulation), les transactions qui n'étaient pas validées au moment de l'échec sont annulées. Mais PostgreSQL n'a pas besoin d'une deuxième phase. Comme nous l'avons considéré précédemment , en raison des particularités de la mise en œuvre des transactions multi-versions, il n'est pas nécessaire de les annuler physiquement; il suffit que le bit de correction ne soit pas défini dans XACT.

À suivre .

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


All Articles