Проблемы с загрузками после изменения дата-центра DO Spaces

Уважаемые коллеги,

После того как я тщательно изучил форум, но не нашел решения, обращаюсь за помощью в связи с необычной ситуацией, возникшей после недавнего переноса дата-центра Digital Ocean.

Изначально все наши загруженные файлы хранились в бакете Digital Ocean Spaces в дата-центре ams3. После двух крупных аппаратных сбоев и последующего прерывания работы сервиса в течение чуть более месяца, в прошлые выходные мы решили перенести все файлы в дата-центр fra1.

Вот шаги, которые я предпринял:

  1. В рамках подготовки к переносу я загрузил все файлы из ams3 (три классических директории: originals, optimized и tombstone) в новый бакет в fra1 с помощью утилиты s3cmd.
  2. Зашел в настройки форума и указал новый эндпоинт для вложений, CDN-бакета и резервного копирования.
  3. Запустил полную пересборку постов (full post rebake), рассчитывая, что это исправит все проблемы за один раз.

К сожалению, это не сработало. Большинство вложений были перенесены корректно, но несколько сотен — нет. Мне не до конца ясно, что произошло, но эти отсутствующие вложения оказались перемещенными в директорию tombstone.

Я думал, что запуск задачи rake uploads:recover_from_tombstone решит проблему, но нет. Файлы обнаруживаются, однако по завершении задачи ни одно вложение не восстанавливается, изображения в постах по-прежнему не отображаются.

Я начал копать глубже и выяснил, что запуск команды UploadRecovery.new(dry_run: true).recover (найденной в процессе изучения метаданных) в консоли Rails дает ценную информацию, такую как URL поста, а также короткий или длинный URL проблемного изображения.

Для URL, возвращаемых в коротком формате, я написал небольшой скрипт на Python, чтобы «перевести» короткое имя загруженного файла в длинный формат, что позволило мне проверить наличие файла в бакете.
Я это сделал, и могу подтвердить: все отсутствующие файлы на месте, как в новом, так и в старом бакете. Часть отсутствующих загрузок, как и ожидалось, находится в директории tombstone, но некоторые другие странно остались в директории original. Файлы не повреждены. Если открыть их по ссылке, они корректно загружаются в обоих дата-центрах, а при локальном сохранении на мой Linux-компьютер открываются без ошибок.

Каким-то образом процесс восстановления загрузок не может их обработать и исправить проблемы в базе данных. :man_shrugging:

Итак, мои вопросы:

  • Можно ли понять, почему задача rake не может восстановить вложения, даже если файлы находятся в tombstone (или в original)?
  • Какие правильные шаги необходимо выполнить, чтобы гарантировать, что при смене бакета или переходе с Digital Ocean на другую среду, совместимую с AWS, все вложения будут перенесены и корректно подготовлены к переключению? Более общо: что именно нужно делать шаг за шагом в таком случае? Очевидно, что простой пересборки (rebake) недостаточно. :confused:
  • Что делает задача posts:invalidate_broken_images? Что именно означает термин invalidate?

Заранее спасибо. Я уже неделю бьюсь над этой проблемой, и мне очень нужно наконец ее решить, иначе я сойду с ума :smiley: :stuck_out_tongue:

P.S. Предложение вручную перезагрузить все 800+ вложений не рассматривается как валидный ответ. Должна быть какая-то алгоритмическая причина… :laugh:

Кажется, вы пропустили DbHelper.remap('oldbucketurl', 'newbucketurl') между шагами 2 и 3.

Привет @falco, спасибо за ответ.
Да, изначально я действительно забыл.
Я запустил это после того, как нашёл эту информацию, копаясь здесь на meta. :confused: Это помогло восстановить некоторые файлы.
Кстати, после запуска я сделал полную пересборку.

Что ещё можно попробовать?

Итак, у меня есть предположение, что происходит здесь.
Я не подумал упомянуть факт, связанный с выводом задачи rake uploads:recover_from_tombstone, который может дать намек на что-то интересное.

