Postgres: AufblÀhen, pg_repack und verzögerte EinschrÀnkungen



Der Effekt des AufblÀhens von Tabellen und Indizes (AufblÀhen) ist weithin bekannt und tritt nicht nur bei Postgres auf. Es gibt Möglichkeiten, wie "VACUUM FULL" oder "CLUSTER" "out of the box" damit umzugehen, aber sie sperren Tabellen wÀhrend des Betriebs und können daher nicht immer verwendet werden.

Der Artikel wird ein wenig Theorie darĂŒber enthalten, wie AufblĂ€hen auftritt, wie damit umgegangen wird, ĂŒber verzögerte EinschrĂ€nkungen und die Probleme, die sie bei der Verwendung der Erweiterung pg_repack mit sich bringen.

Dieser Artikel basiert auf meiner PrÀsentation auf der PgConf.Russia 2020.


Warum BlÀhungen auftreten


Postgres basiert auf dem Multi-Version-Modell ( MVCC ). Das Wesentliche ist, dass jede Zeile in der Tabelle mehrere Versionen haben kann, wĂ€hrend Transaktionen nicht mehr als eine dieser Versionen sehen, aber nicht unbedingt dieselbe. Dadurch können mehrere Transaktionen gleichzeitig ausgefĂŒhrt werden und haben praktisch keine Auswirkungen aufeinander.

NatĂŒrlich mĂŒssen alle diese Versionen gespeichert werden. Postgres arbeitet Seite fĂŒr Seite mit dem Speicher und die Seite ist die minimale Datenmenge, die von der Festplatte gelesen oder geschrieben werden kann. Schauen wir uns ein kleines Beispiel an, um zu verstehen, wie dies geschieht.

Angenommen, wir haben eine Tabelle, in der wir mehrere DatensĂ€tze hinzugefĂŒgt haben. Auf der ersten Seite der Datei, auf der die Tabelle gespeichert ist, wurden neue Daten angezeigt. Hierbei handelt es sich um Live-Versionen von Zeichenfolgen, die nach einem Commit fĂŒr andere Transaktionen verfĂŒgbar sind (der Einfachheit halber wird davon ausgegangen, dass die Isolationsstufe Read Committed verwendet wird).



Dann haben wir einen der EintrÀge aktualisiert und damit die alte Version als irrelevant markiert.



Schritt fĂŒr Schritt, indem wir die Version der Zeilen aktualisieren und löschen, erhalten wir eine Seite, auf der etwa die HĂ€lfte der Daten "MĂŒll" ist. Diese Daten sind fĂŒr keine Transaktion sichtbar.



Postgres hat einen VACUUM- Mechanismus, wodurch irrelevante Versionen bereinigt und Speicherplatz fĂŒr neue Daten frei wird. Wenn es jedoch nicht aggressiv genug konfiguriert ist oder in anderen Tabellen arbeitet, bleiben die „Junk-Daten“ erhalten, und wir mĂŒssen zusĂ€tzliche Seiten fĂŒr neue Daten verwenden.

In unserem Beispiel besteht die Tabelle zu einem bestimmten Zeitpunkt aus vier Seiten, enthÀlt jedoch nur die HÀlfte der Live-Daten. Infolgedessen lesen wir beim Zugriff auf die Tabelle viel mehr Daten als erforderlich.



Selbst wenn VACUUM jetzt alle irrelevanten Versionen von Zeichenfolgen löscht, wird sich die Situation nicht dramatisch verbessern. Wir haben freien Speicherplatz auf den Seiten oder sogar ganze Seiten fĂŒr neue Zeilen, aber wir werden weiterhin mehr Daten als nötig lesen.
Wenn sich ĂŒbrigens eine vollstĂ€ndig leere Seite (die zweite in unserem Beispiel) am Ende der Datei befindet, kann VACUUM sie zuschneiden. Aber jetzt ist sie in der Mitte, also kann nichts mit ihr gemacht werden.



