重新烘焙所有帖子优化

我们的帖子表中有近 3000 万条帖子。这使得烘焙操作(从不同的论坛软件导入并进行 Markdown 修改后)变得相当不可行。我正在研究如何优化这一点。

此特定调用在我的当前测试系统上需要数小时才能完成:

Post.update_all('baked_version = NULL')

此标志具体有什么作用?该字段是整数,但在我们进行任何烘焙操作之前,是否需要重置所有帖子的此字段?如我所述,此单个调用因帖子表的大小而需要数小时才能运行。我已经稍微修改了烘焙代码,以便在调用烘焙之前为每个帖子执行此调用。有没有更好的方法?

此外,性能方面最大的影响似乎是代码中使用了 OFFSET。此行会导致严重问题:

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

当帖子数量达到数百万时,这会成为一个问题,因为 offset 需要处理直到当前 offset 包含的所有表。然后,引擎会在返回最终结果集之前丢弃 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)

请注意实际时间部分中索引扫描的行数: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 行相关数据。运行时间为 274 毫秒,而不是 289,000 毫秒。

这是一个不错的改进,但如果该过程可以多线程处理,那就更好了。这可能超出了我的专业范围,但我或许可以想出一些方法来同时运行多个烘焙进程,使用 ID 的 MOD 来跨各个进程分段帖子表。

欢迎提供反馈。这还能进一步改进吗?

2 个赞

给我GIF

您可以为每个需要重新烘焙的帖子发出 post.rebake!
无需修改 baked_version

那么这段代码可以从 rake 任务中移除吗?如果不需要,为什么会出现在那里?

据我回忆,baked_version 的存在是为了在 Discourse 的重新烘焙逻辑发生变化时进行重新烘焙(与帖子原始内容被更改时的情况相反)。

1 个赞