VACUUM VERBOSE-Ausgänge, nicht entfernbare "Dead Row-Versionen können noch nicht entfernt werden"?

8

Ich habe eine Postgres 9.2-Datenbank, in der eine bestimmte Tabelle viele nicht entfernbare tote Zeilen enthält:

# SELECT * FROM public.pgstattuple('mytable');
 table_len  | tuple_count | tuple_len | tuple_percent | dead_tuple_count | dead_tuple_len | dead_tuple_percent | free_space | free_percent 
------------+-------------+-----------+---------------+------------------+----------------+--------------------+------------+--------------
 2850512896 |      283439 | 100900882 |          3.54 |          2537195 |     2666909495 |              93.56 |   50480156 |         1.77
(1 row)

Normales Staubsaugen zeigt auch viele nicht entfernbare tote Reihen:

# VACUUM VERBOSE mytable;
[...]
INFO:  "mytable": found 0 removable, 2404332 nonremovable row versions in 309938 out of 316307 pages
DETAIL:  2298005 dead row versions cannot be removed yet.
There were 0 unused item pointers.
0 pages are entirely empty.
CPU 1.90s/2.05u sec elapsed 16.79 sec.
[...]

Die Tabelle enthält nur etwa 300.000 tatsächliche Datenzeilen, aber 2,3 Millionen tote Zeilen (und dies scheint bestimmte Abfragen sehr langsam zu machen).

Demnach SELECT * FROM pg_stat_activity where xact_start is not null and datname = 'mydb' order by xact_start;gibt es keine alte Transaktion, die auf die Datenbank zugreift. Die ältesten Transaktionen sind einige Minuten alt und haben noch nichts auf dem Tisch geändert.

Ich habe auch überprüft select * from pg_prepared_xacts(um nach vorbereiteten Transaktionen zu suchen) und select * from pg_stat_replication(um nach ausstehenden Replikationen zu suchen), die beide leer sind.

In dieser Tabelle werden viele Einfügungen, Aktualisierungen und Löschungen durchgeführt, sodass ich verstehen kann, dass viele tote Zeilen erstellt werden. Aber warum werden sie nicht mit dem Befehl VACUUM entfernt?

oliver
quelle
1
Ist das ein Produktionssystem? Es gibt andere Optionen für VACUUM, wie FULL, aber Warnung: Sie möchten dies wahrscheinlich tun, wenn die Datenbank ein geringes Volumen hat, da dadurch die Tabelle gesperrt wird. Verwandte lesen: wiki.postgresql.org/wiki/VACUUM_FULL (und beachten Sie die Details darüber, wann und ob), um ein VACUUM FULL zu machen und über FILLFACTOR und CLUSTER)
ypercubeᵀᴹ
Sie haben Ihre Hausaufgaben gemacht, um zu versuchen, es zu staubsaugen. und Überprüfung auf langjährige Transaktionen. 9.2 ist ein bisschen alt? Ist ein Upgrade auf den neuesten stabilen 9.6 nicht möglich?
Evan Carroll
@ EvansCarroll-Aktualisierung ist zwar möglich, aber immer noch recht schwierig. Ich möchte das nicht wirklich als Experiment durchführen, ohne dass darauf hingewiesen wird, dass 9.3+ tatsächlich Korrekturen für diese Art von Problem enthält.
Oliver
2
Nicht entfernbare tote Zeilen werden normalerweise durch lange laufende Transaktionen verursacht.
Stellen
@oliver freut sich auf meine Antwort.
Evan Carroll

Antworten:

7

Die ältesten Transaktionen sind einige Minuten alt und haben noch nichts auf dem Tisch geändert.

Das reicht nicht aus. Ich denke, was erforderlich ist, um diese Zeilen als tot zu markieren, ist, dass es beim Start dieser Transaktionen keine andere Transaktion gab, die diese Zeilen berührt hat (UPDATE oder DELETE für sie).

Durch das Aktualisieren oder Löschen einer Zeile wird die vorherige Version der Zeile physisch xmaxbeibehalten und das Feld auf die TXID der aktuellen Transaktion gesetzt. Aus Sicht anderer Transaktionen ist diese alte Version der Zeile weiterhin sichtbar, wenn sie Teil ihres Snapshots ist. Jeder Schnappschuss hat ein xminund, xmaxmit dem die xminund xmaxder Zeilenversionen verglichen werden können. Der Punkt ist, dass VACUUM Zeilenversionen mit der kombinierten Sichtbarkeit aller Live-Snapshots vergleichen muss, anstatt einfach zu überprüfen, ob eine Zeilenänderung definitiv festgeschrieben ist. Letzteres ist notwendig, aber nicht ausreichend, um den von der alten Version verwendeten Speicherplatz zu recyceln.

