迁移自 S3 的问题

When migrating from S3 to local storage, we see a number of issues.

The main issue is that the migrate_from_s3 rake task is not taking the Uploads table as a starting point, but the posts. This causes it to skip a lot of uploads which are being left on S3.

  • uploads used as logo’s because they are not referenced in a post
  • uploads for avatars because they are not referenced in a post
  • uploads that are (for some reason) referenced by their CDN URL in raw because they do not match the regex that is being used to identify the uploads
  • uploads that are on non-AWS S3 storage because they do not match the regex because it requires amazonaws in the URL
  • uploads that for some reason do not match the second, different regex (we’re seeing this with non-image uploads like mp3 files, not sure why this is happening)
2 个赞

We’re working on improving the way we associate uploads to posts (by using the upload:// scheme) that will make these storage migrations much more bulletproof. There’s little point to fixing these rake tasks before that’s done.

7 个赞

我最近提交了一个新的 PR,允许一次迁移更少的帖子(例如每次只测试一个),并为该任务添加了测试。

我觉得从帖子开始是合理的,因为在每次迁移后,您都需要重新烘焙帖子以更新已渲染内容中的 URL;如果先迁移所有上传文件再开始重新烘焙,对于大型站点来说,网站可能会长时间处于严重损坏状态(我需要从 S3 迁移约 100GB 数据到本地,所以我很关注这一点)。不过,我写的代码或许有助于开始编写一个 migrate_uploads_from_s3 任务,该任务可在 migrate_from_s3 之后运行,用于清理那些不属于帖子的上传文件。

@zogstrip “我们正在推进……

5 个赞

[quote=“mcdanlj, 帖子:3, 主题:119064”]
@zogstrip “我们正在处理……

2 个赞

等我能够在非工作时间使用批量上传功能迁移我的网站后,我再看看是否值得花时间研究非帖子上传!:smiling_face: 感谢你的 PR 审查!

1 个赞

我接下来要解决两个问题:首先是,限制是有帮助的,但它仅限制搜索空间;接下来,我希望能够指定要修改的帖子数量上限。因此,我打算同时添加要修改的帖子数量上限以及查询的限制。此外,在指定 max 时,为了调试目的,详细说明正在处理的内容是合理的,因此如果 max 不为 nil,我希望让输出变得详细——这将允许人们在继续之前验证处理过程,因为这是此项工作的主要用例。

我认为我会将 max 作为第一个参数,将可选的 limit 作为第二个参数,因为实际上 max 是最重要的;limit 只是为了让“仅一次请求”更便宜。

第二个问题是关于非上传内容的:上传。大约一年前,当我在尝试弄清楚如何编写从 Google+ 到 Discourse 的迁移脚本时,曾尝试将视频上传到我即将加入的 Discourse 站点,当时看到的 URL 格式是 https://#{SiteSettings.absolute_base_url}/original/3X/b/a/ba9e06ebc2f4397f26793bb5cd4e169308dd371d.mp4

而今天,当我上传视频时,得到的却是类似 ![file_example_MP4_480_1_5MG|video](upload://caJ9ykkpshw3PFK4464VUIPWJ4l.mp4) 的格式。

至少在我最近的测试中,migrate_from_s3 完全搞乱了这些 URL,使它们甚至不再成为有效的 URL,因此这绝对需要修复。然后,我认为在实际操作中,这项任务不太可能遇到 ![text](non-upload-url-to-migrate) 这种情况,所以作为初步方案,我打算直接在神奇的 upload 协议周围添加链接 Markdown,而不是让正则表达式同时处理两种情况,从而导致代码更难阅读。不过,我可能会改变主意。

看起来 videoaudio 标签是通过 JavaScript 中的正则表达式匹配添加的,因此我必须将 app/assets/javascripts/discourse/app/lib/uploads.js 中的正则表达式复制到任务中,以便正确识别它们。我会包含这些正则表达式的来源,以便下一个发现它们的人知道从哪里更新它们。:stuck_out_tongue:

今晚,我抽出了一些时间来做这项工作,目前已经有了一个草稿 PR。它尚未完成;我知道其中还存在一些 bug。我认为目前为止我还没有改变 upload: 伪协议 URL(通常用于图片)的任何行为,尽管我添加了一项健全性检查。

https://github.com/discourse/discourse/pull/10093

通过此 PR 中的更改,我已经成功迁移了正常的 upload: 伪协议上传内容,以及目前通过 S3 引用(在我的情况下是 Digital Ocean Spaces)明确引用的视频。我使用以下命令一次只修改一个帖子:

bin/rake uploads:batch_migrate_from_s3[1,1000]

请注意,这不会超过数据库查询返回的前 1000 个帖子( somewhat 随机);设置较低的限制只是为了在逐个迁移帖子、检查其行为正确性然后重新开始寻找下一个帖子时加快查询速度。此命令仅在我当前正在开发的 PR 中按此方式工作!

随着工作的推进,我继续添加诊断输出,并且我开始认为这对于开发之外的用途也很重要。我发现来自 Digital Ocean Spaces 的许多临时下载失败,其中帖子中的某些下载成功迁移,而另一些则失败,在原始情况下这只会打印一个 . 并继续,然后显示 Done,但实际上任务并未完成。我对一个帖子进行了大约五到六次操作,才成功迁移所有文件。(我当时没有计数,因为起初我以为是在调试本地 bug。)我预计需要重复运行此迁移,使用相同的限制,直到诊断结果干净为止。因此,我仅在设置了 max 时才打印详细的进度信息,但无论如何都会打印有用的警告消息。

目前,我正在使用以下针对 Discourse Spaces 间歇性下载失败的变通方法,在实践中这极大地提高了我的成功率(到目前为止,在数百个迁移帖子中,3 次重试已完全足够)。

https://github.com/johnsonm/discourse/commit/7dfac12a2ea6ec04ba4e0616b4e0dbd1d806cff7

此外,我还发现,不知何故,我们在从 Google+ 导入时设置的限制内出现了超过限制的视频——在进行一些超大视频的单次迁移时,我不得不临时增加 SiteSettings.max_image_size_kbSiteSettings.max_attachment_size_kb,因为这些视频是如何出现在站点上尚不清楚,但我不想现在破坏它们……我不知道允许超大上传的 bug 是出在我的导入脚本、Discourse 本身,还是仅仅是对我随时间对设置所做的更改的记忆有误。:wink:

由于我迁移的许多内容是从 G+ 导入的,因此我的某些帖子未能通过当前的验证。我遇到了一些 Unhandled failure: Validation failed: Sorry, new users can only put one image in a post 错误,起初我不明白为什么它们没有再次出现。事实证明,上传已成功移动到本地,并且它们都使用了 upload: 伪协议,因此原始内容并未改变。然而,post.save! 仍然因验证失败而报错,这阻止了 post.rebake! 的执行,因此在我迁移的 3 万个帖子中,有少数帖子包含需要重新烘焙的图片;遗憾的是,我没有记录哪些帖子是这些。我现在已改用 post.save!(validate: false) 作为另一种修复方法,因此这个问题应该不会再次发生。我很高兴我在迁移开始时设置了遇到未处理错误即退出的机制,否则这可能会造成比几个帖子更多的损害。

为了在运行迁移期间保持站点可用(包括发送通知),我不想向 Sidekiq 队列发送大量垃圾消息。我知道命名是计算机科学中最难的两个问题之一,另外两个是缓存失效和差一错误,但我提议使用环境变量 DISCOURSE_MIGRATION_MAX_ENQUEUED 来控制在迁移过程中,在执行 rebake 后迁移下一个项目时,允许填充的总队列槽位(不是作业槽位)数量,以避免向队列发送垃圾消息,从而确保站点继续正常运行。我有一个补丁添加了此功能,默认值为零,用于 lib/tasks/uploads.rake 中所有每个帖子的重新烘焙操作。我已在生产环境的迁移中使用此功能。

https://github.com/discourse/discourse/blob/59a761851b9c8786d3a9692f8c595372b0534f77/lib/tasks/uploads.rake

4 个赞

@zogstrip 你方便审查这个 PR 吗?鉴于你最近审查了我在这个领域的上一个 PR,应该对相关背景有所了解。FIX: Make migrations from S3 more robust; fix bare URL migration by johnsonm · Pull Request #10093 · discourse/discourse · GitHub

我在其中包含了我在这次相对大规模的迁移过程中所做的修复。我并没有尝试为每个修复都添加测试;我不确定如何注入每种形式的错误。但至少新功能已经过测试。

@RGJ 我认为我目前的 PR 可能已经解决了你提出的前两点以外的所有问题,只是我不确定 CDN 的情况。我的站点使用了 CDN,并迁移了带有 CDN URL 的视频,但这可能是由于 Discourse Spaces 的命名方式导致的副作用。如果你有额外的案例,希望我的 PR 能为你提供一个便捷的脚手架,以便向正则表达式中添加内容并为额外的变体添加测试用例。

我认为首先按帖子进行迁移是正确的,因为在迁移帖子中的上传内容后,需要重新烘焙(rebake)该帖子,以确保生成的帖子中包含正确的 URL。在我完成帖子迁移之后(鉴于我已将速率限制改为直接检查队列长度,现在可能不到两周),我将着手处理任何剩余的清理工作。

由于多个帖子可能引用相同的内容(如果多人上传了同一文件),因此需要进行第二轮检查,查看已烘焙数据中的旧 URL,并重新烘焙这些帖子以获取新位置。这可以使用相同的速率限制机制来避免占用队列。

在 makerforums 上,我可能不会看到任何损坏的标志,因为我们在停止向“s3”(对我们来说是 DigitalOcean Spaces)添加新内容后调整了品牌标识。但我可能会看到至少有一批头像上传内容仍留在 S3 中。迁移与帖子无关的上传内容,应在所有帖子迁移完成后才开始。我可能需要在完成帖子中的上传迁移后,再编写相关说明。

@pfaffman 我看到了 https://meta.discourse.org/t/bizarre-problems-with-migrate-from-s3/86337/5,其中描述了未重复出现的错误。如果没有当前 PR 中的修复,错误会被静默吞掉,包括验证失败。我认为这里的工作至少能解决你当时遇到的部分问题。

@hosna 你在 https://meta.discourse.org/t/what-does-rake-uploads-migrate-from-s3-exactly-do/97285 中提出的问题,在本 PR 中已经得到部分或完全解决。如果尚未完全解决,我已添加了测试,这将使后续添加更多测试以验证其他修复变得更加容易。

@sam 既然你给这个 PR 加上了 2.6 标签,我假设它至少几天内不会被合并;我是否应该将我的速率限制功能工作一并拉入该 PR,与修复内容合并?还是你更倾向于将修复和功能开发放在不同的 PR 中?两种方式我都可以。速率限制功能运行得非常顺利;现在由于我在等待 Sidekiq 队列清空,我的迁移速度提高了约三倍,且未影响站点可用性。因此,如果这通常是 PR 中可接受的内容,我认为将其合并是合理的。否则,我需要等待该 PR 所基于的工作被合并。无论如何,希望能听到你的意见。

..

我已将迁移速率限制补丁进行了 DRY 处理,并将其拉入 PR。实际运行效果良好,sar 数据显示在实时迁移期间,我几乎连续处于零空闲时间状态,同时站点仍保持正常运行。批量模式的一个好处是,我可以在每一批完整的迁移完成后检查是否有新的 Discourse 版本。我在 2.6.0beta1 发布后的第一时间就将站点升级到了该版本,并自更新以来一直在 2.6.0beta1 上成功运行迁移,同时叠加了我的迁移 PR。

我认为该 PR 现在已准备好接受审查;我计划提交另一个 PR 来处理最后几个阶段,但先将这部分内容到位,即使在我完成最后几项工作之前,也能提升所有人的整体迁移体验。

5 个赞

嗯,那已经是很久以前的事了……

我很期待这个问题能得以解决。目前,我有几个多站点部署的网站需要从 S3 拉取图片(现在我认为有些图片是本地存储的,另一些则在 S3 上),然后再推送到 S3。

1 个赞

@pfaffman 我启动迁移项目时,如果已经知道后来才弄明白的那些事,恐怕未必是好事。看起来,如果我当时用 minio 客户端把所有 S3 存储桶的内容复制到本地 uploads 文件夹,在 Rails 控制台中将所有 Upload.url 修改为 nil,然后重新构建站点,那么整个迁移过程只需几个小时,而且无需重新生成所有图片。(相反,我现在因为要重新运行所有图片转换而受到速率限制,这仿佛本地 CPU 比直接从 S3 复制所有已处理图片更便宜似的。)

而且,如果当时真的那么简单,我就不会为提升迁移的可靠性而做这些工作,其他人也就无法从中受益了。:smiling_face:

4 个赞

哦。这听起来正是我想知道的。也许我会试试看。

1 个赞

我……在尝试那条路线之前,肯定会先备份一下,因为我只是在推测。我不想误导你。

哦,还有一件事:如果是音频和视频上传,那会完全失败,而我可能要到后来才会注意到,到时候我就得去排查问题并编写自定义代码了。所以,如果你有音频和视频上传功能,你绝对应该从那里入手。如果没有那个已提交但尚未合并的拉取请求(PR),它就无法正常工作;该 PR 被 @sam 标记为 2.6,因此要等到 2.5 版本发布后才会合并。

2 个赞

很抱歉这么晚才加入讨论。我刚看了你的 PR,想知道你为什么选择重新创建 Upload 模型,而不是直接修改它们的 URL?另外,为什么不直接遍历 OptimizedImages 并做同样的处理呢?

2 个赞

@RGJ 我完全没有改动那部分;重新创建 Upload 是 @zogstripCannot execute the rake uploads:migrate_from_s3 - #11 中修复的一个 bug。

我只是试图让已有的代码正常运行,而对 Discourse 的内部机制并不熟悉,一直在黑暗中摸索。我唯一的 Ruby 经验就是为 Discourse 提交过几个 PR。遵循现有代码的模式似乎确实不是最高效的途径(参见我之前与 @pfaffman 关于短路处理的对话),我完全同意。从我今天早些时候甚至没有意识到 OptimizedImages.url 也需要修改为 /uploads 路径,并且 etag 需要设置为 nil(可能还有其他地方)这一点就能看出,我仍在盲人摸象。

至少还需要先遍历帖子,以修复帖子中旧的字面 URL。还需要进行其他一些修复,例如不再重新验证帖子,以及不再静默吞掉错误。我认为仍然需要实施速率限制,以减轻对在线站点的影响。

关于你前两个涉及非帖子更新的问题,这是我正在进行的 工作(尚未在在线站点上测试)提交记录),或许能有所帮助,但在我完成所有帖子上传迁移之前,我不会在我的在线站点上测试它。

调整一些原本就能正常工作的东西,是我当时唯一有精力去做的事。如果你想要更快的迁移方案,我完全支持;也许将我这较慢但至少更好的工作作为帕累托改进先合并进去是合理的,然后你可以用更好的方案完全替换它。即使到时候我本人已不再使用它,我也会是第一个为此欢呼的人。

1 个赞

@mcdanlj 感谢您的解释。我确实是诚心提问,并非暗示目前存在任何问题。我不确定我提出的方式是否真的“更好”——也许这会引入许多新问题。我猜想,当前的代码无论由谁编写,其现状必然有其道理。

3 个赞

@RGJ 同样,我真诚地表示,如果任何人能确切知道如何避免重建所有图片的徒劳工程,我非常支持;如果我是经验丰富的 Discourse 开发者并且知道如何轻松完成,我可能一开始就会那样做。

我猜测,对于拥有大量图片的用户来说,迁移出 S3 并不是常见情况,因此与为 Discourse 添加更具普遍实用性的功能相比,这很难值得花费时间。我从 Google+ 导入到 MakerForums 时大约有 100GB 的上传数据,我们当时选择 S3 是考虑到可能会有很多人从那些社区迁移到 MakerForums,但最终只有少数社区活跃迁移,因此持续增长并不支持继续留在 S3。我认为这是一个相当极端的特例,而重新处理图片是一次性成本。

最后,我非常感谢你首先提出这个话题,否则我很容易就会错过非帖子上传的内容。

@RGJ 你显然说得对,销毁并重建并不是好办法。我添加了一个检查:raise "Error: upload url #{url} changed to #{new_upload.short_url}, should be unchanged." if url != new_upload.short_url,这至少能报告源数据损坏的问题。今天 Digital Ocean Spaces 的故障 导致它向我发送了损坏的数据,触发了许多上传的错误——我不知道具体有多少。但由于旧的上传已经被销毁,我现在有一些损坏的页面,而我直到丢失了包含日志的滚动记录(其中指出了哪些页面损坏)后才意识到这一点,因此我甚至无法通过备份手动修复它们。

因此,虽然我的工作相比旧方法有所改进,因为它至少能报告一些错误,但更好的做法是下载文件,检查下载文件的 SHA1 值,然后将其写入本地文件。同时,我已修改我的 PR,使其在任何未处理的错误发生时停止迁移,以至少限制数据丢失。

我仍然认为我的工作应该被合并,因为这是一种帕累托改进:它不会让情况变得更糟,反而有所改善。但最好还是用正确的方式来做。我认为正确的方式是编写 lib/file_store/from_s3_migration.rb,使其与 lib/file_store/to_s3_migration.rb 并行,但我目前还做不到。

由于我的 PR 尚未被审查,我又往里面添加了一些内容。我添加了一个 uploads:report_missing_uploads 任务,它会遍历 raw 字段,查找 upload://... 实例,其中对应的上传对象不存在。至少在我的情况下,借助备份,我将能够检查备份,找到孤立的文件并将其恢复到站点,然后重新生成这些帖子以恢复丢失的图片。我已经找到了 678 个需要搜索的实例,其中目前只找到了 10 个未找到的,因此我很高兴写了这个测试!在我进入重新生成剩余帖子的阶段之前,需要先处理这个问题。

我已完成迁移的第一阶段,不包括对共享上传的受影响帖子进行重新生成,也不包括非帖子上传。在完成另一次远程备份后,我计划测试这些功能并将其添加到我的 PR 中。

我现在已开始测试迁移的下一阶段:重新生成和非帖子上传迁移。我的第一个教训是,由于引用帖子中的非帖子上传头像,将重新生成作为下一步会失败,因此重新生成帖子必须是最后一步。

下一个发现:感谢外键约束!在迁移非帖子上传时,我发现至少需要先迁移个人资料,然后再进行一般的非帖子上传迁移。

ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation: ERROR:  update or delete on table "uploads" violates foreign key constraint "fk_rails_1d362f2e97" on table "user_profiles"

个人资料迁移很棘手,因为我必须迁移附加到个人资料的两个上传,但在某些情况下人们为两者使用了相同的图片,因此我必须分三个阶段完成这项工作。我已成功迁移了我站点上所有带有 S3 URL 的个人资料。

我已迁移了所有非帖子、非个人资料的上传。@RGJ,如果你想试用我发布的分支,它已包含我的最新工作。其中的所有内容都已用于一次完整的站点迁移。

@cvx 我不知道你何时能有机会审查,但如果没有我的 PR,migrate_from_s3 肯定充满了静默数据破坏的漏洞。我所做的并不完美,但它确实防止了我实际遇到的许多漏洞。

我已经完成了目前打算在这里做的所有工作。我已经迁移了我的站点,这意味着我甚至无法有效评估任何请求的更改。如果你拒绝这个 PR,我强烈建议你直接删除现有的迁移,因为它会静默地破坏用户的数据

关于 PR,我有时会在 CI 中出现测试失败(如当前版本),类似于以下内容:

7867 examples, 0 failures, 11 pending, 1 error occurred outside of examples

##[error]Process completed with exit code 1.

这在我本地无法复现(到目前为止),而且我在 CI 日志中看不到任何信息说明 CI 中出了什么问题,因此我不会再浪费时间尝试在 CI 输出中寻找隐藏信息。

最后一点:在完成迁移后,我们发现一些用户的个人资料图片在迁移过程中丢失,他们正在手动恢复。我猜测可能需要额外的代码才能成功完成该操作,但由于我在为时已晚时才意识到这一点,我没有测试用例来验证这一点。因此,这是一个可能在一些配置中引发问题的剩余漏洞,但我已无法再编写额外的修复。

5 个赞

既然我修复 migrate_from_s3 静默数据损坏的 PR 连一眼都没被看过,那至少能否将 migrate_to_s3 明确标注为单向操作?

说实话,目前它简直是个给新手的陷阱。

@cvx @zogstrip 你们倾向于哪种方案?是查看该 PR,还是直接移除 migrate_from_s3,并诚实地说明它只能单向迁移?

5 个赞

抱歉,这次是我的错。我这周会审查这个 PR。

7 个赞

审查意见的核心是:当前的实现方式完全错误,必须彻底推倒重来,采用不同的方案;试图按原有思路进行修复是不可接受的。我不愿接手这项工作,因此我认为 migrate_from_s3 应当被完全移除,而 migrate_to_s3 应明确标注为“不可逆操作”,一旦执行便需永久坚持。目前,Discourse 源码中只要还保留 migrate_from_s3,就存在数据完整性缺陷。

我在进行迁移工作时未能及时获得审查,如今迁移早已完成(尽管出现了一些问题,例如部分头像损坏),我已失去了有效的测试环境。我完全没有信心能正确完成这项工作,因此选择退出这场争论。接下来,只能由下一位认为将图片迁移到类似 S3 的对象存储服务是个好主意的人来解决问题了。抱歉!

2 个赞

@CxV 鉴于您认为之前的迁移完全错误且需要从头开始,请审查并合并此修复数据损坏漏洞的提交:

2 个赞

正如已经指出的那样,我在进行的迁移中遇到了一些问题。这印证了 @cvx 的观点:我编写的迁移脚本完全错误。在有人正确地重写它之前,必须合并移除现有(充满静默数据损坏的)迁移的补丁。(这是一个非常简单的补丁,审查应该很容易。越早合并,就有人试图通过单向门往回走从而毁掉自己网站的可能性就越小。我不知道该如何更强烈地呼吁合并;如果我能做到,我早就做了。编辑:感谢 @cvx 的批准、合并,以及后续跟进,确保大规模迁移被标记为不可逆。)

以下是我遇到的已知问题:

  • 至少 discobot 的头像没有显示;所有 discobot 的参与都显示为通用人物头像(白色背景上的浅灰色头部和肩部)。
  • 其他人的头像也没有正确迁移。许多人是通过手动修复的。
  • 在配置网站时,原始管理员尝试修改了“S3”配置(在我们的案例中是 DigitalOcean Spaces,包括启用和未启用 CDN 的情况),导致一些孤立的优化图片未在迁移中得到修复。

对于任何尝试使用我的分支并遇到图片丢失情况的人,我在 Rails 控制台中运行了一个临时脚本,修复了一些问题。这不是正确的做法,我只是不够熟悉模型,无法正确操作。 我的 Ruby 技能非常薄弱;除了我对 Discourse 的小贡献外,我不怎么接触 Ruby。

但无论如何,这个糟糕的临时脚本修复了损坏的图片(源于初始配置错误),并至少恢复了 discobot 的头像(可能还有其他头像,但我之前知道的所有头像都已经修复了)。

这里没有展示的是我在各步骤之间所做的所有操作,每进行一步我就查看一遍。这只是一次“在 Rails 控制台中探索现有内容”的会话的一部分,并不是我编写并运行的脚本。它没有任何错误处理,而且在我开始进行任何操作之前,我已经对完整网站进行了备份。

我无法用更强烈的语气说明:这段代码不是修复此问题的 Ruby on Rails 惯用方式。我只是想分享我是如何修复自己在之前尝试将网站从 DigitalOcean Spaces 迁移时造成的部分损害的。

require 'set'

uploadids = Set.new
optimizedimages = Set.new
OptimizedImage.where("url like '%UNIQUEPARTOFS3URL%'").each do |oi|
  uploadids.add(oi.upload_id)
  optimizedimages.add(oi)
end

postids = Set.new
uploadids.each do |u|
  PostUpload.where(upload_id: u).each do |pu|
    postids.add(pu.post_id)
  end
end

optimizedimages.each do |oi|
  oi.delete
end

postids.each do |pid|
  Post.where(id: pid).each do |p|
    p.rebake!
  end
end
2 个赞