Ich möchte Sie daran erinnern, dass wir bereits über
Beziehungssperren, Sperren auf Zeilenebene , über
Sperren anderer Objekte (einschließlich Prädikatsperren) und über die Beziehung zwischen verschiedenen Arten von Sperren gesprochen haben.
Heute beende ich diese Serie mit einem Artikel über
Speichersperren . Wir werden über Spinlocks, leichte Schlösser und Puffersperren sowie Tools zur Überwachung der Erwartungen und zur Probenahme sprechen.

Drehverschluss
Im Gegensatz zu normalen „schweren“ Sperren werden leichtere und billigere (in Bezug auf Overhead) Sperren verwendet, um Strukturen im gemeinsam genutzten RAM zu schützen.
Die einfachsten davon sind
Spin-Locks oder
Spinlocks . Sie sind so konzipiert, dass sie für eine sehr kurze Zeit (mehrere Prozessoranweisungen) erfassen und einzelne Speicherbereiche vor gleichzeitigen Änderungen schützen.
Spin-Locks werden basierend auf atomaren Anweisungen des Prozessors implementiert, z. B. Compare-and-Swap. Sie unterstützen einen einzelnen exklusiven Modus. Wenn die Sperre besetzt ist, führt der Wartevorgang eine aktive Wartezeit durch - der Befehl wird wiederholt („dreht sich“ in der Schleife, daher der Name), bis er erfolgreich ausgeführt wird. Dies ist sinnvoll, da Spin-Locks verwendet werden, wenn die Konfliktwahrscheinlichkeit als sehr gering eingeschätzt wird.
Spin Locks bieten keine Erkennung von Deadlocks (PostgreSQL-Entwickler überwachen dies) und bieten keine Überwachungstools. Im Großen und Ganzen können wir mit Spin Locks nur wissen, ob sie existieren.
Lichtschlösser
Als nächstes kommen die sogenannten
leichten Schlösser (leichte Schlösser, Schlösser).
Sie werden für die kurze Zeit erfasst, die für die Arbeit mit der Datenstruktur erforderlich ist (z. B. eine Hash-Tabelle oder eine Liste von Zeigern). In der Regel wird eine Lichtverriegelung nicht lange gehalten, aber in einigen Fällen schützt eine Lichtverriegelung die E / A-Vorgänge, sodass sich die Zeit im Prinzip als erheblich herausstellen kann.
Es werden zwei Modi unterstützt: exklusiv (zum Ändern von Daten) und gemeinsam genutzt (schreibgeschützt). Daher gibt es keine Warteschlange: Wenn mehrere Prozesse darauf warten, dass die Sperre aufgehoben wird, erhält einer von ihnen mehr oder weniger zufällig Zugriff. In Systemen mit einem hohen Grad an Parallelität und hoher Last kann dies zu unangenehmen Effekten führen (siehe z. B.
Diskussion ).
Ein Mechanismus zum Überprüfen von Deadlocks ist nicht vorgesehen, dies liegt jedoch im Gewissen der Kernelentwickler. Lichtschlösser verfügen jedoch über Überwachungswerkzeuge, daher können sie im Gegensatz zu Drehschlössern "gesehen" werden (etwas später werde ich zeigen, wie).
Clip-Puffer
Eine andere Art von Sperre, die wir bereits im Artikel über den
Puffercache besprochen haben, ist das
Puffer-Pinning .
Mit einem festen Puffer können Sie verschiedene Aktionen ausführen, einschließlich des Änderns von Daten, jedoch unter der Bedingung, dass diese Änderungen aufgrund der Mehrfachversionierung für andere Prozesse nicht sichtbar sind. Das heißt, Sie können der Seite eine neue Zeile hinzufügen, aber Sie können die Seite im Puffer nicht durch eine andere ersetzen.
Wenn der Prozess durch die Bindung behindert wird, überspringt er normalerweise nur einen solchen Puffer und wählt einen anderen aus. In einigen Fällen, in denen dieser bestimmte Puffer benötigt wird, wird der Prozess in die Warteschlange gestellt und schläft ein. Das System aktiviert ihn, wenn die Befestigung entfernt wird.
Konsolidierungserwartungen stehen zur Überwachung zur Verfügung.
Beispiel: Puffercache

