Első tapasztalatom a Postgres adatbázis helyreállításával hiba után (érvénytelen oldal a relatton base/4123007 16490. blokkjában)

Szeretném megosztani Önnel első sikeres tapasztalatomat a Postgres adatbázis teljes funkcionalitásának helyreállításával kapcsolatban. Fél éve ismerkedtem meg a Postgres DBMS-sel, addig adatbázis-adminisztrációban egyáltalán nem volt tapasztalatom.

Első tapasztalatom a Postgres adatbázis helyreállításával hiba után (érvénytelen oldal a relatton base/4123007 16490. blokkjában)

Félig DevOps mérnökként dolgozom egy nagy informatikai cégnél. Cégünk nagy terhelésű szolgáltatásokhoz fejleszt szoftvereket, én felelek a teljesítményért, a karbantartásért és a telepítésért. Szabványos feladatot kaptam: frissítsek egy alkalmazást egy szerveren. Az alkalmazás Django nyelven íródott, a frissítés során migrációkat hajtanak végre (változnak az adatbázis szerkezetében), és ezt megelőzően minden esetre a szabványos pg_dump programmal egy teljes adatbázis kiíratást végzünk.

Váratlan hiba történt a kiíratás közben (Postgres 9.5-ös verzió):

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

Bogár "érvénytelen oldal a blokkban" fájlrendszer szintű problémákról beszél, ami nagyon rossz. Különféle fórumokon ezt javasolták TELJES VÁKUUM opcióval nulla_sérült_oldal hogy megoldja ezt a problémát. Nos, próbáljuk meg…

Felkészülés a gyógyulásra

FIGYELEM Mindenképpen készítsen biztonsági másolatot a Postgresről, mielőtt bármilyen kísérletet tesz az adatbázis visszaállítására. Ha van virtuális gépe, állítsa le az adatbázist, és készítsen pillanatképet. Ha nem lehet pillanatfelvételt készíteni, állítsa le az adatbázist, és másolja át a Postgres könyvtár tartalmát (beleértve a wal fájlokat is) egy biztonságos helyre. Vállalkozásunkban az a legfontosabb, hogy ne rontsuk tovább a dolgokat. Olvas ezt.

Mivel az adatbázis általában működött nekem, egy normál adatbázis kiíratásra korlátozódtam, de kizártam a sérült adatokat tartalmazó táblát (opció -T, --exclude-table=TABLE a pg_dump-ban).

A szerver fizikai volt, nem lehetett pillanatképet készíteni. A biztonsági másolatot eltávolították, menjünk tovább.

Fájlrendszer ellenőrzése

Mielőtt megpróbálnánk visszaállítani az adatbázist, meg kell győződnünk arról, hogy magával a fájlrendszerrel minden rendben van. Hibák esetén pedig javítsd ki, mert különben csak ronthatsz a helyzeten.

Az én esetemben az adatbázist tartalmazó fájlrendszer be volt csatolva "/srv" a típus pedig ext4 volt.

Az adatbázis leállítása: systemctl stop [e-mail védett] és ellenőrizze, hogy a fájlrendszert senki nem használja, és a paranccsal leválasztható-e lsof:
lsof +D /srv

A redis adatbázist is le kellett állítanom, mert az is használt "/srv". Ezután leszereltem / srv (umount).

A fájlrendszert a segédprogrammal ellenőriztük e2fsck az -f kapcsolóval (Kényszerellenőrzés akkor is, ha a fájlrendszer tisztának van jelölve):

Első tapasztalatom a Postgres adatbázis helyreállításával hiba után (érvénytelen oldal a relatton base/4123007 16490. blokkjában)

Ezután használja a segédprogramot dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep ellenőrizve) ellenőrizheti, hogy az ellenőrzést valóban elvégezték:

Első tapasztalatom a Postgres adatbázis helyreállításával hiba után (érvénytelen oldal a relatton base/4123007 16490. blokkjában)

