Project

General

Profile

Development #88761

Partitionner la table ResourceLog

Added by Pierre Ducroquet 7 months ago. Updated about 1 month ago.

Status:
Nouveau
Priority:
Normal
Assignee:
-
Target version:
-
Start date:
27 March 2024
Due date:
% Done:

0%

Estimated time:
Patch proposed:
No
Planning:
No

Description

Cette table contient des données historisées, et qui sont régulièrement purgées.
L'autovacuum est supposé aider ce genre de situation, mais avec le modèle en multi-tenant, il est nécessaire de ne pas compter que sur lui et de l'assister un peu (la table se retrouve avec un bloat significatif).
De plus, même avec l'autovacuum, de nombreuses entrées invalides finissent par augmenter la taille de l'index.
Je voudrais donc que l'on change la table base_resourcelog afin qu'elle soit partitionnée.
J'envisage d'avoir une partition par semaine, avec un cron côté passerelle qui crée 2 partitions par avance, et supprime les anciennes partitions quand elles sont vides.
Le partitionnement PostgreSQL est transparent pour Django, mais il existe dans django-postgres-extra tout un ensemble d'éléments pour automatiser le fonctionnement. Cf. https://django-postgres-extra.readthedocs.io/en/master/table_partitioning.html
Est-ce-que l'on peut utiliser ces outils dans passerelle, ou est-ce-que je vois pour mettre en place manuellement le partitionnement ?


Related issues

Related to Passerelle - Development #88953: Optimiser les requêtes de suppressions des ResourceLog expirésSolution proposée02 April 2024

Actions
Related to Passerelle - Development #88960: ResourceLog: utiliser un index BRIN sur timestampSolution proposée02 April 2024

Actions

History

#1

Updated by Frédéric Péters 7 months ago

Je dirais d'attendre qu'il n'y ait plus personne en buster, le paquet est disponible à partir de bookworm, https://packages.debian.org/bookworm/python3-django-postgres-extra (version 2.0.8).

#2

Updated by Benjamin Dauvergne 7 months ago

Actuellement on a que deux index sur cette table :
  • transaction_id <- uuid.uuid4() (donc random) créé à chaque instanciation d'une instance
  • appname, -timestamp (le -timestamp me semble inutile, ça triera aussi bien dans un sens ou dans l'autre)

Est-ce que si on remplaçait le uuid4() par un UUID croissant (UUIDv7 basé sur un timestamp, https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html, en python1) et qu'on se basait seulement sur un index BRIN pour les deux et du scan séquentiel pour filtrer appname on améliorerait pas le comportement des index vis à vis d'autovacuum sans y perdre grand chose ?

Parce qu'une table de log donc append only ça me semble le cas idéal pour un ramasse miette, ça ne devrait pas poser de problème.

1 https://github.com/oittaa/uuid6-python/blob/main/src/uuid6/__init__.py


Autre chose, le clean_logs est implémenté ainsi :

    def clean_logs(self):
        # clean logs
        timestamp = timezone.now() - datetime.timedelta(
            days=self.logging_parameters.log_retention_days or settings.LOG_RETENTION_DAYS
        )
        ResourceLog.objects.filter(
            appname=self.get_connector_slug(), slug=self.slug, timestamp__lt=timestamp
        ).delete()

On passe sur chaque instance de modèle pour supprimer ses logs spécifiques. J'ai regardé en prod on a cette répartition des délais sur les instances de connecteurs :

jours nombre de modèles
7 1277
10 1
15 1
30 1
60 1
93 1
300 1

Pour les valeurs qui sortent de la valeur par défaut les connecteurs concernés sont :

10 {'ToulouseMaelis'}
15 {'SolisAfiMss'}
30 {'CaluireAxel'}
60 {'ToulouseAxel'}
93 {'OVHSMSGateway'}
300 {'Greco'}

Je me dis qu'on pourrait déjà optimiser ce code de suppression pour ne pas supprimer par connecteur mais tout supprimer d'un coup quand le délai est commun; et peut-être ne plus permettre de paramétrer cette durée par connecteur et ainsi simplifier définitivement.

#3

Updated by Pierre Ducroquet 6 months ago

Benjamin Dauvergne a écrit :

Actuellement on a que deux index sur cette table :
  • transaction_id <- uuid.uuid4() (donc random) créé à chaque instanciation d'une instance
  • appname, -timestamp (le -timestamp me semble inutile, ça triera aussi bien dans un sens ou dans l'autre)

Est-ce que si on remplaçait le uuid4() par un UUID croissant (UUIDv7 basé sur un timestamp, https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html, en python1) et qu'on se basait seulement sur un index BRIN pour les deux et du scan séquentiel pour filtrer appname on améliorerait pas le comportement des index vis à vis d'autovacuum sans y perdre grand chose ?

Parce qu'une table de log donc append only ça me semble le cas idéal pour un ramasse miette, ça ne devrait pas poser de problème.

Sauf que ce n'est pas comme ça que fonctionne l'autovacuum. Le comparer à un ramasse-miettes perd la "subtilité" du stockage par fichiers comparé au stockage en RAM. Là où un ramasse-miettes à la JVM peut se permettre de bouger des objets après coup pour éviter la fragmentation, le vacuum de PG ne peut que marquer de l'espace comme réutilisable ultérieurement. De ce fait, avec la méthode de suppression actuelle, le BRIN va en plus perdre en efficacité (tout en restant plus léger que le BTREE, juste au prix d'un filtrage plus long post-scan, c'est pas la fin du monde normalement mais ça se mesure, je ferai l'expérience sur un tenant). Changer la génération des uuid n'aidera pas grand chose à la situation, ça rendrait juste le brin utilisable sur le transaction_id, pas un changement radical.

Un second problème, plus fondamental et sur lequel je travaille, c'est qu'on approche des limites de l'autovacuum avec notre nombre de tables. Je cherche encore quels indicateurs utiliser pour vérifier ça, mais quand je montre le nombre de tables à n'importe quel développeur PG, la première réaction est que l'autovacuum ne peut pas tenir la charge. Donc je préfère préparer le terrain sur ce qui peut lui simplifier la tâche, voire le rendre inutile. À court terme, à défaut de partitionnement, un vacuum analyze post-purge de la table ne serait pas du luxe (et est d'ailleurs recommandé même quand l'autovacuum n'est pas sous l'eau).

1 https://github.com/oittaa/uuid6-python/blob/main/src/uuid6/__init__.py


Autre chose, le clean_logs est implémenté ainsi :
[...]
On passe sur chaque instance de modèle pour supprimer ses logs spécifiques. J'ai regardé en prod on a cette répartition des délais sur les instances de connecteurs :

jours nombre de modèles
7 1277
10 1
15 1
30 1
60 1
93 1
300 1

Pour les valeurs qui sortent de la valeur par défaut les connecteurs concernés sont :
[...]

Je me dis qu'on pourrait déjà optimiser ce code de suppression pour ne pas supprimer par connecteur mais tout supprimer d'un coup quand le délai est commun; et peut-être ne plus permettre de paramétrer cette durée par connecteur et ainsi simplifier définitivement.

Si on pouvait simplifier, ça ferait du bien dans tous les cas, vu qu'on augmenterait le nombre de pages libres et donc on réduirait la fragmentation du log

#4

Updated by Benjamin Dauvergne 6 months ago

Pierre Ducroquet a écrit :

Benjamin Dauvergne a écrit :

Actuellement on a que deux index sur cette table :
  • transaction_id <- uuid.uuid4() (donc random) créé à chaque instanciation d'une instance
  • appname, -timestamp (le -timestamp me semble inutile, ça triera aussi bien dans un sens ou dans l'autre)

Est-ce que si on remplaçait le uuid4() par un UUID croissant (UUIDv7 basé sur un timestamp, https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html, en python1) et qu'on se basait seulement sur un index BRIN pour les deux et du scan séquentiel pour filtrer appname on améliorerait pas le comportement des index vis à vis d'autovacuum sans y perdre grand chose ?

Parce qu'une table de log donc append only ça me semble le cas idéal pour un ramasse miette, ça ne devrait pas poser de problème.

Sauf que ce n'est pas comme ça que fonctionne l'autovacuum. Le comparer à un ramasse-miettes perd la "subtilité" du stockage par fichiers comparé au stockage en RAM. Là où un ramasse-miettes à la JVM peut se permettre de bouger des objets après coup pour éviter la fragmentation, le vacuum de PG ne peut que marquer de l'espace comme réutilisable ultérieurement.

Je sais ça, ramasse miette c'est un terme générique pour moi je ne pensais pas au ramasse miette de la JVM en particulier. Je sais juste que quand les allocations et désallocation sont faite en masse de manière contiguë ça marche mieux, en général, quelque soit l'algo de gestion mémoire.

De ce fait, avec la méthode de suppression actuelle, le BRIN va en plus perdre en efficacité (tout en restant plus léger que le BTREE, juste au prix d'un filtrage plus long post-scan, c'est pas la fin du monde normalement mais ça se mesure, je ferai l'expérience sur un tenant). Changer la génération des uuid n'aidera pas grand chose à la situation, ça rendrait juste le brin utilisable sur le transaction_id, pas un changement radical.

J'aurai du expliciter ma compréhension du problème. Pour moi il y a deux aspects les suppressions dans la table et dans les index.

Coté table le fait de supprimer les lignes de chaque connecteur séparément ça crée un gruyère progressif, peut-être qu'à la fin on a des pages complètement libre mais je me dis que libérer les pages d'un coup via un DELETE ... WHERE timestamp < ... est peut-être plus efficace, ça reste une intuition.

Coté index le transaction_id actuel produit du gruyère quand on les supprime, et du gruyère persistant, i.e. des pages pas complètement inutilisés, que seul un appel à VACUUM FULL pourra beaucoup améliorer (je suppose que postgres réutilise l'espace vide dans les pages aussi, m'enfin ça reste perfectible) il m'aurait semblé que l'utilisation d'un identifiant séquentiel associé à un index BRIN, dont il me semble l'avantage est quand la progression de la clé est le même que celle des insertions en table, cas typique d'un timestamp.

Idem pour l'index appname,-timestamp le fait de mettre appname en premier au lieu de l'élément qui suit les insertions ça rend le clustering de l'index moins bon je pense, pour un gain en index scan qui doit pas être énorme (c'est pas comme si on pensait notre temps à consulter les logs, 99,9999999% des logs ne sont jamais consultés).

Autre possibilité avec un uuid intégrant un timestamp comme préfixe, on pourrait complètement supprimer l'index sur transaction_id et se servir de l'index sur le timestamp comme hint de recherche, (SELECT ... WHERE transaction_id = ? AND timestamp BETWEN transaction_id.tstamp - 1 minute AND transaction_id.stamp + 1 minute), on gagne un index.

Un second problème, plus fondamental et sur lequel je travaille, c'est qu'on approche des limites de l'autovacuum avec notre nombre de tables. Je cherche encore quels indicateurs utiliser pour vérifier ça, mais quand je montre le nombre de tables à n'importe quel développeur PG, la première réaction est que l'autovacuum ne peut pas tenir la charge. Donc je préfère préparer le terrain sur ce qui peut lui simplifier la tâche, voire le rendre inutile. À court terme, à défaut de partitionnement, un vacuum analyze post-purge de la table ne serait pas du luxe (et est d'ailleurs recommandé même quand l'autovacuum n'est pas sous l'eau).

Je rajoute l'aspect VACUUM ANALYZE au point suivant.

Si on pouvait simplifier, ça ferait du bien dans tous les cas, vu qu'on augmenterait le nombre de pages libres et donc on réduirait la fragmentation du log

Ok sur ça on est bien d'accord, je vais ouvrir un ticket dans ce sens.

#5

Updated by Benjamin Dauvergne 6 months ago

  • Related to Development #88953: Optimiser les requêtes de suppressions des ResourceLog expirés added
#6

Updated by Benjamin Dauvergne 6 months ago

#7

Updated by Robot Gitea about 2 months ago

  • Status changed from Nouveau to Solution proposée
  • Assignee set to Gael Pasgrimaud

Gael Pasgrimaud (gpasgrimaud) a ouvert une pull request sur Gitea concernant cette demande :

#8

Updated by Gael Pasgrimaud about 2 months ago

  • Status changed from Solution proposée to Nouveau
  • Assignee deleted (Gael Pasgrimaud)

J'ai testé d'utiliser django-postgres-extra. TLDR, je suis pas convaincu

On est obligé d'avoir son backend en connecteur de database. C'est hardcodé là https://github.com/SectorLabs/django-postgres-extra/blob/master/psqlextra/manager/manager.py#L33
Ce qui veut dire que pour l'utiliser il faut un patch car on veut conserver notre backend multi tenants.

J'ai viré ce raise en local pour pouvoir continuer.

Le partitioning d'une table existante n'a pas l'air gérée. Quand on lance pgmakemigration sur passerelle.base ça ne fait rien d'utile (à part un migrations.AlterModelManagers pour dire qu'on utilise maintenant psqlextra.manager.manager.PostgresManager)

Ca patch à peut prèt tout. Model, Manager, migrations, introspection, etc. Et rien n'est prévu pour le multi tenants.

Quand on tente de générer une partition, l'introspection n'est donc plus celle du backend pgextra mais celle du multi tenant. Et, forcément, ça marche moins bien

Si on voulait persister, il faudrait patcher https://github.com/SectorLabs/django-postgres-extra/blob/master/psqlextra/backend/introspection.py Et potentiellement plein d'autre bouts.

J'ai tout de même poussé ce que j'avais fait dans une branche. Mais à mon avis c'est une piste à abandonner pour ne gérer que des partitions

J'ajoute aussi une tentative de patch dans django-tenant-schemas pour essayer d'aller plus loin. Mais ça ne veut pas. Il ne trouve pas la table à partitionner non plus:

diff --git a/tenant_schemas/postgresql_backend/base.py b/tenant_schemas/postgresql_backend/base.py
index f927d9f..4e4715f 100644
--- a/tenant_schemas/postgresql_backend/base.py
+++ b/tenant_schemas/postgresql_backend/base.py
@@ -58,7 +58,8 @@ class DatabaseWrapper(original_backend.DatabaseWrapper):

         # Use a patched version of the DatabaseIntrospection that only returns the table list for the
         # currently selected schema.
-        self.introspection = DatabaseSchemaIntrospection(self)
+        self.introspection = type('DatabaseWrapper', (DatabaseSchemaIntrospection,original_backend.DatabaseWrapper.introspection_class), {})(self)
         self.set_schema_to_public()

     def close(self):

Conclusion, si on veut utiliser cette app, il nous faudra beaucoup de (monkey)patches. A part si on souhaite utiliser d'autres fonctionnalités, je serai pour abandonner cette option et gérer les partitions/migrations à la main ou reprendre des bouts à ajouter à django-tenant-schemas

#9

Updated by Benjamin Dauvergne about 2 months ago

J'ai proposé deux patchs pour améliorer les choses (liés à celui-ci) j'aimerai bien qu'on les passe voir si ça améliore les chose avant de se lancer dans des choses plus compliquées.

Pour rappel :
  • #88953 essaye de nettoyer les logs autant que faire se peut via un seul statement DELETE pour éviter les réécriture de pages partiellement vides + VACUUM ANALYZE sur la table après le nettoyage
  • #88960 supprime les index actuels pour un unique index BRIN sur le temps, on gagne pas mal de place et de nettoyage d'index inutiles en échange d'un léger ralentissement quand on pagine puisque qu'on doit filtrer sur appname (appname n'étant pas vraiment spécifique on ne gagnait pas grand chose)
#10

Updated by Gael Pasgrimaud about 2 months ago

Benjamin Dauvergne a écrit :

J'ai proposé deux patchs pour améliorer les choses (liés à celui-ci) j'aimerai bien qu'on les passe voir si ça améliore les chose avant de se lancer dans des choses plus compliquées.

Je plussoie complètement. J'avais regardé ces PR dont la relecture est demandé explicitement à Pierre. Pour moi elles me semblent ok. Même si le partitionnement reste la meilleur solution (ça revient à un delete table instantané si j'ai bien compris), vue la complexité de mise en place, si on pouvait s'en passer, au moins dans un premier temps, ça me semblerait pas mal.

#11

Updated by Benjamin Dauvergne about 1 month ago

Gael Pasgrimaud a écrit :

Je plussoie complètement. J'avais regardé ces PR dont la relecture est demandé explicitement à Pierre.

J'ai retiré cette affectation.

Pour moi elles me semblent ok. Même si le partitionnement reste la meilleur solution (ça revient à un delete table instantané si j'ai bien compris), vue la complexité de mise en place, si on pouvait s'en passer, au moins dans un premier temps, ça me semblerait pas mal.

J'ai encore un peu de mal à voir comment le partitionnement va s'emboîter avec le fait que la durée de rétention est libre par connecteur.

Also available in: Atom PDF