すべての投稿のリベイク最適化

投稿テーブルに約3000万件の投稿があります。これは、別のフォーラムソフトウェアからインポートし、マークダウンの変更を行った後のリベイク操作を非常に実行不可能にしています。最適化方法を検討しています。

この特定の呼び出しは、現在のテストシステムで数時間かかっています。

Post.update_all('baked_version = NULL')

このフラグは具体的に何をするのでしょうか?フィールドは整数ですが、リベイク操作を行う前にすべての投稿に対してリセットする必要がありますか?前述したように、この単一の呼び出しは、投稿テーブルのサイズのために実行に数時間かかります。リベイクコードを、リベイクが呼び出される直前に各投稿に対してこの呼び出しを行うようにわずかに変更しました。もっと良い方法はありますか?

また、パフォーマンスに最も大きな影響を与えるのは、コードでのOFFSETの使用のようです。この行は大きな問題を引き起こします。

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

投稿数が数百万になると、OFFSETはオフセットまでのテーブル全体を処理する必要があるため、問題になります。その後、エンジンは結果セットの終わりを返す前に、オフセット範囲より下のすべてを破棄します。これにより、数百万件の投稿があっても、呼び出しに数分かかります。この発生回数は、バッチサイズ(デフォルトで1000にハードコードされています)に基づいています。

遅延はハードウェア構成によって異なることは理解していますが、これは、問題により多くのハードウェアを投入する必要がないように改善できると思います。

代わりに、このコードを、処理されるIDの範囲に対するBETWEEN演算子に基づいて動作するように変更しました。私の理解では、これはリベイクのロジックをまったく変更するべきではありませんが、代わりにSQLクエリをはるかに高速なものに改善するだけです。

実際、私のケースでは、この変更を行うことで3桁(1000倍)の改善が見られます。

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

iは現在のバッチインデックスであり、BETWEENは包括的です。そのため、各バッチの最後の投稿は除外して、二重処理されないようにします。これにより、すべての投稿が取得されるはずですが、わずかなロジックエラーがある可能性があります。

元のクエリと新しいバージョンのクエリの処理時間の違いは次のとおりです。

オリジナル:

                                                                         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)

実際の時間のセクションのIndex Scanの行数に注目してください:3691000
これはほぼ5分間のクエリです。

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)

関心のある1000行のみが検査され、返されます。289,000ミリ秒ではなく274ミリ秒で実行されます。

これは素晴らしい改善ですが、プロセスをマルチスレッド化できればさらに良くなります。それはおそらく私の専門知識を超えていますが、IDのMODを使用して投稿テーブルをさまざまなプロセスに分割することで、複数のリベイクプロセスを同時に実行する方法を考案できるかもしれません。

フィードバックを歓迎します。さらに改善できますか?

「いいね!」 2

give it to me GIF

リベイクが必要なすべての投稿に対して post.rebake! を発行するだけで済みます。
baked_version をいじる必要はありません。

では、そのコードはレークタスクから削除してもよいということですか?必要ないのであれば、なぜそこにあるのでしょうか?

私の記憶が正しければ、baked_version は、Discourse のリベイクロジックが変更された場合に備えて、リベイクに対応するために存在します(投稿の生のコンテンツが変更された場合とは対照的です)。

「いいね!」 1