e2fsck azt mondja, hogy az ext4 fájlrendszer szintjén nem találtak problémákat, ami azt jelenti, hogy folytathatja az adatbázis visszaállításának próbálkozását, vagy inkább visszatérhet vákuum tele (természetesen vissza kell csatolni a fájlrendszert és elindítani az adatbázist).

Ha van fizikai szervere, feltétlenül ellenőrizze a lemezek állapotát (a smartctl -a /dev/XXX) vagy RAID-vezérlővel, hogy megbizonyosodjon arról, hogy a probléma nem hardverszintű. Az én esetemben a RAID „hardver”-nek bizonyult, ezért megkértem a helyi adminisztrátort, hogy ellenőrizze a RAID állapotát (a szerver több száz kilométerre volt tőlem). Azt mondta, nem volt hiba, ami azt jelenti, hogy biztosan elkezdhetjük a helyreállítást.

1. kísérlet: nulla_sérült_oldal

Az adatbázishoz szuperfelhasználói jogokkal rendelkező fiókkal csatlakozunk psql-n keresztül. Szükségünk van egy szuperfelhasználóra, mert... választási lehetőség nulla_sérült_oldal csak ő tud megváltozni. Az én esetemben ez postgres:

psql -h 127.0.0.1 -U postgres -s [adatbázis_neve]

választási lehetőség nulla_sérült_oldal szükséges az olvasási hibák figyelmen kívül hagyásához (a Postgrespro webhelyről):

Amikor a PostgreSQL sérült oldalfejlécet észlel, általában hibát jelez, és megszakítja az aktuális tranzakciót. Ha a zero_daged_pages engedélyezve van, a rendszer ehelyett figyelmeztetést ad ki, nullázza a sérült oldalt a memóriában, és folytatja a feldolgozást. Ez a viselkedés megsemmisíti az adatokat, nevezetesen a sérült oldal összes sorát.

Engedélyezzük az opciót, és megpróbáljuk a táblázatok teljes vákuumát elvégezni:

VACUUM FULL VERBOSE

Első tapasztalatom a Postgres adatbázis helyreállításával hiba után (érvénytelen oldal a relatton base/4123007 16490. blokkjában)
Sajnos balszerencse.

Hasonló hibával találkoztunk:

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 – „hosszú adatok” tárolására szolgáló mechanizmus a Poetgresben, ha azok nem férnek el egy oldalon (alapértelmezés szerint 8 kb).

2. kísérlet: újraindex

A Google első tanácsa nem segített. Néhány perc keresgélés után megtaláltam a második tippet – az elkészítést újraindex sérült asztal. Sok helyen láttam ezt a tanácsot, de nem keltett bizalmat. Indexeljük újra:

reindex table ws_log_smevlog

Első tapasztalatom a Postgres adatbázis helyreállításával hiba után (érvénytelen oldal a relatton base/4123007 16490. blokkjában)

újraindex probléma nélkül elkészült.

Ez azonban nem segített, VÁKUUM MEGTELT hasonló hibával összeomlott. Mivel hozzászoktam a kudarcokhoz, elkezdtem tanácsot keresni az interneten, és egy meglehetősen érdekes dologra bukkantam статью.

3. kísérlet: KIVÁLASZTÁS, KORLÁTOZÁS, OFFSET

A fenti cikk azt javasolta, hogy soronként nézze meg a táblázatot, és távolítsa el a problémás adatokat. Először meg kellett néznünk az összes sort:

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

Az én esetemben a táblázat tartalmazta +1 628 991 XNUMX sorok! Jól kellett vigyázni rá adatparticionálás, de ez egy külön beszélgetés témája. Szombat volt, ezt a parancsot futtattam a tmux-ban, és lefeküdtem:

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

Reggelre úgy döntöttem, megnézem, hogy mennek a dolgok. Meglepetésemre rájöttem, hogy 20 óra elteltével az adatoknak csak 2%-a lett beolvasva! Nem akartam 50 napot várni. Újabb teljes kudarc.