Wenn die Anzahl solcher leeren oder sehr flachen Seiten groß wird, was als AufblĂ€hen bezeichnet wird, beginnt dies die Leistung zu beeintrĂ€chtigen.

Alles, was oben beschrieben wurde, ist die Mechanik des Auftretens von AufblÀhungen in Tabellen. In Indizes geschieht dies Àhnlich.

Habe ich ein AufblÀhen?


Es gibt verschiedene Möglichkeiten, um festzustellen, ob Sie eine AufblĂ€hung haben. Die Idee der ersten besteht darin, interne Postgres-Statistiken zu verwenden, die ungefĂ€hre Informationen ĂŒber die Anzahl der Zeilen in Tabellen, die Anzahl der "aktiven" Zeilen usw. enthalten. Im Internet finden Sie viele Variationen vorgefertigter Skripte. Wir haben ein Skript von PostgreSQL Experts zugrunde gelegt, das Bloat-Tabellen zusammen mit Toast- und Bloat-Btree-Indizes auswerten kann. Nach unserer Erfahrung betrĂ€gt der Fehler 10-20%.

Eine andere Möglichkeit ist die Verwendung der pgstattuple- Erweiterung , mit der Sie in die Seiten schauen und sowohl geschĂ€tzte als auch genaue AufblĂ€hungswerte erhalten können. Im zweiten Fall mĂŒssen Sie jedoch die gesamte Tabelle scannen.

Einen kleinen AufblĂ€hungswert von bis zu 20% halten wir fĂŒr akzeptabel. Es kann als Analogon des FĂŒllfaktors fĂŒr Tabellen und Indizes betrachtet werden . Bei 50% und mehr können Leistungsprobleme auftreten.

Möglichkeiten, mit AufblÀhen umzugehen


Es gibt verschiedene Möglichkeiten, mit dem AufblĂ€hen in Postgres umzugehen, aber sie sind bei weitem nicht immer fĂŒr jeden geeignet.

Stellen Sie AUTOVACUUM so ein, dass kein AufblĂ€hen auftritt . Und genauer gesagt, um es fĂŒr Sie auf einem akzeptablen Niveau zu halten. Dies scheint der Rat eines KapitĂ€ns zu sein, aber in Wirklichkeit ist dies nicht immer leicht zu erreichen. Beispielsweise entwickeln Sie sich aktiv mit regelmĂ€ĂŸigen Änderungen am Datenschema oder es findet eine Art Datenmigration statt. Infolgedessen kann sich Ihr Lastprofil hĂ€ufig Ă€ndern und in der Regel fĂŒr verschiedene Tabellen unterschiedlich sein. Dies bedeutet, dass Sie stĂ€ndig ein StĂŒck vor der Kurve arbeiten und AUTOVACUUM an das sich Ă€ndernde Profil jeder Tabelle anpassen mĂŒssen. Es ist jedoch offensichtlich, dass dies nicht einfach ist.

Ein weiterer hĂ€ufiger Grund dafĂŒr, dass AUTOVACUUM keine Zeit zum Verarbeiten von Tabellen hat, ist das Vorhandensein langwieriger Transaktionen, die verhindern, dass Daten gelöscht werden, da sie fĂŒr diese Transaktionen verfĂŒgbar sind. Die Empfehlung hier ist auch offensichtlich - beseitigen Sie hĂ€ngende Transaktionen und minimieren Sie die Zeit aktiver Transaktionen. Wenn die Last Ihrer Anwendung jedoch eine Mischung aus OLAP und OLTP ist, können Sie gleichzeitig viele hĂ€ufige Aktualisierungen und kurze Anforderungen sowie langwierige VorgĂ€nge ausfĂŒhren, z. B. das Erstellen eines Berichts. In einer solchen Situation lohnt es sich, ĂŒber eine Verteilung der Last auf verschiedene Basen nachzudenken, um eine feinere Abstimmung der einzelnen Basen zu ermöglichen.