Um einen (unvollständigen!) Einblick zu erhalten, wie und wo Sperren verwendet werden, betrachten Sie ein Beispiel für einen Puffercache.
Um auf eine Hash-Tabelle zuzugreifen, die Verweise auf Puffer enthält, muss der Prozess im gemeinsam genutzten Modus eine Lichtpuffer-Zuordnungssperre erfassen. Wenn die Tabelle geändert werden muss, dann im Ausnahmemodus. Um die Granularität zu verringern, ist dieses Schloss als
Tranche angeordnet und besteht aus 128 separaten Schlössern, von denen jedes seinen eigenen Teil der Hash-Tabelle schützt.
Der Prozess erhält über Spin-Lock Zugriff auf den Header des Puffers. Einzelne Operationen (wie das Inkrementieren des Zählers) können auch ohne explizite Sperren unter Verwendung atomarer Anweisungen des Prozessors ausgeführt werden.
Um den Inhalt eines Puffers zu lesen, ist eine Sperre des Pufferinhalts erforderlich. Normalerweise wird es nur für die Zeit erfasst, die zum Lesen der Zeiger auf die Version der Zeilen erforderlich ist, und dann ist der durch den Pufferclip bereitgestellte Schutz ausreichend. Um den Inhalt des Puffers zu ändern, muss diese Sperre im Ausnahmemodus erfasst werden.
Beim Lesen eines Puffers von der Festplatte (oder beim Schreiben auf die Festplatte) wird auch die Sperre für laufende E / A erfasst, die anderen Prozessen signalisiert, dass die Seite gelesen (oder geschrieben) wird. Sie können in die Warteschlange gestellt werden, wenn sie auch etwas mit dieser Seite tun müssen.
Zeiger auf freie Puffer und auf das nächste Opfer werden durch eine Spin-Sperre mit einer einzelnen Pufferstrategie geschützt.
Beispiel: Protokollpuffer

