Permettez-moi de vous rappeler que nous avons déjà parlé des
verrous de relation, des verrous de niveau ligne , des
verrous d'autres objets (y compris les prédicats) et de la relation entre les différents types de verrous.
Aujourd'hui, je termine cette série avec un article sur
les verrous mémoire . Nous parlerons des verrous tournants, des verrous légers et du verrouillage des tampons, ainsi que des outils de surveillance et d'échantillonnage des attentes.

Verrou rotatif
Contrairement aux verrous conventionnels «lourds», des verrous plus légers et moins chers (en termes de surcharge) sont utilisés pour protéger les structures dans la RAM partagée.
Les plus simples d'entre eux sont
les verrous tournants ou les
verrous tournants . Ils sont conçus pour capturer pendant un temps très court (plusieurs instructions de processeur) et protéger des sections individuelles de la mémoire des changements simultanés.
Les verrous tournants sont implémentés sur la base d'instructions atomiques du processeur, telles que comparer et échanger. Ils prennent en charge un seul mode exclusif. Si le verrou est occupé, le processus d'attente effectue une attente active - la commande est répétée («rotation» dans la boucle, d'où le nom) jusqu'à ce qu'elle soit exécutée avec succès. Cela a du sens, car les verrous tournants sont utilisés lorsque la probabilité de conflit est estimée très faible.
Les verrous tournants ne permettent pas de détecter les blocages (les développeurs PostgreSQL le surveillent) et ne fournissent aucun outil de surveillance. Dans l'ensemble, la seule chose que nous pouvons faire avec les verrous tournants est de connaître leur existence.
Serrures légères
Viennent ensuite les
serrures dites
légères (serrures légères, lwlocks).
Ils sont capturés pour le court laps de temps nécessaire pour travailler avec la structure de données (par exemple, une table de hachage ou une liste de pointeurs). En règle générale, un verrou léger n'est pas maintenu longtemps, mais dans certains cas, un verrou léger protège les opérations d'E / S, donc en principe, le temps peut s'avérer important.
Deux modes sont pris en charge: exclusif (pour la modification des données) et partagé (lecture seule). Ainsi, il n'y a pas de file d'attente: si plusieurs processus attendent la libération du verrou, l'un d'entre eux y aura accès plus ou moins aléatoirement. Dans les systèmes à degré de parallélisme élevé et à forte charge, cela peut entraîner des effets désagréables (voir, par exemple, la
discussion ).
Un mécanisme de vérification des blocages n'est pas fourni, cela reste dans la conscience des développeurs du noyau. Cependant, les verrous légers ont des outils de surveillance, par conséquent, contrairement aux verrous tournants, ils peuvent être «vus» (un peu plus tard, je montrerai comment).
Tampon de clip
Un autre type de verrou dont nous avons déjà parlé dans l'article sur le
cache de tampon est l'
épinglage de tampon .
Avec un tampon fixe, vous pouvez effectuer diverses actions, y compris la modification des données, mais à condition que ces modifications ne soient pas visibles pour les autres processus en raison de la multi-version. Autrement dit, vous pouvez ajouter une nouvelle ligne à la page, mais vous ne pouvez pas remplacer la page du tampon par une autre.
Si le processus est entravé par la liaison, il ignore généralement un tel tampon et en sélectionne un autre. Mais dans certains cas, lorsque ce tampon particulier est requis, le processus fait la queue et s'endort - le système le réveille lorsque la fixation est retirée.
Les attentes de consolidation sont disponibles pour le suivi.
Exemple: cache tampon

Maintenant, pour obtenir un aperçu (incomplet!) De la façon et du lieu d'utilisation des verrous, considérons un exemple de cache de tampon.
Pour accéder à une table de hachage contenant des références aux tampons, le processus doit capturer un verrou de mappage de tampon léger en mode partagé, et si la table doit être modifiée, puis en mode exceptionnel. Pour réduire la granularité, cette serrure est agencée en
tranche , composée de 128 serrures distinctes, chacune protégeant sa propre partie de la table de hachage.
Le processus accède à l'en-tête du tampon à l'aide de spin-lock. Des opérations individuelles (telles que l'incrémentation du compteur) peuvent également être effectuées sans verrous explicites à l'aide d'instructions atomiques du processeur.
Pour lire le contenu d'un tampon, un verrou de contenu de tampon est requis. Habituellement, il n'est capturé que le temps nécessaire pour lire les pointeurs vers la version des lignes, puis la protection fournie par le clip tampon est suffisante. Pour modifier le contenu du tampon, ce verrou doit être capturé en mode exceptionnel.
Lors de la lecture d'un tampon à partir du disque (ou de l'écriture sur le disque), le verrou d'E / S en cours est également capturé, ce qui signale aux autres processus que la page est en cours de lecture (ou d'écriture) - ils peuvent faire la queue s'ils doivent également faire quelque chose avec cette page.
Les pointeurs vers les tampons libres et vers la victime suivante sont protégés par un verrou de rotation de verrouillage de stratégie de tampon unique.
Exemple: tampons de journal

