Помощь в восстановлении: система зависла в полночь

Хорошо, это должно было случиться — слишком долго всё шло слишком хорошо. Годы работы в режиме «автопилота»: система обновлялась автоматически, а я обновлял Discourse каждые несколько недель. В полночь прошлой ночью Amazon показал, что система не отвечает: Discourse был недоступен, а загрузка процессора достигла 100%, пока не закончились ресурсы CPU на AWS. Не удалось войти в систему; единственный раз, когда после нескольких перезагрузок мне удалось на мгновение получить доступ, я увидел в htop процесс, потребляющий много CPU:

snap lxd activate

Если кто-то сталкивался с этим и может объяснить, почему это могло произойти само по себе, буду очень признателен для будущих ссылок.

Переходя к насущной проблеме: я пересобрал новый сервер на AWS под Ubuntu 20 LTS. Настройка Discourse оказалась чрезвычайно простой. У меня была копия файла app.yml, которую я использовал для воссоздания форума Discourse. Старый сервер использовал S3 как для резервных копий, так и для контента (изображения и т. д.).

После создания сервера я загрузил последнюю резервную копию Discourse из S3, вручную загрузил её на сервер Discourse и нажал кнопку «Восстановить». Через несколько минут я получил эту ошибку.

[2022-06-09 09:01:56] ALTER TABLE
[2022-06-09 09:01:56] ALTER TABLE
[2022-06-09 09:01:56] Миграция базы данных...
[2022-06-09 09:02:11] == 20220308201942 CreateUploadReferences: миграция ===========================
-- create_table(:upload_references, {})
   -> 0.0486s
-- add_index(:upload_references, [:upload_id, :target_type, :target_id], {:unique=>true, :name=>"index_upload_references_on_upload_and_target"})
   -> 0.0030s
== 20220308201942 CreateUploadReferences: миграция завершена (0.0580s) ==================

== 20220309132719 CopyPostUploadsToUploadReferences: миграция ================
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT post_uploads.upload_id, 'Post', post_uploads.post_id, uploads.created_at, uploads.updated_at\nFROM post_uploads\nJOIN uploads ON uploads.id = post_uploads.upload_id\nON CONFLICT DO NOTHING\n")
   -> 0.0595s
== 20220309132719 CopyPostUploadsToUploadReferences: миграция завершена (0.0602s) =======

== 20220309132720 CopyPostUploadsToUploadReferencesForSync: миграция =========
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT upload_id, 'Post', post_id, NOW(), NOW()\nFROM post_uploads\nON CONFLICT DO NOTHING\n")
   -> 0.0076s
== 20220309132720 CopyPostUploadsToUploadReferencesForSync: миграция завершена (0.0080s) 

== 20220330160747 CopySiteSettingsUploadsToUploadReferences: миграция ========
-- execute("WITH site_settings_uploads AS (\n  SELECT id, unnest(string_to_array(value, '|'))::integer upload_id\n  FROM site_settings\n  WHERE data_type = 17\n  UNION\n  SELECT id, value::integer\n  FROM site_settings\n  WHERE data_type = 18 AND value != ''\n)\nINSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT site_settings_uploads.upload_id, 'SiteSetting', site_settings_uploads.id, uploads.created_at, uploads.updated_at\nFROM site_settings_uploads\nJOIN uploads ON uploads.id = site_settings_uploads.upload_id\nON CONFLICT DO NOTHING\n")
   -> 0.0034s
== 20220330160747 CopySiteSettingsUploadsToUploadReferences: миграция завершена (0.0038s) 

== 20220330160751 CopyBadgesUploadsToUploadReferences: миграция ==============
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT badges.image_upload_id, 'Badge', badges.id, uploads.created_at, uploads.updated_at\nFROM badges\nJOIN uploads ON uploads.id = badges.image_upload_id\nWHERE badges.image_upload_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0006s
== 20220330160751 CopyBadgesUploadsToUploadReferences: миграция завершена (0.0010s) =====

== 20220330160754 CopyGroupsUploadsToUploadReferences: миграция ==============
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT groups.flair_upload_id, 'Group', groups.id, uploads.created_at, uploads.updated_at\nFROM groups\nJOIN uploads ON uploads.id = groups.flair_upload_id\nWHERE groups.flair_upload_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0050s
== 20220330160754 CopyGroupsUploadsToUploadReferences: миграция завершена (0.0055s) =====

== 20220330160757 CopyUserExportsUploadsToUploadReferences: миграция =========
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT user_exports.upload_id, 'UserExport', user_exports.id, uploads.created_at, uploads.updated_at\nFROM user_exports\nJOIN uploads ON uploads.id = user_exports.upload_id\nON CONFLICT DO NOTHING\n")
   -> 0.0013s
== 20220330160757 CopyUserExportsUploadsToUploadReferences: миграция завершена (0.0041s) 

== 20220330164740 CopyThemeFieldsUploadsToUploadReferences: миграция =========
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT theme_fields.upload_id, 'ThemeField', theme_fields.id, uploads.created_at, uploads.updated_at\nFROM theme_fields\nJOIN uploads ON uploads.id = theme_fields.upload_id\nWHERE type_id = 2\nON CONFLICT DO NOTHING\n")
   -> 0.0006s
== 20220330164740 CopyThemeFieldsUploadsToUploadReferences: миграция завершена (0.0010s) 

== 20220404195635 CopyCategoriesUploadsToUploadReferences: миграция ==========
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT categories.uploaded_logo_id, 'Category', categories.id, uploads.created_at, uploads.updated_at\nFROM categories\nJOIN uploads ON uploads.id = categories.uploaded_logo_id\nWHERE categories.uploaded_logo_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0095s
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT categories.uploaded_background_id, 'Category', categories.id, uploads.created_at, uploads.updated_at\nFROM categories\nJOIN uploads ON uploads.id = categories.uploaded_background_id\nWHERE categories.uploaded_background_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0004s
== 20220404195635 CopyCategoriesUploadsToUploadReferences: миграция завершена (0.0103s) =

== 20220404201949 CopyCustomEmojisUploadsToUploadReferences: миграция ========
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT custom_emojis.upload_id, 'CustomEmoji', custom_emojis.id, uploads.created_at, uploads.updated_at\nFROM custom_emojis\nJOIN uploads ON uploads.id = custom_emojis.upload_id\nWHERE custom_emojis.upload_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0032s
== 20220404201949 CopyCustomEmojisUploadsToUploadReferences: миграция завершена (0.0036s) 

== 20220404203356 CopyUserProfilesUploadsToUploadReferences: миграция ========
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT user_profiles.profile_background_upload_id, 'UserProfile', user_profiles.user_id, uploads.created_at, uploads.updated_at\nFROM user_profiles\nJOIN uploads ON uploads.id = user_profiles.profile_background_upload_id\nWHERE user_profiles.profile_background_upload_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0017s
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT user_profiles.card_background_upload_id, 'UserProfile', user_profiles.user_id, uploads.created_at, uploads.updated_at\nFROM user_profiles\nJOIN uploads ON uploads.id = user_profiles.card_background_upload_id\nWHERE user_profiles.card_background_upload_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0011s
== 20220404203356 CopyUserProfilesUploadsToUploadReferences: миграция завершена (0.0033s) 

== 20220404204439 CopyUserAvatarsUploadsToUploadReferences: миграция =========
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT user_avatars.custom_upload_id, 'UserAvatar', user_avatars.id, uploads.created_at, uploads.updated_at\nFROM user_avatars\nJOIN uploads ON uploads.id = user_avatars.custom_upload_id\nWHERE user_avatars.custom_upload_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0200s
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT user_avatars.gravatar_upload_id, 'UserAvatar', user_avatars.id, uploads.created_at, uploads.updated_at\nFROM user_avatars\nJOIN uploads ON uploads.id = user_avatars.gravatar_upload_id\nWHERE user_avatars.gravatar_upload_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0069s
== 20220404204439 CopyUserAvatarsUploadsToUploadReferences: миграция завершена (0.0276s) 

