Alle Beiträge neu backen Optimierung

Wir haben knapp 30 Millionen Beiträge in unserer Beitrags-Tabelle. Dies macht einen Rebake-Vorgang (nach dem Import aus einer anderen Forensoftware und der Durchführung von Markdown-Änderungen) eher unzumutbar. Ich schaue gerade, wie ich das optimieren kann.

Dieser spezielle Aufruf dauert auf meinem aktuellen Testsystem Stunden:

Post.update_all('baked_version = NULL')

Was genau macht dieses Flag? Das Feld ist eine Ganzzahl, aber ist es notwendig, es für alle Beiträge zurückzusetzen, bevor wir einen Rebake-Vorgang durchführen? Wie gesagt, dieser einzelne Aufruf dauert Stunden, da unsere Beitrags-Tabelle so groß ist. Ich habe den Rebake-Code leicht modifiziert, um diesen Aufruf stattdessen für jeden Beitrag direkt vor dem Aufruf des Rebakes durchzuführen. Gibt es einen besseren Weg?

Außerdem scheint die Verwendung von OFFSET im Code den größten Leistungsengpass darzustellen. Diese Zeile verursacht riesige Probleme:

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

Wenn man in die Millionen von Beiträgen kommt, wird dies zu einem Problem, da OFFSET erfordert, dass die gesamte Tabelle bis einschließlich des aktuellen Offsets verarbeitet wird. Die Engine verwirft dann alles unterhalb des Offset-Bereichs, bevor das Endergebnis zurückgegeben wird. Dies führt dazu, dass der Aufruf selbst bei nur wenigen Millionen Beiträgen mehrere Minuten dauert. Die Häufigkeit, mit der dies geschieht, hängt von der Batch-Größe ab (standardmäßig auf 1000 festgelegt).

Ich erkenne, dass die Verzögerung je nach Hardwarekonfiguration variieren wird, aber ich denke, dies kann verbessert werden, sodass das Hinzufügen von mehr Hardware zur Lösung nicht die Antwort sein muss.

Stattdessen habe ich diesen Code modifiziert, um basierend auf einem BETWEEN-Operator im Bereich der verarbeiteten IDs zu arbeiten. Mein Verständnis ist, dass dies die Logik eines Rebakes überhaupt nicht ändern sollte, sondern nur die SQL-Abfrage zu etwas viel Schnellerem verbessern sollte.

Tatsächlich sehe ich in meinem Fall eine Verbesserung um 3 Größenordnungen (1000x), indem ich diese Änderung vornehme:

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

i ist der aktuelle Batch-Index und BETWEEN ist inklusiv, daher schließe ich den letzten Beitrag in jedem Batch aus, damit nichts doppelt verarbeitet wird. Dies sollte alle Beiträge erfassen, aber es ist möglich, dass ich irgendwo einen kleinen Logikfehler habe…

Hier ist der Unterschied in der Verarbeitungszeit zwischen der ursprünglichen und der neuen Version der Endabfrage:

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)

Beachten Sie die Anzahl der Zeilen im Index Scan im Abschnitt “actual time”: 3691000
Dies ist eine Abfrage von fast 5 Minuten.

Hier ist die neue Version mit 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)

Nur die 1000 interessierenden Zeilen werden untersucht und zurückgegeben. Läuft in 274 ms statt 289.000 ms.

Dies ist eine schöne Verbesserung, aber es wäre noch besser, wenn der Prozess multithreaded sein könnte. Das liegt wahrscheinlich etwas über meiner Expertise, aber ich könnte vielleicht einen Weg finden, mehrere Rebake-Prozesse gleichzeitig auszuführen, indem ich das MOD der ID verwende, um die Beitrags-Tabelle auf die verschiedenen Prozesse aufzuteilen.

Feedback ist willkommen. Kann dies weiter verbessert werden?

2 „Gefällt mir“

Gib mir GIF

Sie können einfach post.rebake! für jeden Beitrag ausgeben, der ein Rebake benötigt.
Keine Notwendigkeit, sich mit baked_version zu befassen.

Kann dieser Code also aus der Rake-Aufgabe entfernt werden? Warum sollte er dort sein, wenn er nicht benötigt wird?

Soweit ich mich erinnere, ist baked_version vorhanden, um das erneute Backen zu ermöglichen, falls sich die Discourse-Logik für das erneute Backen ändert (im Gegensatz zu Änderungen am Rohinhalt des Beitrags).

1 „Gefällt mir“