복구 도움 요청 - 자정에 시스템이 멈춤

결국 이런 일이 일어나리라 예상했다. 그동안 너무 일이 순조롭게 흘러갔다. 수년간 크루즈 컨트롤처럼 시스템을 운영해 왔는데, 시스템이 자동으로 업데이트되고 나는 몇 주에 한 번씩 Discourse를 업데이트하는 정도였다. 어젯밤 자정에 Amazon에서 시스템이 응답하지 않는다는 알림을 받았고, Discourse는 다운되어 있었으며 CPU 사용률이 100%까지 치솟아 AWS CPU 리소스가 소진될 때까지 계속됐다. 시스템에 로그인할 수 없었으며, 여러 번 재부팅 끝에 드디어 잠시 로그인에 성공했을 때 htop에서 CPU를 많이 차지하는 것을 확인했다

snap lxd activate

누군가 이런 현상을 본 적이 있다면, 왜 이런 일이 스스로 일어났는지 설명해 주시면 향후 참고할 수 있어 매우 감사하겠다.

가장 시급한 문제인 서버 복구 작업으로 넘어가겠다. AWS에서 Ubuntu 20LTS를 사용하여 새 서버를 구축했고, Discourse 설정은 매우 쉬웠다. app.yml 파일의 사본이 있어 이를 이용해 Discourse 포럼을 재구성했다. 기존 서버는 백업과 콘텐츠(이미지 등) 모두에 S3를 사용하고 있었다.

서버 생성 후, S3에서 최신 Discourse 백업 파일을 다운로드하여 수동으로 Discourse 서버에 업로드하고 복원(Restore) 버튼을 눌렀다. 몇 분 후 다음과 같은 오류가 발생했다.

[2022-06-09 09:01:56] ALTER TABLE
[2022-06-09 09:01:56] ALTER TABLE
[2022-06-09 09:01:56] Migrating the database...
[2022-06-09 09:02:11] == 20220308201942 CreateUploadReferences: migrating ===========================
-- 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: migrated (0.0580s) ==================

== 20220309132719 CopyPostUploadsToUploadReferences: migrating ================
-- 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: migrated (0.0602s) =======

== 20220309132720 CopyPostUploadsToUploadReferencesForSync: migrating =========
-- 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: migrated (0.0080s) 

== 20220330160747 CopySiteSettingsUploadsToUploadReferences: migrating ========
-- 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: migrated (0.0038s) 

== 20220330160751 CopyBadgesUploadsToUploadReferences: migrating ==============
-- 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: migrated (0.0010s) =====

== 20220330160754 CopyGroupsUploadsToUploadReferences: migrating ==============
-- 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: migrated (0.0055s) =====

== 20220330160757 CopyUserExportsUploadsToUploadReferences: migrating =========
-- 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: migrated (0.0041s) 

== 20220330164740 CopyThemeFieldsUploadsToUploadReferences: migrating =========
-- 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: migrated (0.0010s) 

== 20220404195635 CopyCategoriesUploadsToUploadReferences: migrating ==========
-- 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: migrated (0.0103s) =

== 20220404201949 CopyCustomEmojisUploadsToUploadReferences: migrating ========
-- 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: migrated (0.0036s) 

== 20220404203356 CopyUserProfilesUploadsToUploadReferences: migrating ========
-- 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: migrated (0.0033s) 

== 20220404204439 CopyUserAvatarsUploadsToUploadReferences: migrating =========
-- 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: migrated (0.0276s) 

== 20220404212716 CopyThemeSettingsUploadsToUploadReferences: migrating =======
-- 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: migrated (0.0030s) 

== 20220526203356 CopyUserUploadsToUploadReferences: migrating ================
-- 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: migrated (0.0234s) =======


[2022-06-09 09:02:11] Reconnecting to the database...
[2022-06-09 09:02:12] Reloading site settings...
[2022-06-09 09:02:12] Disabling outgoing emails for non-staff users...
[2022-06-09 09:02:14] Disabling readonly mode...
[2022-06-09 09:02:14] Clearing category cache...
[2022-06-09 09:02:14] Reloading translations...
[2022-06-09 09:02:14] Remapping uploads...
[2022-06-09 09:02:14] Restoring uploads, this may take a while...
[2022-06-09 09:03:05] EXCEPTION: 509 of 1823 uploads are not migrated to S3. S3 migration failed for db '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 설정은 모두 Admin → Settings 페이지에서 정의했으며, 서버가 어제 밤에 다운되기까지 1년 동안 정상적으로 작동했습니다. 그때 복원이 필요했기 때문입니다.

맞습니다. S3 백업과 업로드를 설정할 때 이 방법을 사용했습니다.

