Postgres: Bloat, pg_repack und deferred constraints

Postgres: Bloat, pg_repack und deferred constraints

Der Effekt der Fragmentierung von Tabellen und Indizes (Bloat) ist weit verbreitet und tritt nicht nur in Postgres auf. Es gibt „out-of-the-box“ Methoden wie VACUUM FULL oder CLUSTER, aber diese sperren Tabellen wĂ€hrend des Betriebs, weshalb sie nicht immer verwendet werden können.

Der Artikel enthĂ€lt etwas Theorie darĂŒber, wie Bloat entsteht, wie man ihm entgegenwirken kann, ĂŒber deferred constraints und ĂŒber die Probleme, die sie bei der Verwendung der Erweiterung pg_repack mit sich bringen.

Dieser Artikel basiert auf meinem Vortrag PgConf.Russia 2020.

Video abspielen

Warum entsteht Bloat

Postgres basiert auf einem mehrversionierten Modell (MVCC). Der Kernidee ist, dass jede Zeile in einer Tabelle mehrere Versionen haben kann, wobei Transaktionen nicht mehr als eine dieser Versionen sehen, aber nicht unbedingt dieselbe. Dies ermöglicht es mehreren Transaktionen, gleichzeitig zu arbeiten, ohne sich gegenseitig zu beeinflussen.

Es ist offensichtlich, dass all diese Versionen gespeichert werden mĂŒssen. Postgres arbeitet seitenweise, und eine Seite ist das kleinste Datenvolumen, das von der Festplatte gelesen oder darauf geschrieben werden kann. Lassen Sie uns ein kleines Beispiel betrachten, um zu verstehen, wie das funktioniert.

Angenommen, wir haben eine Tabelle, in die wir mehrere DatensĂ€tze eingefĂŒgt haben. Auf der ersten Seite der Datei, in der die Tabelle gespeichert ist, sind neue Daten hinzugekommen. Das sind die aktuellen Versionen der Zeilen, die nach dem Commit anderen Transaktionen zur VerfĂŒgung stehen (zur Vereinfachung nehmen wir an, dass der Isolationsgrad Read Committed ist).

Postgres: Bloat, pg_repack und deferred constraints

Dann haben wir einen der DatensÀtze aktualisiert und dadurch die alte Version als obsolet markiert.

Postgres: Bloat, pg_repack und deferred constraints

Schritt fĂŒr Schritt, durch das Aktualisieren und Löschen von Versionen der Zeilen, haben wir eine Seite erhalten, auf der etwa die HĂ€lfte der Daten 'MĂŒll' sind. Diese Daten sind fĂŒr keine Transaktion sichtbar.

Postgres: Bloat, pg_repack und deferred constraints

In Postgres gibt es einen Mechanismus VACUUM, der nicht mehr aktuelle Versionen bereinigt und Platz fĂŒr neue Daten schafft. Wenn er jedoch nicht aggressiv genug konfiguriert ist oder mit der Arbeit in anderen Tabellen beschĂ€ftigt ist, bleiben „MĂŒlldaten“ bestehen und wir mĂŒssen zusĂ€tzliche Seiten fĂŒr neue Daten verwenden.

So wird unsere Tabelle in unserem Beispiel zu einem bestimmten Zeitpunkt aus vier Seiten bestehen, aber es werden nur die HÀlfte der Daten aktuell sein. Infolgedessen lesen wir beim Zugriff auf die Tabelle viel mehr Daten, als tatsÀchlich erforderlich ist.

Postgres: Bloat, pg_repack und deferred constraints

Selbst wenn VACUUM alle veralteten Zeilenversionen jetzt entfernt, wird sich die Situation nicht grundlegend verbessern. Wir werden freien Speicherplatz auf Seiten oder sogar ganze Seiten fĂŒr neue Zeilen haben, aber wir werden immer noch mehr Daten lesen, als nötig ist.
Übrigens, wenn eine vollstĂ€ndig leere Seite (die zweite in unserem Beispiel) am Ende der Datei wĂ€re, könnte VACUUM sie abschneiden. Aber da sie sich jetzt in der Mitte befindet, kann nichts damit gemacht werden.

Postgres: Bloat, pg_repack und deferred constraints

Wenn die Anzahl solcher leeren oder stark ausgedĂŒnnten Seiten groß wird, was man als bloat bezeichnet, hat es Auswirkungen auf die Leistung.

Das oben Beschriebene ist die Mechanik, wie bloat in Tabellen entsteht. In Indizes geschieht das ungefÀhr Àhnlich.

