اولین تجربه من در بازیابی پایگاه داده Postgres پس از شکست (صفحه نامعتبر در بلوک 4123007 از relatton base/16490)

من می خواهم اولین تجربه موفقیت آمیز خود را در بازگرداندن یک پایگاه داده Postgres به عملکرد کامل با شما به اشتراک بگذارم. من نیم سال پیش با Postgres DBMS آشنا شدم، قبل از آن اصلاً تجربه ای در مدیریت پایگاه داده نداشتم.

اولین تجربه من در بازیابی پایگاه داده Postgres پس از شکست (صفحه نامعتبر در بلوک 4123007 از relatton base/16490)

من به عنوان یک مهندس نیمه DevOps در یک شرکت بزرگ فناوری اطلاعات کار می کنم. شرکت ما نرم افزاری را برای خدمات با بار بالا توسعه می دهد و من مسئول عملکرد، نگهداری و استقرار هستم. یک وظیفه استاندارد به من داده شد: به روز رسانی یک برنامه در یک سرور. برنامه در جنگو نوشته شده است، در حین به روز رسانی مهاجرت ها انجام می شود (تغییرات در ساختار پایگاه داده) و قبل از این فرآیند، برای هر موردی، از طریق برنامه استاندارد pg_dump یک دیتابیس کامل را تخلیه می کنیم.

یک خطای غیرمنتظره در حین گرفتن 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

اشکال "صفحه نامعتبر در بلوک" از مشکلات در سطح فایل سیستم صحبت می کند که بسیار بد است. در انجمن های مختلف پیشنهاد شد که انجام شود خلاء کامل با گزینه صفر_صفحات_آسیب دیده برای حل این مسئله. خب سعی کنیم...

آماده شدن برای بهبودی

WARNING! قبل از هر تلاشی برای بازیابی پایگاه داده خود، حتما یک نسخه پشتیبان از Postgres تهیه کنید. اگر ماشین مجازی دارید، پایگاه داده را متوقف کنید و یک عکس فوری بگیرید. اگر امکان گرفتن عکس فوری وجود ندارد، پایگاه داده را متوقف کرده و محتویات پوشه Postgres (از جمله فایل های wal) را در مکانی امن کپی کنید. نکته اصلی در تجارت ما این است که اوضاع را بدتر نکنیم. خواندن این.

از آنجایی که پایگاه داده به طور کلی برای من کار می کرد، من خودم را به یک روکش پایگاه داده معمولی محدود کردم، اما جدول با داده های آسیب دیده را حذف کردم (گزینه -T، --exclude-table=TABLE در pg_dump).

سرور فیزیکی بود، گرفتن عکس فوری غیرممکن بود. پشتیبان حذف شده است، بیایید ادامه دهیم.

بررسی سیستم فایل

قبل از تلاش برای بازیابی پایگاه داده، باید مطمئن شویم که همه چیز در خود سیستم فایل مرتب است. و در صورت اشتباه، آنها را اصلاح کنید، زیرا در غیر این صورت فقط می توانید اوضاع را بدتر کنید.

در مورد من، سیستم فایل با پایگاه داده نصب شده بود "/srv" و نوع آن ext4 بود.

توقف پایگاه داده: systemctl توقف [ایمیل محافظت شده] و بررسی کنید که سیستم فایل توسط کسی استفاده نمی شود و با استفاده از دستور می توان آن را از حالت نصب خارج کرد lsof:
lsof +D /srv

همچنین مجبور شدم پایگاه داده redis را متوقف کنم، زیرا آن نیز در حال استفاده بود "/srv". بعد من unmount کردم / srv (مقدار).

فایل سیستم با استفاده از ابزار بررسی شد e2fsck با سوئیچ -f (بررسی اجباری حتی اگر سیستم فایل تمیز علامت گذاری شده باشد):

اولین تجربه من در بازیابی پایگاه داده Postgres پس از شکست (صفحه نامعتبر در بلوک 4123007 از relatton base/16490)

بعد، با استفاده از ابزار dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep بررسی شد) می توانید تأیید کنید که بررسی واقعاً انجام شده است:

اولین تجربه من در بازیابی پایگاه داده Postgres پس از شکست (صفحه نامعتبر در بلوک 4123007 از relatton base/16490)

