تجربتي الأولى في استعادة قاعدة بيانات Postgres بعد الفشل (صفحة غير صالحة في الكتلة 4123007 من قاعدة relatton/16490)

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

تجربتي الأولى في استعادة قاعدة بيانات Postgres بعد الفشل (صفحة غير صالحة في الكتلة 4123007 من قاعدة relatton/16490)

أعمل كمهندس شبه DevOps في شركة كبيرة لتكنولوجيا المعلومات. تقوم شركتنا بتطوير برامج للخدمات عالية التحميل، وأنا مسؤول عن الأداء والصيانة والنشر. تم تكليفي بمهمة قياسية: تحديث تطبيق على خادم واحد. تمت كتابة التطبيق بلغة Django، أثناء إجراء عمليات ترحيل التحديث (التغييرات في بنية قاعدة البيانات)، وقبل هذه العملية، نقوم بتفريغ قاعدة البيانات بالكامل من خلال برنامج pg_dump القياسي، فقط في حالة.

حدث خطأ غير متوقع أثناء عملية التفريغ (Postgres الإصدار 9.5):

pg_dump: Oumping the contents of table “ws_log_smevlog” failed: PQgetResult() failed.
pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989
pg_dump: The command was: COPY public.ws_log_smevlog [...]
pg_dunp: [parallel archtver] a worker process dled unexpectedly

بق "صفحة غير صالحة في الكتلة" يتحدث عن مشاكل على مستوى نظام الملفات، وهو أمر سيء للغاية. في مختلف المنتديات اقترح القيام به فراغ كامل مع الخيار صفر_تالف_صفحات لحل هذه المشكلة. حسناً، دعونا نحاول...

التحضير للتعافي

تحذير! تأكد من أخذ نسخة احتياطية من Postgres قبل أي محاولة لاستعادة قاعدة البيانات الخاصة بك. إذا كان لديك جهاز ظاهري، فأوقف قاعدة البيانات والتقط لقطة. إذا لم يكن من الممكن التقاط لقطة، فأوقف قاعدة البيانات وانسخ محتويات دليل Postgres (بما في ذلك ملفات wal) إلى مكان آمن. الشيء الرئيسي في عملنا هو عدم جعل الأمور أسوأ. يقرأ هذا.

نظرًا لأن قاعدة البيانات تعمل بشكل عام بالنسبة لي، فقد اقتصرت على تفريغ قاعدة بيانات عادية، لكنني استبعدت الجدول الذي يحتوي على بيانات تالفة (الخيار -T، --exclude-table=TABLE في pg_dump).

كان الخادم ماديًا، وكان من المستحيل التقاط لقطة. تمت إزالة النسخة الاحتياطية، فلننتقل.

فحص نظام الملفات

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

في حالتي، تم تثبيت نظام الملفات مع قاعدة البيانات "/سرف" وكان النوع ext4.

إيقاف قاعدة البيانات: توقف Systemctl [البريد الإلكتروني محمي] وتأكد من أن نظام الملفات ليس قيد الاستخدام من قبل أي شخص ويمكن إلغاء تحميله باستخدام الأمر lsof:
lsof +D /srv

واضطررت أيضًا إلى إيقاف قاعدة بيانات redis، نظرًا لأنها كانت تستخدم أيضًا "/سرف". التالي أنا غير مثبتة / srv (كمونت).

تم فحص نظام الملفات باستخدام الأداة المساعدة e2fsck مع التبديل -f (فرض التحقق حتى إذا تم وضع علامة على نظام الملفات نظيفًا):

تجربتي الأولى في استعادة قاعدة بيانات Postgres بعد الفشل (صفحة غير صالحة في الكتلة 4123007 من قاعدة relatton/16490)

بعد ذلك، باستخدام الأداة المساعدة تفريغ (Sudo dumpe2fs /dev/mapper/gu2—sys-srv | فحص grep) يمكنك التحقق من إجراء الفحص بالفعل:

تجربتي الأولى في استعادة قاعدة بيانات Postgres بعد الفشل (صفحة غير صالحة في الكتلة 4123007 من قاعدة relatton/16490)

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

إذا كان لديك خادم فعلي، فتأكد من التحقق من حالة الأقراص (عبر Smartctl -a /dev/XXX) أو وحدة تحكم RAID للتأكد من أن المشكلة ليست على مستوى الأجهزة. في حالتي، تبين أن RAID عبارة عن "جهاز"، لذلك طلبت من المسؤول المحلي التحقق من حالة RAID (كان الخادم على بعد عدة مئات من الكيلومترات مني). وقال إنه لم تكن هناك أخطاء، مما يعني أنه يمكننا بالتأكيد البدء في عملية الترميم.

