Ribake di tutti i post Ottimizzazione

Abbiamo poco meno di 30 milioni di post nella nostra tabella dei post. Questo rende un’operazione di rebake (dopo l’importazione da un diverso software per forum e modifiche al markdown) piuttosto insostenibile. Sto cercando come ottimizzare questo.

Questa particolare chiamata richiede ore per essere completata sul mio attuale sistema di test:

Post.update_all('baked_version = NULL')

Cosa fa esattamente questo flag? Il campo è un intero ma è necessario resettarlo per tutti i post prima di eseguire qualsiasi operazione di rebake? Come ho detto, questa singola chiamata richiede ore per essere eseguita a causa delle dimensioni della nostra tabella dei post. Ho leggermente modificato il codice di rebake per effettuare questa chiamata per ogni post subito prima che venga chiamata la rebake. C’è un modo migliore?

Inoltre, ciò che sembra essere il maggiore impatto sulle prestazioni è l’uso di OFFSET nel codice. Questa riga causa enormi problemi:

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

Quando si arriva a milioni di post, questo diventa un problema perché offset richiede che venga elaborata l’intera tabella fino all’offset corrente incluso. Il motore quindi scarta tutto ciò che è al di sotto dell’intervallo di offset prima di restituire il set di risultati finale. Ciò fa sì che la chiamata richieda diversi minuti anche quando si hanno solo pochi milioni di post. Il numero di volte in cui ciò accade si basa sulla dimensione del batch (impostata di default a 1000).

Mi rendo conto che il ritardo varierà anche in base alla configurazione hardware, ma penso che questo possa essere migliorato in modo che l’aggiunta di più hardware al problema non debba essere la risposta.

Invece, ho modificato questo codice per operare in base a un operatore BETWEEN sull’intervallo di ID elaborati. La mia comprensione è che questo non dovrebbe cambiare la logica di una rebake, ma solo migliorare la query SQL in qualcosa di molto più veloce.

Infatti, nel mio caso, sto vedendo un miglioramento di 3 ordini di grandezza (1000x) apportando questa modifica:

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

i è l’indice del batch corrente e BETWEEN è inclusivo, quindi escludo l’ultimo post in ogni batch in modo che nulla venga elaborato due volte. Questo dovrebbe ottenere tutti i post, ma è possibile che ci sia un piccolo errore logico da qualche parte…

Ecco la differenza nei tempi di elaborazione tra la versione originale e la nuova versione della query finale:

Originale:

                                                                         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)

Notare il numero di righe nell’Index Scan nella sezione del tempo effettivo: 3691000
Questa è una query di quasi 5 minuti.

Ecco la nuova versione che utilizza 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)

Vengono esaminate e restituite solo le 1000 righe di interesse. Esegue in 274 ms invece di 289.000 ms.

Questo è un bel miglioramento, ma sarebbe ancora meglio se il processo potesse essere multithread. Probabilmente è un po’ al di sopra delle mie competenze, ma potrei essere in grado di escogitare un modo per eseguire più processi di rebake contemporaneamente utilizzando il MOD dell’ID per segmentare la tabella dei post tra i vari processi.

I feedback sono benvenuti. Si può migliorare ulteriormente?

2 Mi Piace

dammi GIF

Puoi semplicemente emettere un post.rebake! per ogni post che necessita di un rebake.
Non c’è bisogno di modificare baked_version.

Quindi quel pezzo di codice può essere rimosso dal rake task? Perché dovrebbe essere lì se non è necessario?

Per quanto ricordo, baked_version è lì per gestire il ribaking nel caso in cui la logica di ribaking di Discourse sia cambiata (in contrasto con quando il contenuto grezzo del post è stato modificato).

1 Mi Piace