DO Spaces 数据中心更改后上传出现问题

各位好,

在尽我所能搜索表单却未找到解决方案后,我特此就最近一次 Digital Ocean 数据中心变更引发的一个异常情况寻求支持。

此前,我们将所有上传文件存储在位于 ams3 数据中心的 Digital Ocean Spaces 存储桶中。在短短一个多月内发生了两次严重的硬件问题并导致服务中断后,上周末我们决定将所有文件迁移至 fra1 数据中心。

以下是我所遵循的步骤:

  1. 为准备迁移,我使用 s3cmd 将 ams3 中的所有文件(即经典的三个目录:originals、optimized 和 tombstone)上传到了 fra1 的新存储桶中。
  2. 我进入论坛设置,更新了附件、CDN 和备份存储桶的新端点。
  3. 我启动了完整的帖子重新渲染(re-bake),期望能一次性解决所有问题。

不幸的是,情况并非如此。大部分附件已成功“迁移”,但仍有数百个未能正常处理。目前尚不清楚具体原因,但这些缺失的附件被移动到了 tombstone 目录中。

我原以为运行 rake 任务 rake uploads:recover_from_tombstone 就能解决这一问题,但事实并非如此。系统虽能识别这些文件,但在任务结束时并未恢复任何附件,帖子中的图片仍然无法显示。

于是我开始深入排查,发现在 Rails 控制台中运行 UploadRecovery.new(dry_run: true).recover(此方法是在查阅元数据时找到的)能提供宝贵信息,例如帖子 URL 以及问题图片的短链接或长链接。

对于返回的短链接格式,我编写了一段 Python 代码,将短文件名“翻译”成长文件名,以便检查文件在存储桶中是否存在。经核查,我可以确认所有缺失的文件均存在,既在新存储桶中,也在旧存储桶中。部分缺失的上传文件如预期般位于 tombstone 目录,但另一些却奇怪地仍留在 original 目录中。文件并未损坏。通过 URL 访问时,它们在两个数据中心均能正常打开;若将其转储到本地 Linux 机器上,也能无错误地打开。

不知为何,上传恢复流程未能识别并修复数据库中相关的错误。:man_shrugging:

因此,我的问题如下:

  • 是否有办法理解:即使上传文件确实存在于 tombstone(或 original)目录中,为何 rake 任务仍无法恢复它们?
  • 在存储桶变更,或从 Digital Ocean 迁移至其他兼容 AWS 的环境时,确保所有附件被正确迁移并准备好进行切换的正确步骤是什么?更一般地说,在这种情况下,应按怎样的步骤逐一操作?显然,简单的重新渲染是不够的。:confused:
  • 任务 posts:invalidate_broken_images 的作用是什么?具体来说,这里的“invalidate”(失效)究竟指什么?

提前感谢!这个问题已经困扰我一周了,我真的需要彻底解决它,否则就要崩溃了 :smiley: :face_with_tongue:
顺便一提,建议手动重新加载 800 多个附件的方案不被视为有效答案。这背后一定存在某种算法层面的原因……:joy:

2 个赞

我认为你在步骤 2 和步骤 3 之间漏掉了 DbHelper.remap('oldbucketurl', 'newbucketurl')

4 个赞

@falco,谢谢你的回复。
是的,起初我确实忘了。
我是在 meta 上翻找时发现的,运行之后它帮助恢复了一些文件。 :confused:
顺便说一下,运行完该命令后,我进行了完整的重新烘焙。

我还能尝试些什么呢?

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 任务警告为错误的文件名。:thinking:

为什么工具声称它是错误的,而应该是 c87c4f08d1a9aac3f43d19722cfd5a94f2544272 呢?

无论如何,我多次运行了 rake uploads:fix_relative_upload_links 任务,然后再次运行 rake uploads:recover_from_tombstone,但似乎没有任何变化。

编辑:
在我更换存储桶之前制作的数据库备份中搜索 487b613752a0c338646fecc942512e5de9afeb3f,我发现上传表中属于该图像的记录显示的正是这个十六进制文件名。因此,我更无法理解为什么 rake 任务会对此提出抱怨。

image

这是 Meta 上最古老的误解之一。
在进行有针对性的重映射后,你不需要重新烘焙。

2 个赞

你说得可能有道理,但问题在于,如果没有开发团队提供的教程或指南,很难确切知道在这些情况下该做什么、不该做什么。人们总有一种感觉,觉得自己本可以做得更多,或者以不同的顺序操作,就像从过去三四年里发布的数十篇帖子中提炼出一套行之有效的“配方”一样。:winking_face_with_tongue:
重新烘焙(Rebaking)似乎成了许多问题的万能解法,而且对已有帖子无害。

这么说可能有点绕,但鉴于人们多次在上传管理等问题上遇到困扰,官方发布一份清晰的指南将是一个非常重要的参考。:wink:

1 个赞

抱歉,需要重新提出这个问题。
上周我花了一些时间阅读上传相关的 rake 任务代码,试图理解 recover_from_tombstonerecover 这两个任务在底层究竟是如何工作的。由于类的封装性,这是一件很困难的事情,可以说我大部分都没搞明白。

不过,我理解到的内容如下(如果我有错,请 @Falco 纠正我):上传文件在磁盘上的文件名是通过将其 SHA1 值与原始扩展名组合生成的。然后,该文件被存储在磁盘或 AWS 上,其目录路径取决于文件名中的第一个(有时是第二个)字母,位于 1X、2X、3X 等目录下(这些是如何确定的,我并不清楚)。最后,SHA1 和文件名以及其他信息一起存储在 PostgreSQL 的 uploads 表记录中。

回到我们更换 Digital Ocean 数据中心时发生的情况,根据我的最佳理解,情况如下:

  1. 我们将所有文件从 ams3 复制到了 fra1。
  2. 我们未能按照 @falco 的建议执行 DbHelper.remap('oldbucketurl', 'newbucketurl'),因为当时我们并不清楚在这种情况下必须这样做。
  3. 我们启动了全局重新构建(rebake)。在这个阶段,成千上万张图片“损坏”了,许多被移入了 tombstone(墓碑状态)。我目前还不完全清楚为什么会这样。
  4. 我意识到出了问题,中断了正在进行的重新构建,并通过在 meta 中搜索找到了 remap 命令。随后我们执行了 DbHelper.remap('oldbucketurl', 'newbucketurl') 任务。
  5. 为了恢复在第 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),该脚本会:

  1. 查找所有包含字符串 upload:// 的帖子
  2. 提取每个上传的短链接,并将其转换为对应的长形式 sha1 哈希
  3. 查询 Uploads 表
  4. 如果在 Uploads 中找到具有该 sha1 哈希的附件,则跳过该上传;否则,检查该上传的 URL 是否存在于旧的 Digital Ocean bucket/spaces 中。
  5. 如果在旧的 bucket/spaces 中找到该链接,则将短链接替换为旧 bucket 中同一上传的 URL。
  6. 如果进行了修改,则触发原始帖子的重新烘焙(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 个赞