المحاولة الأولى: صفر_صفحات_تالفة

نحن نتصل بقاعدة البيانات عبر psql بحساب يتمتع بحقوق المستخدم المتميز. نحن بحاجة إلى مستخدم خارق، لأن... خيار صفر_تالف_صفحات هو وحده القادر على التغيير. في حالتي هو postgres:

psql -h 127.0.0.1 -U postgres -s [اسم قاعدة البيانات]

خيار صفر_تالف_صفحات مطلوب لتجاهل أخطاء القراءة (من موقع postgrespro):

عندما يكتشف PostgreSQL رأس صفحة تالفًا، فإنه عادةً ما يُبلغ عن خطأ ويُحبط المعاملة الحالية. إذا تم تمكين Zero_damaged_pages، يصدر النظام تحذيرًا بدلاً من ذلك، ويقوم بتصفية الصفحة التالفة في الذاكرة، ويستمر في المعالجة. يؤدي هذا السلوك إلى إتلاف البيانات، أي كافة الصفوف الموجودة في الصفحة التالفة.

نقوم بتمكين الخيار ونحاول إجراء فراغ كامل للجداول:

VACUUM FULL VERBOSE

تجربتي الأولى في استعادة قاعدة بيانات Postgres بعد الفشل (صفحة غير صالحة في الكتلة 4123007 من قاعدة relatton/16490)
لسوء الحظ، حظ سيء.

لقد واجهنا خطأً مماثلاً:

INFO: vacuuming "“public.ws_log_smevlog”
WARNING: invalid page in block 4123007 of relation base/16400/21396989; zeroing out page
ERROR: unexpected chunk number 573 (expected 565) for toast value 21648541 in pg_toast_106070

pg_toast - آلية لتخزين "البيانات الطويلة" في Poetgres إذا لم تكن مناسبة لصفحة واحدة (8 كيلو بايت افتراضيًا).

المحاولة الثانية: إعادة الفهرسة

النصيحة الأولى من جوجل لم تساعد. وبعد بضع دقائق من البحث، وجدت النصيحة الثانية - التي يجب القيام بها الفهرسة طاولة تالفة. لقد رأيت هذه النصيحة في أماكن كثيرة، لكنها لم تبعث على الثقة. دعونا نعيد الفهرسة:

reindex table ws_log_smevlog

تجربتي الأولى في استعادة قاعدة بيانات Postgres بعد الفشل (صفحة غير صالحة في الكتلة 4123007 من قاعدة relatton/16490)

الفهرسة اكتملت دون مشاكل.

ومع ذلك، فإن هذا لم يساعد، فراغ ممتلئ تحطمت مع خطأ مماثل. منذ أن اعتدت على الفشل، بدأت في البحث عن المزيد من النصائح على الإنترنت ووجدت شيئًا مثيرًا للاهتمام إلى حد ما статью.

المحاولة 3: التحديد، الحد، الإزاحة

اقترحت المقالة أعلاه النظر في الجدول صفًا تلو الآخر وإزالة البيانات التي بها مشكلات. أولاً نحن بحاجة إلى إلقاء نظرة على جميع الخطوط:

for ((i=0; i<"Number_of_rows_in_nodes"; i++ )); do psql -U "Username" "Database Name" -c "SELECT * FROM nodes LIMIT 1 offset $i" >/dev/null || echo $i; done

في حالتي، يحتوي الجدول 1 628 991 خطوط! كان من الضروري الاعتناء بها جيدًا تقسيم البيانات، ولكن هذا موضوع لمناقشة منفصلة. كان يوم السبت، قمت بتشغيل هذا الأمر في tmux وذهبت إلى السرير:

