各位好,
在尽我所能搜索表单却未找到解决方案后,我特此就最近一次 Digital Ocean 数据中心变更引发的一个异常情况寻求支持。
此前,我们将所有上传文件存储在位于 ams3 数据中心的 Digital Ocean Spaces 存储桶中。在短短一个多月内发生了两次严重的硬件问题并导致服务中断后,上周末我们决定将所有文件迁移至 fra1 数据中心。
以下是我所遵循的步骤:
为准备迁移,我使用 s3cmd 将 ams3 中的所有文件(即经典的三个目录:originals、optimized 和 tombstone)上传到了 fra1 的新存储桶中。
我进入论坛设置,更新了附件、CDN 和备份存储桶的新端点。
我启动了完整的帖子重新渲染(re-bake),期望能一次性解决所有问题。
不幸的是,情况并非如此。大部分 附件已成功“迁移”,但仍有数百个未能正常处理。目前尚不清楚具体原因,但这些缺失的附件被移动到了 tombstone 目录中。
我原以为运行 rake 任务 rake uploads:recover_from_tombstone 就能解决这一问题,但事实并非如此。系统虽能识别这些文件,但在任务结束时并未恢复任何附件,帖子中的图片仍然无法显示。
于是我开始深入排查,发现在 Rails 控制台中运行 UploadRecovery.new(dry_run: true).recover(此方法是在查阅元数据时找到的)能提供宝贵信息,例如帖子 URL 以及问题图片的短链接或长链接。
对于返回的短链接格式,我编写了一段 Python 代码,将短文件名“翻译”成长文件名,以便检查文件在存储桶中是否存在。经核查,我可以确认所有缺失的文件均存在 ,既在新存储桶中,也在旧存储桶中。部分缺失的上传文件如预期般位于 tombstone 目录,但另一些却奇怪地仍留在 original 目录中。文件并未损坏。通过 URL 访问时,它们在两个数据中心均能正常打开;若将其转储到本地 Linux 机器上,也能无错误地打开。
不知为何,上传恢复流程未能识别并修复数据库中相关的错误。
因此,我的问题如下:
是否有办法理解:即使上传文件确实存在于 tombstone(或 original)目录中,为何 rake 任务仍无法恢复它们?
在存储桶变更,或从 Digital Ocean 迁移至其他兼容 AWS 的环境时,确保所有附件被正确迁移并准备好进行切换的正确步骤是什么?更一般地说,在这种情况下,应按怎样的步骤逐一操作?显然,简单的重新渲染是不够的。
任务 posts:invalidate_broken_images 的作用是什么?具体来说,这里的“invalidate”(失效)究竟指什么?
提前感谢!这个问题已经困扰我一周了,我真的需要彻底解决它,否则就要崩溃了
顺便一提,建议手动重新加载 800 多个附件的方案不被视为有效答案。这背后一定存在某种算法层面的原因……
2 个赞
Falco
(Falco)
2021 年6 月 2 日 21:56
2
marcozambi:
为准备迁移,我使用 s3cmd 将我们在 ams3 上的所有文件(3 个经典目录的原始文件、优化文件和墓碑文件)上传到了 fra1 的新存储桶。
我进入论坛设置,设置了附件、CDN 和备份存储桶的新端点。
我启动了完整的帖子重新烘焙,期望能一次性修复所有问题。
我认为你在步骤 2 和步骤 3 之间漏掉了 DbHelper.remap('oldbucketurl', 'newbucketurl')。
4 个赞
嗨 @falco ,谢谢你的回复。
是的,起初我确实忘了。
我是在 meta 上翻找时发现的,运行之后它帮助恢复了一些文件。
顺便说一下,运行完该命令后,我进行了完整的重新烘焙。
我还能尝试些什么呢?
1 个赞
所以,我可能知道这里发生了什么。
我没想到要提到一个与 rake uploads:recover_from_tombstone 任务输出相关的事实,这可能指向一些有趣的线索。
看起来该任务确实找到了墓碑中的上传文件,但它向我抛出了一个警告,指出某个内容(即上传的完整文件名)不正确。例如:
Warning /t/i-miei-modellini-volanti/28272/212 had an incorrect 487b613752a0c338646fecc942512e5de9afeb3f should be c87c4f08d1a9aac3f43d19722cfd5a94f2544272 storing in custom field 'rake uploads:fix_relative_upload_links' can fix this
在我的本地上传目录副本上运行 find 命令后发现,我确实有一个名为 487b613752a0c338646fecc942512e5de9afeb3f.jpeg 的文件。
该特定上传的短链接是 upload://alcIv6jVlmjiEOEBh8fNDJyRms7.jpeg。应用计算对应完整文件名的 base62 算法后,得出的值正好是 487b613752a0c338646fecc942512e5de9afeb3f,这正是 recover_from_tombstone 任务警告为错误的文件名。
为什么工具声称它是错误的,而应该是 c87c4f08d1a9aac3f43d19722cfd5a94f2544272 呢?
无论如何,我多次运行了 rake uploads:fix_relative_upload_links 任务,然后再次运行 rake uploads:recover_from_tombstone,但似乎没有任何变化。
编辑:
在我更换存储桶之前制作的数据库备份中搜索 487b613752a0c338646fecc942512e5de9afeb3f,我发现上传表中属于该图像的记录显示的正是这个十六进制文件名。因此,我更无法理解为什么 rake 任务会对此提出抱怨。
RGJ
(Richard - Communiteq)
2021 年6 月 3 日 08:16
5
marcozambi:
顺便提一下,运行后我进行了完整的重新烘焙。
这是 Meta 上最古老的误解之一。
在进行有针对性的重映射后,你不需要重新烘焙。
2 个赞
你说得可能有道理,但问题在于,如果没有开发团队提供的教程或指南,很难确切知道在这些情况下该做什么、不该做什么。人们总有一种感觉,觉得自己本可以做得更多,或者以不同的顺序操作,就像从过去三四年里发布的数十篇帖子中提炼出一套行之有效的“配方”一样。
重新烘焙(Rebaking)似乎成了许多问题的万能解法,而且对已有帖子无害。
这么说可能有点绕,但鉴于人们多次在上传管理等问题上遇到困扰,官方发布一份清晰的指南将是一个非常重要的参考。
1 个赞
抱歉,需要重新提出这个问题。
上周我花了一些时间阅读上传相关的 rake 任务代码,试图理解 recover_from_tombstone 和 recover 这两个任务在底层究竟是如何工作的。由于类的封装性,这是一件很困难的事情,可以说我大部分都没搞明白。
不过,我理解到的内容如下(如果我有错,请 @Falco 纠正我):上传文件在磁盘上的文件名是通过将其 SHA1 值与原始扩展名组合生成的。然后,该文件被存储在磁盘或 AWS 上,其目录路径取决于文件名中的第一个(有时是第二个)字母,位于 1X、2X、3X 等目录下(这些是如何确定的,我并不清楚)。最后,SHA1 和文件名以及其他信息一起存储在 PostgreSQL 的 uploads 表记录中。
回到我们更换 Digital Ocean 数据中心时发生的情况,根据我的最佳理解,情况如下:
我们将所有文件从 ams3 复制到了 fra1。
我们未能按照 @falco 的建议执行 DbHelper.remap('oldbucketurl', 'newbucketurl'),因为当时我们并不清楚在这种情况下必须这样做。
我们启动了全局重新构建(rebake)。在这个阶段,成千上万张图片“损坏”了,许多被移入了 tombstone(墓碑状态)。我目前还不完全清楚为什么会这样。
我意识到出了问题,中断了正在进行的重新构建,并通过在 meta 中搜索找到了 remap 命令。随后我们执行了 DbHelper.remap('oldbucketurl', 'newbucketurl') 任务。
为了恢复在第 3 步中被移入 tombstone 的图片,我们执行了 rake uploads:recover_from_tombstone。该任务恢复了一些文件,但仍有数百个未能恢复,并显示了关于文件 SHA1 的错误,例如:
Warning /t/eclisse-parziale-di-sole-04-01-2011/14456/50 had an incorrect 3f5a1c136b97aebac4a188432c8e3ab7487f3bca should be ec88ee9eea18f3b8424bfef796345c68582911b5 storing in custom field 'rake uploads:fix_relative_upload_links' can fix this
这似乎表明文件在某种程度上被修改了,因此 SHA1 值发生了变化,导致这些文件的恢复失败。
我们在两个数据中心之间移动文件时从未修改过这些文件。使用 s3cmd 时,我们只是将文件从旧存储桶本地转储出来,然后立即重新上传到新存储桶。
那么,为什么 Discourse 计算出的 SHA1 会不同呢?
是否有可能强制 recover 任务忽略 SHA1 的不一致,直接根据数据库中现有的内容进行导入,或者在恢复时将现有文件重命名为新的 SHA1 值?
我是否忽略了某些显而易见的问题?感谢大家的帮助。
那么,为了给这个线程一个可能对其他人也有用的收尾,以下是我们解决该情况的方法。
基本上,由于无法通过各种上传恢复 rake 任务找回丢失的附件,我编写了一个 Ruby 脚本(提前致歉,我绝对不是 Ruby 或 Rails 开发者,所以代码可能效率低下且丑陋,但这无关紧要 :P),该脚本会:
查找所有包含字符串 upload:// 的帖子
提取每个上传的短链接,并将其转换为对应的长形式 sha1 哈希
查询 Uploads 表
如果在 Uploads 中找到具有该 sha1 哈希的附件,则跳过该上传;否则,检查该上传的 URL 是否存在于旧的 Digital Ocean bucket/spaces 中。
如果在旧的 bucket/spaces 中找到该链接,则将短链接替换为旧 bucket 中同一上传的 URL。
如果进行了修改,则触发原始帖子的重新烘焙(rebake),让 Discourse 负责重新下载“丢失”的上传到本地,并在数据库中重建所有所需内容。
为避免被拉黑并减轻服务器负载,每次请求重新烘焙时都会引入 20 秒的间隔。
def remoteFileExist(url, retries=3)
puts "Requesting #{url} ..."
uri = URI(url)
response = nil
res = Net::HTTP.get_response(uri)
puts res['content-type']
if res.code[0,1] == "2" and res['content-type'].include? 'image'
return true
else
return false
end
rescue Net::ReadTimeout => e
puts "TRY #{retries}/n ERROR: timed out while trying to connect #{e}"
if retries <= 1
raise
end
remoteFileExist(url, retries - 1)
end
####################################################################
posts=Post.where("raw like '%upload://%' ").order('topic_id ASC, post_number DESC');
idx = 0;
posts.each do |p|
idx = idx + 1;
puts ""
matches = p.raw.scan(/(!\[(.)*\]\(upload:\/\/([a-zA-Z0-9]+)\.(jpeg|jpg|png|gif|pdf|mp3|mp4|mov)\))/)
new_raw = p.raw
matches.each do |m|
short_url = m[0];
short_sha = m[2];
ext = m[3];
long_sha = Base62.decode(short_sha).to_s(16).rjust(40,"0")
upload = Upload.where('sha1 = ?', long_sha)
puts "#{short_url} -> #{long_sha}\n"
if upload.all.count == 0
puts "#{long_sha} not found in DB. Recovering from ams3...\n"
subdir1 = long_sha[0]
subdir2 = long_sha[1]
new_url1 = "https://discourse-data.ams3.digitaloceanspaces.com/original/3X/#{subdir1}/#{subdir2}/#{long_sha}.#{ext}"
test1 = remoteFileExist(new_url1)
if test1
new_raw = new_raw.gsub(short_url, "\n#{new_url1}")
else
new_url2 = "https://discourse-data.ams3.digitaloceanspaces.com/original/2X/#{subdir1}/#{long_sha}.#{ext}"
if remoteFileExist(new_url2)
new_raw = new_raw.gsub(short_url, "\n#{new_url2}")
end
end
puts ""
sleep 5
end
end
if p.raw != new_raw
puts "OLD\n"
puts p.raw
puts "-----------"
puts "NEW\n"
puts new_raw
puts "-----------"
puts "UPDATING!"
# goahead = gets
p.raw = new_raw
p.cooked = ''
p.save
p.rebake!(invalidate_broken_images: true);
puts "*******************************************"
sleep 30
else
puts "SKIP!"
puts "*******************************************"
sleep 1
end
end
1 个赞