Habe ich bloat?

Es gibt mehrere Möglichkeiten, um festzustellen, ob Sie bloat haben. Die Idee des ersten Ansatzes ist die Verwendung interner Statistiken von Postgres, die ungefĂ€hre Informationen ĂŒber die Anzahl der Zeilen in Tabellen, die Anzahl der 'lebenden' Zeilen usw. enthalten. Im Internet findet man viele Variationen bereits fertiger Skripte. Wir haben als Basis genommen Skript von PostgreSQL Experts, das bloat sowohl in Tabellen als auch in Toast- und Bloat-Btree-Indizes bewerten kann. Nach unserer Erfahrung liegt sein Fehlerbereich bei 10-20%.

Ein weiterer Weg ist die Verwendung der Erweiterung pgstattuple, die es ermöglicht, in die Seiten hineinzusehen und sowohl geschĂ€tzte als auch exakte Werte fĂŒr bloat zu erhalten. Im zweiten Fall muss jedoch die gesamte Tabelle gescannt werden.

Ein geringer bloat-Wert von bis zu 20% wird als akzeptabel angesehen. Er kann als Analogon zum fillfactor fĂŒr Tabellen und Indizes. Bei 50% und mehr können Leistungsprobleme auftreten.

Methoden zur BekÀmpfung von bloat

In Postgres gibt es mehrere Möglichkeiten zur BekĂ€mpfung von bloat 'out of the box', jedoch sind sie nicht immer fĂŒr jeden geeignet.

AUTOVACUUM so konfigurieren, dass bloat nicht entsteht. Und genauer gesagt, damit er auf einem fĂŒr Sie akzeptablen Niveau bleibt. Es scheint ein „KapitĂ€ne“-Ratschlag zu sein, aber in der RealitĂ€t ist das nicht immer leicht zu erreichen. Zum Beispiel, wenn Sie aktiv an der Entwicklung sind und regelmĂ€ĂŸig das Datenschema Ă€ndern oder eine Datenmigration stattfindet. Infolgedessen kann sich Ihr Lastprofil hĂ€ufig Ă€ndern und ist in der Regel unterschiedlich fĂŒr verschiedene Tabellen. Das bedeutet, dass Sie stĂ€ndig ein wenig vorausplanen und AUTOVACUUM an das sich Ă€ndernde Profil jeder Tabelle anpassen mĂŒssen. Aber offensichtlich ist das nicht einfach zu bewerkstelligen.

Ein weiterer hĂ€ufiger Grund, warum AUTOVACUUM nicht rechtzeitig mit der Verarbeitung der Tabellen fertig wird, sind lĂ€ngere Transaktionen, die es ihm nicht ermöglichen, Daten zu bereinigen, da diese von diesen Transaktionen genutzt werden. Die Empfehlung hier ist ebenfalls offensichtlich – „hĂ€ngende“ Transaktionen loszuwerden und die Zeit aktiver Transaktionen zu minimieren. Wenn jedoch die Last auf Ihre Anwendung eine Mischung aus OLAP und OLTP ist, können Sie gleichzeitig sowohl viele hĂ€ufige Aktualisierungen und kurzen Anfragen als auch lĂ€ngere VorgĂ€nge haben – zum Beispiel den Aufbau eines Berichts. In diesem Fall sollten Sie in Betracht ziehen, die Last auf verschiedene Datenbanken zu verteilen, was eine feinere Abstimmung jeder einzelnen ermöglicht.

Ein weiteres Beispiel – selbst wenn das Profil homogen ist, aber die DB einer sehr hohen Last ausgesetzt ist, kann selbst das aggressivste AUTOVACUUM scheitern, und Bloat wird auftreten. Skalierung (vertikal oder horizontal) ist die einzige Lösung.

Was tun in einer Situation, in der Sie AUTOVACUUM eingerichtet haben, aber Bloat weiterhin wÀchst.

Team VACUUM FULL stellt den Inhalt von Tabellen und Indizes um und lĂ€sst nur aktuelle Daten zurĂŒck. FĂŒr die Beseitigung von Bloat funktioniert es bestens, aber wĂ€hrend seiner AusfĂŒhrung wird eine exklusive Sperre auf die Tabelle (AccessExclusiveLock) gesetzt, die das AusfĂŒhren von Abfragen auf dieser Tabelle, selbst Selects, verhindert. Wenn Sie sich eine Unterbrechung Ihres Dienstes oder eines Teils davon fĂŒr eine gewisse Zeit (von mehreren Minuten bis zu mehreren Stunden, abhĂ€ngig von der GrĂ¶ĂŸe der DB und Ihrer Hardware) leisten können, ist diese Option die beste. Leider schaffen wir es nicht, VACUUM FULL innerhalb der geplanten Wartung durchzufĂŒhren, daher ist dieser Ansatz fĂŒr uns nicht geeignet.