Ein weiteres Beispiel: Selbst wenn das Profil einheitlich ist, die Datenbank jedoch sehr stark ausgelastet ist, kann es sein, dass selbst das aggressivste AUTOVACUUM nicht zurechtkommt und ein AufblÀhen auftritt. Die Skalierung (vertikal oder horizontal) ist die einzige Lösung.

Aber was ist mit der Situation, als Sie AUTOVACUUM konfiguriert haben, aber das AufblÀhen wÀchst weiter? VACUUM FULL

- Befehlerstellt den Inhalt von Tabellen und Indizes neu und belĂ€sst nur relevante Daten darin. Um das AufblĂ€hen zu vermeiden, funktioniert es einwandfrei. WĂ€hrend der AusfĂŒhrung wird jedoch eine exklusive Sperre fĂŒr die Tabelle (AccessExclusiveLock) erfasst, die keine Abfragen fĂŒr diese Tabelle zulĂ€sst, selbst wenn diese ausgewĂ€hlt werden. Wenn Sie es sich leisten können, Ihren Dienst oder einen Teil davon fĂŒr eine Weile einzustellen (von zehn Minuten bis zu mehreren Stunden, abhĂ€ngig von der GrĂ¶ĂŸe der Datenbank und Ihrer Hardware), ist diese Option die beste. Leider haben wir wĂ€hrend der geplanten Wartung keine Zeit, VACUUM FULL auszufĂŒhren, sodass diese Methode nicht zu uns passt. CLUSTER-

BefehlAußerdem wird der Inhalt von Tabellen neu erstellt, ebenso wie VACUUM FULL. Gleichzeitig können Sie den Index angeben, nach dem die Daten physisch auf der Festplatte sortiert werden (in Zukunft ist die Reihenfolge jedoch nicht garantiert). In bestimmten Situationen ist dies eine gute Optimierung fĂŒr eine Reihe von Abfragen - mit dem Lesen mehrerer DatensĂ€tze nach Index. Der Nachteil des Befehls ist der gleiche wie der von VACUUM FULL - er sperrt die Tabelle wĂ€hrend des Betriebs.

Der Befehl REINDEX Ă€hnelt den beiden vorherigen, erstellt jedoch einen bestimmten Index oder alle Indizes in der Tabelle neu. Sperren sind etwas schwĂ€cher: ShareLock fĂŒr die Tabelle (verhindert Änderungen, aber Sie können auswĂ€hlen) und AccessExclusiveLock fĂŒr den wiederherstellbaren Index (blockiert Anforderungen, die diesen Index verwenden). In Version 12 von Postgres jedoch der Parameter CONCURRENTLYHiermit können Sie den Index neu erstellen, ohne das parallele HinzufĂŒgen, Ändern oder Löschen von DatensĂ€tzen zu blockieren.

In frĂŒheren Versionen von Postgres können Sie mit CREATE INDEX CONCURRENTLY ein Ă€hnliches Ergebnis wie REINDEX CONCURRENTLY erzielen . Sie können einen Index ohne strikte Blockierung erstellen (ShareUpdateExclusiveLock, der parallele Abfragen nicht beeintrĂ€chtigt), dann den alten Index durch einen neuen ersetzen und den alten Index löschen. Dadurch werden aufgeblĂ€hte Indizes eliminiert, ohne Ihre Anwendung zu beeintrĂ€chtigen. Es ist wichtig zu berĂŒcksichtigen, dass beim Neuerstellen von Indizes das Festplattensubsystem zusĂ€tzlich belastet wird.

Wenn es also Möglichkeiten fĂŒr Indizes gibt, das AufblĂ€hen „heiß“ zu beseitigen, gibt es fĂŒr Tabellen keine. Hier kommen verschiedene externe Erweiterungen ins Spiel : pg_repack(frĂŒher pg_reorg), pgcompact , pgcompacttable und andere. Im Rahmen dieses Artikels werde ich sie nicht vergleichen und nur ĂŒber pg_repack sprechen, das wir nach einiger Verfeinerung zu Hause verwenden.

Wie pg_repack funktioniert



