Pengalaman pertama saya memulihkan database Postgres setelah kegagalan (halaman tidak valid di blok 4123007 dari relatton base/16490)

Saya ingin berbagi dengan Anda pengalaman sukses pertama saya dalam memulihkan database Postgres ke fungsionalitas penuh. Saya mengenal Postgres DBMS setengah tahun yang lalu, sebelumnya saya tidak memiliki pengalaman sama sekali dalam administrasi database.

Pengalaman pertama saya memulihkan database Postgres setelah kegagalan (halaman tidak valid di blok 4123007 dari relatton base/16490)

Saya bekerja sebagai insinyur semi-DevOps di sebuah perusahaan IT besar. Perusahaan kami mengembangkan perangkat lunak untuk layanan beban tinggi, dan saya bertanggung jawab atas kinerja, pemeliharaan, dan penerapan. Saya diberi tugas standar: memperbarui aplikasi di satu server. Aplikasi ini ditulis dalam Django, selama pembaruan, migrasi dilakukan (perubahan dalam struktur basis data), dan sebelum proses ini kami mengambil dump basis data lengkap melalui program pg_dump standar, untuk berjaga-jaga.

Terjadi kesalahan tak terduga saat mengambil dump (Postgres versi 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

Kesalahan "halaman tidak valid di blok" berbicara tentang masalah pada tingkat sistem file, yang sangat buruk. Di berbagai forum disarankan untuk dilakukan VAKUM PENUH dengan opsi zero_damaged_pages untuk memecahkan masalah ini. Baiklah, mari kita coba...

Mempersiapkan pemulihan

PERINGATAN! Pastikan untuk mengambil cadangan Postgres sebelum mencoba memulihkan database Anda. Jika Anda memiliki mesin virtual, hentikan database dan ambil snapshot. Jika tidak memungkinkan untuk mengambil snapshot, hentikan database dan salin konten direktori Postgres (termasuk file wal) ke tempat yang aman. Hal utama dalam bisnis kita adalah jangan memperburuk keadaan. Membaca ini.

Karena database umumnya berfungsi untuk saya, saya membatasi diri pada dump database biasa, tetapi mengecualikan tabel dengan data yang rusak (opsi -T, --exclude-table=TABEL di pg_dump).

Servernya bersifat fisik, tidak mungkin mengambil snapshot. Cadangan telah dihapus, mari kita lanjutkan.

Pemeriksaan sistem file

Sebelum mencoba memulihkan database, kita perlu memastikan semuanya beres dengan sistem file itu sendiri. Dan jika ada kesalahan, perbaiki, karena jika tidak, Anda hanya akan memperburuk keadaan.

Dalam kasus saya, sistem file dengan database sudah terpasang "/srv" dan tipenya adalah ext4.

Menghentikan basis data: sistemctl berhenti [email dilindungi] dan periksa apakah sistem file tidak digunakan oleh siapa pun dan dapat dilepas menggunakan perintah lsof:
lsof +D /srv

Saya juga harus menghentikan database redis, karena database tersebut juga menggunakan "/srv". Selanjutnya saya melepas / srv (jumlah).

Sistem file diperiksa menggunakan utilitas e2fsck.dll dengan saklar -f (Periksa paksa meskipun sistem file ditandai bersih):

Pengalaman pertama saya memulihkan database Postgres setelah kegagalan (halaman tidak valid di blok 4123007 dari relatton base/16490)

Selanjutnya, menggunakan utilitas dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep diperiksa) Anda dapat memverifikasi bahwa pemeriksaan benar-benar dilakukan:

Pengalaman pertama saya memulihkan database Postgres setelah kegagalan (halaman tidak valid di blok 4123007 dari relatton base/16490)

e2fsck.dll mengatakan bahwa tidak ada masalah yang ditemukan pada tingkat sistem file ext4, yang berarti Anda dapat terus mencoba memulihkan database, atau lebih tepatnya kembali ke vakum penuh (tentu saja, Anda perlu memasang kembali sistem file dan memulai database).

Jika Anda memiliki server fisik, pastikan untuk memeriksa status disk (melalui smartctl -a /dev/XXX) atau pengontrol RAID untuk memastikan bahwa masalahnya bukan pada tingkat perangkat keras. Dalam kasus saya, RAID ternyata adalah “perangkat keras”, jadi saya meminta admin lokal untuk memeriksa status RAID (server berjarak beberapa ratus kilometer dari saya). Katanya tidak ada kesalahan, artinya restorasi pasti bisa dimulai.

Percobaan 1: zero_damaged_pages

Kami terhubung ke database melalui psql dengan akun yang memiliki hak pengguna super. Kami membutuhkan pengguna super, karena... pilihan zero_damaged_pages hanya dia yang bisa berubah. Dalam kasus saya ini adalah postgres:

psql -h 127.0.0.1 -U postgres -s [nama_database]

Pilihan zero_damaged_pages diperlukan untuk mengabaikan kesalahan baca (dari situs web postgrespro):