Team CLUSTER Es rekonstruiert auch den Inhalt von Tabellen wie VACUUM FULL, erlaubt aber die Angabe eines Indexes, nach dem die Daten physisch auf der Festplatte sortiert werden (allerdings wird in Zukunft die Reihenfolge fĂŒr neue Zeilen nicht garantiert). In bestimmten Situationen ist dies eine gute Optimierung fĂŒr eine Reihe von Abfragen – insbesondere beim Lesen mehrerer DatensĂ€tze nach Index. Der Nachteil des Befehls ist derselbe wie bei VACUUM FULL – er sperrt die Tabelle wĂ€hrend der AusfĂŒhrung.

Team REINDEX Ă€hnelt den beiden vorherigen, fĂŒhrt jedoch die Rekonstruktion eines bestimmten Index oder aller Indizes einer Tabelle durch. Die Sperren sind etwas schwĂ€cher: ShareLock auf die Tabelle (verhindert Modifikationen, erlaubt jedoch Selects) und AccessExclusiveLock auf den rekonstruierten Index (sperrt Abfragen, die diesen Index verwenden). In der 12. Version von Postgres wurde jedoch ein Parameter eingefĂŒhrt, CONCURRENTLY, der es ermöglicht, den Index ohne Sperrung paralleler EinfĂŒgungen, Änderungen oder Löschungen von DatensĂ€tzen neu zu bauen.

In frĂŒheren Versionen von Postgres kann man ein Ă€hnliches Ergebnis wie REINDEX CONCURRENTLY mit CREATE INDEX CONCURRENTLYerreichen. Er erlaubt die Erstellung eines Index ohne strikte Sperrung (ShareUpdateExclusiveLock, die parallele Abfragen nicht stört), danach den alten Index durch den neuen zu ersetzen und den alten Index zu löschen. Dadurch kann Bloat in den Indizes behoben werden, ohne den Betrieb Ihrer Anwendung zu stören. Es ist wichtig zu beachten, dass beim Rekonstruieren der Indizes die Festplattensubsysteme zusĂ€tzlich belastet werden.

Somit gibt es fĂŒr Indizes Möglichkeiten, Bloat "live" zu beheben, fĂŒr Tabellen hingegen nicht. Hier kommen verschiedene externe Erweiterungen ins Spiel: pg_repack (frĂŒher pg_reorg), pgcompact, pgcompacttable und andere. In diesem Artikel werde ich sie nicht vergleichen, sondern nur ĂŒber pg_repack sprechen, das wir nach einigen Anpassungen bei uns verwenden.

Wie funktioniert pg_repack?

Postgres: Bloat, pg_repack und deferred constraints
Angenommen, wir haben eine ganz normale Tabelle – mit Indizes, EinschrĂ€nkungen und, leider, mit Bloat. Im ersten Schritt erstellt pg_repack eine Log-Tabelle, um Daten ĂŒber alle Änderungen wĂ€hrend der AusfĂŒhrung zu speichern. Ein Trigger repliziert diese Änderungen bei jedem Insert, Update und Delete. Anschließend wird eine Tabelle erstellt, die der Ausgangstabelle hinsichtlich der Struktur Ă€hnlich ist, jedoch ohne Indizes und EinschrĂ€nkungen, um den Dateninsertionsprozess nicht zu verlangsamen.

Anschließend ĂŒbertrĂ€gt pg_repack Daten aus der alten Tabelle in die neue und filtert dabei automatisch alle irrelevanten Zeilen heraus. Danach erstellt es Indizes fĂŒr die neue Tabelle. WĂ€hrend der AusfĂŒhrung aller dieser Operationen sammeln sich Änderungen in der Protokolltabelle an.

Der nĂ€chste Schritt besteht darin, die Änderungen in die neue Tabelle zu ĂŒbertragen. Der Transfer erfolgt in mehreren Iterationen, und wenn weniger als 20 EintrĂ€ge in der Protokolltabelle verbleiben, erfasst pg_repack eine strikte Sperre, ĂŒbertrĂ€gt die letzten Daten und ersetzt die alte Tabelle in den Systemtabellen von Postgres durch die neue. Dies ist der einzige und sehr kurze Moment, in dem Sie nicht mit der Tabelle arbeiten können. Danach werden die alte Tabelle und die Protokolltabelle gelöscht, und der Speicherplatz im Dateisystem wird freigegeben. Der Prozess ist abgeschlossen.