Angenommen, wir haben eine ganz normale Tabelle fĂŒr uns - mit Indizes, EinschrĂ€nkungen und leider mit AufblĂ€hen. Der erste Schritt ist, dass pg_repack eine Protokolltabelle erstellt, in der Daten zu allen Änderungen wĂ€hrend des Betriebs gespeichert werden. Der Trigger repliziert diese Änderungen bei jedem EinfĂŒgen, Aktualisieren und Löschen. Anschließend wird eine Tabelle erstellt, die in ihrer Struktur dem Original Ă€hnelt, jedoch keine Indizes und EinschrĂ€nkungen aufweist, um das EinfĂŒgen von Daten nicht zu verlangsamen.

Als NĂ€chstes ĂŒbertrĂ€gt pg_repack Daten von der alten in die neue Tabelle, filtert automatisch alle irrelevanten Zeilen und erstellt dann Indizes fĂŒr die neue Tabelle. WĂ€hrend der AusfĂŒhrung all dieser VorgĂ€nge werden Änderungen in der Protokolltabelle gesammelt.

Der nĂ€chste Schritt besteht darin, die Änderungen in die neue Tabelle zu ĂŒbertragen. Die Migration wird in mehreren Iterationen durchgefĂŒhrt. Wenn weniger als 20 EintrĂ€ge in der Protokolltabelle verbleiben, erfasst pg_repack eine strikte Sperre, ĂŒbertrĂ€gt die neuesten Daten und ersetzt die alte Tabelle durch die neue in den Postgres-Systemtabellen. Dies ist der einzige und sehr kurze Zeitpunkt, an dem Sie nicht mit der Tabelle arbeiten können. Danach werden die alte Tabelle und die Tabelle mit den Protokollen gelöscht und Speicherplatz im Dateisystem freigegeben. Der Vorgang ist abgeschlossen.

Theoretisch sieht alles gut aus, was in der Praxis? Wir haben pg_repack ohne Last getestet und unter Last die Funktion im Falle eines vorzeitigen Stopps ĂŒberprĂŒft (mit anderen Worten, Strg + C). Alle Tests waren positiv.

Wir gingen zum Produkt - und dann lief alles schief, wie wir es erwartet hatten.

Der erste Pfannkuchen auf Prod


Beim ersten Cluster wurde ein Fehler bezĂŒglich der Verletzung einer eindeutigen EinschrĂ€nkung angezeigt:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Diese EinschrĂ€nkung hatte den automatisch generierten Namen index_16508 - er wurde von pg_repack erstellt. Durch die in seiner Zusammensetzung enthaltenen Attribute haben wir „unsere“ EinschrĂ€nkung bestimmt, die dieser entspricht. Es stellte sich heraus, dass dies keine gewöhnliche EinschrĂ€nkung ist, sondern eine verzögerte EinschrĂ€nkung , d.h. Die ÜberprĂŒfung erfolgt spĂ€ter als der Befehl sql, was zu unerwarteten Konsequenzen fĂŒhrt.

Aufgeschobene EinschrÀnkungen: Warum werden sie benötigt und wie funktionieren sie?


Ein bisschen Theorie ĂŒber verzögerte EinschrĂ€nkungen.
Stellen Sie sich ein einfaches Beispiel vor: Wir haben eine Fahrzeugreferenztabelle mit zwei Attributen - dem Namen und der Reihenfolge des Fahrzeugs im Verzeichnis.

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


Nehmen wir an, wir mussten das erste und das zweite Auto tauschen. Die Lösung "in der Stirn" besteht darin, 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;

Bei der AusfĂŒhrung dieses Codes wird jedoch eine Verletzung der EinschrĂ€nkung erwartet, da die Reihenfolge der Werte in der Tabelle eindeutig ist:

[23305] ERROR: duplicate key value violates unique constraint “uk_cars”
Detail: Key (ord)=(2) already exists.

Wie mache ich das anders? Option 1: FĂŒgen Sie einen zusĂ€tzlichen Ersatz des Werts durch eine Bestellung hinzu, die garantiert nicht in der Tabelle vorhanden ist, z. B. "-1". In der Programmierung wird dies als "Austausch der Werte zweier Variablen durch die dritte" bezeichnet. Der einzige Nachteil dieser Methode ist das zusĂ€tzliche Update.

Option 2: Entwerfen Sie die Tabelle neu, um anstelle von Ganzzahlen einen Gleitkomma-Datentyp fĂŒr den Auftragswert zu verwenden. Wenn Sie dann den Wert von beispielsweise 1 auf 2,5 aktualisieren, wird der erste Datensatz automatisch zwischen dem zweiten und dem dritten Datensatz "aufstehen". Diese Lösung funktioniert, es gibt jedoch zwei EinschrĂ€nkungen. Erstens funktioniert es fĂŒr Sie nicht, wenn der Wert irgendwo in der Schnittstelle verwendet wird. Zweitens haben Sie abhĂ€ngig von der Genauigkeit des Datentyps eine begrenzte Anzahl möglicher EinfĂŒgungen, bevor Sie die Werte aller DatensĂ€tze neu berechnen.

Option 3: Verschieben Sie die EinschrĂ€nkung so, dass sie nur zum Zeitpunkt des Festschreibens ĂŒ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 ersten Anforderung sicherstellt, dass alle Werte zum Zeitpunkt des Festschreibens eindeutig sind, ist dies erfolgreich.

Das obige Beispiel ist natĂŒrlich sehr synthetisch, aber es zeigt die Idee. In unserer Anwendung verwenden wir verzögerte EinschrĂ€nkungen, um Logik zu implementieren, die fĂŒr die Lösung von Konflikten verantwortlich ist, wĂ€hrend gleichzeitig mit allgemeinen Widget-Objekten auf der Karte gearbeitet wird. Durch die Verwendung solcher EinschrĂ€nkungen können wir den Anwendungscode etwas vereinfachen.

AbhĂ€ngig von der Art der EinschrĂ€nkung in Postgres gibt es im Allgemeinen drei GranularitĂ€tsebenen, um sie zu ĂŒberprĂŒfen: Zeilenebene, Transaktion und Ausdruck.

Quelle: begriffs

CHECK und NOT NULL werden immer auf Zeilenebene ĂŒberprĂŒft. FĂŒr andere EinschrĂ€nkungen gibt es, wie aus der Tabelle hervorgeht, verschiedene Optionen. Lesen Sie hier mehr .

Kurz zusammengefasst ergeben ausstehende EinschrĂ€nkungen in einigen Situationen mehr lesbaren Code und weniger Befehle. Sie mĂŒssen dies jedoch bezahlen, indem Sie den Debug-Prozess komplizieren, da der Moment, in dem der Fehler aufgetreten ist und der Moment, in dem Sie davon erfahren, zeitlich getrennt sind. Ein weiteres mögliches Problem besteht darin, dass der Scheduler nicht immer den optimalen Plan erstellen kann, wenn die Anforderung eine verzögerte EinschrĂ€nkung enthĂ€lt.

Verfeinerung pg_repack


Wir haben herausgefunden, welche EinschrÀnkungen noch ausstehen, aber wie hÀngen sie mit unserem Problem zusammen? Erinnern Sie sich an den Fehler, den wir zuvor erhalten haben:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Es tritt zum Zeitpunkt des Kopierens von Daten aus der Protokolltabelle in die neue Tabelle auf. Es sieht komisch aus, weil Die Daten in der Protokolltabelle werden zusammen mit den Daten in der Originaltabelle festgeschrieben. 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 von pg_repack, fĂŒr den nur Indizes erstellt werden, jedoch keine EinschrĂ€nkungen: Die alte Tabelle hatte eine eindeutige EinschrĂ€nkung, und die neue Tabelle erstellte stattdessen einen eindeutigen Index.