== 20220404212716 CopyThemeSettingsUploadsToUploadReferences: миграция =======
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT theme_settings.value::int, 'ThemeSetting', theme_settings.id, uploads.created_at, uploads.updated_at\nFROM theme_settings\nJOIN uploads ON uploads.id = theme_settings.value::int\nWHERE data_type = 6 AND theme_settings.value IS NOT NULL AND theme_settings.value != ''\nON CONFLICT DO NOTHING\n")
   -> 0.0025s
== 20220404212716 CopyThemeSettingsUploadsToUploadReferences: миграция завершена (0.0030s) 

== 20220526203356 CopyUserUploadsToUploadReferences: миграция ================
-- execute("INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at)\nSELECT users.uploaded_avatar_id, 'User', users.id, uploads.created_at, uploads.updated_at\nFROM users\nJOIN uploads ON uploads.id = users.uploaded_avatar_id\nWHERE users.uploaded_avatar_id IS NOT NULL\nON CONFLICT DO NOTHING\n")
   -> 0.0227s
== 20220526203356 CopyUserUploadsToUploadReferences: миграция завершена (0.0234s) =======


[2022-06-09 09:02:11] Переподключение к базе данных...
[2022-06-09 09:02:12] Перезагрузка настроек сайта...
[2022-06-09 09:02:12] Отключение исходящей почты для неадминистративных пользователей...
[2022-06-09 09:02:14] Отключение режима только для чтения...
[2022-06-09 09:02:14] Очистка кэша категорий...
[2022-06-09 09:02:14] Перезагрузка переводов...
[2022-06-09 09:02:14] Переназначение загрузок...
[2022-06-09 09:02:14] Восстановление загрузок, это может занять некоторое время...
[2022-06-09 09:03:05] ИСКЛЮЧЕНИЕ: 509 из 1823 загрузок не были перенесены в S3. Перенос в S3 не удался для базы данных 'default'.
[2022-06-09 09:03:05] /var/www/discourse/lib/file_store/to_s3_migration.rb:132:in `raise_or_log'
/var/www/discourse/lib/file_store/to_s3_migration.rb:79:in `migration_successful?'
/var/www/discourse/lib/file_store/to_s3_migration.rb:373:in `migrate_to_s3'
/var/www/discourse/lib/file_store/to_s3_migration.rb:66:in `migrate'
/var/www/discourse/lib/file_store/s3_store.rb:328:in `copy_from'
/var/www/discourse/lib/backup_restore/uploads_restorer.rb:62:in `restore_uploads'
/var/www/discourse/lib/backup_restore/uploads_restorer.rb:44:in `restore'
/var/www/discourse/lib/backup_restore/restorer.rb:61:in `run'
/var/www/discourse/script/spawn_backup_restore.rb:23:in `restore'
/var/www/discourse/script/spawn_backup_restore.rb:36:in `block in <main>'
/var/www/discourse/script/spawn_backup_restore.rb:4:in `fork'
/var/www/discourse/script/spawn_backup_restore.rb:4:in `<main>'

Может ли кто-нибудь подсказать, в чём проблема и как я могу восстановить сервер из резервных копий Amazon S3?

Привет,
после воссоздания нового сервера из app.yml у вас был доступ к резервным копиям в разделе https://your.domain/admin/backups?

Нет, после пересоздания из app.yml у меня просто появился чистый новый набор с пустыми данными. Я загрузил последнюю резервную копию из S3 и вручную загрузил её в локальный Discourse, затем нажал «Восстановить».

Процесс восстановления начался: я увидел, что все настройки (включая S3, учётные данные и всё остальное со страницы настроек) вернулись, появились все категории, отображались посты и всё остальное. Но внезапно через несколько минут появилось сообщение о выходе из системы, все категории и темы исчезли, а в логах появилась эта ошибка (кажется, что произошло откатывание).

:thinking: так что конфигурация S3 отсутствует в app.yml (как описано здесь Configure an S3 compatible object storage provider for uploads)? Но настроена согласно Set up file and image uploads to S3

Нет, я не вижу этого в моем app.yml.

Все настройки S3 были определены на страницах Администрирование → Настройки, и всё работало нормально в течение года, пока мне не пришлось восстановить сервер после его отключения прошлой ночью.