Ketika PostgreSQL mendeteksi header halaman yang rusak, biasanya PostgreSQL melaporkan kesalahan dan membatalkan transaksi saat ini. Jika zero_damaged_pages diaktifkan, sistem akan mengeluarkan peringatan, menghapus halaman yang rusak di memori, dan melanjutkan pemrosesan. Perilaku ini merusak data, yaitu semua baris di halaman yang rusak.

Kami mengaktifkan opsi dan mencoba melakukan kekosongan penuh pada tabel:

VACUUM FULL VERBOSE

Pengalaman pertama saya memulihkan database Postgres setelah kegagalan (halaman tidak valid di blok 4123007 dari relatton base/16490)
Sayangnya, nasib buruk.

Kami mengalami kesalahan serupa:

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 – mekanisme untuk menyimpan “data panjang” di Poetgres jika tidak muat dalam satu halaman (default 8kb).

Percobaan 2: indeks ulang

Saran pertama dari Google tidak membantu. Setelah beberapa menit mencari, saya menemukan tip kedua - membuatnya indeks ulang meja rusak. Saya melihat nasihat ini di banyak tempat, namun tidak membangkitkan rasa percaya diri. Mari kita indeks ulang:

reindex table ws_log_smevlog

Pengalaman pertama saya memulihkan database Postgres setelah kegagalan (halaman tidak valid di blok 4123007 dari relatton base/16490)

indeks ulang selesai tanpa masalah.

Namun, hal ini tidak membantu, VAKUM PENUH jatuh dengan kesalahan serupa. Karena saya sudah terbiasa dengan kegagalan, saya mulai mencari nasihat lebih jauh di Internet dan menemukan sesuatu yang cukup menarik sebuah artikel.

Percobaan 3: PILIH, BATAS, OFFSET

Artikel di atas menyarankan untuk melihat tabel baris demi baris dan menghapus data yang bermasalah. Pertama kita perlu melihat semua baris:

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

Dalam kasus saya, tabelnya berisi 1 628 991 garis! Itu perlu untuk dijaga dengan baik partisi data, tapi ini adalah topik untuk diskusi terpisah. Saat itu hari Sabtu, saya menjalankan perintah ini di tmux dan pergi tidur:

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

Pada pagi hari saya memutuskan untuk memeriksa bagaimana keadaannya. Yang mengejutkan saya, saya menemukan bahwa setelah 20 jam, hanya 2% data yang dipindai! Saya tidak ingin menunggu 50 hari. Kegagalan total lainnya.

Tapi saya tidak menyerah. Saya bertanya-tanya mengapa pemindaiannya memakan waktu lama. Dari dokumentasi (sekali lagi di postgrespro) saya menemukan:

OFFSET menentukan untuk melewati jumlah baris yang ditentukan sebelum mulai mengeluarkan baris.
Jika OFFSET dan LIMIT keduanya ditentukan, sistem terlebih dahulu melewatkan baris OFFSET dan kemudian mulai menghitung baris untuk batasan LIMIT.

Saat menggunakan LIMIT, penting juga untuk menggunakan klausa ORDER BY sehingga baris hasil dikembalikan dalam urutan tertentu. Jika tidak, subkumpulan baris yang tidak dapat diprediksi akan dikembalikan.

Jelas sekali, perintah di atas salah: pertama, tidak ada dipesan oleh, hasilnya bisa saja salah. Kedua, Postgres pertama-tama harus memindai dan melewati baris OFFSET, dan dengan peningkatan OFFSET produktivitas akan semakin menurun.

Percobaan 4: buang dalam bentuk teks

Kemudian sebuah ide cemerlang muncul di benak saya: membuang sampah dalam bentuk teks dan menganalisis baris rekaman terakhir.

Tapi pertama-tama, mari kita lihat struktur tabelnya. ws_log_smevlog:

Pengalaman pertama saya memulihkan database Postgres setelah kegagalan (halaman tidak valid di blok 4123007 dari relatton base/16490)

Dalam kasus kami, kami memiliki kolom "Indo", yang berisi pengidentifikasi unik (penghitung) baris. Rencananya seperti ini:

  1. Kita mulai mengambil dump dalam bentuk teks (dalam bentuk perintah sql)
  2. Pada titik waktu tertentu, dump akan terganggu karena kesalahan, namun file teks akan tetap disimpan di disk
  3. Kami melihat akhir file teks, sehingga kami menemukan pengidentifikasi (id) dari baris terakhir yang berhasil dihapus

Saya mulai membuang sampah dalam bentuk teks:

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

Dump, seperti yang diharapkan, terganggu dengan kesalahan yang sama:

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

Lebih jauh melalui ekor Saya melihat ke ujung tempat pembuangan sampah (ekor -5 ./my_dump.dump) menemukan bahwa dump terputus pada saluran dengan id 186 525. “Jadi masalahnya ada di baris id 186, rusak, dan perlu dihapus!” - Saya pikir. Namun, membuat kueri ke database:
«pilih * dari ws_log_smevlog dimana id=186529"Ternyata semuanya baik-baik saja dengan baris ini... Baris dengan indeks 186 - 530 juga berfungsi tanpa masalah. “Ide cemerlang” lainnya gagal. Belakangan saya mengerti mengapa ini terjadi: ketika menghapus dan mengubah data dari tabel, data tersebut tidak dihapus secara fisik, tetapi ditandai sebagai "tupel mati", lalu muncul vakum otomatis dan menandai baris-baris ini sebagai dihapus dan mengizinkan baris-baris ini digunakan kembali. Untuk memahaminya, jika data pada tabel berubah dan autovacuum diaktifkan, maka tidak disimpan secara berurutan.

