Импорт не удаётся: «не удалось создать уникальный индекс»

TL;DR: Мы допустили ошибку при обновлении и ищем помощь


Мы используем Home Assistant и Discourse для работы нашего сообщества. Мы работаем с методом discourse_docker на экземпляре EC2 в AWS.

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

Чтобы усугубить ситуацию, при предыдущем обновлении мы зафиксировали версию Postgres на 9.5, так как у нас не было достаточно места на диске для перехода на Postgres 10. Мы так и не решили эту проблему.

Кроме того, мы однажды внесли изменения в шаблон Cloudflare и закоммитили их в репозиторий, что помешало ветке docker_discourse обновиться до последней версии.

Вчера мы решили провести обновление…

При миграции базы данных возникла проблема: использовался синтаксис, несовместимый с версией 9.5:

== 20200429095034 AddTopicThumbnailInformation: migrating =====================
-- execute("ALTER TABLE posts\nADD COLUMN IF NOT EXISTS image_upload_id bigint\n")

Мы быстро поняли проблему с фиксированной версией 9.5 и решили мигрировать на Postgres 10. Это не сработало, и мы получили ошибку:

I, [2020-06-12T00:30:55.448351 #1]  INFO -- : Upgrading PostgreSQL from version 9.5 to 10
WARNING: Upgrading PostgresSQL would require an addtional 89M of disk space
Please free up some space, or expand your disk, before continuing.

У нас было доступно 47 ГБ, что казалось странным. Затем мы поняли, что discourse_docker устарел, и обновили его до последней версии. К нашему удивлению, только что вышла версия Postgres 12.

После повторного запуска rebuild на этот раз мы получили следующую ошибку:

I, [2020-06-12T00:41:17.378129 #1]  INFO -- : Upgrading PostgreSQL from version 9.5 to 12
WARNING: Upgrading PostgresSQL would require an addtional 92G of disk space
Please free up some space, or expand your disk, before continuing.

Это уже немного больше места, но ладно. Давайте просто увеличим объем диска до 300 ГБ и попробуем снова.

На этот раз pg_upgrade прервался во время миграции:

Restoring database schemas in the new cluster
  template1
  discourse

*failure* Consult the last few lines of "pg_upgrade_dump_16384.log" for the probable cause of the failure. Failure, exiting

Когда мы посмотрели файл pg_upgrade_dump_16384.log, то увидели следующую ошибку:

pg_restore: creating VIEW "postgres_exporter.pg_stat_activity"
pg_restore: [archiver (db)] Error while PROCESSING TOC:
pg_restore: [archiver (db)] Error from TOC entry 721; 1259 678554 VIEW pg_stat_activity postgres
pg_restore: [archiver (db)] could not execute query: ERROR:  column pg_stat_activity.waiting does not exist
LINE 27:     "pg_stat_activity"."waiting",
             ^
    Command was:
-- For binary upgrade, must preserve pg_type oid
SELECT pg_catalog.binary_upgrade_set_next_pg_type_oid('678556'::pg_catalog.oid);


-- For binary upgrade, must preserve pg_type array oid
SELECT pg_catalog.binary_upgrade_set_next_array_pg_type_oid('678555'::pg_catalog.oid);


-- For binary upgrade, must preserve pg_class oids
SELECT pg_catalog.binary_upgrade_set_next_heap_pg_class_oid('678554'::pg_catalog.oid);

CREATE VIEW "postgres_exporter"."pg_stat_activity" AS
 SELECT "pg_stat_activity"."datid",
    "pg_stat_activity"."datname",
    "pg_stat_activity"."pid",
    "pg_stat_activity"."usesysid",
    "pg_stat_activity"."usename",
    "pg_stat_activity"."application_name",
    "pg_stat_activity"."client_addr",
    "pg_stat_activity"."client_hostname",
    "pg_stat_activity"."client_port",
    "pg_stat_activity"."backend_start",
    "pg_stat_activity"."xact_start",
    "pg_stat_activity"."query_start",
    "pg_stat_activity"."state_change",
    "pg_stat_activity"."waiting",
    "pg_stat_activity"."state",
    "pg_stat_activity"."backend_xid",
    "pg_stat_activity"."backend_xmin",
    "pg_stat_activity"."query"
   FROM "pg_stat_activity";

О нет.

Тогда мы решили сделать несколько шагов назад. Могли бы мы просто запустить форум снова и перевести его в режим только для чтения, пока разбираемся с этой историей резервного копирования? Нам удалось это сделать, исправив некоторые проблемы с правами доступа для postgres и redis, и форум снова заработал на старой версии. Не всё работает, например, переход в админку → пользователи → группы вызывает эту ошибку:

NoMethodError (undefined method `automatic_membership_retroactive' for #<Group:0x00007fcaca3045e8>)
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/activemodel-6.0.1/lib/active_model/attribute_methods.rb:431:in `method_missing'

Но остальное, кажется, работает.

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

Итак, мы запустили новый экземпляр EC2, выполнили инструкции по началу работы с discourse_docker и начали импорт. Затем мы столкнулись со странной проблемой: не удалось создать индекс, потому что данные не соответствовали требованиям уникальности индекса:

ERROR:  could not create unique index "index_incoming_domains_on_name_and_https_and_port"
DETAIL:  Key (name, https, port)=(homeassistant.home, f, 8123) is duplicated.
EXCEPTION: psql failed: DETAIL:  Key (name, https, port)=(homeassistant.home, f, 8123) is duplicated.
/var/www/discourse/lib/backup_restore/database_restorer.rb:95:in `restore_dump'

Но когда мы зашли в консоль Rails нашего работающего экземпляра, дубликата не было:

[7] pry(main)> IncomingDomain.where(name: "homeassistant.home")
=> [#<IncomingDomain:0x000055e5cabc3760 id: 8648, name: "homeassistant.home", https: false, port: 8123>]

Вот где мы сейчас находимся. И мы немного потерялись.

  • У нас есть работающий экземпляр с плохой БД по сравнению с Ruby-кодом, который не может мигрировать на новый Postgres.
  • У нас есть резервная копия, которую нельзя импортировать в новый экземпляр.

Мы изучили возможность перехода на платный хостинг Discourse, но так как у нас 3 миллиона просмотров страниц и миллион сообщений, корпоративное ценообразование слишком обременительно для нас.

Поэтому нам нужно найти выход. В идеале мы хотели бы импортировать нашу резервную копию, но миграция старого экземпляра тоже подошла бы.

Есть ли у кого-нибудь идеи? Мы не против заплатить кому-то за помощь.

Я считаю, что самый простой путь для вас — сделать рабочую резервную копию и импортировать её в новый экземпляр, как вы пытались сделать в прошлый раз.

Давайте попробуем исправить дублирующиеся данные:

# Подключитесь к машине по SSH
cd /var/discourse
./launcher enter app
su postgres
psql
\connect discourse
SELECT * FROM incoming_domains WHERE name LIKE '%homeassistant.home%';

# Это должно вывести несколько строк
# Используйте SQL-запросы DELETE для исправления
# и выйдите, введя \q

Попробуйте выполнить эти действия и обратитесь за дополнительной помощью, если возникнут трудности.

Мне тоже нужно будет почистить IncomingLink и IncoingReferrer, так как referrer указывает на IncomingDomain, а IncomingLink — на IncomingReferrer?

Сейчас выполняю запрос и попробую импортировать ещё одну резервную копию. Запрос через postgres выдаёт результаты, отличающиеся от тех, что получаются через Rails. Но, наверное, это из-за default scope?

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

Будем держать вас в курсе.

Отлично, мы выбрались и снова онлайн. Спасибо за подсказки, @Falco.

Чтобы помочь другим с их проблемами, вот подробный разбор того, что мы сделали.

У нас было несколько повреждённых индексов, из-за чего импорт не удавался. Нам удалось исправить это, удалив дубликаты вручную. Также у 8 пользователей были дублирующиеся значения username_lower (слишком много Mike и Marco). Мы переименовали их, обновив и username, и username_lower. Из данных пользователей мы увидели, что первое повреждение произошло в декабре 2019 года.

Вместо цикла «создать резервную копию» → «восстановить из резервной копии» → «ошибка из-за дубликатов» → «исправить», мы решили перестроить все индексы. Мы нашли все индексы с уникальными ограничениями с помощью следующего запроса:

select idx.relname as index_name, 
       insp.nspname as index_schema,
       tbl.relname as table_name,
       tnsp.nspname as table_schema
from pg_index pgi
  join pg_class idx on idx.oid = pgi.indexrelid
  join pg_namespace insp on insp.oid = idx.relnamespace
  join pg_class tbl on tbl.oid = pgi.indrelid
  join pg_namespace tnsp on tnsp.oid = tbl.relnamespace
where pgi.indisunique --<< только уникальные индексы
  and tnsp.nspname = 'public'

Как только все индексы заработали, мы смогли сделать резервную копию и успешно импортировать её в новый экземпляр. Миграции прошли как ожидалось, мы переключили экземпляры и всё заработало :+1: За устойчивость Discourse :beers:

Ещё раз спасибо, @Falco.

Хороших выходных :slight_smile:

Ещё один совет для тех, кто отлаживает проблемы с повреждением данных. Когда наш импорт изначально не удался из-за дублирующихся данных, я сразу зашёл в консоль Rails и начал искать по тем данным, которые привели к сбою при создании индекса.

Однако, выполняя запросы по индексированным полям, Postgres использовал повреждённый индекс для формирования результатов! Поэтому мой первоначальный запрос показал 1 запись, а позже, при удалении этой записи, он показал 0 записей.

Запросы, выполняющие полное сканирование таблицы, — это победа :slight_smile: