فشل الاستيراد بسبب "لم يُمكن إنشاء فهرس فريد"

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، وعاد المنتدى للعمل على الإصدار القديم. لا يعمل كل شيء، فمثلاً عند الانتقال إلى admin → user → groups نحصل على هذا الخطأ:

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'

لكن الباقي يبدو أنه يعمل.

في هذه النقطة، قررنا أنه نظرًا لأننا اضطررنا إلى تغيير ملكية الملفات (chown) للعودة إلى مثيل يعمل، يجب علينا ببساطة بدء مثيل جديد واستيراد نسخة الاحتياطية لدينا.

لذا قمنا ببدء مثيل 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. لكنني أعتقد أن ذلك قد يكون بسبب نطاق افتراضي؟

لقد حاولنا استيراد نسخة احتياطية أخرى وفشل ذلك بسبب فهرس تالف آخر. قمنا بإعادة فهرسة جميع الفهارس الفريدة على المثيل الأصلي، ونواجه الآن هذه المشكلة مع عدد قليل من المستخدمين.

سنبقيكم على اطلاع.

حسناً، لقد تجاوزنا الصعوبات وعادنا للعمل عبر الإنترنت. شكراً للتلميحات يا @Falco.

لمساعدة الآخرين في حل مشاكلهم، إليك تفصيلاً للخطوات التي اتبعناها.

لقد واجهنا بعض الفهارس التالفة التي تسببت في فشل عملية الاستيراد. تمكنا من إصلاح ذلك بحذف التكرارات يدوياً. كما كان لدينا 8 مستخدمين يحملون قيمة مكررة في الحقل username_lower (كثرة أسماء مايك وماركو). قمنا بإعادة تسميتهم بتحديث كل من 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 وقمت بالبحث عن البيانات التي تسببت في فشل إنشاء الفهرس.

ومع ذلك، عند الاستعلام باستخدام الحقول المفهرسة، استخدم PostgreSQL الفهرس التالف لتوليد النتائج! لذا أظهر استعلامي الأولي نتيجة واحدة، وفي وقت لاحق عند حذف تلك الإدخالة أظهرت نتيجة صفر.

الاستعلامات التي تقوم بفحص كامل الجدول هي الرابحة :slight_smile: