Development #88761
Partitionner la table ResourceLog
0%
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
History
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).
Updated by Benjamin Dauvergne 7 months ago
- 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.
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
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.
Updated by Benjamin Dauvergne 6 months ago
- Related to Development #88953: Optimiser les requêtes de suppressions des ResourceLog expirés added
Updated by Benjamin Dauvergne 6 months ago
- Related to Development #88960: ResourceLog: utiliser un index BRIN sur timestamp added
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 :
- URL : https://git.entrouvert.org/entrouvert/passerelle/pulls/618
- Titre : base: use postgres-extra and partitions for ResourceLog table (#88761)
- Modifications : https://git.entrouvert.org/entrouvert/passerelle/pulls/618/files
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
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)
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.
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.