Верно, именно так я настраивал резервное копирование и загрузку файлов в S3.

Я думаю, стоит попробовать отредактировать файл app.yml с вашими настройками, и, как мне кажется (надеюсь?), вы увидите свои резервные копии в разделе администратора и сможете восстановить их оттуда, без ручного импорта и загрузки файлов, которые, по идее, не нужно восстанавливать, но которые, похоже, включены в резервные копии. Правда, я не знаю, почему это не работает…

Я думаю, что это может быть связано с тем, что ваша резервная копия содержит смесь загрузок из S3 и локальных загрузок. Боюсь, это не моя сфера экспертизы, но в этой теме есть обсуждение и обходное решение, которое позволяет обойти эту ошибку. Однако оно было предназначено для значительно меньшего количества ошибок, поэтому, возможно, стоит это учесть:

Спасибо. К сожалению, мой сайт упал, и у меня остались только загрузки и резервные копии в S3. Я предполагаю, что теперь нет способа перенести оставшиеся локальные файлы в S3.

Так что меня интересует: какие у меня есть варианты сейчас? Можно ли восстановить данные из резервных копий S3 и проигнорировать локальные файлы? Я нашёл способ заставить систему игнорировать загрузки в S3, но в результате почти все посты имеют битые ссылки и изображения (вероятно, более 90% контента находится в S3, так как я настроил загрузку в S3 много лет назад).

Итак, обновление для тех, кто, возможно, сталкивается с той же проблемой (по сути, я не могу восстановить данные из резервной копии, и сервер упал из-за сбоя при обновлении системы).

Насколько я понимаю, корень проблемы в том, что существуют как локальные загрузки, так и загрузки в S3. Поэтому при попытке восстановления инструмент восстановления начинает сбоить, так как не знает, как одновременно обрабатывать восстановление из локальных хранилищ и из S3 (возможно, пора Discourse пересмотреть подход к резервному копированию и восстановлению).

Благодарю @RGJ за этот совет: он предложил принудительно заставить Discourse игнорировать загрузку в S3 во время восстановления:

  1. Добавьте строку в ваш файл app.yml: DISCOURSE_ENABLE_S3_UPLOADS=false
  2. Пересоберите Discourse: ./launcher rebuild app
  3. Попробуйте выполнить восстановление (либо через страницу резервного копирования в GUI, либо используя CLI)
  4. После восстановления удалите эту строку из app.yml и снова выполните пересборку.

Хотя это сработало, стоит отметить, что форум был сильно повреждён: категории, настройки и посты восстановились, однако все изображения, ссылки, встроенные документы и т. д. оказались нерабочими и выдавали ошибки.

Радикальное решение:
Мне удалось восстановить старый сервер, извлечь каталог /var/discourse (в формате tar/gz), скопировать его на новый сервер и выполнить ./launcher rebuild app. Это полностью вернуло работоспособность форума, однако фундаментальная проблема остаётся — резервные копии НЕ работают, так как содержат смесь локальных загрузок и загрузок в S3.

Поэтому мне действительно нужна консультация о наилучшем способе окончательно решить эту проблему. Что лучше и проще: перенести все загрузки с локального хранилища в S3 или наоборот, и как это сделать? Весь смысл резервного копирования — помогать в таких ситуациях, как эта, но оно меня подвело, поэтому мне необходимо, чтобы вы помогли разобраться с этим.

Если вы настроите систему, как описано в использовании объектного хранилища для загрузки файлов (S3 и клоны), вы сможете выполнить:

 rake uploads:migrate_to_s3

Если вы хотите прекратить использование S3, откройте консоль Rails и установите:

  SiteSetting.include_s3_uploads_in_backups=true

Затем создайте резервную копию, убедитесь, что в вашем файле app.yml не настроен S3, и восстановите резервную копию. Думаю, это восстановит резервные копии на локальное хранилище.

Однако в любом случае я всё же рекомендую указать ваши ключи и бакет для резервного копирования в переменных окружения в файле app.yml, а затем проверить, что вы можете восстановить их на новом сайте.

Хорошо, я, кажется, немного запутался.