앱 설정을 반영하여 app.yml 파일을 수정해 보는 걸 추천합니다. 그렇게 하면 관리자 섹션에서 백업 파일을 확인하고 거기서 바로 복원할 수 있을 거라고 생각합니다(희망은 하지만요). 수동 가져오기나 업로드 과정은 생략할 수 있을 텐데, 복원할 필요가 없는 것들이 백업에 포함되어 있는 것 같네요. 왜 실패하는 건지는 모르겠네요…

이 문제는 백업에 S3 업로드와 로컬 업로드가 혼합되어 있기 때문일 수 있습니다. 제 전문 분야는 아니지만, 해당 오류를 우회할 수 있는 해결책과 관련 논의가 아래 주제에서 이루어지고 있습니다. 다만, 해당 해결책은 오류 수가 훨씬 적은 경우에 적용된 것이므로 이 점을 고려해 주시면 좋겠습니다:

감사합니다. 불행히도 제 사이트가 충돌해서 지금은 S3 업로드와 백업만 남아 있습니다. 남은 로컬 파일을 이제 S3로 마이그레이션할 방법이 없다고 가정하고 있습니다.

그래서 지금 제 선택지가 무엇인지 궁금합니다. S3 백업에서 복원하되 로컬 파일은 무시하는 방법이 있을까요? S3 업로드를 무시하는 방법은 찾았지만, 그렇게 하면 게시물의 링크와 이미지가 거의 다 깨집니다(수년 전부터 S3 업로드를 설정해 왔기 때문에 90% 이상이 S3에 있을 가능성이 큽니다).

같은 문제를 겪고 계신 분들을 위해 업데이트를 공유합니다. (결국 백업에서 복원할 수 없고, 시스템 업그레이드 오류로 인해 서버가 충돌한 상태입니다.)

제 이해에 따르면, 이 문제의 근본 원인은 로컬 업로드와 S3 업로드가 모두 존재하기 때문인 것 같습니다. 복원 도구가 로컬과 S3 복원을 동시에 처리하는 방법을 알지 못해 오류가 발생하는 것 같습니다. (아마도 Discourse가 백업/복원 방식을 재검토할 때가 된 것 같습니다.)

이 팁을 알려주신 @RGJ님께 감사드립니다. 그는 복원할 때 S3 업로드를 무시하도록 Discourse를 강제하는 방법을 제안했습니다:

  1. app.yml 파일에 DISCOURSE_ENABLE_S3_UPLOADS=false라는 한 줄을 추가합니다.
  2. ./launcher rebuild app 명령어로 Discourse를 다시 빌드합니다.
  3. 복원을 시도합니다. (GUI의 백업 페이지를 사용하거나 CLI을 사용할 수 있습니다.)
  4. 복원이 완료된 후, app.yml에서 해당 줄을 삭제하고 한 번 더 다시 빌드합니다.

이 방법은 작동했지만, 포럼이 심각하게 손상되어 있다는 점을 유의해야 합니다. 카테고리, 설정 및 게시글은 복원되었으나, 모든 이미지, 링크, 내장 문서 등이 깨지고 오류가 발생했습니다.

마지막 수단으로 시도한 해결책:
기존 서버를 구해 /var/discourse 디렉터리(tar/gz)를 추출한 후, 새 서버에 복사하고 ./launcher rebuild app을 실행했습니다. 이를 통해 포럼의 기능이 완전히 복구되었지만, 근본적인 문제는 여전히 남아 있습니다. 로컬 업로드와 S3 업로드가 혼재되어 있기 때문에 백업이 작동하지 않습니다.

따라서 이 문제를 근본적으로 해결하는 최선의 방법에 대한 조언이 정말 필요합니다. 모든 업로드를 로컬에서 S3로 옮기는 것이 더 나은가, 아니면 S3에서 로컬로 옮기는 것이 더 나은가요? 그리고 어떻게 해야 하나요? 백업의 본질적인 목적은 이런 상황에서 도움을 주는 것이지만, 제 경우에는 실패했으므로 이 문제를 제대로 정리해 주시길 부탁드립니다.

업로드를 위한 객체 저장소 사용 (S3 & Clones)에 설명된 대로 구성하면 다음을 실행할 수 있습니다.

 rake uploads:migrate_to_s3

S3 사용을 중단하려면 rails 콘솔에 진입하여 다음을 설정할 수 있습니다.

  SiteSetting.include_s3_uploads_in_backups=true

그 후 백업을 수행하고, app.yml 파일에 S3가 설정되지 않았는지 확인한 뒤 백업을 복원하세요. 이렇게 하면 백업이 로컬로 복원될 것으로 생각합니다.

어떤 경우든, app.yml 파일의 ENV 변수에 키와 백업 버킷을 설정하고, 이를 새 사이트로 복원할 수 있는지 확인하는 것을 여전히 권장합니다.

음, 여기에서 조금 혼란스러웠던 것 같습니다.