Percobaan 5: PILIH, DARI, DI MANA id=

Kegagalan membuat kita lebih kuat. Anda tidak boleh menyerah, Anda harus mencapai akhir dan percaya pada diri sendiri dan kemampuan Anda. Jadi saya memutuskan untuk mencoba opsi lain: lihat saja semua catatan di database satu per satu. Mengetahui struktur tabel saya (lihat di atas), kami memiliki bidang id yang unik (kunci utama). Kami memiliki 1 baris dalam tabel dan id berurutan, artinya kita bisa membahasnya satu per satu:

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

Jika ada yang tidak mengerti, perintahnya berfungsi sebagai berikut: memindai tabel baris demi baris dan mengirimkan stdout ke / dev / null, tetapi jika perintah SELECT gagal, maka teks kesalahan dicetak (stderr dikirim ke konsol) dan baris yang berisi kesalahan dicetak (terima kasih kepada ||, yang berarti pemilihan mengalami masalah (kode pengembalian perintah bukan 0)).

Saya beruntung, saya telah membuat indeks di lapangan id:

Pengalaman pertama saya memulihkan database Postgres setelah kegagalan (halaman tidak valid di blok 4123007 dari relatton base/16490)

Artinya, menemukan baris dengan id yang diinginkan tidak memakan banyak waktu. Secara teori itu seharusnya berhasil. Baiklah, mari kita jalankan perintahnya tmux dan ayo tidur.

Pada pagi hari saya menemukan bahwa sekitar 90 entri telah dilihat, yaitu lebih dari 000%. Hasil yang luar biasa jika dibandingkan dengan metode sebelumnya (5%)! Tapi saya tidak ingin menunggu 2 hari...

Percobaan 6: PILIH, DARI, DI MANA id >= dan id

Pelanggan memiliki server luar biasa yang didedikasikan untuk database: prosesor ganda Intel Xeon E5-2697 v2, ada sebanyak 48 thread di lokasi kami! Beban pada server rata-rata; kami dapat mengunduh sekitar 20 thread tanpa masalah. RAM-nya juga cukup: sebanyak 384 gigabyte!

Oleh karena itu, perintah perlu diparalelkan:

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

Di sini dimungkinkan untuk menulis skrip yang indah dan elegan, tetapi saya memilih metode paralelisasi tercepat: membagi rentang 0-1628991 secara manual menjadi interval 100 catatan dan menjalankan 000 perintah dalam bentuk secara terpisah:

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

Tapi bukan itu saja. Secara teori, menyambung ke database juga memerlukan waktu dan sumber daya sistem. Menghubungkan 1 tidak terlalu cerdas, Anda setuju. Oleh karena itu, mari kita ambil 628 baris, bukan koneksi satu lawan satu. Hasilnya, tim berubah menjadi ini:

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

Buka 16 jendela dalam sesi tmux dan jalankan perintah:

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

Sehari kemudian saya menerima hasil pertama! Yaitu (nilai XXX dan ZZZ tidak lagi dipertahankan):

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

Artinya tiga baris mengandung kesalahan. Id dari catatan masalah pertama dan kedua adalah antara 829 dan 000, id dari catatan masalah ketiga adalah antara 830 dan 000. Selanjutnya, kita hanya perlu menemukan nilai id yang tepat dari catatan masalah. Untuk melakukan ini, kami memeriksa rentang kami dengan catatan bermasalah dengan langkah 146 dan mengidentifikasi id:

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

Selamat berakhir

Kami menemukan garis yang bermasalah. Kami masuk ke database melalui psql dan mencoba menghapusnya:

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

Yang mengejutkan saya, entri tersebut dihapus tanpa masalah bahkan tanpa opsi zero_damaged_pages.

Lalu saya terhubung ke database, berhasil VAKUM PENUH (Saya pikir hal ini tidak perlu dilakukan), dan akhirnya saya berhasil menghapus cadangan menggunakan hal_dump. Tempat pembuangan sampah diambil tanpa kesalahan apa pun! Masalahnya diselesaikan dengan cara yang bodoh. Kegembiraan tidak mengenal batas, setelah banyak kegagalan kami berhasil menemukan solusi!

Ucapan Terima Kasih dan Kesimpulan

Beginilah pengalaman pertama saya memulihkan database Postgres yang sebenarnya. Saya akan mengingat pengalaman ini untuk waktu yang lama.

Dan terakhir, saya ingin mengucapkan terima kasih kepada PostgresPro karena telah menerjemahkan dokumentasi ke dalam bahasa Rusia dan sebagainya kursus online yang sepenuhnya gratis, yang banyak membantu selama analisis masalah.

Sumber: www.habr.com

Tambah komentar