Ein weiteres Beispiel: Protokollpuffer.
Für den Journalcache wird auch eine Hash-Tabelle verwendet, die die Zuordnung von Seiten zu Puffern enthält. Im Gegensatz zum Puffercache ist diese Hash-Tabelle durch die einzige leichte Sperre von WALBufMappingLock geschützt, da der Journalcache kleiner ist (normalerweise 1/32 des Puffercaches) und der Zugriff auf Puffer effizienter ist.
Das Schreiben von Seiten auf die Festplatte ist durch eine einfache WALWriteLock-Sperre geschützt, sodass jeweils nur ein Prozess diesen Vorgang ausführen kann.
Um einen Journaleintrag zu erstellen, muss der Prozess zunächst einen Platz auf der WAL-Seite reservieren. Zu diesem Zweck wird die Positionssperre für den Spin-Lock-Einsatz erfasst. Nachdem ein Platz reserviert wurde, kopiert der Prozess den Inhalt seines Datensatzes an den angegebenen Ort. Das Kopieren kann von mehreren Prozessen gleichzeitig durchgeführt werden, für die der Datensatz durch eine Tranche von 8 einfachen Sperren zum Einfügen von Sperren geschützt ist (der Prozess muss
eine davon erfassen).
Die Abbildung zeigt nicht alle Sperren, die sich auf das Voraufzeichnungsprotokoll beziehen. Dieses und das vorherige Beispiel sollten jedoch einen Eindruck von der Verwendung von Sperren im RAM vermitteln.
Erwartungsüberwachung
Ab PostgreSQL 9.6 sind Warteüberwachungstools in die Ansicht pg_stat_activity integriert. Wenn ein Prozess (System oder Wartung) seine Arbeit nicht ausführen kann und auf etwas wartet, wird diese Erwartung in der Ansicht angezeigt: Die Spalte wait_event_type gibt den Typ der Erwartung an und die Spalte wait_event gibt den Namen einer bestimmten Erwartung an.
Beachten Sie, dass in einer Ansicht nur die Erwartungen angezeigt werden, die im Quellcode angemessen behandelt werden. Wenn die Ansicht die Erwartung nicht anzeigt, bedeutet dies im Allgemeinen nicht mit einer 100-prozentigen Wahrscheinlichkeit, dass der Prozess wirklich nichts erwartet.
Leider sind nur
aktuelle Informationen zu den Erwartungen verfügbar. Es werden keine Statistiken geführt. Die einzige Möglichkeit, sich ein Bild von den Erwartungen im Zeitverlauf zu machen, besteht darin,
den Status der Ansicht in einem bestimmten Intervall abzutasten.
Hierfür gibt es keine integrierten Mittel, aber Sie können Erweiterungen verwenden, z. B.
pg_wait_sampling .
Es ist notwendig, den probabilistischen Charakter der Probenahme zu berücksichtigen. Um ein mehr oder weniger zuverlässiges Bild zu erhalten, muss die Anzahl der Messungen groß genug sein. Das Abtasten mit einer niedrigen Frequenz liefert möglicherweise kein zuverlässiges Bild, und eine Erhöhung der Frequenz führt zu einer Erhöhung des Overheads. Aus dem gleichen Grund ist die Stichprobe für die Analyse kurzlebiger Sitzungen unbrauchbar.
Alle Erwartungen können in verschiedene Typen unterteilt werden.
Die Erwartungen an die betrachteten Schlösser bilden eine große Kategorie:
- Warten auf Objektsperren (Sperrwert in der Spalte wait_event_type);
- Warten auf Lichtschlösser (LWLock);
- Warten auf einen angehefteten Puffer (BufferPin).
Prozesse können jedoch andere Ereignisse erwarten:
- E / A-Erwartungen (I / O) treten auf, wenn ein Prozess Daten schreiben oder lesen muss.
- Der Prozess kann auf die für die Arbeit erforderlichen Daten vom Client (Client) oder von einem anderen Prozess (IPC) warten.
- Erweiterungen können ihre spezifischen Erwartungen registrieren (Erweiterung).
Es gibt Situationen, in denen ein Prozess einfach keine nützliche Arbeit leistet. Diese Kategorie umfasst:
- Warten auf Hintergrundprozesse in der Hauptschleife (Aktivität);
- Warten auf einen Timer (Timeout).
Solche Erwartungen sind in der Regel „normal“ und sprechen nicht von Problemen.
Auf die Art der Erwartung folgt der Name der jeweiligen Erwartung. Die vollständige Tabelle finden Sie
in der Dokumentation .
Wenn kein Warte-Name angegeben wird, befindet sich der Prozess nicht in einem Wartezustand. Eine solche Zeit sollte als
nicht berücksichtigt betrachtet werden, da tatsächlich nicht bekannt ist, was genau in diesem Moment geschieht.
Es ist jedoch Zeit zu schauen.
=> 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)
Es ist zu sehen, dass alle Hintergrund-Service-Prozesse „herumspielen“. Leere Werte in wait_event_type und wait_event zeigen an, dass der Prozess nichts erwartet - in unserem Fall ist der Serving-Prozess damit beschäftigt, die Anforderung auszuführen.
Probenahme
Um mithilfe von Sampling ein mehr oder weniger vollständiges Bild der Erwartungen zu erhalten, verwenden wir die Erweiterung
pg_wait_sampling . Es muss aus dem Quellcode kompiliert werden. Ich werde diesen Teil weglassen. Dann registrieren wir die Bibliothek im Parameter
shared_preload_libraries und starten den Server neu.
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_wait_sampling';
student$ sudo pg_ctlcluster 11 main restart
Installieren Sie nun die Erweiterung in der Datenbank.
=> CREATE EXTENSION pg_wait_sampling;
Mit der Erweiterung können Sie den Erwartungsverlauf anzeigen, der in einem Ringpuffer gespeichert ist. Am interessantesten ist es jedoch, das Profil der Erwartungen zu sehen - die gesammelten Statistiken für die gesamte Arbeitszeit.
Folgendes werden wir in wenigen Sekunden sehen:
=> 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)
Da seit dem Start des Servers nichts passiert ist, sind die Haupterwartungen vom Typ Aktivität (Serviceprozesse warten, bis die Arbeit angezeigt wird) und vom Client (psql wartet darauf, dass der Benutzer eine Anfrage sendet).
Mit den Standardeinstellungen (Parameter
pg_wait_sampling.profile_period ) beträgt die Abtastperiode 10 Millisekunden,
dh die Werte werden 100 Mal pro Sekunde gespeichert. Um die Wartezeit in Sekunden abzuschätzen, muss der Zählwert daher durch 100 geteilt werden.
Um zu verstehen, zu welchen Prozesserwartungen gehören, fügen wir der Anforderung die Ansicht pg_stat_activity hinzu:
=> 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)
Laden wir mit pgbench und sehen, wie sich das Bild ändert.
student$ pgbench -i test
Wir setzen das gesammelte Profil auf Null zurück und führen den Test 30 Sekunden lang in einem separaten Prozess aus.
=> SELECT pg_wait_sampling_reset_profile();
student$ pgbench -T 30 test
Die Anforderung muss abgeschlossen sein, bevor der pgbench-Prozess abgeschlossen ist:
=> 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)
Natürlich werden sich die Erwartungen an den pgbench-Prozess je nach System geringfügig unterscheiden. In unserem Fall ist es sehr wahrscheinlich, dass das Warten auf einen Protokolleintrag (IO / WALWrite) angezeigt wird, aber die meiste Zeit wurde der Prozess nicht gestoppt, sondern es wurde vermutlich etwas Nützliches getan.
Lichtschlösser
Sie sollten immer daran denken, dass das Fehlen jeglicher Erwartung bei der Probenahme nicht bedeutet, dass keine Erwartung bestand. Wenn es kürzer als die Abtastperiode war (in unserem Beispiel die Hundertstelsekunde), konnte es einfach nicht in die Probe fallen.
Daher wurden im Profil keine leichten Sperren angezeigt. Sie werden jedoch angezeigt, wenn Sie Daten über einen längeren Zeitraum erfassen. Um einen Blick darauf zu werfen, können Sie das Dateisystem künstlich verlangsamen. Verwenden
Sie beispielsweise das Projekt
slowfs , das auf dem
FUSE- Dateisystem
basiert .
Dies können wir im selben Test sehen, wenn eine E / A-Operation 1/10 Sekunde dauert.
=> 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)
Die Haupterwartung des pgbench-Prozesses bezieht sich nun auf E / A bzw. einen Protokolleintrag, der bei jedem Commit im synchronen Modus ausgeführt wird. Da (wie im obigen Beispiel gezeigt) das Schreiben eines Protokolls auf die Festplatte durch die WALWriteLock-Lichtsperre geschützt ist, ist diese Sperre auch im Profil vorhanden - wir wollten sie uns ansehen.
Clip-Puffer
Um das Fixieren des Puffers zu sehen, nutzen wir die Tatsache, dass offene Cursor den Pin halten, so dass das Lesen der nächsten Zeile schneller ist.
Wir starten die Transaktion, öffnen den Cursor und wählen eine Zeile aus.
=> 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)
Überprüfen Sie, ob der Puffer fixiert ist (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
Jetzt werden wir
den Tisch
abräumen :
| => 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
Wie wir sehen können, wurde die Seite übersprungen (1 Seite wegen Pufferstiften übersprungen). In der Tat kann die Bereinigung nicht damit umgehen, da es verboten ist, Zeilenversionen physisch von einer Seite in einem angehefteten Puffer zu löschen. Die Reinigung wartet jedoch nicht - die Seite wird beim nächsten Mal verarbeitet.
Und jetzt werden wir die
Reinigung mit Gefrieren durchführen :
| => VACUUM FREEZE VERBOSE pgbench_history;
Mit einem eindeutig angeforderten Einfrieren können Sie keine einzelne Seite überspringen, die nicht in der Einfrierkarte markiert ist. Andernfalls kann das maximale Alter nicht eingefrorener Transaktionen in pg_class.relfrozenxid nicht verringert werden. Daher bleibt die Bereinigung hängen, bis sich der Cursor schließt.
=> 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)
Schauen wir uns das Erwartungsprofil der zweiten psql-Sitzung an, in der die VACUUM-Befehle ausgeführt wurden:
=> 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)
Der Wartetyp BufferPin gibt an, dass der Flush darauf gewartet hat, dass der Puffer freigegeben wird.
Hierbei gehen wir davon aus, dass wir die Sperren abgeschlossen haben. Vielen Dank für Ihre Aufmerksamkeit und Kommentare!