In der Theorie sieht alles großartig aus, aber wie sieht es in der Praxis aus? Wir haben pg_repack ohne und unter Last getestet und seine Funktionsweise im Falle eines vorzeitigen Stopps (kurz gesagt, durch DrĂŒcken von Ctrl+C) ĂŒberprĂŒft. Alle Tests waren positiv.

Wir sind in die Produktionsumgebung gegangen – und hier lief alles nicht so, wie wir es erwartet hatten.

Der erste Versuch in der Produktion

Im ersten Cluster erhielten wir einen Fehler wegen eines Verstoßes gegen die eindeutige EinschrĂ€nkung:

$ ./pg_repack -t tablename -o id
INFO: Tabelle "tablename" wird neu verpackt
ERROR: Abfrage gescheitert:
    ERROR: Duplicater SchlĂŒsselwert verletzt eindeutige EinschrĂ€nkung "index_16508"
DETAIL: SchlĂŒssel (id, index)=(100500, 42) existiert bereits.

Diese EinschrĂ€nkung hatte den automatisch generierten Namen index_16508 – sie wurde von pg_repack erstellt. Anhand der Attribute, die Teil davon sind, haben wir „unser“ entsprechendes Limit identifiziert. Das Problem war, dass es sich nicht um eine ganz gewöhnliche EinschrĂ€nkung handelte, sondern um eine verzögerte (deferred constraint), d.h. deren ÜberprĂŒfung erfolgt spĂ€ter als der sql-Befehl, was zu unerwarteten Folgen fĂŒhrt.

Verzögerte EinschrÀnkungen: Warum sie notwendig sind und wie sie funktionieren

Ein wenig Theorie ĂŒber verzögerte EinschrĂ€nkungen.
Betrachten wir ein einfaches Beispiel: Wir haben eine Referenztabelle fĂŒr Autos mit zwei Attributen – dem Namen und der Reihenfolge des Autos in der Referenz.
Postgres: Bloat, pg_repack und deferred constraints

create table cars
(
  name text constraint pk_cars primary key,
  ord integer not null constraint uk_cars unique
);



Angenommen, wir mĂŒssen das erste und das zweite Auto vertauschen. Die direkte Lösung wĂ€re, den ersten Wert auf den zweiten und den zweiten auf den ersten zu aktualisieren:

begin;
  update cars set ord = 2 where name = 'audi';
  update cars set ord = 1 where name = 'bmw';
commit;

Aber bei der AusfĂŒhrung dieses Codes werden wir erwartungsgemĂ€ĂŸ einen Verstoß gegen die EinschrĂ€nkung erhalten, da die Reihenfolge der Werte in der Tabelle eindeutig ist:

[23305] FEHLER: doppelter SchlĂŒsselwert verstĂ¶ĂŸt gegen die einzigartige EinschrĂ€nkung „uk_cars“
Detail: SchlĂŒssel (ord)=(2) existiert bereits.

Was kann man anders machen? Erste Möglichkeit: FĂŒgen Sie einen zusĂ€tzlichen Wert fĂŒr die Reihenfolge hinzu, der in der Tabelle garantiert nicht existiert, zum Beispiel „-1“. In der Programmierung nennt man das „Wertetausch zweier Variablen ĂŒber eine dritte“. Der einzige Nachteil dieser Methode ist das zusĂ€tzliche Update.

Zweite Möglichkeit: Entwerfen Sie die Tabelle neu, um fĂŒr den Reihenfolgewert den Datentyp mit Fließkomma anstelle von Ganzzahlen zu verwenden. Dann wird beim Aktualisieren eines Wertes von 1, zum Beispiel auf 2,5, der erste Datensatz automatisch zwischen dem zweiten und dritten stehen. Diese Lösung funktioniert, hat jedoch zwei EinschrĂ€nkungen. Erstens ist sie nicht geeignet, wenn der Wert irgendwo in der BenutzeroberflĂ€che verwendet wird. Zweitens haben Sie, je nach Genauigkeit des Datentyps, eine begrenzte Anzahl möglicher EinfĂŒgungen, bevor Sie eine Neuberechnung der Werte aller DatensĂ€tze durchfĂŒhren.