e2fsck می گوید که هیچ مشکلی در سطح سیستم فایل ext4 یافت نشد، به این معنی که می توانید به تلاش برای بازیابی پایگاه داده ادامه دهید، یا بهتر است به بازگشت به خلاء پر (البته، شما باید فایل سیستم را دوباره مونت کنید و پایگاه داده را راه اندازی کنید).

اگر سرور فیزیکی دارید، حتما وضعیت دیسک ها را بررسی کنید (از طریق smartctl -a /dev/XXX) یا کنترلر RAID برای اطمینان از اینکه مشکل در سطح سخت افزاری نیست. در مورد من، RAID "سخت افزار" بود، بنابراین از مدیر محلی خواستم وضعیت RAID را بررسی کند (سرور چند صد کیلومتر با من فاصله داشت). او گفت که هیچ خطایی وجود ندارد، به این معنی که ما قطعا می توانیم بازسازی را شروع کنیم.

تلاش 1: صفر_صفحات_آسیب دیده

ما از طریق psql با اکانتی که دارای حقوق superuser است به پایگاه داده متصل می شویم. ما به یک سوپرکاربر نیاز داریم، زیرا ... گزینه صفر_صفحات_آسیب دیده فقط او می تواند تغییر کند در مورد من postgres است:

psql -h 127.0.0.1 -U postgres -s [پایگاه_داده]

گزینه صفر_صفحات_آسیب دیده برای نادیده گرفتن خطاهای خواندن (از وب سایت postgrespro) مورد نیاز است:

هنگامی که PostgreSQL یک هدر صفحه خراب را شناسایی می کند، معمولاً یک خطا را گزارش می کند و تراکنش فعلی را لغو می کند. اگر zero_damaged_pages فعال باشد، سیستم در عوض یک هشدار صادر می کند، صفحه آسیب دیده را در حافظه صفر می کند و به پردازش ادامه می دهد. این رفتار داده ها، یعنی تمام ردیف های صفحه آسیب دیده را از بین می برد.

ما این گزینه را فعال می کنیم و سعی می کنیم جداول را به طور کامل خلاء کنیم:

VACUUM FULL VERBOSE

اولین تجربه من در بازیابی پایگاه داده Postgres پس از شکست (صفحه نامعتبر در بلوک 4123007 از relatton base/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 کیلوبایت).

تلاش 2: فهرست مجدد

اولین توصیه گوگل کمکی نکرد. بعد از چند دقیقه جستجو، نکته دوم را پیدا کردم - ساختن مجدداً میز آسیب دیده من این توصیه را خیلی جاها دیدم، اما اعتماد به نفس ایجاد نکرد. بیایید دوباره فهرست کنیم:

reindex table ws_log_smevlog

اولین تجربه من در بازیابی پایگاه داده Postgres پس از شکست (صفحه نامعتبر در بلوک 4123007 از relatton base/16490)

مجدداً بدون مشکل تکمیل شد

با این حال، این کمکی نکرد، VACUUM FULL با یک خطای مشابه خراب شد. از آنجایی که من به شکست ها عادت کرده ام، شروع به جستجوی بیشتر برای مشاوره در اینترنت کردم و به یک چیز نسبتاً جالب برخوردم یک مقاله.

تلاش 3: SELECT، LIMIT، OFFSET

مقاله بالا پیشنهاد می کند که جدول را ردیف به ردیف نگاه کنید و داده های مشکل ساز را حذف کنید. ابتدا باید تمام خطوط را بررسی کنیم:

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 ابتدا مجبور شد ردیف‌های OFFSET را اسکن کند و از آن بگذرد، و با افزایش آن انحراف بهره وری حتی بیشتر کاهش می یابد.

تلاش 4: یک روگرفت به شکل متن

سپس یک ایده به ظاهر درخشان به ذهنم خطور کرد: یک کمپرسی به صورت متن بردارید و آخرین خط ضبط شده را تجزیه و تحلیل کنید.

اما ابتدا اجازه دهید نگاهی به ساختار جدول بیندازیم. ws_log_smevlog:

اولین تجربه من در بازیابی پایگاه داده Postgres پس از شکست (صفحه نامعتبر در بلوک 4123007 از relatton base/16490)

