Bir başarısızlıktan sonra Postgres veritabanını kurtarma konusundaki ilk deneyimim (relatton base/4123007'ın 16490 bloğundaki geçersiz sayfa)

Postgres veritabanını tam işlevselliğe geri yükleme konusundaki ilk başarılı deneyimimi sizinle paylaşmak istiyorum. Postgres DBMS ile altı ay önce tanıştım; ondan önce veritabanı yönetimi konusunda hiç deneyimim yoktu.

Bir başarısızlıktan sonra Postgres veritabanını kurtarma konusundaki ilk deneyimim (relatton base/4123007'ın 16490 bloğundaki geçersiz sayfa)

Büyük bir BT şirketinde yarı DevOps mühendisi olarak çalışıyorum. Şirketimiz yüksek yüklü hizmetler için yazılım geliştirmektedir ve ben performans, bakım ve dağıtımdan sorumluyum. Bana standart bir görev verildi: bir sunucudaki uygulamayı güncellemek. Uygulama Django'da yazılıyor, güncelleme sırasında geçişler yapılıyor (veritabanı yapısındaki değişiklikler) ve bu işlemden önce her ihtimale karşı standart pg_dump programı aracılığıyla tam bir veritabanı dökümü alıyoruz.

Döküm alınırken beklenmeyen bir hata oluştu (Postgres sürüm 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

Böcek "blokta geçersiz sayfa" dosya sistemi düzeyindeki sorunlardan bahsediyor ki bu çok kötü. Çeşitli forumlarda yapılması önerildi TAM VAKUM seçeneği ile sıfır_hasarlı_sayfalar bu problemi çözmek için. Peki deneyelim...

İyileşmeye hazırlanıyor

UYARI! Veritabanınızı geri yükleme girişiminde bulunmadan önce Postgres yedeğini aldığınızdan emin olun. Eğer sanal makineniz varsa veritabanını durdurup snapshot alın. Anlık görüntü almak mümkün değilse veritabanını durdurun ve Postgres dizininin içeriğini (wal dosyaları dahil) güvenli bir yere kopyalayın. Bizim işimizde asıl mesele işleri daha da kötüleştirmemek. Okumak bu.

Veritabanı genel olarak işime yaradığından, kendimi normal bir veritabanı dökümüyle sınırladım, ancak hasarlı verileri içeren tabloyu hariç tuttum (seçenek) -T, --hariç-tablo=TABLO pg_dump'ta).

Sunucu fizikseldi, anlık görüntü almak imkansızdı. Yedekleme kaldırıldı, devam edelim.

Dosya sistemi kontrolü

Veritabanını geri yüklemeyi denemeden önce, dosya sisteminin kendisinde her şeyin yolunda olduğundan emin olmamız gerekir. Ve hata durumunda onları düzeltin, aksi takdirde işleri daha da kötüleştirebilirsiniz.

Benim durumumda, veritabanına sahip dosya sistemi monte edildi "/srv" ve tür ext4'tü.

Veritabanının durdurulması: sistemctl durdurma [e-posta korumalı] ve dosya sisteminin hiç kimse tarafından kullanılmadığını ve şu komut kullanılarak bağlantısının kesilebildiğini kontrol edin of:
lsof +D /srv

Ayrıca redis veritabanını da kullandığı için durdurmak zorunda kaldım. "/srv". Daha sonra bağlantıyı kestim / srv (tutar).

Dosya sistemi yardımcı program kullanılarak kontrol edildi e2fsck'nin -f anahtarıyla (Dosya sistemi temiz olarak işaretlenmiş olsa bile denetimi zorla):

Bir başarısızlıktan sonra Postgres veritabanını kurtarma konusundaki ilk deneyimim (relatton base/4123007'ın 16490 bloğundaki geçersiz sayfa)

Daha sonra yardımcı programı kullanarak çöplük2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep kontrol edildi) kontrolün gerçekten gerçekleştirildiğini doğrulayabilirsiniz:

Bir başarısızlıktan sonra Postgres veritabanını kurtarma konusundaki ilk deneyimim (relatton base/4123007'ın 16490 bloğundaki geçersiz sayfa)

e2fsck'nin ext4 dosya sistemi düzeyinde herhangi bir sorun bulunmadığını söylüyor; bu, veritabanını geri yüklemeyi denemeye devam edebileceğiniz veya daha doğrusu geri dönebileceğiniz anlamına gelir. vakum dolu (elbette dosya sistemini tekrar monte etmeniz ve veritabanını başlatmanız gerekir).

Fiziksel bir sunucunuz varsa disklerin durumunu kontrol ettiğinizden emin olun (aracılığıyla) smartctl -a /dev/XXXSorunun donanım düzeyinde olmadığından emin olmak için ) veya RAID denetleyicisini kullanın. Benim durumumda RAID'in "donanım" olduğu ortaya çıktı, bu yüzden yerel yöneticiden RAID'in durumunu kontrol etmesini istedim (sunucu benden birkaç yüz kilometre uzaktaydı). Herhangi bir hata olmadığını, bu da kesinlikle restorasyona başlayabileceğimiz anlamına geliyor dedi.

1. Deneme: sıfır_hasarlı_sayfalar

Veritabanına süper kullanıcı haklarına sahip bir hesapla psql üzerinden bağlanıyoruz. Bir süper kullanıcıya ihtiyacımız var çünkü... seçenek sıfır_hasarlı_sayfalar yalnızca o değişebilir. Benim durumumda bu postgres:

psql -h 127.0.0.1 -U postgres -s [veritabanı_adı]

Seçenek sıfır_hasarlı_sayfalar okuma hatalarını göz ardı etmek için gereklidir (postgrespro web sitesinden):

PostgreSQL bozuk bir sayfa başlığı tespit ettiğinde genellikle bir hata bildirir ve mevcut işlemi iptal eder. Zero_damaged_pages etkinleştirilirse sistem bunun yerine bir uyarı verir, bellekteki hasarlı sayfayı sıfırlar ve işleme devam eder. Bu davranış, verileri, yani hasarlı sayfadaki tüm satırları yok eder.

Seçeneği etkinleştiriyoruz ve tabloları tamamen boşaltmaya çalışıyoruz:

VACUUM FULL VERBOSE

Bir başarısızlıktan sonra Postgres veritabanını kurtarma konusundaki ilk deneyimim (relatton base/4123007'ın 16490 bloğundaki geçersiz sayfa)
Maalesef şanssızlık.

Benzer bir hatayla karşılaştık:

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'te bir sayfaya sığmaması durumunda "uzun verileri" depolamak için bir mekanizma (varsayılan olarak 8kb).

Deneme 2: yeniden dizin oluşturma

Google'ın ilk tavsiyesi işe yaramadı. Birkaç dakika aradıktan sonra ikinci ipucunu buldum: reindex hasarlı masa. Bu tavsiyeyi birçok yerde gördüm ama güven uyandırmadı. Tekrar indeksleyelim:

reindex table ws_log_smevlog

Bir başarısızlıktan sonra Postgres veritabanını kurtarma konusundaki ilk deneyimim (relatton base/4123007'ın 16490 bloğundaki geçersiz sayfa)

reindex sorunsuz tamamlandı.

Ancak bu işe yaramadı VAKUM DOLU benzer bir hatayla çöktü. Başarısızlıklara alışkın olduğum için internette tavsiye aramaya başladım ve oldukça ilginç bir şeyle karşılaştım Makale.

Deneme 3: SEÇ, SINIR, OFSET

Yukarıdaki makale tabloya satır satır bakmayı ve sorunlu verileri kaldırmayı önerdi. İlk önce tüm satırlara bakmamız gerekiyordu:

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

Benim durumumda, tablonun içerdiği +1 628 991 çizgiler! İyi bakmak gerekiyordu veri bölümlemeama bu ayrı bir tartışma konusu. Cumartesi günüydü, bu komutu tmux'ta çalıştırdım ve yattım:

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

Sabah işlerin nasıl gittiğini kontrol etmeye karar verdim. Şaşırtıcı bir şekilde, 20 saat sonra verilerin yalnızca %2'sinin tarandığını keşfettim! 50 gün beklemek istemedim. Başka bir tam başarısızlık.

Ama pes etmedim. Taramanın neden bu kadar uzun sürdüğünü merak ettim. Dokümantasyondan (yine postgrespro'da) şunu öğrendim:

OFFSET, satırların çıktısını almaya başlamadan önce belirtilen sayıda satırın atlanacağını belirtir.
Hem OFFSET hem de LIMIT belirtilirse, sistem önce OFFSET satırlarını atlar ve ardından LIMIT kısıtlaması için satırları saymaya başlar.

LIMIT kullanılırken, sonuç satırlarının belirli bir sırayla döndürülmesi için ORDER BY cümlesinin de kullanılması önemlidir. Aksi takdirde, öngörülemeyen satır alt kümeleri döndürülür.

Açıkçası yukarıdaki komut yanlıştı: öncelikle, tarafından siparişsonuç hatalı olabilir. İkinci olarak, Postgres öncelikle OFFSET satırlarını tarayıp atlamak zorunda kaldı ve giderek OFFSET üretkenlik daha da düşecektir.

Deneme 4: metin biçiminde bir dökümü alın

Sonra aklıma harika görünen bir fikir geldi: metin biçimindeki bir dökümü alın ve son kaydedilen satırı analiz edin.

Ama önce tablonun yapısına bir bakalım. ws_log_smevlog:

Bir başarısızlıktan sonra Postgres veritabanını kurtarma konusundaki ilk deneyimim (relatton base/4123007'ın 16490 bloğundaki geçersiz sayfa)

Bizim durumumuzda bir sütun var "İD"satırın benzersiz tanımlayıcısını (sayacı) içeren. Plan şu şekildeydi:

  1. Metin biçiminde (sql komutları biçiminde) dökümü almaya başlıyoruz.
  2. Belirli bir noktada, bir hata nedeniyle döküm kesintiye uğrar, ancak metin dosyası hâlâ diskte kayıtlı olur.
  3. Metin dosyasının sonuna bakıyoruz, böylece başarıyla kaldırılan son satırın tanımlayıcısını (id) buluyoruz

Metin biçiminde bir dökümü almaya başladım:

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

Döküm, beklendiği gibi aynı hatayla kesintiye uğradı:

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

Daha da ileriye kuyruk Çöplüğün sonuna baktım (kuyruk -5 ./my_dump.dump) dökümün kimlik satırında kesintiye uğradığını keşfetti 186 525. "Yani sorun 186 526 no'lu satırda, bozuk ve silinmesi gerekiyor!" - Düşündüm. Ancak veritabanına bir sorgu yaparak:
«ws_log_smevlog'dan * seçin; burada id=186529"Bu çizgide her şeyin yolunda olduğu ortaya çıktı... 186 - 530 endeksli satırlar da sorunsuz çalıştı. Başka bir "harika fikir" başarısız oldu. Daha sonra bunun neden olduğunu anladım: Bir tablodaki verileri silerken veya değiştirirken, bunlar fiziksel olarak silinmez, ancak "ölü kayıtlar" olarak işaretlenir ve ardından gelir. otovakum bu satırları silinmiş olarak işaretler ve bu satırların yeniden kullanılmasına olanak sağlar. Anlamak gerekirse, tablodaki veriler değişirse ve otomatik vakum etkinleştirilirse, sıralı olarak saklanmaz.

5. Deneme: SELECT, FROM, WHERE kimliği=

Başarısızlıklar bizi daha güçlü kılar. Asla pes etmemeli, sonuna kadar gitmeli, kendinize ve yeteneklerinize inanmalısınız. Bu yüzden başka bir seçeneği denemeye karar verdim: Veritabanındaki tüm kayıtları tek tek incelemek. Tablomun yapısını bildiğim için (yukarı bakın), benzersiz bir kimlik alanımız var (birincil anahtar). Tabloda 1 satırımız var ve id sıralıdır, bu da onları tek tek inceleyebileceğimiz anlamına gelir:

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

Anlamayan varsa komut şu şekilde çalışır: tabloyu satır satır tarar ve stdout'u gönderir. / Dev / null, ancak SELECT komutu başarısız olursa, hata metni yazdırılır (stderr konsola gönderilir) ve hatayı içeren bir satır yazdırılır (|| sayesinde, bu seçimde sorun olduğu anlamına gelir (komutun dönüş kodu) 0 değil)).

Şanslıydım, sahada oluşturulmuş indekslerim vardı id:

Bir başarısızlıktan sonra Postgres veritabanını kurtarma konusundaki ilk deneyimim (relatton base/4123007'ın 16490 bloğundaki geçersiz sayfa)

Bu, istenen kimliğe sahip bir satır bulmanın fazla zaman almaması gerektiği anlamına gelir. Teorik olarak çalışması gerekir. Peki, komutu çalıştıralım tmux ve hadi yatalım.

Sabaha doğru yaklaşık 90 girişin görüntülendiğini gördüm, bu da %000'in biraz üzerindedir. Önceki yöntemle (%5) karşılaştırıldığında mükemmel bir sonuç! Ama 2 gün beklemek istemedim...

6. Deneme: SELECT, FROM, WHERE id >= ve id

Müşterinin veritabanına ayrılmış mükemmel bir sunucusu vardı: çift işlemcili Intel Xeon E5-2697 v2, konumumuzda 48'e kadar konu vardı! Sunucudaki yük ortalamaydı; yaklaşık 20 konuyu sorunsuzca indirebildik. Ayrıca yeterli RAM vardı: 384 gigabayta kadar!

Bu nedenle komutun paralelleştirilmesi gerekiyordu:

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

Burada güzel ve zarif bir komut dosyası yazmak mümkündü, ancak ben en hızlı paralelleştirme yöntemini seçtim: 0-1628991 aralığını manuel olarak 100 kayıtlık aralıklarla bölün ve formun 000 komutunu ayrı ayrı çalıştırın:

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

Ama hepsi bu değil. Teorik olarak bir veritabanına bağlanmak da biraz zaman ve sistem kaynağı gerektirir. 1'i bağlamak pek akıllıca değildi, kabul edersiniz. Bu nedenle bire bir bağlantı yerine 628 satır alalım. Sonuç olarak ekip şuna dönüştü:

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

Bir tmux oturumunda 16 pencere açın ve komutları çalıştırın:

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

Bir gün sonra ilk sonuçları aldım! Yani (XXX ve ZZZ değerleri artık korunmamaktadır):

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

Bu, üç satırın hata içerdiği anlamına gelir. Birinci ve ikinci problem kayıtlarının id'leri 829 ile 000 arasında, üçüncünün id'leri ise 830 ile 000 arasındaydı.Daha sonra problem kayıtlarının tam id değerini bulmamız gerekiyordu. Bunu yapmak için 146 adımlı sorunlu kayıtlar içeren yelpazemize bakıyoruz ve kimliği belirliyoruz:

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

Mutlu final

Sorunlu hatları bulduk. Veritabanına psql üzerinden girip silmeye çalışıyoruz:

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

Şaşırtıcı bir şekilde, girişler seçenek olmadan bile sorunsuz bir şekilde silindi sıfır_hasarlı_sayfalar.

Sonra veritabanına bağlandım, yaptım VAKUM DOLU (Bunu yapmanın gerekli olmadığını düşünüyorum) ve sonunda kullanarak yedeği başarıyla kaldırdım. pg_dump. Dökümü hatasız alındı! Sorun çok saçma bir şekilde çözüldü. Sevinç sınır tanımıyordu, pek çok başarısızlıktan sonra bir çözüm bulmayı başardık!

Teşekkür ve Sonuç

Gerçek bir Postgres veritabanını geri yükleme konusundaki ilk deneyimim bu şekilde ortaya çıktı. Bu deneyimi uzun süre hatırlayacağım.

Son olarak, belgeleri Rusçaya çevirdiği için PostgresPro'ya teşekkür etmek istiyorum. tamamen ücretsiz çevrimiçi kurslarSorunun analizi sırasında çok yardımcı oldu.

Kaynak: habr.com

Yorum ekle