Développement #101319
Performances: revoir le stockage des Evolution::parts et les charger à la volée
0%
Description
Aujourd'hui, le champ parts des Evolution est probablement le plus consommateur en temps CPU (parce que c'est souvent un champ volumineux, stocké en pickle donc à désérialiser) et en bande passante (sur par exemple isereconnect, un workflow sur une fiche envoie plus de 100GB de requêtes par jour à cause de ces champs). Je pense que ce champ parts est responsable de la quasi saturation de la bande passante par les crons wcs en production.
Si on regarde le code qui manipule ces parts, le paramètre klass de iter_evolution_parts est très souvent utilisé, notamment dans le calcul des sauts.
On peut donc imaginer un nouveau stockage des parts, et un chargement à la volée, afin de ne les charger que très rarement, voire jamais.
Je suggère, mais je n'ai plus la possibilité de le tester désormais, le découpage suivant:
- un champ parts_data, bytea[], reprenant le pickle de chaque part mais dans un tableau SQL histoire de pouvoir sélectionner un "part" intéressant
- un champ parts_class, text[], donnant la classe de chaque pickle pour savoir si une evolution est intéressante, et savoir quels parts nous concernent
L'objectif est de réduire à la fois le nombre de lecture des parts ET le nombre d'écriture.
À la base, j'espérais que remplacer le champ parts de l'objet Evolution par une propriété et un booléen __parts_loaded pour ne charger absolument que ce qui est nécessaire soit suffisant, mais je ne pense pas que cela soit le cas, d'où cette suggestion de refactoring du stockage.
Quelques exemples concrets des conséquences du fonctionnement actuel : rien que sur l'isere, en production, on a plus de 100GB de requêtes SQL de plus de 100ms, à cause de la sauvegarde en boucle d'objets evolution alors que le champ parts n'y change pas. J'ai observé ce comportement sur plusieurs tenants, avec plusieurs workflows. Si les update se faisaient a minima sans le parts, ce serait beaucoup plus rapide et ferait des requêtes beaucoup plus légères.
History
Updated by Frédéric Péters about 2 months ago
- Status changed from Nouveau to Information nécessaire
- Assignee set to Pierre Ducroquet
rien que sur l'isere, en production, on a plus de 100GB de requêtes SQL de plus de 100ms
Quel outil pour mesurer ça ?
Updated by Frédéric Péters about 2 months ago
J'ai observé ce comportement sur plusieurs tenants, avec plusieurs workflows.
Ça reste utile de les pointer, qu'au-delà du code dans w.c.s. on puisse les identifier et regarder ce qu'il y a moyen de construire différemment.
Updated by Pierre Ducroquet about 2 months ago
Frédéric Péters (de retour le 30/1) a écrit :
J'ai observé ce comportement sur plusieurs tenants, avec plusieurs workflows.
Ça reste utile de les pointer, qu'au-delà du code dans w.c.s. on puisse les identifier et regarder ce qu'il y a moyen de construire différemment.
Ce que j'ai toujours fait, le dernier a été pointé à Stéphane dans le passé, cf. https://dev.entrouvert.org/issues/100674
Updated by Pierre Ducroquet about 2 months ago
Frédéric Péters (de retour le 30/1) a écrit :
rien que sur l'isere, en production, on a plus de 100GB de requêtes SQL de plus de 100ms
Quel outil pour mesurer ça ?
pducroquet@db2.prod:~ (PROD) $ sudo zstdcat /var/log/postgresql/postgresql-13-publik.log-20250127.zst | ~/log-stats.py
Et dans la sortie (un dictionnaire que je colle dans un shell python), le top 1 est l'isère avec donc 110GB de logs hier
>>> bad_boys['wcs_demarches_isereconnect_fr'] {'lines': 74250, 'bytes': 103403068844}
Et j'ai le script log-extract.py pour pouvoir lire les requêtes en question
Updated by Frédéric Péters about 2 months ago
Ce que j'ai toujours fait, le dernier a été pointé à Stéphane dans le passé,
Je l'ai lié au ticket; si tu as les références à d'autres, c'est cool de les lier aussi, ça permet ensuite de valider les changements sur des vraies situations.
Updated by Frédéric Péters about 2 months ago
Aussi, sur la forme technique en elle-même, pourquoi pas la suggstion de tables ..._evolution_parts à côté, (id, evolution_id, part_type, part_content) ? Et en y appliquant le stockage "TOAST donc compressée" cité dans #101025 mais que je n'ai pas compris ?
Updated by Pierre Ducroquet about 2 months ago
- Assignee changed from Pierre Ducroquet to Frédéric Péters
Frédéric Péters (de retour le 30/1) a écrit :
Aussi, sur la forme technique en elle-même, pourquoi pas la suggstion de tables ..._evolution_parts à côté, (id, evolution_id, part_type, part_content) ? Et en y appliquant le stockage "TOAST donc compressée" cité dans #101025 mais que je n'ai pas compris ?
Les données en question sont déjà compressées dans PostgreSQL avec le stockage actuel, le champ parts étant "TOASTé". Ce qui nous pose problème aujourd'hui, c'est le besoin pour wcs de charger énormément de données, et donc de consommer beaucoup de bande passante pour cela. Sachant que si les données sont compressées de manière transparente par PG, elles sont envoyées décompressées sur le réseau (pire encore si elles sont codées en hexadecimal)
Updated by Benjamin Dauvergne about 2 months ago
Je pense que ce champ parts est responsable de la quasi saturation de la bande passante par les crons wcs en production.
Pour traduire en bon français le but de Pierre ici est d'arrivé à écrêter les pics sur ce graphe: https://grafana.entrouvert.org/d/qtBfPyamk/node-exporter-full?orgId=1&from=now-7d&to=now&timezone=browser&var-job=nodexporter&var-node=db2.prod.saas.entrouvert.org&var-port=9100&viewPanel=panel-74
Coté écriture¶
Je ne vois vraiment de souci de bande-passante et les pics sont moins marqués, on a un traffic de fond à 15Mib/s et des pics max à 64Mib/s, disons un rapport de 1 à 4 mais bon ça reste bas.
S'il y a trop d'écritures effectivement stocker un bytea[] au lieu d'un bytea permet de faire unupdate _evolution update parts=parts || newparts
certainement moins volumineux en bande passante; modulo le fait de faire attention aux endroits où on modifie les parts existantes qu'il faudrait marquer d'un booléen "_dirty". Pour l'instant la seule optimisation faite de ce coté c'est au niveau des évolutions elle même avec _store_all_evolutions et le test sur evo._sql_id, il faudrait être plus fin ici et descendre au niveau des parts; un simple grep des endroits où on utilise _store_all_evolution me semble un bon départ.$ git grep _store_all_evolution\ =\ True wcs wcs/admin/workflows.py: item._store_all_evolution = True wcs/clamd.py: formdata._store_all_evolution = True wcs/formdata.py: self._store_all_evolution = True wcs/formdata.py: self._store_all_evolution = True wcs/formdata.py: self._store_all_evolution = True wcs/formdata.py: self._store_all_evolution = True wcs/formdata.py: self._store_all_evolution = True wcs/sql.py: # to store them all using formdata._store_all_evolution = True wcs/sql.py: formdata._store_all_evolution = True wcs/wf/wscall.py: formdata._store_all_evolution = True
Je vois:
- les actions d'appel de web-service (à mon avis 99% du souci)
- le scan clamd (peut-être...)
- l'anonymisation (pas fréquent, bas)
- le remapping de workflow (c'est pas si fréquent, priorité très très basse)
On pourrait tenter d'un booléen _dirty = False
au chargement des evolutions et des parts (avec _dirty = True
au niveau classe) et introduire une nouvelle colonne new_parts bytea[] pour éviter une migration (on lit parts + new_parts, à l'écriture on vide parts et on écrit plus que dans new_parts) et remplacer _store_all_evolutions par cette gestion plus fine.
Coté lecture¶
Là traffic de fond autour de 60Mib/s et pics jusqu'à 384Mib/s on est plus sur un rapport 1 à 6.
On pourrait charger les "parts" de manière paresseuse en fonction de leur type comme on charge les évolutions de manière paresseuse et/ou par batch dans certains chemins du code.
Mais j'ai quand même un doute personnellement parce que c'est pas juste une question de connaître la classe de chaque part, notamment pour le cron de saut cité ici parce qu'un saut ça implique des actions, avec des conditions et des gabarits en pagaille et le calcul de ces gabarits nécessitent des variables de substitution qui sont difficiles à calculer sans lire toutes les "parts" (sauf à exécuter/analyser l'expression en avance, et en déduire les parts nécessaires, compliqué).
Pour moi il y a à réfléchir d'avantage sur la représentation qui permettrait d'en lire le moins possible; c'est pas du tout évident que les pics rouges dans le graphe plus haut ce soit juste "la lecture de parts inutiles pour le processus en cours" par exemple pour ne charger que les WorkflowWsCallEvolutionPart nécessaire via les variables lazy, il faut déterminer son varname, il faudrait donc stocker varname en dehors du part de la même manière que la classe (ou stocker le lien part -> item du workflow, problématique si l'item change). Pour chaque part il faudrait séparer un ensemble de donnée à garder dans une/des colonnes de petite taille et une autre pour le vrac.
Et attention aussi parce que charger de manière paresseuse ça réduit la bande passante en lecture mais ça va augmenter la latence.
Sachant qu'une autre méthode immédiate ce serait d'aplatir lez pics pour répartir la charge, la nuit c'est bijoe qui charge un max (déprécier bijoe) le jour c'est toutes les heures à x:15, je ne sais pas pourquoi mais déterminer le coupable me semble utile.
Updated by Pierre Ducroquet about 2 months ago
Benjamin Dauvergne a écrit :
Sachant qu'une autre méthode immédiate ce serait d'aplatir lez pics pour répartir la charge, la nuit c'est bijoe qui charge un max (déprécier bijoe) le jour c'est toutes les heures à x:15, je ne sais pas pourquoi mais déterminer le coupable me semble utile.
C'est vrai que j'affirme toujours sans vérifier histoire de faire perdre du temps aux autres.
Au hasard des stats de pgbouncer, utilisé par wcs seul:
2025-01-28 15:17:16.719 CET [3735724] LOG stats: 4598 xacts/s, 6705 queries/s, in 21774324 B/s, out 313883698 B/s, xact 2595 us, query 1554 us, wait 34965 us
Voilà, la bande passante est consommée par wcs.
Updated by Benjamin Dauvergne about 2 months ago
Pierre Ducroquet a écrit :
C'est vrai que j'affirme toujours sans vérifier histoire de faire perdre du temps aux autres.
Au hasard des stats de pgbouncer, utilisé par wcs seul:
[...]
Voilà, la bande passante est consommée par wcs.
Je ne sais pas à qui tu parles puisque je vais dans ton sens...
Updated by Benjamin Dauvergne about 2 months ago
Aussi à regarder le trafic sur 6 mois on voit1 bien une légère inflexion au niveau du 9 janvier de disons 25Mib/s en plus en moyenne du au chargement des objets de configuration depuis la base plutôt que le NFS. C'était à prévoir, c'est pas forcément un souci. On voit une baisse de 10Mib/s coté filer au même moment2 c'est pas équivalent parce qu'au niveau NFS on avait un poil de cache via les délégations.
Ça ne remet pas en cause qu'il y ait beaucoup de trafic mais ça explique l'augmentation récente.
Updated by Frédéric Péters about 2 months ago
- Assignee changed from Frédéric Péters to Pierre Ducroquet
Aussi, sur la forme technique en elle-même, pourquoi pas la suggstion de tables ..._evolution_parts à côté, (id, evolution_id, part_type, part_content) ? Et en y appliquant le stockage "TOAST donc compressée" cité dans #101025 mais que je n'ai pas compris ?
Ok pour la réponse sur le côté TOAST mais je n'aurais pas dû mettre deux questions sur la même ligne : sur la forme technique en elle-même, pourquoi pas la suggestion de tables ..._evolution_parts à côté, (id, evolution_id, part_type, part_content) ?
Updated by Pierre Ducroquet about 2 months ago
- Assignee changed from Pierre Ducroquet to Frédéric Péters
Frédéric Péters (de retour le 30/1) a écrit :
Aussi, sur la forme technique en elle-même, pourquoi pas la suggstion de tables ..._evolution_parts à côté, (id, evolution_id, part_type, part_content) ? Et en y appliquant le stockage "TOAST donc compressée" cité dans #101025 mais que je n'ai pas compris ?
Ok pour la réponse sur le côté TOAST mais je n'aurais pas dû mettre deux questions sur la même ligne : sur la forme technique en elle-même, pourquoi pas la suggestion de tables ..._evolution_parts à côté, (id, evolution_id, part_type, part_content) ?
Pas d'opinion sur ça, j'avais en tête de faire évoluer les tables existantes pour ne pas trop impacter le code de wcs, mais cela peut être une table à côté. Je pensais que l'ordre du tableau python sérialisé dans parts était important, ce n'est pas le cas ?
Updated by Frédéric Péters about 2 months ago
Pas d'opinion sur ça, j'avais en tête de faire évoluer les tables existantes pour ne pas trop impacter le code de wcs, mais cela peut être une table à côté. Je pensais que l'ordre du tableau python sérialisé dans parts était important, ce n'est pas le cas ?
Si mais (id, evolution_id, part_type, part_content), les id étant croissant on a un ordre, ou je rate un truc ?
Updated by Frédéric Péters about 2 months ago
- Assignee changed from Frédéric Péters to Pierre Ducroquet
Updated by Pierre Ducroquet about 2 months ago
- Assignee changed from Pierre Ducroquet to Frédéric Péters
Frédéric Péters (de retour le 30/1) a écrit :
Pas d'opinion sur ça, j'avais en tête de faire évoluer les tables existantes pour ne pas trop impacter le code de wcs, mais cela peut être une table à côté. Je pensais que l'ordre du tableau python sérialisé dans parts était important, ce n'est pas le cas ?
Si mais (id, evolution_id, part_type, part_content), les id étant croissant on a un ordre, ou je rate un truc ?
On a un ordre qu'on ne peut pas changer facilement a posteriori, alors que dans l'existant c'est possible (mais j'ignore si c'est utilisé dans le python)
Updated by Frédéric Péters about 2 months ago
On a un ordre qu'on ne peut pas changer facilement a posteriori, alors que dans l'existant c'est possible (mais j'ignore si c'est utilisé dans le python)
Non l'ordre est fixe.