Hierbei ist zu beachten, dass, wenn die EinschrĂ€nkung normal und nicht zurĂŒckgestellt ist, der stattdessen erstellte eindeutige Index dieser EinschrĂ€nkung entspricht, da Postgres-eindeutige EinschrĂ€nkungen werden durch Erstellen eines eindeutigen Index implementiert. Im Fall einer verzögerten EinschrĂ€nkung ist das Verhalten jedoch nicht dasselbe, da der Index nicht verzögert werden kann und immer zum Zeitpunkt der AusfĂŒhrung des Befehls sql ĂŒberprĂŒft wird.

Das Wesentliche des Problems liegt also in der „Verschiebung“ der PrĂŒfung: In der ursprĂŒnglichen Tabelle tritt sie zum Zeitpunkt des Festschreibens und in der neuen Tabelle zum Zeitpunkt der AusfĂŒhrung des Befehls sql auf. Wir mĂŒssen also sicherstellen, dass die ÜberprĂŒfungen in beiden FĂ€llen auf die gleiche Weise durchgefĂŒhrt werden: entweder immer zurĂŒckgestellt oder immer sofort.

Welche Ideen hatten wir?

Erstellen Sie einen Index Àhnlich dem verzögerten


Die erste Idee besteht darin, beide ÜberprĂŒfungen im Sofortmodus durchzufĂŒhren. Dies kann zu mehreren falsch positiven Auslösern der EinschrĂ€nkung fĂŒhren. Wenn jedoch nur wenige davon vorhanden sind, sollte dies die Arbeit der Benutzer nicht beeintrĂ€chtigen, da solche Konflikte fĂŒr sie eine normale Situation sind. Sie treten beispielsweise auf, wenn zwei Benutzer gleichzeitig dasselbe Widget bearbeiten und der Client des zweiten Benutzers keine Zeit hat, Informationen abzurufen, dass das Widget bereits fĂŒr die Bearbeitung durch den ersten Benutzer gesperrt ist. In dieser Situation lehnt der Server den zweiten Benutzer ab, und sein Client setzt die Änderungen zurĂŒck und blockiert das Widget. Wenig spĂ€ter, wenn der erste Benutzer die Bearbeitung beendet hat, erhĂ€lt der zweite Benutzer die Information, dass das Widget nicht mehr gesperrt ist, und kann seine Aktion wiederholen.



Um sicherzustellen, dass sich die ÜberprĂŒfungen immer im Notfallmodus befinden, haben wir einen neuen Index erstellt, der der ursprĂŒnglichen verzögerten EinschrĂ€nkung Ă€hnelt:

CREATE UNIQUE INDEX CONCURRENTLY uk_tablename__immediate ON tablename (id, index);
-- run pg_repack
DROP INDEX CONCURRENTLY uk_tablename__immediate;

In der Testumgebung haben wir nur wenige erwartete Fehler erhalten. Erfolg! Wir haben pg_repack erneut auf dem Produkt gestartet und 5 Fehler im ersten Cluster innerhalb einer Arbeitsstunde erhalten. Dies ist ein akzeptables Ergebnis. Bereits im zweiten Cluster hat sich die Anzahl der Fehler jedoch um ein Vielfaches erhöht, und wir mussten pg_repack stoppen.

Warum ist das geschehen? Die Wahrscheinlichkeit eines Fehlers hĂ€ngt davon ab, wie viele Benutzer gleichzeitig mit denselben Widgets arbeiten. Anscheinend gab es zu diesem Zeitpunkt mit den im ersten Cluster gespeicherten Daten viel weniger WettbewerbsĂ€nderungen als im Rest, d. H. Wir hatten einfach "GlĂŒck".

Die Idee hat nicht funktioniert. In diesem Moment sahen wir zwei weitere Lösungsoptionen: Schreiben Sie unseren Anwendungscode neu, um ausstehende EinschrĂ€nkungen aufzuheben, oder „lehren“ Sie pg_repack, mit ihnen zu arbeiten. Wir haben die zweite gewĂ€hlt.

