Saya ingin berbagi pengalaman pertama saya yang berhasil memulihkan fungsionalitas penuh basis data Postgres. Saya pertama kali mengenal Postgres enam bulan yang lalu; sebelumnya, saya tidak punya pengalaman mengelola basis data.

Saya bekerja sebagai insinyur semi-DevOps di sebuah perusahaan TI besar. Perusahaan kami mengembangkan perangkat lunak untuk layanan beban tinggi, dan saya bertanggung jawab atas keandalan operasional, pemeliharaan, dan penerapan. Saya diberi tugas standar: memperbarui aplikasi di satu server. Aplikasi ini ditulis dalam Django, dan migrasi (perubahan struktur basis data) dilakukan selama pembaruan. Sebelum proses ini, kami melakukan dump basis data lengkap menggunakan program pg_dump standar, untuk berjaga-jaga.
Terjadi kesalahan tak terduga saat melakukan dumping (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" menunjukkan masalah pada tingkat sistem berkas, yang sangat buruk. Berbagai forum telah menyarankan untuk melakukan Vakum Penuh dengan opsi zero_damaged_pages untuk menyelesaikan masalah ini. Nah, poprrobeum...
Mempersiapkan pemulihan
PERINGATAN! Pastikan untuk mencadangkan instalasi Postgres Anda sebelum mencoba memulihkan basis data. Jika Anda menggunakan mesin virtual, hentikan basis data dan ambil snapshot. Jika Anda tidak dapat mengambil snapshot, hentikan basis data dan salin isi direktori Postgres (termasuk berkas .wal) ke lokasi yang aman. Yang terpenting adalah menghindari memperburuk keadaan. Baca terus. .
Karena database saya pada umumnya berfungsi, saya membatasi diri pada dump database biasa, tetapi mengecualikan tabel dengan data yang rusak (opsi -T, --exclude-table=TABEL di pg_dump).
Servernya fisik, jadi mustahil untuk mengambil snapshot. Cadangannya sudah ada, mari kita lanjutkan.
Memeriksa sistem berkas
Sebelum mencoba memulihkan basis data, kita perlu memastikan bahwa sistem berkasnya sendiri masih utuh. Dan jika ada kesalahan, kita perlu memperbaikinya, karena jika tidak, kita hanya akan memperburuk keadaan.
Dalam kasus saya, sistem file dengan database dipasang di "/srv" dan tipenya adalah ext4.
Hentikan basis data: systemctl hentikan postgresql@9.5-main.service dan kami memeriksa bahwa sistem file tidak digunakan oleh siapa pun dan dapat dilepas menggunakan perintah lsof:
lsof +D /srv
Saya juga harus menghentikan database redis karena juga menggunakan "/srv"Lalu saya melepasnya. / srv (jumlah).
Pemeriksaan sistem berkas dilakukan menggunakan utilitas e2fsck.dll dengan tombol -f (Paksa pemeriksaan bahkan jika sistem berkas ditandai bersih):

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

e2fsck.dll mengatakan 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 berkas dan memulai basis data).
Jika Anda memiliki server fisik, pastikan untuk memeriksa status disk (melalui smartctl -a /dev/XXX) atau pengontrol RAID untuk memastikan masalahnya bukan terkait perangkat keras. Dalam kasus saya, RAID ternyata berbasis perangkat keras, jadi saya meminta admin lokal untuk memeriksa status RAID (server berjarak beberapa ratus kilometer). Dia mengatakan tidak ada kesalahan, yang berarti kami pasti bisa memulai pemulihan.
Percobaan 1: zero_damaged_pages
Hubungkan ke database melalui psql menggunakan akun dengan hak akses superuser. Kita membutuhkan superuser karena opsi zero_damaged_pages Hanya itu yang bisa mengubahnya. Dalam kasus saya, ini Postgres:
psql -h 127.0.0.1 -U postgres -s [nama_basis_data]
Pilihan zero_damaged_pages diperlukan untuk mengabaikan kesalahan baca (dari situs web postgrespro):
Ketika header halaman yang rusak terdeteksi, PostgreSQL biasanya melaporkan kesalahan dan membatalkan transaksi yang sedang berjalan. Jika parameter zero_damaged_pages diaktifkan, sistem akan mengeluarkan peringatan, menolkan halaman yang rusak, dan melanjutkan pemrosesan. Perilaku ini merusak data, khususnya semua baris pada halaman yang rusak.
Kami mengaktifkan opsi tersebut dan mencoba melakukan penyedotan penuh pada tabel:
VACUUM FULL VERBOSE 
Sayangnya, gagal.
Kami menemukan 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– mekanisme untuk menyimpan “data panjang” di Poetgres jika data tersebut tidak muat dalam satu halaman (default 8kb).
Upaya 2: indeks ulang
Tips pertama dari Google tidak membantu. Setelah beberapa menit mencari, saya menemukan tips kedua: indeks ulang Tabel rusak. Saya sudah melihat saran ini di banyak tempat, tetapi kurang meyakinkan. Mari kita indeks ulang:
reindex table ws_log_smevlog 
indeks ulang diselesaikan tanpa masalah.
Namun, hal itu tidak membantu, VAKUM PENUH mengalami kesalahan serupa. Karena saya terbiasa dengan kegagalan, saya terus mencari saran daring dan menemukan sesuatu yang cukup menarik .
Percobaan 3: PILIH, BATAS, OFFSET
Artikel di atas menyarankan untuk meninjau tabel baris demi baris dan menghapus data yang bermasalah. Pertama, perlu meninjau 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; doneDalam kasus saya tabel tersebut berisi 1 628 991 garis! Itu perlu untuk menjaga , tapi itu topik untuk diskusi lain. 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; donePagi harinya, saya memutuskan untuk memeriksa perkembangannya. Yang mengejutkan, saya menemukan bahwa setelah 20 jam, hanya 2% data yang terpindai! Saya tidak ingin menunggu 50 hari. Kegagalan total lagi.
Tapi saya tidak menyerah. Saya bertanya-tanya mengapa pemindaiannya memakan waktu begitu lama. Dari dokumentasi (lagi-lagi di postgrespro), saya belajar:
OFFSET memerintahkannya untuk melewati sejumlah baris yang ditentukan sebelum mulai mengeluarkan baris.
Jika OFFSET dan LIMIT ditentukan, sistem akan terlebih dahulu melewati baris OFFSET dan kemudian mulai menghitung baris untuk LIMIT.Saat menggunakan LIMIT, penting juga untuk menggunakan klausa ORDER BY guna memastikan baris hasil dikembalikan dalam urutan tertentu. Jika tidak, subset baris yang tidak dapat diprediksi akan dikembalikan.
Jelas bahwa 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 meningkatnya OFFSET produktivitas akan menurun lebih jauh lagi.
Percobaan 4: Menangkap teks dump
Lalu sebuah ide cemerlang muncul di benak saya: menuangkannya dalam bentuk teks dan menganalisis baris terakhir yang direkam.
Namun pertama-tama, mari kita berkenalan dengan struktur tabel. ws_log_smevlog:

