أيتها السادة الأفاضل،
بعد أن بحثت في المنتدى بأقصى ما أملك من جهد دون العثور على إجابة مُرضية، أتقدم بطلب دعم بشأن حالة غريبة ظهرت بعد تغيير مركز بيانات Digital Ocean الأخير.
لقد كنا نخزن جميع ملفاتنا المرفوعة في سلة تخزين (Bucket) على Digital Ocean Spaces في مركز بيانات ams3.
بعد مشكلتين ضخمتين في العتاد (HW) وتعطّل الخدمة لأكثر من شهر بقليل، قررنا في عطلة نهاية الأسبوع الماضية نقل جميع ملفاتنا إلى مركز بيانات fra1.
إليك الخطوات التي اتبعتها:
استعدادًا للنقل، قمت برفع جميع الملفات الموجودة في ams3 (المجلدات الكلاسيكية الثلاثة: الأصلية، والمُحسَّنة، ومجلد المقابر) إلى السلة الجديدة في fra1 باستخدام أداة s3cmd.
انتقلت إلى إعدادات المنتدى وضبطت نقطة النهاية الجديدة للمرفقات، وسجل CDN، وسلة النسخ الاحتياطي.
شغّلت عملية إعادة تشكيل (re-bake) كاملة للمشاركات، متوقعًا أن تُصلح كل شيء دفعة واحدة.
للأسف، لم يكن الأمر كذلك. تم “نقل” معظم المرفقات بشكل صحيح، لكن بضع مئات لم تُنقل. ليس واضحًا لي ما الذي حدث، لكن هذه المرفقات المفقودة نُقلت إلى مجلد المقابر (tombstone).
ظننت أن تشغيل مهمة Rake rake uploads:recover_from_tombstone ستعتني بذلك، لكن لا. الملفات مرئية، لكن في نهاية المهمة لم يتم استعادة أي مرفق، ولا تزال الصور غير ظاهرة في المشاركات.
بدأت في التعمق قليلًا واكتشفت أن تشغيل UploadRecovery.new(dry_run: true).recover (عثرنا عليه أثناء البحث في قسم الميتا) في وحدة تحكم Rails كان يعطيني معلومات ثمينة، مثل رابط المشاركة بالإضافة إلى الرابط القصير أو الطويل للصورة المشكلة.
بالنسبة للرؤوس التي تُرجع في الصيغة القصيرة، كتبتُ كودًا بسيطًا بلغة بايثون لـ “ترجمة” اسم ملف الرفع القصير إلى الصيغة الطويلة، حتى أتمكن من التحقق من وجود الملف في السلة.
لقد فعلت ذلك، ويمكنني تأكيد أن جميع الملفات المفقودة موجودة، في السلة الجديدة وكذلك في القديمة. وجدت جزءًا من الرفع المفقود في مجلد tombstone، كما هو متوقع، لكن البعض الآخر لا يزال غريبًا في مجلد original. الملفات غير تالفة. إذا قمت بالوصول إليها عبر الرابط، فإنها تفتح بشكل صحيح في كلا مركزَي البيانات، وإذا قمت بتصديرها محليًا على جهاز Linux الخاص بي، يمكنني فتحها دون أخطاء.
بشكل ما، تفشل عملية استعادة الرفع في التقاط هذه الملفات وإصلاح أي خلل في قاعدة البيانات.
إذن، أسئلتي هي:
هل هناك طريقة لفهم لماذا تفشل مهمة Rake في استعادة الملفات حتى لو كانت ملفات الرفع موجودة في tombstone (أو في original)؟
ما هي مجموعة الخطوات الصحيحة لضمان أنه في حالة تغيير السلة أو حتى الانتقال من Digital Ocean إلى بيئة أخرى متوافقة مع AWS، يتم نقل جميع المرفقات وتحضيرها بشكل صحيح للاستبدال؟ بشكل عام، ماذا يجب أن يفعل المرء خطوة بخطوة في مثل هذه الحالة؟ من الواضح أن إعادة التشكيل البسيطة (rebake) ليست كافية.
ما وظيفة المهمة posts:invalidate_broken_images؟ أعني، ماذا تعني كلمة invalidate (إبطال/إلغاء الصلاحية)؟
شكرًا مقدّمًا، لقد كنت أتعامل مع هذه المشكلة منذ أسبوع وأحتاج حقًا إلى إنهاء هذا الأمر وإلا سأجنّ
ملاحظة: لا يُعتبر اقتراح إعادة تحميل جميع المرفقات الـ 800+ يدويًا إجابة صحيحة. يجب أن يكون هناك سبب خوارزمي…
مرحبًا @falco، شكرًا لك على ردك.
نعم، في البداية نسيت ذلك.
شغلت الأمر بعد أن عثرت عليه أثناء البحث هنا في الميتا. وساعد في استعادة بعض الملفات.
أنا قمت بعمل إعادة بناء كاملة، بالمناسبة، بعد تشغيله.
إذن، قد يكون لدي فكرة عما يحدث هنا.
لم أفكر في ذكر حقيقة تتعلق بمخرجات مهمة 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 بأنه خاطئ.
لماذا تدعي الأداة أنه خاطئ، ويجب أن يكون c87c4f08d1a9aac3f43d19722cfd5a94f2544272 بدلاً من ذلك؟
في أي حال، قمت بتشغيل مهمة rake uploads:fix_relative_upload_links عدة مرات، ثم أعدت تشغيل rake uploads:recover_from_tombstone، لكن لا يبدو أن أي شيء قد تغير.
تعديل:
عند البحث عن 487b613752a0c338646fecc942512e5de9afeb3f في نسخة احتياطية من قاعدة البيانات قمت بإنشائها قبل تغيير الدلو (bucket)، يمكنني رؤية أن السجل في جدول الuploads التابع لهذه الصورة كان يعرض تمامًا هذا الاسم الست عشري، لذا لا أفهم أكثر لماذا تشتكي مهمة rake منه.
قد تكون محقًا، لكن المشكلة هي أنه من الصعب معرفة ما يجب فعله وما لا يجب فعله في هذه الحالات بدقة دون وجود دليل أو تعليمات من المطورين.
دائمًا ما يكون لدى المرء شعور بأنه كان يجب عليه فعل شيء أكثر أو بطريقة مختلفة، كأنه يحاول اكتشاف وصفة فعالة من خلال استخلاصها من عشرات المنشورات المكتوبة على مدار آخر 3-4 سنوات.
يبدو إعادة الخبز حلاً سحريًا للعديد من الأمور وغير ضار بالمنشورات الموجودة.
هذه طريقة معقدة لقول إنه نظرًا لعدد المرات التي تعثّر فيها الناس في مشاكل تتعلق بإدارة التحميلات وما شابه، فإن وجود دليل رسمي رائع من الطاقم سيكون مرجعًا مهمًا.
خلال الأسبوع الماضي، قضيت بعض الوقت في قراءة كود مهام rake الخاصة برفع الملفات لمحاولة فهم ما يحدث تحت غطاء مهام recover_from_tombstone و recover.
الأمر صعب بسبب تغليف الكائنات (encapsulation) في الفئات، لذا يمكنني القول إنني فشلت إلى حد كبير في ذلك.
ومع ذلك، ما فهمته (يرجى @Falco تصحيحي إذا كنت مخطئاً) هو أن اسم الملف على القرص لملف مرفق يُنشأ بدمج تجزئته SHA1 مع امتداده الأصلي. ثم يُخزن على القرص أو على AWS في مجلد يعتمد مساره على الحرف الأول وأحياناً الثاني في اسمه، ضمن 1X أو 2X أو 3X… (كيف تُحدد هذه لا أفهم).
أخيراً، تُخزن تجزئة SHA1 واسم الملف، بين أمور أخرى، في سجلات جدول uploads في PostgreSQL.
بالعودة إلى ما حدث أثناء تغييرنا لمركز بيانات Digital Ocean، هذا ما حدث وفقاً لأفضل فهم لدي:
قمنا بنسخ جميع الملفات من ams3 إلى fra1.
فشلنا في تنفيذ DbHelper.remap('oldbucketurl', 'newbucketurl') كما اقترح @falco، لكن لم يكن واضحاً لنا أننا بحاجة إلى ذلك في هذه الحالة.
أطلقنا عملية إعادة بناء شاملة (global rebake). في هذه المرحلة، تعطلت آلاف الصور، وتم نقل العديد منها إلى حالة “tombstone”. ليس واضحاً تماماً لدي السبب.
أدركت أن شيئاً ما كان خاطئاً، قمت بإيقاف عملية إعادة البناء الجارية، واكتشفت أمر remap بعد البحث هنا في قسم Meta. ثم أطلقنا مهمة DbHelper.remap('oldbucketurl', 'newbucketurl').
لاستعادة الصور التي نُقلت إلى حالة 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، لذا أعتقد أن الكود غير فعال وقبيح، لكن هذا جانب آخر من القصة ) يقوم بما يلي:
البحث عن جميع المنشورات التي تحتوي على السلسلة upload://
استخراج الرابط القصير لكل تحميل وتحويله إلى تجزئة SHA1 الطويلة الخاصة به
استعلام جدول Uploads
إذا تم العثور على مرفق يحمل تجزئة SHA1 في جدول Uploads، يتم تخطي هذا التحميل، وإلا يتم التحقق من عنوان URL لذلك التحميل في حوض Digital Ocean القديم أو المساحة القديمة.
إذا تم العثور على الرابط في الحوض/المساحة القديمة، يتم استبدال الرابط القصير بعنوان URL لنفس التحميل في الحوض القديم.
إذا تم التعديل، يتم تفعيل إعادة خبز المنشور الأصلي، للسماح لـ 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