De nem adtam fel. Kíváncsi voltam, miért tartott ilyen sokáig a szkennelés. A dokumentációból (ismét a postgrespro-n) megtudtam:

Az OFFSET megadja, hogy a megadott számú sor átugorja a sorok kiadásának megkezdése előtt.
Ha az OFFSET és a LIMIT is meg van adva, a rendszer először kihagyja az OFFSET sorokat, majd elkezdi számolni a sorokat a LIMIT kényszerhez.

A LIMIT használatakor fontos az ORDER BY záradék használata is, hogy az eredménysorok meghatározott sorrendben jelenjenek meg. Ellenkező esetben a sorok előre nem látható részhalmazai kerülnek visszaadásra.

Nyilvánvalóan rossz volt a fenti parancs: először is nem volt Rendezés, az eredmény hibás lehet. Másodszor, a Postgresnek először be kellett vizsgálnia és ki kellett hagynia az OFFSET sorokat, és növelve OFFSET a termelékenység tovább csökkenne.

4. kísérlet: vegyen ki egy kiírást szöveges formában

Aztán eszembe jutott egy zseniálisnak tűnő ötlet: vegyek egy kirakást szöveges formában, és elemezzem az utolsó rögzített sort.

De először vessünk egy pillantást a táblázat szerkezetére. ws_log_smevlog:

Első tapasztalatom a Postgres adatbázis helyreállításával hiba után (érvénytelen oldal a relatton base/4123007 16490. blokkjában)

A mi esetünkben van egy oszlopunk "Id", amely a sor egyedi azonosítóját (számlálóját) tartalmazta. A terv a következő volt:

  1. Elkezdjük a kiíratást szöveges formában (sql parancsok formájában)
  2. Egy bizonyos időpontban a kiírás megszakad egy hiba miatt, de a szöveges fájl továbbra is a lemezre kerül.
  3. Megnézzük a szöveges fájl végét, így megtaláljuk az utolsó sikeresen eltávolított sor azonosítóját (id)

Elkezdtem kiíratni szöveges formában:

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

A kiírás, ahogy az várható volt, ugyanazzal a hibával szakadt meg:

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

Tovább tovább farok Megnéztem a szemétlerakó végét (farok -5 ./my_dump.dump) felfedezte, hogy a dump megszakadt az id 186 525. "Tehát a probléma a 186 526-os azonosítójú sorban van, elromlott, és törölni kell!" - Azt gondoltam. De lekérdezés az adatbázisban:
«válassza ki a *-ot a ws_log_smevlogból, ahol id=186529„Kiderült, hogy ezzel a sorral minden rendben van... A 186 530 - 186 540 indexű sorok is gond nélkül működtek. Egy másik „zseniális ötlet” kudarcot vallott. Később megértettem, miért történt ez: amikor töröljük és módosítjuk az adatokat egy táblából, azok fizikailag nem törlődnek, hanem „halott tuples”-ként vannak megjelölve, majd jön autovákuum és töröltként jelöli meg ezeket a sorokat, és lehetővé teszi a sorok újrafelhasználását. Hogy megértsük, ha a táblázatban szereplő adatok megváltoznak, és az automatikus vákuum engedélyezve van, akkor azok nem szekvenciálisan kerülnek tárolásra.

5. kísérlet: SELECT, FROM, WHERE id=

A kudarcok erősebbé tesznek bennünket. Soha nem szabad feladni, a végsőkig kell menned, és hinned kell magadban és a képességeidben. Ezért úgy döntöttem, hogy kipróbálok egy másik lehetőséget: csak nézze át egyenként az adatbázis összes rekordját. Ismerve a táblázatom szerkezetét (lásd fent), van egy id mezőnk, amely egyedi (elsődleges kulcs). 1 628 991 sorunk van a táblázatban és id rendben vannak, ami azt jelenti, hogy egyenként végignézhetjük őket:

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

