web-dev-qa-db-fra.com

Sorties VACUUM VERBOSE, non amovibles "les versions de ligne morte ne peuvent pas encore être supprimées"?

J'ai une DB Postgres 9.2 où une certaine table a beaucoup de lignes mortes non amovibles:

# 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)

L'aspiration normale montre également de nombreuses lignes mortes non amovibles:

# 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.
[...]

Le tableau ne contient qu'environ 300 000 lignes de données réelles, mais 2,3 millions de lignes mortes (et cela semble rendre certaines requêtes très lentes).

Selon SELECT * FROM pg_stat_activity where xact_start is not null and datname = 'mydb' order by xact_start; aucune ancienne transaction n'accède à la base de données. Les transactions les plus anciennes datent de quelques minutes et n'ont encore rien modifié sur la table.

J'ai également vérifié select * from pg_prepared_xacts (pour vérifier les transactions préparées) et select * from pg_stat_replication (pour vérifier les réplications en attente), les deux étant vides.

Il y a beaucoup d'insertions, de mises à jour et de suppressions effectuées sur cette table, donc je peux comprendre que de nombreuses lignes mortes sont en cours de création. Mais pourquoi ne sont-ils pas supprimés par la commande VACUUM?

8
oliver

Les transactions les plus anciennes datent de quelques minutes et n'ont encore rien modifié sur la table.

Ce n'est pas suffisant. Je pense que ce qui est requis pour marquer ces lignes comme mortes est que, lorsque ces transactions ont été lancées, aucune autre transaction n'avait touché ces lignes (en effectuant une mise à jour ou une suppression sur elles).

La mise à jour ou la suppression d'une ligne conservera physiquement la version précédente de la ligne et définira son champ xmax sur le TXID de la transaction en cours. Du point de vue des autres transactions, cette ancienne version de la ligne est toujours visible si elle fait partie de leur instantané. Chaque instantané a un xmin et xmax auxquels les xmin et xmax des versions de ligne peuvent être comparés. Le fait est que VACUUM doit comparer les versions des lignes avec la visibilité combinée de tous les instantanés en direct, au lieu de simplement vérifier si un changement de ligne est définitivement validé. Cette dernière est nécessaire mais pas suffisante pour recycler l'espace utilisé par l'ancienne version.

Par exemple, voici une séquence d'événements telle que VACUUM ne peut pas nettoyer les lignes mortes même si la transaction qui les a modifiées est terminée:

  • t0: La transaction longue TX1 démarre
  • t0+30mn: TX2 démarre et se met en mode REPEATABLE READ.
  • t0+35mn: TX1 se termine.
  • t0+40mn: pg_stat_activity affiche uniquement le TX2 vieux de 10 minutes
  • t0+45mn: VACUUM s'exécute mais n'éliminera pas les anciennes versions des lignes modifiées par TX1 (car TX2 pourrait en avoir besoin).
7
Daniel Vérité

J'ai pu recréer cela. Essentiellement, à l'intérieur d'une transaction,

  • Dans READ COMMITTED le niveau de transaction par défaut:
  • Dans SERIALIZABLE ou REPEATABLE READ niveaux de transaction:
    • SELECT obtient un AccessShareLock
    • VACUUM ne peut pas nettoyer les versions des lignes mortes
    • pg_stat_activity.backend_xmin IS NOT NULL pour la transaction
    • VERBOSE signale ces lignes comme "versions de ligne non amovibles" et "versions de ligne morte"

Exemples de données

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

En remarque, si vous supprimez quelque chose de bar après avoir créé la table, ces lignes deviennent removable, et sur VACUUM vous verrez.

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

Séquence de transaction

Maintenant, voici la table txn pour recréer le scénario.

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"

VACUUM ne peut pas supprimer ces versions de ligne car un SELECT * FROM bar; en dessous de REPEATABLE READ les verra toujours! Le VACUUM ci-dessus produit,

# 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.

C'est exactement ce que vous voyez.

Débogage du problème

Pour savoir quelle requête empêche VACUUM de nettoyer les lignes mortes, exécutez ceci.

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;

Cela retournera quelque chose comme ça ..

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

Solution

Revenons donc à nos TXN. Nous devons tuer/valider/restaurer txn1 et relancer VACUUM

txn1       - COMMIT;
      txn2 - VACUUM VERBOSE bar;

Et maintenant, nous voyons,

# 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.

Notes spéciales

  1. Peu importe les lignes supprimées et les lignes que vous avez sélectionnées. La sélection obtient le ACCESS SHARE verrouiller la table. Et puis, VACUUM ne peut pas supprimer les lignes mortes, elles sont donc marquées comme "non amovibles".
  2. Je pense que c'est un comportement assez mauvais pour VACUUM VERBOSE. J'aurais aimé voir ..

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

Lectures complémentaires

Merci aussi à Daniel Vérité pour m'avoir fait regarder dans le catalogue système et le comportement de VACUUM sur celui-ci.

6
Evan Carroll

J'étais confronté à ce problème même après avoir vérifié que ma base de données n'avait aucune transaction active ou verrou actif sur une certaine table "foo".

La méthode suivante a réussi à supprimer toutes ces lignes mortes non amovibles de "foo":

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

Gardez à l'esprit que si vous avez une grande table avec trop de lignes, cela peut ne pas être une solution viable, car toutes les lignes de table sont transférées vers une table temporaire, puis retransférées vers la table d'origine.