Ersetzen Sie Indizes in einer neuen Tabelle durch verzögerte EinschrÀnkungen aus der Quelltabelle


Der Zweck der Überarbeitung lag auf der Hand: Wenn die ursprĂŒngliche Tabelle eine verzögerte EinschrĂ€nkung aufweist, mĂŒssen Sie fĂŒr die neue Tabelle eine solche EinschrĂ€nkung erstellen, keinen Index.

Um unsere Änderungen zu testen, haben wir einen einfachen Test geschrieben:

  • Tabelle mit aufgeschobener EinschrĂ€nkung und einem Datensatz;
  • FĂŒgen Sie Daten in die Schleife ein, die mit dem vorhandenen Datensatz in Konflikt stehen.
  • Update durchfĂŒhren - die Daten stehen nicht mehr in Konflikt;
  • VerĂ€nderung begehen.

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 Originalversion von pg_repack stĂŒrzte beim ersten EinfĂŒgen immer ab, die ĂŒberarbeitete Version funktionierte fehlerfrei. Fein.

Wir gehen zum Produkt 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: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Klassische Situation: Alles funktioniert in Testumgebungen, aber nicht auf Produkten ?!

APPLY_COUNT und die Verbindung zweier Chargen


Wir begannen, den Code buchstĂ€blich Zeile fĂŒr Zeile zu analysieren und fanden einen wichtigen Punkt: Daten werden mit Stapeln von der Protokolltabelle in die neue ĂŒbertragen. Die Konstante APPLY_COUNT gab die GrĂ¶ĂŸe des Stapels an:

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

if (num > MIN_TUPLES_BEFORE_SWITCH)
     continue;  /* there might be still some tuples, repeat. */
...
}

Das Problem besteht darin, dass die Daten der ursprĂŒnglichen Transaktion, bei denen mehrere VorgĂ€nge möglicherweise gegen die BeschrĂ€nkung verstoßen können, wĂ€hrend der Übertragung auf zwei Chargen ĂŒbertragen werden können - die HĂ€lfte der Teams wird im ersten Spiel und die andere HĂ€lfte im zweiten Spiel festgeschrieben. Und so viel GlĂŒck: Wenn die Teams in der ersten Partie nichts verletzen, ist alles in Ordnung, aber wenn sie verletzen, tritt ein Fehler auf.

APPLY_COUNT entspricht 1000 EintrĂ€gen, was erklĂ€rt, warum unsere Tests erfolgreich waren - sie deckten nicht den Fall der „Verbindung von Chargen“ ab. Wir haben zwei Befehle verwendet - EinfĂŒgen und Aktualisieren, sodass immer genau 500 Transaktionen von zwei Teams im Stapel platziert wurden und wir keine Probleme hatten. Nach dem HinzufĂŒgen des zweiten Updates funktionierte unsere Bearbeitung 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; -- one more update
    COMMIT;
  END;
END LOOP;

Die nÀchste Aufgabe besteht also darin, sicherzustellen, dass die Daten aus der Quelltabelle, die sich in einer Transaktion geÀndert haben, auch innerhalb derselben Transaktion in die neue Tabelle fallen.

Ablehnung des Schlachtens


Und wieder hatten wir zwei Lösungen. Erstens: Lassen Sie uns die Stapelverarbeitung vollstĂ€ndig aufgeben und die DatenĂŒbertragung in einer Transaktion durchfĂŒhren. FĂŒr diese Lösung war ihre Einfachheit von Vorteil - die erforderlichen CodeĂ€nderungen waren minimal (in Ă€lteren Versionen funktionierte pg_reorg ĂŒbrigens genau so). Aber es gibt ein Problem - wir schaffen eine lange Transaktion, und dies ist, wie bereits gesagt, eine Bedrohung fĂŒr die Entstehung eines neuen AufblĂ€hens.