Ha valaki nem érti, a parancs a következőképpen működik: soronként átvizsgálja a táblázatot, és elküldi az stdout-ot / Dev / null, de ha a SELECT parancs meghiúsul, akkor a hibaszöveg kinyomtatásra kerül (az stderr elküldésre kerül a konzolba) és a hibát tartalmazó sor kerül kinyomtatásra (köszönhetően a ||-nek, ami azt jelenti, hogy a select-ben problémák voltak (a parancs visszatérési kódja). nem 0)).

Szerencsém volt, indexeket készítettem a pályán id:

Első tapasztalatom a Postgres adatbázis helyreállításával hiba után (érvénytelen oldal a relatton base/4123007 16490. blokkjában)

Ez azt jelenti, hogy a kívánt azonosítóval rendelkező sor megtalálása nem vesz igénybe sok időt. Elméletileg működnie kellene. Nos, futtassuk be a parancsot tmux és feküdjünk le.

Reggelre azt tapasztaltam, hogy körülbelül 90 000 bejegyzést tekintettek meg, ami valamivel több, mint 5%. Kiváló eredmény az előző módszerhez képest (2%)! De nem akartam 20 napot várni...

6. kísérlet: SELECT, FROM, WHERE id >= és id <

Az ügyfélnek kiváló szervere volt az adatbázisnak: kétprocesszoros Intel Xeon E5-2697 v2, akár 48 szál is volt a telephelyünkön! A szerver terhelése átlagos volt, körülbelül 20 szálat tudtunk letölteni gond nélkül. RAM is volt elég: 384 gigabájt!

Ezért a parancsot párhuzamosítani kellett:

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

Itt lehetett szép és elegáns szkriptet írni, de én a leggyorsabb párhuzamosítási módszert választottam: manuálisan osztottam fel a 0-1628991 tartományt 100 000 rekordos intervallumokra, és futtassunk külön 16 űrlapparancsot:

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

De ez még nem minden. Elméletileg az adatbázishoz való csatlakozás is némi időt és rendszererőforrást igényel. Az 1 628 991 csatlakoztatása nem volt túl okos, ezzel egyetértesz. Ezért egy kapcsolat helyett 1000 sort kérjünk le. Ennek eredményeként a csapat a következővé változott:

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

Nyisson meg 16 ablakot egy tmux munkamenetben, és futtassa a parancsokat:

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

Egy nappal később megkaptam az első eredményeket! Nevezetesen (az XXX és ZZZ értékek már nem őrződnek meg):

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

Ez azt jelenti, hogy három sor hibát tartalmaz. Az első és a második problémás rekord azonosítója 829 000 és 830 000 között volt, a harmadiké 146 000 és 147 000 között volt. Ezután egyszerűen meg kellett találnunk a problémás rekordok pontos azonosítóját. Ehhez átnézzük a problémás rekordokat tartalmazó tartományunkat 1-es lépéssel, és azonosítjuk az azonosítót:

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

Boldog befejezés

Megtaláltuk a problémás vonalakat. Bemegyünk az adatbázisba psql-n keresztül, és megpróbáljuk törölni őket:

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

Meglepetésemre a bejegyzések a lehetőség nélkül is gond nélkül törölve lettek nulla_sérült_oldal.

Aztán csatlakoztam az adatbázishoz, sikerült VÁKUUM MEGTELT (szerintem erre nem volt szükség), és végül sikeresen eltávolítottam a biztonsági másolatot a segítségével pg_dump. A szemétlerakást hiba nélkül vették! A problémát ilyen hülye módon megoldották. Az öröm nem ismert határokat, annyi kudarc után sikerült megoldást találni!

Köszönetnyilvánítás és következtetés

Így alakult az első tapasztalatom egy igazi Postgres adatbázis helyreállításával kapcsolatban. Erre az élményre sokáig emlékezni fogok.

Végül pedig szeretném megköszönni a PostgresPro-nak, hogy lefordította a dokumentációt orosz nyelvre és azért teljesen ingyenes online tanfolyamok, ami sokat segített a probléma elemzése során.

Forrás: will.com

Hozzászólás