for ((i=0; i<1628991; i++ )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog LIMIT 1 offset $i" >/dev/null || echo $i; done

بحلول الصباح قررت التحقق من سير الأمور. ولدهشتي، اكتشفت أنه بعد 20 ساعة، تم فحص 2% فقط من البيانات! لم أكن أريد الانتظار 50 يوما. فشل كامل آخر.

لكنني لم أستسلم. تساءلت لماذا استغرق المسح وقتًا طويلاً. من الوثائق (مرة أخرى على postgrespro) اكتشفت:

يحدد OFFSET تخطي العدد المحدد من الصفوف قبل البدء في إخراج الصفوف.
إذا تم تحديد كل من OFFSET وLIMIT، فسيتخطى النظام أولاً صفوف OFFSET ثم يبدأ في حساب الصفوف الخاصة بقيد LIMIT.

عند استخدام LIMIT، من المهم أيضًا استخدام عبارة ORDER BY بحيث يتم إرجاع صفوف النتائج بترتيب معين. وإلا، سيتم إرجاع مجموعات فرعية غير متوقعة من الصفوف.

من الواضح أن الأمر أعلاه كان خاطئا: أولا، لم يكن هناك ترتيب حسب، قد تكون النتيجة خاطئة. ثانيًا، كان على Postgres أولاً مسح صفوف الإزاحة وتخطيها، ومع الزيادة الأوفست وستنخفض الإنتاجية إلى أبعد من ذلك.

المحاولة الرابعة: خذ تفريغًا في شكل نص

ثم خطرت في ذهني فكرة تبدو رائعة: خذ تفريغًا في شكل نص وقم بتحليل آخر سطر مسجل.

ولكن أولا، دعونا نلقي نظرة على هيكل الجدول. ws_log_smevlog:

تجربتي الأولى في استعادة قاعدة بيانات Postgres بعد الفشل (صفحة غير صالحة في الكتلة 4123007 من قاعدة relatton/16490)

في حالتنا لدينا عمود "هوية شخصية"، الذي يحتوي على المعرف الفريد (العداد) للصف. وكانت الخطة كالآتي:

  1. نبدأ في أخذ التفريغ في شكل نص (في شكل أوامر SQL)
  2. في وقت معين، قد تتم مقاطعة التفريغ بسبب خطأ، ولكن الملف النصي سيظل محفوظًا على القرص
  3. ننظر إلى نهاية الملف النصي وبذلك نجد معرف (id) السطر الأخير الذي تمت إزالته بنجاح

لقد بدأت في أخذ تفريغ في شكل نص:

pg_dump -U my_user -d my_database -F p -t ws_log_smevlog -f ./my_dump.dump

تمت مقاطعة عملية التفريغ، كما هو متوقع، بنفس الخطأ:

pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989

مزيد من خلال ذيل نظرت إلى نهاية التفريغ (الذيل -5 ./my_dump.dump) اكتشف أن التفريغ تمت مقاطعته على السطر بالمعرف 186 525. "لذا فإن المشكلة في السطر مع المعرف 186 526، وهي معطلة ويجب حذفها!" - اعتقدت. ولكن، إجراء استعلام إلى قاعدة البيانات:
«حدد * من ws_log_smevlog حيث المعرف = 186529"اتضح أن كل شيء على ما يرام مع هذا الخط... الصفوف ذات المؤشرات 186 - 530 عملت أيضًا دون مشاكل. فشلت "فكرة رائعة" أخرى. لاحقًا فهمت سبب حدوث ذلك: عند حذف البيانات وتغييرها من جدول، لا يتم حذفها فعليًا، ولكن يتم وضع علامة عليها على أنها "مجموعات ميتة"، ثم تأتي فراغ تلقائي ويضع علامة على هذه الأسطر على أنها محذوفة ويسمح بإعادة استخدام هذه الأسطر. لفهم ذلك، إذا تغيرت البيانات الموجودة في الجدول وتم تمكين الفراغ التلقائي، فلن يتم تخزينها بشكل تسلسلي.

المحاولة 5: حدد، من، حيث المعرف =

الفشل يجعلنا أقوى. يجب ألا تستسلم أبدًا، عليك أن تصل إلى النهاية وتؤمن بنفسك وقدراتك. لذلك قررت تجربة خيار آخر: ما عليك سوى الاطلاع على جميع السجلات الموجودة في قاعدة البيانات واحدًا تلو الآخر. بمعرفة بنية الجدول الخاص بي (انظر أعلاه)، لدينا حقل معرف فريد (المفتاح الأساسي). لدينا 1 صفًا في الجدول و id مرتبة، مما يعني أنه يمكننا الاطلاع عليها واحدًا تلو الآخر:

for ((i=1; i<1628991; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

إذا لم يفهم أي شخص، يعمل الأمر على النحو التالي: يقوم بمسح الجدول صفًا تلو الآخر ويرسل stdout إلى / ديف / لاغيةولكن إذا فشل أمر SELECT، فسيتم طباعة نص الخطأ (يتم إرسال stderr إلى وحدة التحكم) ويتم طباعة سطر يحتوي على الخطأ (بفضل ||، مما يعني أن التحديد واجه مشاكل (رمز الإرجاع للأمر ليس 0)).

لقد كنت محظوظًا، فقد قمت بإنشاء فهارس في الحقل id:

تجربتي الأولى في استعادة قاعدة بيانات Postgres بعد الفشل (صفحة غير صالحة في الكتلة 4123007 من قاعدة relatton/16490)

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

بحلول الصباح، وجدت أنه تمت مشاهدة حوالي 90 مشاركة، وهو ما يزيد قليلاً عن 000%. نتيجة ممتازة بالمقارنة مع الطريقة السابقة (5%)! لكنني لم أرغب في الانتظار 2 يومًا ...

المحاولة 6: حدد، من، حيث المعرف >= والمعرف

كان لدى العميل خادم ممتاز مخصص لقاعدة البيانات: معالج مزدوج Intel Xeon E5-2697 v2، كان هناك ما يصل إلى 48 موضوعًا في موقعنا! كان التحميل على الخادم متوسطًا، حيث تمكنا من تنزيل حوالي 20 موضوعًا دون أي مشاكل. كان هناك أيضًا ما يكفي من ذاكرة الوصول العشوائي: ما يصل إلى 384 جيجابايت!

لذلك، يجب أن يكون الأمر متوازيًا:

for ((i=1; i<1628991; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

هنا كان من الممكن كتابة نص جميل وأنيق، لكنني اخترت أسرع طريقة للتوازي: قم بتقسيم النطاق 0-1628991 يدويًا إلى فترات زمنية مكونة من 100 سجل وتشغيل 000 أمرًا من النموذج بشكل منفصل:

for ((i=N; i<M; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

ولكن هذا ليس كل شيء. من الناحية النظرية، يستغرق الاتصال بقاعدة بيانات أيضًا بعض الوقت وموارد النظام. إن ربط 1 لم يكن ذكيًا جدًا، ستوافق على ذلك. لذلك، دعونا نستعيد 628 صف بدلاً من اتصال واحد على واحد. ونتيجة لذلك تحول الفريق إلى هذا:

for ((i=N; i<M; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done

افتح 16 نافذة في جلسة tmux وقم بتشغيل الأوامر:

1) for ((i=0; i<100000; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
2) for ((i=100000; i<200000; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
…
15) for ((i=1400000; i<1500000; i=$((i+1000)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
16) for ((i=1500000; i<1628991; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done

وبعد يوم تلقيت النتائج الأولى! وهي (لم تعد القيم XXX وZZZ محفوظة):

ERROR:  missing chunk number 0 for toast value 37837571 in pg_toast_106070
829000
ERROR:  missing chunk number 0 for toast value XXX in pg_toast_106070
829000
ERROR:  missing chunk number 0 for toast value ZZZ in pg_toast_106070
146000

وهذا يعني أن ثلاثة أسطر تحتوي على خطأ. كانت معرفات سجلات المشكلة الأولى والثانية بين 829 و000، وكانت معرفات الثالثة بين 830 و000. بعد ذلك، كان علينا ببساطة العثور على قيمة المعرف الدقيقة لسجلات المشكلة. للقيام بذلك، ننظر إلى نطاقنا الذي يحتوي على سجلات بها مشكلات بخطوة 146 ونحدد المعرف:

for ((i=829000; i<830000; i=$((i+1)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done
829417
ERROR:  unexpected chunk number 2 (expected 0) for toast value 37837843 in pg_toast_106070
829449
for ((i=146000; i<147000; i=$((i+1)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done
829417
ERROR:  unexpected chunk number ZZZ (expected 0) for toast value XXX in pg_toast_106070
146911

نهاية سعيدة

لقد وجدنا الخطوط الإشكالية. نذهب إلى قاعدة البيانات عبر psql ونحاول حذفها:

my_database=# delete from ws_log_smevlog where id=829417;
DELETE 1
my_database=# delete from ws_log_smevlog where id=829449;
DELETE 1
my_database=# delete from ws_log_smevlog where id=146911;
DELETE 1

لدهشتي، تم حذف الإدخالات دون أي مشاكل حتى بدون هذا الخيار صفر_تالف_صفحات.

ثم قمت بالاتصال بقاعدة البيانات، فعلت فراغ ممتلئ (أعتقد أنه لم يكن من الضروري القيام بذلك)، وأخيراً قمت بإزالة النسخة الاحتياطية بنجاح باستخدام pg_dump. تم أخذ التفريغ دون أي أخطاء! تم حل المشكلة بهذه الطريقة الغبية. الفرحة لا تعرف حدوداً، بعد العديد من الإخفاقات تمكنا من إيجاد الحل!

شكر وتقدير

هذه هي الطريقة التي انتهت بها تجربتي الأولى في استعادة قاعدة بيانات Postgres الحقيقية. سأتذكر هذه التجربة لفترة طويلة.

وأخيرًا، أود أن أشكر PostgresPro على ترجمة الوثائق إلى اللغة الروسية وعلى دورات مجانية تماما على الانترنتمما ساعد كثيرًا أثناء تحليل المشكلة.

المصدر: www.habr.com

إضافة تعليق