Hier ist beispielsweise eine Folge von Ereignissen, sodass VACUUM keine toten Zeilen bereinigen kann, obwohl die Transaktion, die sie geändert hat, abgeschlossen wurde:

  • t0: Die lang laufende Transaktion TX1 wird gestartet
  • t0+30mn: TX2 startet und versetzt sich in den REPEATABLE READ-Modus.
  • t0+35mn: TX1 wird beendet.
  • t0+40mn: pg_stat_activity zeigt nur den 10 Minuten alten TX2 an
  • t0+45mn: VACUUM wird ausgeführt, entfernt jedoch nicht die alten Versionen der von TX1 geänderten Zeilen (da TX2 sie möglicherweise benötigt).
Daniel Vérité
quelle
Unter REPEATABLE READwürde nicht sofort TX2 wenn das der Fall nach TX1 „beendet“ (Commits?) Ohne Rollback bekommen ist ERROR: could not serialize access due to concurrent updateauf der nächsten DML (wenn txn2 nicht eine Sperre auf Zeilen nach dem txn2 Snapshot geändert bekommen)?
Evan Carroll
1
@EvanCarroll: Es ist viel einfacher als das, TX2 muss nicht einmal etwas schreiben und es gibt keine Sperre oder Konflikte, es ist nur eine Frage der Sichtbarkeit. Das System kann keine Zeilen zerstören, die beim Starten von TX2 sichtbar waren, bis TX2 beendet wird. Das ist alles.
Daniel Vérité
2
@EvanCarroll: AFAIK, der eine Reihe besucht, markiert nichts. Zum einen würde die Leseleistung zerstört, wenn bei jedem gelesenen Zeilen ein Schreibvorgang ausgeführt würde.
Daniel Vérité
3
Aufschlussreich! Der Principal arbeitet sogar ohne lange laufende Transaktionen. Eine unaufhörlich hohe Last kann dasselbe erreichen.
Erwin Brandstetter
2
Dies würde also bedeuten, dass eine "Kette" überlappender Transaktionen (die jeweils Einfügungen / Aktualisierungen durchführen) das Staubsaugen aller durch eine der überlappenden Transaktionen erzeugten toten Zeilen verhindern würde? Das würde in der Tat die Probleme erklären, die ich sehe - meine Software führt genau dieses Nutzungsmuster aus. Jede meiner Transaktionen dauert weniger als eine Minute (und erzeugt möglicherweise 1.000 tote Zeilen). aber die gesamte Kette bleibt tagelang ungebrochen.
Oliver
6

Ich konnte das nachbauen. Im Wesentlichen, wenn innerhalb einer Transaktion,

  • In READ COMMITTEDder Standardtransaktionsstufe:
  • In- SERIALIZABLEoder REPEATABLE READTransaktionsebenen:
    • SELECT bekommt eine AccessShareLock
    • VACUUMVersionen für tote Zeilen können nicht bereinigt werden
    • pg_stat_activity.backend_xmin IS NOT NULL für die Transaktion
    • VERBOSEmeldet diese Zeilen als "nicht entfernbare Zeilenversionen" und "tote Zeilenversionen"

Beispieldaten

CREATE TABLE bar AS
SELECT x::int FROM generate_series(1,10) AS t(x);

Als Seite beachten, wenn Sie etwas aus löschen , barnachdem Sie die Tabelle erstellen, werden die Zeilen removable, und VACUUMSie werden sehen.

INFO:  "bar": removed # row versions in # pages

Transaktionssequenz

Hier ist die TXN-Tabelle, um das Szenario neu zu erstellen.

txn1       - BEGIN; SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
txn1       - SELECT * FROM bar;
      txn2 - DELETE FROM bar;      -- We delete after the select
      txn2 - VACUUM VERBOSE bar;   -- Can't remove the "dead row versions"

VACUUMDiese Zeilenversionen können nicht entfernt werden, da sie in einem nachfolgenden SELECT * FROM bar;Unter REPEATABLE READweiterhin angezeigt werden! Das VACUUMobige erzeugt,

# VACUUM VERBOSE bar;
INFO:  vacuuming "public.bar"
INFO:  "bar": found 0 removable, 10 nonremovable row versions in 1 out of 1 pages
DETAIL:  10 dead row versions cannot be removed yet.
There were 0 unused item pointers.
Skipped 0 pages due to buffer pins.
0 pages are entirely empty.
CPU 0.00s/0.00u sec elapsed 0.00 sec.

Welches ist genau das, was Sie sehen.

Debuggen des Problems

VACUUMFühren Sie Folgendes aus, um herauszufinden, welche Abfrage die Bereinigung der toten Zeilen verhindert.

SELECT query, state,locktype,mode
FROM pg_locks
JOIN pg_stat_activity
  USING (pid)
WHERE relation::regclass = 'bar'::regclass
  AND granted IS TRUE
  AND backend_xmin IS NOT NULL;

Dies wird so etwas zurückgeben ..

       query                state         locktype       mode       
────────────────────┼─────────────────────┼──────────┼─────────────────
 SELECT * FROM bar;  idle in transaction  relation  AccessShareLock

Lösung

Kehren wir also zu unseren TXNs zurück. Wir müssen txn1 beenden / festschreiben / zurücksetzen und erneut ausführen VACUUM

txn1       - COMMIT;
      txn2 - VACUUM VERBOSE bar;

Und jetzt sehen wir,

# VACUUM VERBOSE bar;
INFO:  vacuuming "public.bar"
INFO:  "bar": removed 10 row versions in 1 pages
INFO:  "bar": found 10 removable, 0 nonremovable row versions in 1 out of 1 pages
DETAIL:  0 dead row versions cannot be removed yet.
There were 0 unused item pointers.
Skipped 0 pages due to buffer pins.
0 pages are entirely empty.
CPU 0.00s/0.00u sec elapsed 0.00 sec.
INFO:  "bar": truncated 1 to 0 pages
DETAIL:  CPU 0.00s/0.00u sec elapsed 0.01 sec.

Besondere Hinweise

  1. Es spielt keine Rolle, welche Zeilen gelöscht wurden und für welche Zeilen Sie ausgewählt haben. Die Auswahl erhält die ACCESS SHARESperre für den Tisch. Und dann VACUUMkönnen die toten Reihen nicht entfernt werden, so dass sie als "nicht entfernbar" markiert sind.
  2. Ich denke das ist ziemlich schlechtes Benehmen für VACUUM VERBOSE. Ich hätte gerne gesehen ..

    DETAIL:  10 dead row versions cannot be removed yet
             could not aquire SHARE UPDATE EXCLUSIVE lock on %TABLE
    

Weiterführende Literatur

Vielen Dank auch an Daniel Vérité , der mich dazu gebracht hat, den Systemkatalog und das Verhalten VACUUMin diesem zu untersuchen.

Evan Carroll
quelle
1
Ausgezeichnete Post. Sieht aber so aus, als hätte Daniel es geschafft. Und dies ist die Deluxe-Version seiner Antwort mit Demo, Hintergrund, Links und weiteren Erklärungen.
Erwin Brandstetter
Erstaunliche Analyse! Ich habe am Wochenende keinen Zugriff auf die Software, werde dies aber am Montag überprüfen. Aber ich denke, das Problem sind nicht so sehr die toten Zeilen, die durch ausstehende Transaktionen erstellt wurden , sondern die toten Zeilen, die durch vergangene Transaktionen erstellt wurden, die vor langer Zeit abgeschlossen wurden.
Oliver
Ein SELECT mit einer ACCESS SHARESperre für eine Tabelle hindert VACUUM nicht daran, diese Tabelle zu SHARE UPDATE EXCLUSIVEsperren. Der Abschnitt "Was ist los?" Der Antwort hat es rückwärts, scheint mir. Auch diese andere Frage: dba.stackexchange.com/questions/21068/… ist eine gute Lektüre dafür, wie eine zu starke Verriegelung verhindern kann, dass Vakuum funktioniert, aber Vanille-Lesungen verursachen dieses Problem nicht.
Daniel Vérité
Richtig, ein Teil davon ist darauf zurückzuführen, dass dies mit dem Schließsystem nicht implementiert ist. Ich schaue mir das an und nachdem ich eine bessere Vorstellung davon habe, wie es funktioniert, werde ich es noch weiter aktualisieren. Tatsächlich ist die Sperre, die das Vakuum erfordern sollte, eine Funktion der backend_xmin. Ich werde diesen Abschnitt in einem Kommentar auf der Website ausblenden, weil ich damit einverstanden bin. Es ist eindeutig nicht ganz richtig. In der Zwischenzeit helfen Sie mir, diese Frage auf Interna zu beantworten: dba.stackexchange.com/q/161050/2639 =)
Evan Carroll
1

Ich war mit diesem Problem konfrontiert, obwohl ich überprüft hatte, dass meine Datenbank keine aktive Transaktion oder aktive Sperre für eine bestimmte "foo" -Tabelle hatte.

Mit der folgenden Methode wurden alle nicht entfernbaren toten Zeilen erfolgreich aus "foo" entfernt:

CREATE TEMP TABLE temp_foo AS SELECT * FROM "foo";
TRUNCATE TABLE "foo";
INSERT INTO "foo" SELECT * FROM temp_foo;
DROP table temp_foo;

Beachten Sie jedoch, dass eine große Tabelle mit zu vielen Zeilen möglicherweise keine praktikable Lösung darstellt, da alle Tabellenzeilen in eine temporäre Tabelle und dann zurück in die ursprüngliche Tabelle übertragen werden.

Thomas CG de Vilhena
quelle