Dritte Möglichkeit: Machen Sie die EinschrĂ€nkung verzögert, sodass sie nur zum Zeitpunkt des Commit ĂŒberprĂŒft wird:

create table cars
(
  name text constraint pk_cars primary key,
  ord integer not null constraint uk_cars unique deferrable initially deferred
);

Da die Logik unserer ursprĂŒnglichen Anfrage garantiert, dass alle Werte zum Zeitpunkt des Commits eindeutig sind, wird sie erfolgreich ausgefĂŒhrt.

Das oben betrachtete Beispiel ist zwar sehr synthetisch, verdeutlicht jedoch die Idee. In unserer Anwendung nutzen wir verzögerte EinschrĂ€nkungen zur Implementierung der Logik, die fĂŒr die Konfliktlösung verantwortlich ist, wenn mehrere Benutzer gleichzeitig mit gemeinsamen Objekten-Widgets auf einem Board arbeiten. Der Einsatz solcher EinschrĂ€nkungen ermöglicht es uns, den Anwendungscode etwas einfacher zu gestalten.

Insgesamt gibt es, je nach Art der EinschrĂ€nkung in Postgres, drei Ebenen der GranularitĂ€t ihrer ÜberprĂŒfung: Zeilenebene, Transaktionsebene und Ausdrucksebene.
Postgres: Bloat, pg_repack und deferred constraints
Quelle: begriffs

CHECK und NOT NULL werden immer auf Zeilenebene ĂŒberprĂŒft, fĂŒr die anderen EinschrĂ€nkungen gibt es je nach Tabelle unterschiedliche Varianten. Weitere Informationen finden Sie hier. hier.

Kurz gesagt, fĂŒhren verzögerte EinschrĂ€nkungen in bestimmten Situationen zu einem besser lesbaren Code und einer geringeren Anzahl von Befehlen. Allerdings hat dies den Preis, dass der Debugging-Prozess komplizierter wird, da der Zeitpunkt, an dem ein Fehler auftritt, und der Zeitpunkt, an dem Sie davon erfahren, zeitlich auseinanderliegen. Ein weiteres mögliches Problem besteht darin, dass der Planer nicht immer in der Lage ist, einen optimalen Plan zu erstellen, wenn eine verzögerte EinschrĂ€nkung in der Anfrage beteiligt ist.

Überarbeitung von pg_repack

Wir haben uns angesehen, was verzögerte EinschrÀnkungen sind, aber wie hÀngen sie mit unserem Problem zusammen? Denken wir an den Fehler, den wir zuvor erhalten haben:

$ ./pg_repack -t tablename -o id
INFO: Tabelle "tablename" wird neu verpackt
ERROR: Abfrage gescheitert:
    ERROR: Duplicater SchlĂŒsselwert verletzt eindeutige EinschrĂ€nkung "index_16508"
DETAIL: SchlĂŒssel (id, index)=(100500, 42) existiert bereits.

Dieser tritt auf, wenn Daten aus der Log-Tabelle in die neue Tabelle kopiert werden. Das sieht seltsam aus, da die Daten in der Log-Tabelle zusammen mit den Daten der UrsprĂŒnglichen Tabelle committiert werden. Wenn sie die EinschrĂ€nkungen der ursprĂŒnglichen Tabelle erfĂŒllen, wie können sie dann dieselben EinschrĂ€nkungen in der neuen Tabelle verletzen?

Wie sich herausstellte, liegt die Wurzel des Problems im vorherigen Schritt der pg_repack-Arbeit, bei dem nur Indizes, jedoch keine EinschrÀnkungen erstellt werden: In der alten Tabelle gab es eine unique constraint, und in der neuen wurde an deren Stelle ein eindeutiger Index erstellt.

Postgres: Bloat, pg_repack und deferred constraints

Hier ist wichtig zu beachten, dass, wenn die EinschrĂ€nkung gewöhnlich und nicht verzögert ist, der erstellte eindeutige Index dieser EinschrĂ€nkung gleichwertig ist, da eindeutige EinschrĂ€nkungen in Postgres durch die Erstellung eines einzigartigen Indexes umgesetzt werden. Aber im Fall von verzögerten EinschrĂ€nkungen ist das Verhalten nicht identisch, weil der Index nicht verzögert sein kann und immer zum Zeitpunkt der AusfĂŒhrung des SQL-Befehls ĂŒberprĂŒft wird.