Un autre exemple: les tampons de journaux.
Pour le cache de journal, une table de hachage est également utilisée qui contient le mappage des pages aux tampons. Contrairement au cache de tampon, cette table de hachage est protégée par le seul verrou léger de WALBufMappingLock, car la taille du cache de journal est plus petite (généralement 1/32 du cache de tampon) et l'accès aux tampons est plus rationalisé.
L'écriture de pages sur le disque est protégée par un verrou WALWriteLock léger afin qu'un seul processus puisse effectuer cette opération à la fois.
Pour créer une entrée de journal, le processus doit d'abord réserver un espace sur la page WAL. Pour ce faire, il capture le verrou de position d'insertion de verrouillage de rotation. Une fois qu'un lieu est réservé, le processus copie le contenu de son dossier à l'endroit désigné. La copie peut être effectuée par plusieurs processus en même temps, pour lesquels l'enregistrement est protégé par une tranche de 8 verrous à insertion facile (le processus doit capturer l'
un d'entre eux).
La figure ne montre pas tous les verrous liés au journal de pré-enregistrement, mais cela et l'exemple précédent devraient donner une idée de l'utilisation des verrous dans la RAM.
Suivi des attentes
À partir de PostgreSQL 9.6, des outils de surveillance d'attente sont intégrés à la vue pg_stat_activity. Lorsqu'un processus (système ou maintenance) ne peut pas faire son travail et attend quelque chose, cette attente peut être vue dans la vue: la colonne wait_event_type indique le type d'attente et la colonne wait_event indique le nom d'une attente spécifique.
Gardez à l'esprit qu'une vue affiche uniquement les attentes qui sont correctement gérées dans le code source. Si la vue ne montre pas l'attente, cela ne signifie généralement pas avec une probabilité de 100% que le processus n'attend vraiment rien.
Malheureusement, les seules informations disponibles sur les attentes sont
les informations
actuelles . Aucune statistique n'est conservée. La seule façon d'obtenir une image des attentes au fil du temps est d'
échantillonner l' état de la vue à un intervalle spécifique. Il n'y a pas de moyen intégré pour cela, mais vous pouvez utiliser des extensions, par exemple,
pg_wait_sampling .
Il est nécessaire de prendre en compte la nature probabiliste de l'échantillonnage. Pour obtenir une image plus ou moins fiable, le nombre de mesures doit être suffisamment important. L'échantillonnage à basse fréquence peut ne pas donner une image fiable, et l'augmentation de la fréquence entraînera une augmentation des frais généraux. Pour la même raison, l'échantillonnage est inutile pour analyser des sessions de courte durée.
Toutes les attentes peuvent être divisées en plusieurs types.
Les attentes des verrous considérés constituent une grande catégorie:
- en attente de verrous d'objet (valeur de verrouillage dans la colonne wait_event_type);
- attente de verrous légers (LWLock);
- en attente d'un tampon épinglé (BufferPin).
Mais les processus peuvent s'attendre à d'autres événements:
- Les attentes d'E / S (E / S) se produisent lorsqu'un processus doit écrire ou lire des données;
- le processus peut attendre les données nécessaires au travail du client (Client) ou d'un autre processus (IPC);
- les extensions peuvent enregistrer leurs attentes spécifiques (Extension).
Il y a des situations où un processus ne fait tout simplement pas de travail utile. Cette catégorie comprend:
- attendre les processus d'arrière-plan dans sa boucle principale (activité);
- en attente d'une minuterie (Timeout).
En règle générale, ces attentes sont «normales» et ne parlent d'aucun problème.
Le type d'attente est suivi du nom de l'attente particulière. Le tableau complet se trouve
dans la documentation .
Si aucun nom d'attente n'est spécifié, le processus n'est pas dans un état d'attente. Un tel moment doit être considéré comme
inexpliqué , car en fait on ne sait pas exactement ce qui se passe en ce moment.
Cependant, il est temps de regarder.
=> SELECT pid, backend_type, wait_event_type, wait_event FROM pg_stat_activity;
pid | backend_type | wait_event_type | wait_event -------+------------------------------+-----------------+--------------------- 28739 | logical replication launcher | Activity | LogicalLauncherMain 28736 | autovacuum launcher | Activity | AutoVacuumMain 28963 | client backend | | 28734 | background writer | Activity | BgWriterMain 28733 | checkpointer | Activity | CheckpointerMain 28735 | walwriter | Activity | WalWriterMain (6 rows)
On peut voir que tous les processus de service en arrière-plan «déconnent». Des valeurs vides dans wait_event_type et wait_event indiquent que le processus n'attend rien - dans notre cas, le processus de desserte est occupé à exécuter la demande.
Échantillonnage
Pour obtenir une image plus ou moins complète des attentes à l'aide de l'échantillonnage, nous utilisons l'extension
pg_wait_sampling . Il doit être compilé à partir du code source; Je vais omettre cette partie. Ensuite, nous enregistrons la bibliothèque dans le paramètre
shared_preload_libraries et redémarrons le serveur.
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_wait_sampling';
student$ sudo pg_ctlcluster 11 main restart
Installez maintenant l'extension dans la base de données.
=> CREATE EXTENSION pg_wait_sampling;
L'extension vous permet de visualiser l'historique des attentes, qui est stocké dans un tampon circulaire. Mais la chose la plus intéressante est de voir le profil des attentes - les statistiques accumulées pour toute la durée du travail.
Voici ce que nous verrons dans quelques secondes:
=> SELECT * FROM pg_wait_sampling_profile;
pid | event_type | event | queryid | count -------+------------+---------------------+---------+------- 29074 | Activity | LogicalLauncherMain | 0 | 220 29070 | Activity | WalWriterMain | 0 | 220 29071 | Activity | AutoVacuumMain | 0 | 219 29069 | Activity | BgWriterMain | 0 | 220 29111 | Client | ClientRead | 0 | 3 29068 | Activity | CheckpointerMain | 0 | 220 (6 rows)
Étant donné que rien ne s'est produit depuis le démarrage du serveur, les principales attentes sont de type Activité (les processus de service attendent que le travail apparaisse) et Client (psql attend que l'utilisateur envoie une demande).
Avec les paramètres par défaut (paramètre
pg_wait_sampling.profile_period ), la période d'échantillonnage est de 10 millisecondes, c'est-à-dire que les valeurs sont enregistrées 100 fois par seconde. Par conséquent, pour estimer la durée de l'attente en secondes, la valeur de count doit être divisée par 100.
Pour comprendre à quoi appartiennent les attentes de processus, nous ajoutons la vue pg_stat_activity à la demande:
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+------------------------------+------+------------+----------------------+------- 29068 | checkpointer | | Activity | CheckpointerMain | 222 29069 | background writer | | Activity | BgWriterMain | 222 29070 | walwriter | | Activity | WalWriterMain | 222 29071 | autovacuum launcher | | Activity | AutoVacuumMain | 221 29074 | logical replication launcher | | Activity | LogicalLauncherMain | 222 29111 | client backend | psql | Client | ClientRead | 4 29111 | client backend | psql | IPC | MessageQueueInternal | 1 (7 rows)
Chargeons pgbench et voyons comment l'image change.
student$ pgbench -i test
Nous remettons à zéro le profil collecté et exécutons le test pendant 30 secondes dans un processus distinct.
=> SELECT pg_wait_sampling_reset_profile();
student$ pgbench -T 30 test
La demande doit être terminée avant la fin du processus pgbench:
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+------------+------- 29148 | client backend | pgbench | IO | WALWrite | 8 29148 | client backend | pgbench | Client | ClientRead | 1 (2 rows)
Bien sûr, les attentes du processus pgbench se révéleront légèrement différentes selon le système spécifique. Dans notre cas, il est très probable que l'attente d'une entrée de journal (IO / WALWrite) soit présentée, mais la plupart du temps, le processus ne s'est pas arrêté, mais a fait quelque chose de vraisemblablement utile.
Serrures légères
Vous devez toujours vous rappeler que l'absence de toute attente lors de l'échantillonnage ne signifie pas qu'il n'y avait aucune attente. Si elle était plus courte que la période d'échantillonnage (le centième de seconde dans notre exemple), elle ne pourrait tout simplement pas tomber dans l'échantillon.
Par conséquent, les verrous lumineux n'apparaissaient pas dans le profil - mais ils apparaîtront si vous collectez des données pendant une longue période. Pour garantir un aperçu, vous pouvez ralentir artificiellement le système de fichiers, par exemple, utiliser le projet
slowfs construit sur le système de fichiers
FUSE .
C'est ce que nous pouvons voir dans le même test si une opération d'E / S prend 1/10 de seconde.
=> SELECT pg_wait_sampling_reset_profile();
student$ pgbench -T 30 test
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+----------------+------- 29240 | client backend | pgbench | IO | WALWrite | 1445 29240 | client backend | pgbench | LWLock | WALWriteLock | 803 29240 | client backend | pgbench | IO | DataFileExtend | 20 (3 rows)
Maintenant, l'attente principale du processus pgbench est liée aux E / S, ou plutôt à une entrée de journal qui est exécutée en mode synchrone à chaque validation. Puisque (comme illustré dans l'exemple ci-dessus), l'écriture d'un journal sur le disque est protégée par le verrou lumineux WALWriteLock, ce verrou est également présent dans le profil - nous voulions le regarder.
Tampon de clip
Pour voir l'épinglage du tampon, nous profitons du fait que les curseurs ouverts maintiennent l'épingle afin que la lecture de la ligne suivante soit plus rapide.
Nous commençons la transaction, ouvrons le curseur et sélectionnons une ligne.
=> BEGIN; => DECLARE c CURSOR FOR SELECT * FROM pgbench_history; => FETCH c;
tid | bid | aid | delta | mtime | filler -----+-----+-------+-------+----------------------------+-------- 9 | 1 | 35092 | 477 | 2019-09-04 16:16:18.596564 | (1 row)
Vérifiez que le tampon est épinglé (pinning_backends):
=> SELECT * FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('pgbench_history') AND relforknumber = 0 \gx
-[ RECORD 1 ]----+------ bufferid | 190 relfilenode | 47050 reltablespace | 1663 reldatabase | 16386 relforknumber | 0 relblocknumber | 0 isdirty | t usagecount | 1 pinning_backends | 1 <-- 1
Maintenant, nous allons
vider le tableau:
| => SELECT pg_backend_pid();
| pg_backend_pid | ---------------- | 29367 | (1 row)
| => VACUUM VERBOSE pgbench_history;
| INFO: vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 0 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers.
| Skipped 1 page 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
Comme nous pouvons le voir, la page a été ignorée (page ignorée 1 en raison des broches du tampon). En effet, le nettoyage ne peut pas le gérer, car il est interdit de supprimer physiquement les versions de ligne d'une page dans un tampon épinglé. Mais le nettoyage n'attendra pas - la page sera traitée la prochaine fois.
Et maintenant, nous allons effectuer le
nettoyage avec congélation :
| => VACUUM FREEZE VERBOSE pgbench_history;
Avec un gel clairement demandé, vous ne pouvez pas ignorer une seule page qui n'est pas marquée dans la carte de gel - sinon il est impossible de réduire l'âge maximal des transactions non gelées dans pg_class.relfrozenxid. Par conséquent, l'effacement se bloque jusqu'à la fermeture du curseur.
=> SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass;
age ----- 27 (1 row)
=> COMMIT;
| INFO: aggressively vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 26 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers.
| 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: 3.01 s. | VACUUM
=> SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass;
age ----- 0 (1 row)
Eh bien, regardons le profil des attentes de la deuxième session psql dans laquelle les commandes VACUUM ont été exécutées:
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE p.pid = 29367 ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+----------------+------+------------+------------+------- 29367 | client backend | psql | BufferPin | BufferPin | 294 29367 | client backend | psql | Client | ClientRead | 10 (2 rows)
Le type d'attente BufferPin indique que le vidage attendait que le tampon soit libéré.
Sur cela, nous supposerons que nous avons terminé les écluses. Merci à tous pour votre attention et vos commentaires!