Мне кажется, идеальный вариант — хранить все загрузки локально, а резервные копии (zip-архивы) сохранять в S3. Таким образом, если с сервером что-то случится, резервная копия останется доступной в S3, а сама она будет самодостаточной, без зависимостей, так что её должно быть легко восстановить на новом сервере.

Правильно ли я понял, что мне нужно следовать этим инструкциям:

Если вы хотите перестать использовать S3, откройте консоль Rails и установите:

  SiteSetting.include_s3_uploads_in_backups=true
Затем создайте резервную копию, убедитесь, что в вашем файле app.yml не настроен S3, и восстановите эту резервную копию. Я думаю, что это восстановит резервные копии локально.

а затем:

  1. Отключить опцию enable upload to S3 в разделе Администрирование → Настройки → Файлы
  2. Включить опцию резервного копирования в S3 на странице Администрирование → Настройки → Резервные копии

Правильно?

Вот этот момент меня смутил: зачем мне нужно указывать конфигурацию S3 в файле app.yml?

Это необходимо, чтобы у вас был доступ к резервным копиям через восстановление из командной строки до того, как вы восстановите саму резервную копию. В противном случае вам придется создать учетную запись администратора, затем настроить S3 и только потом выполнить восстановление. Аналогично, любые настройки, которые вы добавляете в базу данных, будут перезаписаны при восстановлении базы данных.

Я считаю, что наилучшей практикой является настройка S3 исключительно через переменные окружения в файле app.yml. Скорее всего, имеет смысл сделать эти настройки скрытыми, если бы не сотни пользователей, которые были бы удивлены, обнаружив, что они исчезли.

Потому что в противном случае у вас возникнут проблемы с восстановлением.

Как восстановить резервную копию из S3 через командную строку? Согласно инструкциям здесь: Restore a backup from the command line
там сказано, что можно поместить файл резервной копии в папку /var/discourse/shared/standalone/backups/default, а затем запустить восстановление из CLI. Я уже делал это по вашей рекомендации ранее (что, к сожалению, привело к битым ссылкам), но этот способ работает.

Как восстановить резервную копию напрямую из S3 через CLI?

cd /var/discourse
./launcher enter app
discourse restore

Он выведет список доступных резервных копий, после чего вы сможете скопировать и вставить ту, которую хотите восстановить.

Спасибо, значит, он будет читать резервные копии S3 и предлагать их в качестве варианта.

Джей, в продолжение вашего предложения переместить активы локально:

Я думаю, вы можете установить скрытую настройку include_s3_uploads_in_backups в значение true, затем создать резервную копию и восстановить её, когда S3 будет отключён, чтобы перестать использовать S3.

Наличие резервных копий S3 с их конфигурацией в app.yml означает, что вы можете выполнить восстановление через командную строку, имея только файл app.yml (после клонирования Discourse и установки Docker).

Для первого шага мне нужно создать резервную копию бакетов S3 или это безопасная операция для бакета?

Что ж, по крайней мере я выяснил, почему мой сервер упал昨晚 (и снова сегодня после полной пересборки :frowning:, подробности см. в этой теме: Ubuntu 20.04 kernel update with docker causing a crash on EC2 and Lightsail)

Итак, чтобы запустить систему из резервной копии, мне пришлось:

  1. Отключить Настройки → Файлывключить загрузки S3
  2. В Настройки → Резервные копииместо хранения резервных копий выбрать S3
  3. Включить Настройки → Резервные копиирезервное копирование с вложениями

Затем я создал резервную копию и успешно восстановил её. Однако что-то пошло не так: все вложения (файлы) теперь содержат некорректные ссылки. Изображения отображаются нормально, но ссылки на вложения, например https://domain.com/uploads/short-url/phu1HOLvkE8LWpkKYfnMPSWsvHh.zip, вызывают ошибку:

Упс! Эта страница не существует или доступна только для авторизованных пользователей.

Есть ли способ исправить эти короткие ссылки?

Попробуйте выполнить пересборку HTML (также известную как rebake) для одной из этих тем, чтобы проверить, исправит ли это проблему.

Спасибо. Есть ли где-нибудь руководство о том, как выполнить команду для выпечки конкретных тем?