Somit besteht das Wesentliche des Problems in der "Verzögerung" der ÜberprĂŒfung: In der ursprĂŒnglichen Tabelle erfolgt sie zum Zeitpunkt des Commits, in der neuen hingegen zum Zeitpunkt der AusfĂŒhrung des SQL-Befehls. Das bedeutet, wir mĂŒssen sicherstellen, dass die PrĂŒfungen in beiden FĂ€llen gleich durchgefĂŒhrt werden: entweder immer verzögert oder immer sofort.

Also, welche Ideen hatten wir.

Einen Index erstellen, der der deferred-Variante Àhnelt.

Die erste Idee bestand darin, beide PrĂŒfungen im unmittelbaren Modus durchzufĂŒhren. Dies kann einige False-Positive-Warnungen der EinschrĂ€nkung verursachen, aber wenn es nicht viele sind, sollte sich das nicht auf die Arbeit der Benutzer auswirken, da solche Konflikte fĂŒr sie normal sind. Sie treten beispielsweise auf, wenn zwei Benutzer gleichzeitig dasselbe Widget zu bearbeiten versuchen und der Client des zweiten Benutzers keine Informationen darĂŒber erhĂ€lt, dass das Widget bereits von dem ersten Benutzer fĂŒr die Bearbeitung gesperrt ist. In dieser Situation erhĂ€lt der Server vom zweiten Benutzer eine Ablehnung, woraufhin sein Client die Änderungen zurĂŒcksetzt und das Widget sperrt. Etwas spĂ€ter, wenn der erste Benutzer die Bearbeitung abgeschlossen hat, erfĂ€hrt der zweite, dass das Widget nicht mehr gesperrt ist, und kann seine Aktion wiederholen.

Postgres: Bloat, pg_repack und deferred constraints

Um sicherzustellen, dass die PrĂŒfungen immer im sofortigen Modus ausgefĂŒhrt werden, haben wir einen neuen Index erstellt, der dem ursprĂŒnglichen verzögerten Limit entspricht:

CREATE UNIQUE INDEX CONCURRENTLY uk_tablename__immediate ON tablename (id, index);
-- pg_repack ausfĂŒhren
DROP INDEX CONCURRENTLY uk_tablename__immediate;

In der Testumgebung erhielten wir nur einige erwartete Fehler. Erfolg! Wir haben pg_repack erneut in der Produktion gestartet und erhielten 5 Fehler im ersten Cluster innerhalb einer Stunde. Das ist ein akzeptables Ergebnis. Allerdings stieg die Anzahl der Fehler im zweiten Cluster erheblich an, sodass wir pg_repack stoppen mussten.

Warum ist das passiert? Die Wahrscheinlichkeit eines Fehlers hĂ€ngt davon ab, wie viele Benutzer gleichzeitig mit denselben Widgets arbeiten. Anscheinend gab es zu diesem Zeitpunkt im ersten Cluster viel weniger konkurrierende Änderungen als in den anderen, also hatten wir einfach "GlĂŒck".

Die Idee hat nicht funktioniert. Zu diesem Zeitpunkt sahen wir zwei andere LösungsansÀtze: unseren Anwendungscode umschreiben, um auf verzögerte EinschrÀnkungen zu verzichten, oder pg_repack "beizubringen", damit zu arbeiten. Wir wÀhlten den zweiten.

Die Indizes in der neuen Tabelle durch die verzögerten EinschrĂ€nkungen der ursprĂŒnglichen Tabelle ersetzen.

Das Ziel der Überarbeitung war offensichtlich – wenn die ursprĂŒngliche Tabelle eine verzögerte EinschrĂ€nkung hat, sollte fĂŒr die neue eine solche EinschrĂ€nkung und kein Index erstellt werden.

Um unsere Änderungen zu ĂŒberprĂŒfen, schrieben wir einen einfachen Test:

  • Tabelle mit verzögerter EinschrĂ€nkung und einem Datensatz;
  • Wir fĂŒgen in einer Schleife Daten ein, die mit dem vorhandenen Datensatz in Konflikt stehen;
  • Wir fĂŒhren das Update durch – die Daten stehen nicht mehr im Konflikt;
  • Wir committen die Änderungen.

create table test_table
(
  id serial,
  val int,
  constraint uk_test_table__val unique (val) deferrable initially deferred 
);

INSERT INTO test_table (val) VALUES (0);
FOR i IN 1..10000 LOOP
  BEGIN
    INSERT INTO test_table VALUES (0) RETURNING id INTO v_id;
    UPDATE test_table set val = i where id = v_id;
    COMMIT;
  END;
END LOOP;