이상적인 방법은 모든 업로드를 로컬에 두고 백업 파일(zip)만 S3에 저장하는 것이라고 생각합니다. 이렇게 하면 서버에 문제가 발생하더라도 S3에서 백업 파일을 사용할 수 있고, 백업 자체는 의존성이 없는 독립적인 구조이므로 새 서버에 복원하기가 쉬울 것입니다.

제가 이해한 대로라면, 다음 지침을 따르는 것이 맞습니다:

S3 사용을 중단하려면 rails 콘솔에 들어가서 다음을 설정할 수 있습니다.

  SiteSetting.include_s3_uploads_in_backups=true
그 다음 백업을 생성하고, app.yml 파일에 S3가 설정되어 있지 않은지 확인한 뒤 백업을 복원하세요. 이렇게 하면 백업이 로컬로 복원될 것이라고 생각합니다.

그리고 나서

  1. Admin → Settings → Files에서 enable upload to S3 옵션을 비활성화합니다.
  2. Admin → Settings → Backups 페이지에서 S3 백업 옵션을 활성화합니다.

이렇게 하면 맞나요?

이 부분이 저를 혼란스럽게 했습니다. 왜 S3 설정을 app.yml 파일에 넣고 싶은 건가요?

백업 복원 전에 커맨드 라인으로 백업에 접근할 수 있도록 하기 위해서입니다. 그렇지 않으면 관리자 계정을 설정한 후 S3를 구성하고, 그 다음에야 복원을 진행해야 합니다. 마찬가지로, 데이터베이스에 설정한 값도 데이터베이스를 복원할 때 덮어쓰기됩니다.

최선의 관행은 app.yml 파일에서 S3를 환경 변수(ENV variables)를 통해서만 구성하는 것이라고 생각합니다. 수백 명의 사용자가 설정이 사라진 것에 놀라지 않도록 하기 위해, 이를 숨김 설정으로 만드는 것이 합리적일 것입니다.

그렇지 않으면 복원하는 데 문제가 생길 수 있기 때문입니다.

명령줄을 통해 S3에서 백업을 복원하는 방법은 무엇인가요? 여기 설명에 따르면: Restore a backup from the command line
백업 파일을 /var/discourse/shared/standalone/backups/default 폴더에 넣고 CLI에서 복원을 시작할 수 있다고 합니다. 이전에 제안해 주신 방법으로 그렇게 했었는데요(결국 불행히도 깨진 링크로 이어졌지만), 그 방법은 작동했습니다.

CLI를 통해 S3에서 직접 복원하는 방법은 무엇인가요?

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

사용 가능한 백업 목록이 출력되며, 복원할 백업을 복사/붙여넣기할 수 있습니다.

감사합니다. 그러면 S3 백업을 읽고 이를 옵션으로 나열하겠군요.

제이, 자원이 로컬로 이동되어야 한다는 제안을 이어가자면:

숨겨진 설정인 include_s3_uploads_in_backups를 true로 설정한 뒤, S3를 끄고 백업과 복원을 수행하여 S3 사용을 중단할 수 있다고 생각합니다.

app.yml에 S3 백업이 구성되어 있으면, discourse를 클론하고 docker를 설치한 후 app.yml 파일만으로 커맨드라인 복원이 가능합니다.

첫 번째 단계에서 S3 버킷을 백업해야 하는지, 아니면 이 작업이 버킷에 안전한 작업인지 궁금합니다.

음, 어쨌든 내 서버가 지난밤에 (그리고 완전 재빌드 후 오늘 또 :frowning: ) 충돌한 이유를 알아냈습니다. 자세한 내용은 이 토픽을 참고하세요: Ubuntu 20.04 kernel update with docker causing a crash on EC2 and Lightsail

백업에서 다시 실행하려면 다음을 수행해야 했습니다.

  1. 설정 → 파일 → enable s3 uploads 비활성화
  2. 설정 → 백업 → backup location → S3
  3. 설정 → 백업 → backup with uploads 활성화

그 후 백업을 수행했고 성공적으로 복원할 수 있었습니다. 하지만 한 가지 문제가 발생했는데, 모든 첨부 파일(파일)의 링크가 무효가 되었습니다. 이미지는 모두 정상이지만, https://domain.com/uploads/short-url/phu1HOLvkE8LWpkKYfnMPSWsvHh.zip와 같은 첨부 파일 링크는 이제 오류를 표시합니다.

Oops! That page doesn’t exist or is private.

이러한 short-url 링크를 수정하는 방법이 있을까요?

해당 주제 중 하나에서 HTML 재구축(또는 rebake)을 시도해 보시는 것이 좋습니다. 이렇게 하면 문제가 해결되는지 확인할 수 있습니다.

감사합니다. 특정 주제를 베이킹하기 위해 명령어를 실행하는 방법에 대한 가이드가 어딘가에 있나요?