Reassar todos os posts Otimização

Temos pouco menos de 30 milhões de posts em nossa tabela de posts. Isso torna uma operação de rebake (após importar de um software de fórum diferente e fazer modificações em markdown) bastante inviável. Estou analisando como otimizar isso.

Esta chamada em particular está levando horas para ser concluída em meu sistema de teste atual:

Post.update_all('baked_version = NULL')

O que exatamente essa flag faz? O campo é um inteiro, mas é necessário redefini-lo para todos os posts antes de realizarmos qualquer operação de rebake? Como eu disse, essa única chamada leva horas para ser executada devido ao tamanho de nossa tabela de posts. Modifiquei ligeiramente o código de rebake para fazer essa chamada para cada post logo antes que o rebake seja chamado. Existe uma maneira melhor?

Além disso, o que parece ser o maior impacto no desempenho é o uso de OFFSET no código. Esta linha causa enormes problemas:

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

Quando você chega a milhões de posts, isso se torna um problema porque o offset exige que toda a tabela até e incluindo o offset atual seja processada. O motor então descarta tudo abaixo do intervalo de offset antes de retornar o conjunto de resultados final. Isso faz com que a chamada leve vários minutos, mesmo quando apenas alguns milhões de posts estão envolvidos. O número de vezes que isso acontece é baseado no tamanho do lote (hardcoded para 1000 por padrão).

Percebo que o atraso variará com base na configuração do hardware também, mas acho que isso pode ser melhorado para que a adição de mais hardware à questão não precise ser a resposta.

Em vez disso, modifiquei este código para operar com base em um operador BETWEEN no intervalo de IDs que estão sendo processados. Minha compreensão é que isso não mudará a lógica de um rebake, mas sim melhorará a consulta SQL para algo muito mais rápido.

De fato, no meu caso, estou vendo uma melhoria de 3 ordens de magnitude (1000x) ao fazer essa alteração:

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

i é o índice do lote atual e BETWEEN é inclusivo, então eu excluo o último post em cada lote para que nada seja processado duas vezes. Isso deve obter todos os posts, mas é possível que eu tenha um pequeno erro de lógica em algum lugar…

Aqui está a diferença no tempo de processamento entre a versão original e a nova versão da consulta final:

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)

Note o número de linhas no Index Scan na seção de tempo real: 3691000
Esta é uma consulta de quase 5 minutos.

Aqui está a nova versão usando 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)

Apenas as 1000 linhas de interesse são examinadas e retornadas. Executa em 274 ms em vez de 289.000 ms.

Esta é uma boa melhoria, mas seria ainda melhor se o processo pudesse ser multithreaded. Isso provavelmente está um pouco acima da minha expertise, mas talvez eu possa criar alguma maneira de executar vários processos de rebake simultaneamente usando o MOD do ID para segmentar a tabela de posts entre os vários processos.

Feedback é bem-vindo. Isso pode ser melhorado ainda mais?

2 curtidas

dê para mim GIF

Você pode simplesmente emitir um post.rebake! para cada postagem que precisa de um rebake.
Não há necessidade de mexer com baked_version.

Então esse trecho de código pode ser removido da tarefa do rake? Por que estaria lá se não for necessário?

Pelo que me lembro, o baked_version existe para atender à repetição de cozimento caso a lógica de cozimento do Discourse tenha mudado (em contraste com quando o conteúdo bruto da postagem foi alterado).

1 curtida