Die ursprĂŒngliche Version von pg_repack schlug beim ersten Insert fehl, die ĂŒberarbeitete Version funktionierte fehlerfrei. Ausgezeichnet.

Wir gehen in die Produktion und erhalten erneut einen Fehler in derselben Phase des Kopierens von Daten aus der Protokolltabelle in die neue:

$ ./pg_repack -t tablename -o id
INFO: Tabelle "tablename" wird neu verpackt
ERROR: Abfrage gescheitert:
    ERROR: Duplicater SchlĂŒsselwert verletzt eindeutige EinschrĂ€nkung "index_16508"
DETAIL: SchlĂŒssel (id, index)=(100500, 42) existiert bereits.

Klassische Situation: In den Testumgebungen funktioniert alles, aber in der Produktion – nicht?!

APPLY_COUNT und die Schnittstelle zweier Batch-Prozesse

Wir begannen, den Code zeilenweise zu analysieren und fanden einen wichtigen Punkt: Der Transfer von Daten aus der Protokolltabelle in die neue erfolgt in Batches, die Konstante APPLY_COUNT gab die GrĂ¶ĂŸe des Batches an:

for (;;)
{
num = apply_log(connection, table, APPLY_COUNT);

if (num > MIN_TUPLES_BEFORE_SWITCH)
     continue;  
/* Es könnten noch einige Tupel vorhanden sein, wiederholen. */
...
}

Das Problem ist, dass die Daten der ursprĂŒnglichen Transaktion, bei der mehrere Operationen potenziell die EinschrĂ€nkung verletzen könnten, beim Transfer auf die Schnittstelle zweier Batches gelangen können – die HĂ€lfte der Befehle wird im ersten Batch committen, die andere HĂ€lfte im zweiten. Und hier kommt es darauf an: Wenn die Befehle im ersten Batch nichts verletzen, ist alles gut, aber wenn sie verletzen – tritt ein Fehler auf.

APPLY_COUNT betrĂ€gt 1000 DatensĂ€tze, was erklĂ€rt, warum unsere Tests erfolgreich waren – sie deckten den Fall des "Übergangs zwischen Batches" nicht ab. Wir verwendeten zwei Befehle – Insert und Update, daher am Ende 500 Transaktionen mit jeweils zwei Befehlen gingen immer in einen Batch und wir hatten keine Probleme. Nach dem HinzufĂŒgen eines zweiten Updates funktionierte unsere Anpassung nicht mehr:

FOR i IN 1..10000 LOOP
  BEGIN
    INSERT INTO test_table VALUES (1) RETURNING id INTO v_id;
    UPDATE test_table set val = i where id = v_id;
    UPDATE test_table set val = i where id = v_id; -- ein weiteres Update
    COMMIT;
  END;
END LOOP;

Die nĂ€chste Aufgabe besteht darin, sicherzustellen, dass die Daten aus der ursprĂŒnglichen Tabelle, die in einer Transaktion geĂ€ndert wurden, auch in die neue Tabelle innerhalb derselben Transaktion gelangen.

Verzicht auf Batching

Und wir hatten wieder zwei LösungsansĂ€tze. Erster Vorschlag: Lassen Sie uns ganz auf die Batch-Verarbeitung verzichten und die Daten in einer einzigen Transaktion ĂŒbertragen. FĂŒr diese Lösung sprach ihre Einfachheit – die erforderlichen CodeĂ€nderungen sind minimal (ĂŒbrigens funktionierte in Ă€lteren Versionen pg_reorg genau so). Aber es gibt ein Problem – wir schaffen eine lange Transaktion, und das ist, wie bereits erwĂ€hnt, eine Gefahr fĂŒr das Entstehen von neuem Bloat.

Die zweite Lösung ist komplexer, aber wahrscheinlich die bessere: Erstellen Sie in der Log-Tabelle eine Spalte mit der ID der Transaktion, die die Daten in die Tabelle eingefĂŒgt hat. Dann können wir beim Kopieren der Daten diese nach diesem Attribut gruppieren und garantieren, dass zusammenhĂ€ngende Änderungen gemeinsam ĂŒbertragen werden. Der Batch wird aus mehreren Transaktionen (oder einer großen) bestehen, und seine GrĂ¶ĂŸe wird variieren, je nachdem, wie viele Daten in diesen Transaktionen geĂ€ndert wurden. Es ist wichtig zu beachten, dass, da die Daten unterschiedlicher Transaktionen in zufĂ€lliger Reihenfolge in die Log-Tabelle gelangen, es nicht mehr möglich ist, sie sequenziell zu lesen, wie es frĂŒher war. Ein seqscan bei jeder Anfrage mit Filterung nach tx_id ist zu teuer, ein Index ist notwendig, aber dieser wird die Methode aufgrund der Überheadkosten bei dessen Aktualisierung verlangsamen. Im Allgemeinen muss man wie immer etwas opfern.