Кажется, что задача на самом деле находит файлы загрузок в 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.
Вам не нужно делать перевыпечку после точечно направленного ремэппинга.

Возможно, вы правы, но дело в том, что без официального руководства от разработчиков сложно точно понять, что следует делать, а чего — нет в таких случаях.
Всегда возникает ощущение, что стоило бы сделать что-то ещё или в другом порядке — словно приходится выводить рабочую «рецептуру», анализируя десятки постов, написанных за последние 3–4 года. :stuck_out_tongue:
Пересоздание (rebaking) кажется панацеей во многих ситуациях и безопасным для уже существующих постов.

Это сложно сформулированное мнение о том, что учитывая, как часто люди сталкиваются с проблемами при управлении загрузками и тому подобном, хорошее официальное руководство от команды было бы важным ориентиром. :wink:

Извините, нужно снова поднять эту тему.
На прошлой неделе я потратил время на изучение кода задач rake для загрузки файлов, пытаясь понять, что происходит внутри методов recover_from_tombstone и recover.
Это сложно из-за инкапсуляции классов, поэтому я, пожалуй, в основном не справился.

Однако я понял следующее (поправьте меня, @Falco, если я ошибаюсь): имя файла на диске для загруженного файла формируется путем объединения его SHA1 и оригинального расширения. Затем он сохраняется на диске или в AWS в каталоге, путь к которому зависит от первой, а иногда и второй буквы имени файла, в пределах 1X, 2X, 3X и так далее (как именно определяются эти значения — я не понимаю).
Наконец, SHA1 и имя файла, среди прочего, сохраняются в записях таблицы uploads в PostgreSQL.

Возвращаясь к тому, что произошло во время смены дата-центра Digital Ocean, вот как я это понимаю:

  1. Мы скопировали все файлы из ams3 в fra1.
  2. Мы не выполнили DbHelper.remap('oldbucketurl', 'newbucketurl'), как рекомендовал @falco, но нам не было ясно, что в данном случае это необходимо сделать.
  3. Мы запустили глобальную пересборку (rebuild). На этом этапе тысячи изображений «сломались», и многие были перемещены в tombstone. Мне не совсем понятно, почему это произошло.
  4. Я понял, что что-то не так, прервал текущую пересборку и, поискав здесь, в Meta, обнаружил команду remap. Мы запустили задачу DbHelper.remap('oldbucketurl', 'newbucketurl').
  5. Чтобы восстановить изображения, перемещенные в tombstone на шаге 3, мы запустили 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, мы буквально выгружали их локально из старого бакета и сразу же загружали в новый.

Почему SHA1, рассчитанный Discourse, вообще может отличаться?

Возможно ли заставить задачу recover игнорировать расхождение в SHA1 и просто адаптировать импорт в базе данных к тому, что есть, или переименовать существующие файлы с новым SHA1 при их восстановлении?

Не упускаю ли я что-то очевидное? Спасибо всем за помощь.

Итак, чтобы закрыть эту тему и, возможно, помочь кому-то ещё, вот как мы решили проблему.

По сути, поскольку восстановить отсутствующие вложения с помощью различных задач восстановления загрузок (rake tasks) было невозможно, я написал скрипт на Ruby (извините заранее, я определённо НЕ разработчик Ruby или Rails, так что код, скорее всего, неэффективен и некрасив, но это не главное :stuck_out_tongue: ), который:

  1. Находит все сообщения, содержащие строку upload://.
  2. Извлекает короткую ссылку каждой загрузки и преобразует её в её длинную форму — хэш sha1.
  3. Запрашивает таблицу Uploads.
  4. Если в таблице Uploads найдено вложение с данным хэшем sha1, загрузка пропускается; иначе проверяется URL этой загрузки в старом хранилище Digital Ocean (bucket/spaces).
  5. Если ссылка найдена в старом хранилище, короткая ссылка заменяется на 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
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