Die zweite Lösung ist komplizierter, aber wahrscheinlich korrekter: Erstellen Sie eine Spalte in der Protokolltabelle mit der Kennung der Transaktion, die die Daten zur Tabelle hinzugefĂŒgt hat. Wenn wir dann Daten kopieren, können wir sie nach diesem Attribut gruppieren und sicherstellen, dass die zugehörigen Änderungen zusammen ĂŒbertragen werden. Ein Stapel wird aus mehreren Transaktionen (oder einer großen) gebildet und seine GrĂ¶ĂŸe hĂ€ngt davon ab, wie viele Daten sich in diesen Transaktionen geĂ€ndert haben. Es ist wichtig zu beachten, dass, da die Daten verschiedener Transaktionen in zufĂ€lliger Reihenfolge in die Protokolltabelle fallen, es nicht möglich ist, sie wie zuvor nacheinander zu lesen. seqscan fĂŒr jede von tx_id gefilterte Anfrage ist zu teuer. Sie benötigen einen Index, verlangsamen jedoch die Methode aufgrund des Aktualisierungsaufwands. Im Allgemeinen mĂŒssen Sie wie immer etwas opfern.

Deshalb haben wir uns entschlossen, mit der ersten Option zu beginnen, als einer einfacheren. ZunĂ€chst musste verstanden werden, ob eine lange Transaktion ein echtes Problem darstellt. Da die HauptdatenĂŒbertragung von der alten zur neuen Tabelle auch in einer langen Transaktion erfolgt, hat sich die Frage in "Wie viel werden wir diese Transaktion erhöhen?" 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 Lauf pg_repack trat wĂ€hrend der minimalen Auslastung des Dienstes auf, und der Änderungsbetrag war im Vergleich zur ursprĂŒnglichen TabellengrĂ¶ĂŸe unvergleichlich gering. Wir haben beschlossen, die Zeit der neuen Transaktion zu vernachlĂ€ssigen (zum Vergleich sind dies durchschnittlich 1 Stunde und 2-3 Minuten).

Die Experimente waren positiv. Laufen auch auf Prod. Aus GrĂŒnden der Übersichtlichkeit ein Bild mit der GrĂ¶ĂŸe einer der Basen nach dem Lauf:



Da diese Lösung vollstĂ€ndig zu uns passte, haben wir nicht versucht, die zweite zu implementieren, sondern erwĂ€gen, sie mit den Entwicklern der Erweiterung zu diskutieren. Leider ist unsere aktuelle Version noch nicht zur Veröffentlichung bereit, da wir das Problem nur mit eindeutigen ausstehenden EinschrĂ€nkungen gelöst haben und fĂŒr einen vollwertigen Patch andere Typen unterstĂŒtzt werden mĂŒssen. Wir hoffen, dies in Zukunft tun zu können.

Vielleicht haben Sie eine Frage, warum wir uns mit der Fertigstellung von pg_repack auf diese Geschichte eingelassen haben und zum Beispiel ihre Analoga nicht verwendet haben? Irgendwann haben wir auch darĂŒber nachgedacht, aber die positive Erfahrung, es frĂŒher auf Tabellen ohne ausstehende EinschrĂ€nkungen zu verwenden, hat uns motiviert, zu versuchen, das Wesentliche des Problems zu verstehen und es zu beheben. Um andere Lösungen zu verwenden, dauert es auch einige Zeit, um Tests durchzufĂŒhren. Daher haben wir beschlossen, zunĂ€chst zu versuchen, das Problem darin zu beheben. Wenn wir feststellen, dass wir dies nicht in angemessener Zeit tun können, werden wir Analoga in Betracht ziehen.

Ergebnisse


Was wir aufgrund unserer eigenen Erfahrung empfehlen können:

  1. Überwachen Sie Ihr AufblĂ€hen. Anhand der Überwachungsdaten können Sie nachvollziehen, wie gut das Autovakuum konfiguriert ist.
  2. Stellen Sie AUTOVACUUM so ein, dass das AufblĂ€hen auf einem vernĂŒnftigen Niveau bleibt.
  3. bloat “ ”, . – .
  4. – , .

All Articles