Also entschieden wir uns dafĂŒr, mit der ersten Variante zu beginnen, da sie einfacher war. ZunĂ€chst musste geklĂ€rt werden, ob die lange Transaktion ein echtes Problem darstellen wĂŒrde. Da die HauptĂŒbertragung der Daten von der alten Tabelle in die neue ebenfalls in einer langen Transaktion erfolgt, verwandelte sich die Frage in „Wie sehr werden wir diese Transaktion verlĂ€ngern?“. Die Dauer der ersten Transaktion hĂ€ngt hauptsĂ€chlich von der GrĂ¶ĂŸe der Tabelle ab. Die Dauer der neuen hĂ€ngt davon ab, wie viele Änderungen sich wĂ€hrend der DatenĂŒbertragung in der Tabelle ansammeln, d.h. von der IntensitĂ€t der Last. Der Durchlauf von pg_repack fand bei minimaler Belastung des Dienstes statt, und das Volumen der Änderungen war im Vergleich zur ursprĂŒnglichen GrĂ¶ĂŸe der Tabelle bedeutend gering. Wir entschieden, dass wir die Dauer der neuen Transaktion ignorieren können (zum Vergleich: durchschnittlich sind das 1 Stunde und 2-3 Minuten).

Die Experimente waren positiv. Der Einsatz in der Produktion ebenfalls. Zur Veranschaulichung – ein Bild von der GrĂ¶ĂŸe einer der Datenbanken nach dem Durchlauf:

Postgres: Bloat, pg_repack und deferred constraints

Da dies eine Lösung war, die uns vollstĂ€ndig zufriedenstellte, haben wir es nicht versucht, eine zweite umzusetzen, ziehen jedoch in Betracht, deren Diskussion mit den Entwicklern der Erweiterung zu fĂŒhren. Unser derzeitiger Fortschritt ist leider noch nicht bereit zur Veröffentlichung, da wir das Problem lediglich mit den einzigartigen verzögerten EinschrĂ€nkungen gelöst haben, und fĂŒr einen vollstĂ€ndigen Patch ist es notwendig, auch andere Typen zu unterstĂŒtzen. Wir hoffen, dass wir dies in Zukunft erreichen können.

Vielleicht fragen Sie sich, warum wir uns ĂŒberhaupt mit der Überarbeitung von pg_repack beschĂ€ftigt haben und nicht stattdessen seine Alternativen verwendet haben? Auch wir haben einen Moment lang darĂŒber nachgedacht, aber die positiven Erfahrungen, die wir zuvor mit ihm auf Tabellen ohne verzögerte EinschrĂ€nkungen gemacht haben, motivierten uns dazu, das Problem zu verstehen und zu beheben. Außerdem erfordert die Nutzung anderer Lösungen ebenfalls Zeit fĂŒr Tests, daher haben wir beschlossen, zunĂ€chst zu versuchen, das Problem darin zu beheben, und wenn wir feststellen, dass wir dies in einem angemessenen Zeitrahmen nicht schaffen, werden wir Alternativen in Betracht ziehen.

Das DBMS Tarantool ist ein attraktives, zukunftstrÀchtiges Produkt zur Erstellung von hochbelasteten Anwendungen.

Was wir basierend auf eigener Erfahrung empfehlen können:

  1. Überwachen Sie Ihren Bloat. Anhand der Monitoring-Daten können Sie verstehen, wie gut autovacuum konfiguriert ist.
  2. Konfigurieren Sie AUTOVACUUM, um den Bloat auf einem akzeptablen Niveau zu halten.
  3. Wenn der Bloat dennoch zunimmt und Sie ihn mit den "Out-of-the-box"-Mitteln nicht bekÀmpfen können, scheuen Sie sich nicht, externe Erweiterungen zu verwenden. Das Wichtigste ist, alles gut zu testen.
  4. Scheuen Sie sich nicht, externe Lösungen nach Ihren BedĂŒrfnissen anzupassen – manchmal kann dies effektiver und sogar einfacher sein als Änderungen an Ihrem eigenen Code.

Quelle: habr.com

60GB SSD 8Gb DDR4