Dalam kasus kami, kami memiliki kolom "Indo", yang berisi pengidentifikasi unik (penghitung) untuk baris tersebut. Rencananya adalah sebagai berikut:
- Kita mulai membuang data dalam bentuk teks (dalam bentuk perintah SQL)
- Pada suatu saat, dump akan terganggu karena terjadi kesalahan, tetapi berkas teks akan tetap tersimpan pada disk.
- Kita melihat pada akhir file teks, sehingga kita menemukan pengenal (id) dari baris terakhir yang berhasil dihapus
Saya mulai menuangkannya dalam bentuk teks:
pg_dump -U my_user -d my_database -F p -t ws_log_smevlog -f ./my_dump.dumpDump, seperti yang diharapkan, gagal 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 Aku melihat ujung tempat pembuangan sampah (ekor -5 ./my_dump.dump) menemukan bahwa dump terputus pada baris dengan id 186 525"Jadi masalahnya ada di baris ID 186 526, rusak, dan perlu dihapus!" pikir saya. Tapi setelah memeriksa database:
«pilih * dari ws_log_smevlog di mana id=186529“Ternyata semuanya baik-baik saja dengan baris ini… Baris dengan indeks 186.530 - 186.540 juga berfungsi tanpa masalah. "Ide cemerlang" lainnya gagal. Kemudian, saya mengerti mengapa ini terjadi: ketika menghapus/mengubah data dari tabel, itu tidak dihapus secara fisik, tetapi ditandai sebagai "tupel mati", lalu muncul vakum otomatis dan menandai baris-baris ini sebagai terhapus dan memungkinkannya untuk digunakan kembali. Untuk kejelasan, jika data dalam tabel berubah dan autovacuum diaktifkan, data tersebut tidak disimpan secara berurutan.
Percobaan 5: PILIH, DARI, DI MANA id=
Kegagalan membuat kita lebih kuat. Jangan pernah menyerah, teruslah maju dan percaya pada diri sendiri serta kemampuan Anda. Jadi, saya memutuskan untuk mencoba opsi lain: cukup periksa semua data di database satu per satu. Dengan mengetahui struktur tabel saya (lihat di atas), kami memiliki kolom id yang unik (kunci utama). Kami memiliki 1.628.991 baris di tabel dan id Mereka berurutan, yang berarti kita dapat mengulanginya 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; doneBagi mereka yang tidak mengerti, perintahnya bekerja seperti ini: memindai tabel baris demi baris dan mengirim 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 bahwa select mengalami masalah (kode pengembalian perintah bukan 0)).
Saya beruntung, saya memiliki indeks yang dibuat di lapangan id:

Artinya, menemukan baris dengan ID yang dibutuhkan seharusnya tidak memakan waktu lama. Secara teori, seharusnya berhasil. Jadi, mari kita jalankan perintah di tmux dan mari kita tidur.
Pagi harinya, saya mendapati sekitar 90.000 postingan telah dilihat, atau hanya sekitar 5%. Hasil yang luar biasa dibandingkan metode sebelumnya (2%)! Tapi saya tidak mau menunggu 20 hari...
Percobaan 6: PILIH, DARI, DI MANA id >= dan id
Pelanggan memiliki server yang sangat bagus yang dialokasikan untuk pangkalan data: server dengan prosesor ganda. Intel Xeon E5-2697 v2Kami memiliki 48 thread yang tersedia! Beban server rata-rata, jadi kami dapat dengan mudah menangani sekitar 20 thread. RAM kami juga sangat besar: 384 gigabita!
Oleh karena itu, tim harus 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; doneSaya bisa saja menulis skrip yang indah dan elegan di sini, tetapi saya memilih metode paralelisasi tercepat: membagi rentang 0-1628991 secara manual menjadi interval 100.000 rekaman dan menjalankan 16 perintah jenis berikut 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; doneTapi bukan itu saja. Menghubungkan ke basis data juga membutuhkan waktu dan sumber daya sistem. Menghubungkan 1.628.991 bukanlah langkah yang cerdas, Anda pasti setuju. Jadi, mari kita ambil 1000 baris per koneksi, bukan hanya satu. Perintahnya akhirnya terlihat seperti 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; doneBuka 16 jendela dalam sesi tmux dan jalankan perintah berikut:
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! Khususnya (nilai XXX dan ZZZ tidak lagi tersimpan):
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
146000Ini berarti kita memiliki tiga baris dengan kesalahan. ID rekaman bermasalah pertama dan kedua berada di antara 829.000 dan 830.000, dan ID rekaman ketiga berada di antara 146.000 dan 147.000. Selanjutnya, kita hanya perlu menemukan nilai ID yang tepat dari rekaman bermasalah tersebut. Untuk melakukannya, kita memindai rentang rekaman bermasalah kita dengan kelipatan 1 dan mengidentifikasi ID-nya:
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 telah menemukan baris yang bermasalah. Mari kita akses database menggunakan psql dan coba hapus baris-baris tersebut:
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 1Yang mengejutkan saya, catatan tersebut dihapus tanpa masalah bahkan tanpa opsi zero_damaged_pages.
Kemudian saya terhubung ke database, melakukan VAKUM PENUH (Saya pikir tidak perlu melakukan ini), dan akhirnya berhasil mengambil cadangan menggunakan hal_dumpDump-nya diambil tanpa kesalahan! Masalahnya diselesaikan dengan metode yang luar biasa bodoh ini. Saya sangat senang akhirnya menemukan solusi setelah sekian banyak kegagalan!
Ucapan Terima Kasih dan Kesimpulan
Ini pengalaman pertama saya memulihkan basis data Postgres yang asli. Pengalaman ini akan saya ingat selamanya.
Dan terakhir, saya ingin mengucapkan terima kasih kepada PostgresPro karena telah menerjemahkan dokumentasi ke dalam bahasa Rusia dan , yang sangat membantu selama analisis masalah.
Sumber: www.habr.com
