ฉันอยากจะแบ่งปันประสบการณ์ความสำเร็จครั้งแรกของฉันในการกู้คืนฐานข้อมูล Postgres ให้มีฟังก์ชันการทำงานเต็มรูปแบบ ฉันคุ้นเคยกับ Postgres DBMS เมื่อครึ่งปีที่แล้ว ก่อนหน้านั้น ฉันไม่มีประสบการณ์ในการจัดการฐานข้อมูลเลย
ฉันทำงานเป็นวิศวกรกึ่ง 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
แมลง "หน้าไม่ถูกต้องในการบล็อก" พูดถึงปัญหาในระดับระบบไฟล์ซึ่งแย่มาก ในกระดานสนทนาต่างๆ ก็แนะนำให้ทำ สุญญากาศเต็ม พร้อมตัวเลือก zero_damaged_pages เพื่อแก้ไขปัญหานี้ เอาล่ะ มาลองกัน...
การเตรียมตัวสำหรับการฟื้นฟู
คำเตือน! อย่าลืมสำรองข้อมูล Postgres ก่อนที่จะพยายามกู้คืนฐานข้อมูลของคุณ หากคุณมีเครื่องเสมือน ให้หยุดฐานข้อมูลและถ่ายภาพสแนปช็อต หากไม่สามารถถ่ายภาพสแน็ปช็อตได้ ให้หยุดฐานข้อมูลและคัดลอกเนื้อหาของไดเร็กทอรี Postgres (รวมถึงไฟล์ wal) ไปยังที่ปลอดภัย สิ่งสำคัญในธุรกิจของเราคือการไม่ทำให้สิ่งต่างๆ แย่ลง อ่าน
เนื่องจากโดยทั่วไปแล้วฐานข้อมูลใช้งานได้สำหรับฉัน ฉันจึงจำกัดตัวเองให้อยู่ในดัมพ์ฐานข้อมูลทั่วไป แต่ไม่รวมตารางที่มีข้อมูลที่เสียหาย (ตัวเลือก -T, --ไม่รวมตาราง=TABLE ใน pg_dump)
เซิร์ฟเวอร์เป็นแบบฟิสิคัล จึงไม่สามารถถ่ายภาพสแนปช็อตได้ ข้อมูลสำรองถูกลบออกแล้ว ดำเนินการต่อได้เลย
การตรวจสอบระบบไฟล์
ก่อนที่จะพยายามกู้คืนฐานข้อมูล เราต้องตรวจสอบให้แน่ใจว่าทุกอย่างเป็นไปตามระบบไฟล์นั้นเอง และในกรณีที่เกิดข้อผิดพลาด ให้แก้ไขให้ถูกต้อง เพราะไม่เช่นนั้น คุณจะยิ่งทำให้เรื่องแย่ลงได้
ในกรณีของฉัน ระบบไฟล์ที่มีฐานข้อมูลถูกเมานท์อยู่ "/srv" และประเภทคือ ex4
การหยุดฐานข้อมูล: systemctl หยุด [ป้องกันอีเมล] และตรวจสอบว่าระบบไฟล์ไม่ได้ถูกใช้งานโดยใครก็ตาม และสามารถยกเลิกการต่อเชื่อมได้โดยใช้คำสั่ง ลซ:
lsof +D /srv
ฉันยังต้องหยุดฐานข้อมูล Redis เนื่องจากฐานข้อมูลนั้นใช้งานอยู่ด้วย "/srv". ต่อไปฉันยกเลิกการต่อเชื่อม / srv (จำนวน)
ตรวจสอบระบบไฟล์โดยใช้ยูทิลิตี้ e2fsck ด้วยสวิตช์ -f (บังคับให้ตรวจสอบแม้ว่าระบบไฟล์จะถูกทำเครื่องหมายว่าสะอาดก็ตาม):
ถัดไปโดยใช้ยูทิลิตี้ ทิ้ง2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep ตรวจสอบแล้ว) คุณสามารถตรวจสอบได้ว่ามีการดำเนินการตรวจสอบจริงหรือไม่:
e2fsck บอกว่าไม่พบปัญหาในระดับระบบไฟล์ ext4 ซึ่งหมายความว่าคุณสามารถพยายามกู้คืนฐานข้อมูลต่อไปหรือกลับไปที่ สูญญากาศเต็ม (แน่นอน คุณต้องเมานต์ระบบไฟล์กลับและเริ่มฐานข้อมูล)
หากคุณมีเซิร์ฟเวอร์จริง โปรดตรวจสอบสถานะของดิสก์ (ผ่าน smartctl -a /dev/XXX) หรือตัวควบคุม RAID เพื่อให้แน่ใจว่าปัญหาไม่ได้อยู่ที่ระดับฮาร์ดแวร์ ในกรณีของฉัน RAID กลายเป็น “ฮาร์ดแวร์” ดังนั้นฉันจึงขอให้ผู้ดูแลระบบในพื้นที่ตรวจสอบสถานะของ RAID (เซิร์ฟเวอร์อยู่ห่างจากฉันหลายร้อยกิโลเมตร) เขาบอกว่าไม่มีข้อผิดพลาด ซึ่งหมายความว่าเราสามารถเริ่มการบูรณะได้อย่างแน่นอน
ความพยายามที่ 1: zero_damaged_pages
เราเชื่อมต่อกับฐานข้อมูลผ่าน psql ด้วยบัญชีที่มีสิทธิ์ superuser เราต้องการ superuser เพราะ... ตัวเลือก zero_damaged_pages มีเพียงเขาเท่านั้นที่สามารถเปลี่ยนแปลงได้ ในกรณีของฉันมันคือ postgres:
psql -h 127.0.0.1 -U postgres -s [ฐานข้อมูล_ชื่อ]
ตัวเลือก zero_damaged_pages จำเป็นเพื่อเพิกเฉยต่อข้อผิดพลาดในการอ่าน (จากเว็บไซต์ postgrespro):
เมื่อ PostgreSQL ตรวจพบส่วนหัวของเพจที่เสียหาย โดยทั่วไปจะรายงานข้อผิดพลาดและยกเลิกธุรกรรมปัจจุบัน หากเปิดใช้งาน zero_damaged_pages ระบบจะออกคำเตือนแทน กำจัดหน้าที่เสียหายในหน่วยความจำเป็นศูนย์ และดำเนินการประมวลผลต่อ ลักษณะการทำงานนี้จะทำลายข้อมูล กล่าวคือ แถวทั้งหมดในเพจที่เสียหาย
เราเปิดใช้งานตัวเลือกนี้และพยายามดูดตารางทั้งหมด:
VACUUM FULL VERBOSE
น่าเสียดายที่โชคร้าย
เราพบข้อผิดพลาดที่คล้ายกัน:
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
ความพยายามที่ 2: จัดทำดัชนีใหม่
คำแนะนำแรกจาก Google ไม่ได้ช่วยอะไร หลังจากค้นหาไม่กี่นาที ฉันพบเคล็ดลับที่สอง - ต้องทำ สร้างดัชนีใหม่ โต๊ะเสียหาย ฉันเห็นคำแนะนำนี้ในหลายๆ แห่ง แต่ไม่ได้สร้างแรงบันดาลใจให้เกิดความมั่นใจ มาจัดทำดัชนีใหม่:
reindex table ws_log_smevlog
สร้างดัชนีใหม่ เสร็จสิ้นโดยไม่มีปัญหา
อย่างไรก็ตาม สิ่งนี้ไม่ได้ช่วยอะไร สูญญากาศเต็ม ขัดข้องด้วยข้อผิดพลาดที่คล้ายกัน เนื่องจากฉันคุ้นเคยกับความล้มเหลว ฉันจึงเริ่มค้นหาคำแนะนำเพิ่มเติมทางอินเทอร์เน็ตและพบสิ่งที่ค่อนข้างน่าสนใจ
ความพยายามที่ 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 เส้น! จำเป็นต้องดูแลอย่างดี
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 ก่อน และเพิ่มขึ้นเรื่อยๆ OFFSET ผลผลิตก็จะลดลงไปอีก
ความพยายามที่ 4: ถ่ายโอนข้อมูลในรูปแบบข้อความ
จากนั้นความคิดที่ยอดเยี่ยมก็เข้ามาในใจของฉัน: ทิ้งข้อมูลในรูปแบบข้อความและวิเคราะห์บรรทัดสุดท้ายที่บันทึกไว้
แต่ก่อนอื่นเรามาดูโครงสร้างของตารางกันก่อน ws_log_smevlog:
ในกรณีของเรา เรามีคอลัมน์ "Id"ซึ่งมีตัวระบุเฉพาะ (ตัวนับ) ของแถว แผนเป็นดังนี้:
- เราเริ่มถ่ายโอนข้อมูลในรูปแบบข้อความ (ในรูปแบบของคำสั่ง sql)
- ณ จุดหนึ่ง ดัมพ์จะถูกขัดจังหวะเนื่องจากมีข้อผิดพลาด แต่ไฟล์ข้อความจะยังคงถูกบันทึกไว้บนดิสก์
- เราดูที่ส่วนท้ายของไฟล์ข้อความดังนั้นเราจึงพบตัวระบุ (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) พบว่าดัมพ์ถูกขัดจังหวะในบรรทัดด้วย id 186 525. “ดังนั้นปัญหาอยู่ในบรรทัด id 186 526 มันใช้งานไม่ได้และจำเป็นต้องลบ!” - ฉันคิด. แต่การสืบค้นไปยังฐานข้อมูล:
«เลือก * จาก ws_log_smevlog โดยที่ id=186529“ ปรากฎว่าทุกอย่างเรียบร้อยดีกับบรรทัดนี้... แถวที่มีดัชนี 186 - 530 ก็ใช้งานได้โดยไม่มีปัญหาเช่นกัน “ความคิดที่ยอดเยี่ยม” อีกอย่างหนึ่งล้มเหลว ต่อมาฉันเข้าใจว่าทำไมสิ่งนี้จึงเกิดขึ้น: เมื่อลบและเปลี่ยนแปลงข้อมูลจากตาราง พวกเขาจะไม่ถูกลบทางกายภาพ แต่ถูกทำเครื่องหมายเป็น "สิ่งอันดับที่ตายแล้ว" จากนั้นก็มา เครื่องดูดฝุ่นอัตโนมัติ และทำเครื่องหมายบรรทัดเหล่านี้ว่าลบแล้ว และอนุญาตให้ใช้บรรทัดเหล่านี้ซ้ำได้ เพื่อให้เข้าใจว่าหากข้อมูลในตารางเปลี่ยนแปลงและเปิดใช้งาน autovacuum ข้อมูลดังกล่าวจะไม่ถูกจัดเก็บตามลำดับ
ความพยายามที่ 5: SELECT, FROM, WHERE 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:
ซึ่งหมายความว่าการค้นหาบรรทัดที่มีรหัสที่ต้องการไม่ควรใช้เวลามากนัก ตามทฤษฎีแล้ว มันควรจะได้ผล เอาล่ะ เรามารันคำสั่งกัน tmux และไปนอนกันเถอะ
ในตอนเช้าฉันพบว่ามีการดูรายการประมาณ 90 รายการ ซึ่งมากกว่า 000% เท่านั้น ผลลัพธ์ที่ยอดเยี่ยมเมื่อเทียบกับวิธีก่อนหน้า (5%)! แต่ฉันไม่อยากรอถึง 2 วัน...
ความพยายามที่ 6: SELECT, FROM, WHERE id >= และ id <
ลูกค้ามีเซิร์ฟเวอร์ที่ยอดเยี่ยมสำหรับฐานข้อมูลโดยเฉพาะ: โปรเซสเซอร์คู่ Intel Xeon E5-2697 v2มีกระทู้มากถึง 48 กระทู้ในตำแหน่งของเรา! โหลดบนเซิร์ฟเวอร์อยู่ในระดับปานกลาง เราสามารถดาวน์โหลดได้ประมาณ 20 เธรดโดยไม่มีปัญหาใดๆ นอกจากนี้ยังมี RAM เพียงพอ: มากถึง 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
ฉันประหลาดใจมากที่รายการถูกลบโดยไม่มีปัญหาใดๆ แม้ว่าจะไม่มีตัวเลือกก็ตาม zero_damaged_pages.
จากนั้นฉันก็เชื่อมต่อกับฐานข้อมูลแล้ว สูญญากาศเต็ม (ฉันคิดว่าไม่จำเป็นต้องทำเช่นนี้) และในที่สุดฉันก็ลบข้อมูลสำรองโดยใช้ได้สำเร็จ pg_dump. การถ่ายโอนข้อมูลถูกถ่ายโดยไม่มีข้อผิดพลาด! ปัญหาได้รับการแก้ไขด้วยวิธีที่โง่เขลา ความสุขไม่มีขอบเขต หลังจากความล้มเหลวมากมาย เราก็พยายามหาทางแก้ไข!
รับทราบและสรุป
นี่เป็นประสบการณ์ครั้งแรกของฉันในการกู้คืนฐานข้อมูล Postgres จริง ฉันจะจดจำประสบการณ์นี้ไปอีกนาน
และสุดท้ายนี้ ฉันอยากจะกล่าวขอบคุณ PostgresPro สำหรับการแปลเอกสารเป็นภาษารัสเซียและสำหรับ
ที่มา: will.com