Re-cuisson de toutes les optimisations de publication

Nous avons un peu moins de 30 millions de publications dans notre table de publications. Cela rend une opération de re-cuisson (après importation d’un autre logiciel de forum et modifications markdown) plutôt intenable. Je cherche comment optimiser cela.

Cet appel particulier prend des heures à se terminer sur mon système de test actuel :

Post.update_all('baked_version = NULL')

Que fait exactement ce drapeau ? Le champ est un entier, mais est-il nécessaire de le réinitialiser pour toutes les publications avant de procéder à une opération de re-cuisson ? Comme je l’ai dit, cet appel unique prend des heures à s’exécuter en raison de la taille de notre table de publications. J’ai légèrement modifié le code de re-cuisson pour plutôt faire cet appel pour chaque publication juste avant que la re-cuisson ne soit appelée. Y a-t-il une meilleure façon ?

De plus, ce qui semble être le plus grand frein aux performances est l’utilisation de OFFSET dans le code. Cette ligne cause d’énormes problèmes :

Post.order(id: :desc).offset(i).limit(batch).each do |post|

Lorsque vous atteignez des millions de publications, cela devient un problème car OFFSET nécessite que toute la table jusqu’à l’offset actuel inclus soit traitée. Le moteur rejette ensuite tout ce qui est en dessous de la plage d’offset avant de retourner le jeu de résultats final. Cela fait que l’appel prend plusieurs minutes, même lorsqu’il ne s’agit que de quelques millions de publications. Le nombre de fois où cela se produit est basé sur la taille du lot (codée en dur à 1000 par défaut).

Je réalise que le délai variera en fonction de la configuration matérielle, mais je pense que cela peut être amélioré afin que l’ajout de plus de matériel à ce problème ne soit pas la seule réponse.

Au lieu de cela, j’ai modifié ce code pour fonctionner sur la base d’un opérateur BETWEEN sur la plage d’ID traités. Ma compréhension est que cela ne changera pas la logique d’une re-cuisson, mais améliorera plutôt la requête SQL pour quelque chose de beaucoup plus rapide.

En fait, dans mon cas, je constate une amélioration de 3 ordres de grandeur (1000x) en apportant cette modification :

(0..(total).abs).step(batch) do |i|
      Post.order(id: :desc).where('id BETWEEN ? AND ?', i, (i + batch - 1)).each do |post|

i est l’index du lot actuel et BETWEEN est inclusif, donc j’exclus la dernière publication de chaque lot afin que rien ne soit traité deux fois. Cela devrait récupérer toutes les publications, mais il est possible que j’aie une légère erreur de logique quelque part…

Voici la différence de temps de traitement entre la version originale et la nouvelle version de la requête finale :

Original :

                                                                         QUERY PLAN                                                          
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=12541237.13..12544635.84 rows=1000 width=783) (actual time=289375.555..289456.445 rows=1000 loops=1)
   ->  Index Scan Backward using posts_pkey on posts  (cost=0.56..100746846.76 rows=29642680 width=783) (actual time=0.038..288798.236 rows=3691000 loops=1)
         Filter: (deleted_at IS NULL)
 Planning Time: 0.175 ms
 JIT:
   Functions: 6
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 80.019 ms, Inlining 47.933 ms, Optimization 261.731 ms, Emission 106.263 ms, Total 495.947 ms
 Execution Time: 289538.294 ms
(9 rows)

Notez le nombre de lignes dans l’Index Scan dans la section temps réel : 3691000
C’est une requête de près de 5 minutes.

Voici la nouvelle version utilisant BETWEEN :

                                                               QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
 Index Scan Backward using posts_pkey on posts  (cost=0.56..4137.79 rows=1100 width=783) (actual time=1.956..273.467 rows=1001 loops=1)
   Index Cond: ((id >= 3690000) AND (id <= 3691000))
   Filter: (deleted_at IS NULL)
 Planning Time: 26.421 ms
 Execution Time: 274.035 ms
(5 rows)

Seules les 1000 lignes d’intérêt sont examinées et retournées. S’exécute en 274 ms au lieu de 289 000 ms.

C’est une belle amélioration, mais ce serait encore mieux si le processus pouvait être multithreadé. C’est probablement un peu au-delà de mon expertise, mais je pourrais peut-être trouver un moyen d’exécuter plusieurs processus de re-cuisson simultanément en utilisant le MOD de l’ID pour segmenter la table des publications entre les différents processus.

Vos commentaires sont les bienvenus. Peut-on encore améliorer cela ?

2 « J'aime »

donnez-le moi GIF

Vous pouvez simplement émettre un post.rebake! pour chaque message qui nécessite un rebake.
Pas besoin de toucher à baked_version.

Donc, ce morceau de code peut être supprimé de la tâche Rake ? Pourquoi serait-il là s’il n’est pas nécessaire ?

Autant que je me souvienne, baked_version est là pour permettre une nouvelle cuisson au cas où la logique de cuisson de Discourse changerait (par opposition au cas où le contenu brut du message a été modifié).

1 « J'aime »