در مورد ما یک ستون داریم "شناسه"، که حاوی شناسه (شمارگر) منحصر به فرد سطر بود. طرح به این صورت بود:

  1. ما شروع به گرفتن یک Dump به شکل متن (به شکل دستورات sql) می کنیم.
  2. در یک نقطه زمانی مشخص، dump به دلیل یک خطا قطع می شود، اما فایل متنی همچنان روی دیسک ذخیره می شود.
  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) متوجه شد که Dump در خط با شناسه قطع شده است 186 525. بنابراین مشکل در خط شناسه 186 526 است، خراب است و باید حذف شود! - فکر کردم اما، ایجاد یک پرس و جو در پایگاه داده:
«* را از ws_log_smevlog که id=186529 است انتخاب کنید"معلوم شد که همه چیز با این خط خوب است... ردیف هایی با شاخص های 186 - 530 نیز بدون مشکل کار می کنند. یک "ایده درخشان" دیگر شکست خورد. بعداً متوجه شدم که چرا این اتفاق افتاده است: هنگام حذف و تغییر داده ها از یک جدول، آنها به صورت فیزیکی حذف نمی شوند، بلکه به عنوان "تاپل های مرده" علامت گذاری می شوند، سپس می آید. اتو وکیوم و این خطوط را به عنوان حذف شده علامت گذاری می کند و اجازه استفاده مجدد از این خطوط را می دهد. برای درک، اگر داده های جدول تغییر کند و autovacuum فعال باشد، به ترتیب ذخیره نمی شود.

تلاش 5: SELECT، FROM، WHERE id=

شکست‌ها ما را قوی‌تر می‌کنند. هرگز نباید تسلیم شوید، باید تا آخر پیش بروید و به خود و توانایی هایتان ایمان داشته باشید. بنابراین تصمیم گرفتم گزینه دیگری را امتحان کنم: فقط تمام رکوردهای موجود در پایگاه داده را یک به یک نگاه کنید. با دانستن ساختار جدول من (به بالا مراجعه کنید)، یک فیلد id داریم که منحصر به فرد است (کلید اصلی). ما 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 را به / dev / null، اما اگر دستور SELECT از کار بیفتد، متن خطا چاپ می شود (stderr به کنسول ارسال می شود) و یک خط حاوی خطا چاپ می شود (به لطف ||، یعنی انتخاب مشکل داشت (کد بازگشتی دستور). 0 نیست)).

من خوش شانس بودم، فهرست هایی را در زمین ایجاد کردم id:

اولین تجربه من در بازیابی پایگاه داده Postgres پس از شکست (صفحه نامعتبر در بلوک 4123007 از relatton base/16490)

یعنی یافتن خطی با شناسه مورد نظر نباید زمان زیادی را ببرد. در تئوری باید کار کند. خب، بیایید دستور را در داخل اجرا کنیم tmux و بریم بخوابیم

تا صبح متوجه شدم که حدود 90 ورودی مشاهده شده است که کمی بیش از 000٪ است. یک نتیجه عالی در مقایسه با روش قبلی (5٪)! اما نمی خواستم 2 روز صبر کنم...

تلاش 6: SELECT، FROM، WHERE id >= و id

مشتری یک سرور عالی اختصاص داده شده به پایگاه داده داشت: پردازنده دوگانه 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

در کمال تعجب، ورودی ها بدون هیچ مشکلی حتی بدون گزینه حذف شدند صفر_صفحات_آسیب دیده.

سپس به پایگاه داده وصل شدم، انجام دادم VACUUM FULL (فکر می کنم نیازی به انجام این کار نبود) و در نهایت با استفاده از بک آپ با موفقیت حذف کردم pg_dump. دامپ بدون هیچ خطایی گرفته شد! مشکل به این شکل احمقانه حل شد. شادی حد و مرزی نداشت، بعد از این همه شکست موفق شدیم راه حلی پیدا کنیم!

قدردانی و نتیجه گیری

اولین تجربه من از بازیابی پایگاه داده واقعی Postgres اینگونه بود. این تجربه را برای مدت طولانی به یاد خواهم داشت.

و در آخر، من می خواهم از PostgresPro برای ترجمه اسناد به روسی و برای ترجمه تشکر کنم دوره های آنلاین کاملا رایگان، که در طول تجزیه و تحلیل مشکل کمک زیادی کرد.

منبع: www.habr.com

اضافه کردن نظر