Szeretném megosztani veletek az első sikeres tapasztalatomat egy Postgres adatbázis teljes funkcionalitásának visszaállításával kapcsolatban. Hat hónapja találkoztam először a Postgres-szel; előtte nem volt tapasztalatom adatbázisok adminisztrálásá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, és én felelek a működési megbízhatóságért, a karbantartásért és a telepítésért. Egy standard feladatot kaptam: egy alkalmazás frissítése egyetlen szerveren. Az alkalmazás Django nyelven íródott, és a migrációkat (az adatbázis-struktúra módosításait) a frissítés során hajtjuk végre. Ezt a folyamatot megelőzően egy teljes adatbázis-kiíratást végzünk a standard pg_dump programmal, csak a biztonság kedvéért.
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ákat jelez, ami nagyon rossz. Különböző fórumokon javasolták a következőket: Teljes vákuum opcióval nulla_sérült_oldal hogy megoldja ezt a problémát. Nos, poprrobeum...
Felkészülés a felépülésre
FIGYELEM Mielőtt megpróbálná visszaállítani az adatbázist, feltétlenül készítsen biztonsági másolatot a Postgres telepítéséről. Ha virtuális gépet használ, állítsa le az adatbázist, és készítsen pillanatképet. Ha nem tud pillanatképet készíteni, állítsa le az adatbázist, és másolja a Postgres könyvtár tartalmát (beleértve a .wal fájlokat is) egy biztonságos helyre. A legfontosabb, hogy elkerülje a helyzet súlyosbodását. Olvasson tovább. .
Mivel az adatbázisom általánosságban működött, egy szokásos adatbázis-kiíratásra korlátoztam magam, de kizártam a sérült adatokat tartalmazó táblát (opció -T, --exclude-table=TÁBLÁZAT a pg_dump-ban).
A szerver fizikailag volt, így pillanatkép készítése lehetetlen volt. A biztonsági mentés elkészült, haladjunk tovább.
A 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 maga a fájlrendszer sértetlen. És ha vannak hibák, ki kell javítanunk azokat, mert különben csak ronthatunk a helyzeten.
Az én esetemben az adatbázissal rendelkező fájlrendszert a következő helyre csatoltam: "/srv" és a típus ext4 volt.
Az adatbázis leállítása: systemctl stop postgresql@9.5-main.service és ellenőrizzük, hogy a fájlrendszert senki sem 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álta "/srv"Aztán leszereltem. / srv (összeg).
A fájlrendszer-ellenőrzést a segédprogrammal végeztük el. e2fsck az -f billentyűvel (Ellenőrzés kényszerítése akkor is, ha a fájlrendszer tisztaként van megjelölve):

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

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át, vagy pontosabban visszatérhet a teli vákuum (természetesen újra kell csatolni a fájlrendszert és el kell indítani az adatbázist).
Ha fizikai szerverrel rendelkezik, feltétlenül ellenőrizze a lemezek állapotát (pl. smartctl -a /dev/XXX) vagy a RAID vezérlőt, hogy megbizonyosodjak arról, hogy a probléma nem hardverrel kapcsolatos. Az én esetemben a RAID hardver alapúnak bizonyult, ezért megkértem a helyi rendszergazdát, hogy ellenőrizze a RAID állapotát (a szerver több száz kilométerre volt). Azt mondta, hogy nincsenek hibák, ami azt jelenti, hogy mindenképpen megkezdhetjük a helyreállítást.
1. kísérlet: zero_damaged_pages
Csatlakozz az adatbázishoz psql-en keresztül egy szuperfelhasználói jogosultságokkal rendelkező fiókkal. Szükségünk van egy szuperfelhasználóra, mert a beállítás nulla_sérült_oldal Csak az tudja megváltoztatni. Az én esetemben a postgresről van szó:
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 weboldaláról):
Sérült oldalfejléc észlelésekor a PostgreSQL jellemzően hibát jelez, és megszakítja az aktuális tranzakciót. Ha a zero_damaged_pages paraméter engedélyezve van, a rendszer ehelyett figyelmeztetést ad, lenullázza a sérült oldalt, és folytatja a feldolgozást. Ez a viselkedés az adatokat, konkrétan a sérült oldalon található összes sort sérti.
Engedélyezzük az opciót, és megpróbáljuk teljesen kiporszívózni az asztalt:
VACUUM FULL VERBOSE 
Sajnos, kudarc.
Hasonló hibába ütköztünk:
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– egy mechanizmus a „hosszú adatok” Poetgresben történő tárolására, ha azok nem férnek el egy oldalon (alapértelmezett 8kb).
2. kísérlet: újraindexelés
A Google első tippje nem segített. Néhány perc keresgélés után megtaláltam a másodikat: újraindex Sérült tábla. Sok helyen láttam már ezt a tanácsot, de nem keltett bennem bizalmat. Indexeljük újra:
reindex table ws_log_smevlog 
újraindex minden probléma nélkül elkészült.
Azonban az sem segített, VÁKUUM MEGTELT hasonló hibával összeomlott. Mivel hozzászoktam a kudarchoz, folytattam a tanácsok keresését az interneten, és egy meglehetősen érdekes dologra bukkantam. .
3. kísérlet: KIVÁLASZTÁS, LIMITÁLÁS, ELTOLÁS
A fenti cikk azt javasolta, hogy soronként ellenőrizzük a táblázatot, és töröljük a problémás adatokat. Először is az összes sort át kellett tekinteni:
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; doneAz én esetemben a táblázat tartalmazta 1 alkalmazott vonalak! Szükséges volt gondoskodni róla , de ez egy másik beszélgetés témája. Szombat volt, lefuttattam ezt a parancsot 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; doneReggelre úgy döntöttem, hogy megnézem, hogy állnak a dolgok. Meglepetésemre azt tapasztaltam, hogy 20 óra elteltével az adatoknak csak 2%-át szkennelték be! Nem akartam 50 napot várni. Újabb teljes kudarc.
De nem adtam fel. Azon tűnődtem, miért tart ilyen sokáig a vizsgálat. A dokumentációból (ismét a postgrespro-n) megtudtam:
Az OFFSET utasítás arra utasítja, hogy a megadott számú sort hagyja ki a sorok kimenetének megkezdése előtt.
Ha mind az OFFSET, mind a LIMIT meg van adva, a rendszer először kihagyja az OFFSET sorokat, majd elkezdi számolni a LIMIT sorait.A LIMIT függvény használatakor fontos az ORDER BY záradék használata is, hogy az eredmény sorok egy adott sorrendben kerüljenek visszaadásra. Ellenkező esetben a sorok kiszámíthatatlan részhalmazai kerülnek visszaadásra.
Nyilvánvaló, hogy a fenti parancs hibás volt: először is, nem volt rendezés szerint, az eredmény hibás lehet. Másodszor, a Postgresnek először be kellett szkennelnie és ki kellett hagynia az OFFSET sorokat, és a növekvő OFFSET a termelékenység még tovább csökkenne.
4. kísérlet: Szöveges dump rögzítése
Aztán egy látszólag zseniális ötletem támadt: szöveges formában elemezni az utolsó felvett sort.
De először is ismerkedjünk meg a táblázat felépítésével. ws_log_smevlog:

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:
- Elkezdjük szöveges formában kiíratni az adatokat (SQL parancsok formájában).
- Valamikor a memóriakép egy hiba miatt megszakadt, de a szövegfájl továbbra is mentésre került a lemezre.
- A szövegfájl végét nézzük, így megtaláljuk az utolsó sikeresen eltávolított sor azonosítóját (id).
Elkezdtem szöveges formában kirakni:
pg_dump -U my_user -d my_database -F p -t ws_log_smevlog -f ./my_dump.dumpA dump, ahogy várható volt, ugyanazzal a hibával meghiúsult:
pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989 Továbbhaladva farok Megnéztem a szeméttelep végét (farok -5 ./saját_dump.dump) megállapította, hogy a kiíratás megszakadt a(z) azonosítójú sorban 186 525„Tehát a probléma a 186 526-os sorazonosítóval van, az hibás, és törölni kell!” – gondoltam. De miután lekérdeztem az adatbázist:
«válassza a * karakterláncot a ws_log_smevlog mezőből, ahol az id=186529„Kiderült, hogy ezzel a sorral minden rendben volt... 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énik ez: egy táblázat adatainak törlésekor/módosításakor azok fizikailag nem törlődnek, hanem „halott tuple”-ként vannak megjelölve, majd jön…” autovákuum és töröltként jelöli meg ezeket a sorokat, majd lehetővé teszi azok újbóli felhasználását. Az érthetőség kedvéért, ha egy táblázatban lévő adatok megváltoznak, és az automatikus vákuum engedélyezve van, akkor azok nem kerülnek szekvenciálisan tárolásra.
5. kísérlet: SELECT, FROM, WHERE azonosító=
A kudarcok erősebbé tesznek minket. Soha nem szabad feladni, folytatni kell, és hinni kell magadban és a képességeidben. Úgyhogy úgy döntöttem, kipróbálok egy másik lehetőséget: egyszerűen végignézem az adatbázis összes rekordját egyesével. Ismerve a táblázatom szerkezetét (lásd fent), van egy azonosító mezőnk, amely egyedi (az elsődleges kulcs). 1 628 991 sorunk van a táblázatban, és id Sorrendben vannak, ami azt jelenti, hogy egyszerűen végigmehetünk rajtuk egyesével:
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; doneAzoknak, akik nem értik, a parancs így működik: sorról sorra átvizsgálja a táblázatot, és stdout kimenetet küld a következőre: / Dev / null, de ha a SELECT parancs sikertelen, akkor a hibaüzenet szövege kinyomtatásra kerül (a stderr elküldésre kerül a konzolra), és a hibát tartalmazó sor is kinyomtatásra kerül (a || karakternek köszönhetően, ami azt jelenti, hogy a select-tel problémák voltak (a parancs visszatérési kódja nem 0)).
Szerencsém volt, indexeket készítettem a terepen id:

Ez azt jelenti, hogy a szükséges azonosítóval rendelkező sor megtalálása nem tarthat sok időt. Elméletileg működnie kellene. Tehát futtassuk a parancsot a következőben: tmux és menjünk aludni.
Reggelre körülbelül 90 000 bejegyzést láttam, ami valamivel több mint 5%. Ez kiváló eredmény az előző módszerhez (2%) képest! De nem akartam 20 napot várni...
6. kísérlet: SELECT, FROM, WHERE id >= és id
Az ügyfél egy kiváló szervert kapott az adatbázishoz: egy kétprocesszorosat. Intel Xeon E5-2697 v2Óriási, 48 szál állt rendelkezésünkre! A szerver terhelése átlagos volt, így könnyedén kezeltünk körülbelül 20 szálat. Emellett rengeteg RAM-unk is volt: óriási, 384 gigabájt!
Ezért a csapatot 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Írhattam volna egy gyönyörű és elegáns szkriptet, de a leggyorsabb párhuzamosítási módszert választottam: manuálisan osztottam fel a 0-1628991 tartományt 100 000 rekordos intervallumokra, és külön-külön futtattam 16 parancsot a következő típusokból:
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; doneDe ez még nem minden. Az adatbázishoz való csatlakozás is időt és rendszererőforrásokat igényel. Az 1 628 991-es sor csatlakoztatása nem volt túl okos dolog, ebben egyetérthet. Tehát kérjünk le 1000 sort kapcsolatonként egy helyett. A parancs végül így nézett ki:
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; doneNyisson meg 16 ablakot egy tmux munkamenetben, és futtassa a következő 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! Konkrétan (az XXX és ZZZ értékeket már nem mentettem):
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
146000Ez azt jelenti, hogy három hibás sorunk van. Az első és a második problémás rekord azonosítója 829 000 és 830 000 között volt, a harmadiké pedig 146 000 és 147 000 között. Ezután már csak a problémás rekordok pontos azonosítóértékeit kellett megkeresnünk. Ehhez egyesével pásztázzuk a problémás rekordok tartományát, és azonosítjuk az azonosítókat:
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 sorokat. Érjük el az adatbázist a psql segítségével, és próbáljuk meg 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 1Meglepetésemre a rekordok gond nélkül törlődtek, még a lehetőség nélkül is. nulla_sérült_oldal.
Aztán csatlakoztam az adatbázishoz, és VÁKUUM MEGTELT (Szerintem erre nem volt szükség), és végül sikeresen készítettem egy biztonsági mentést a következővel: pg_dumpA memóriakép hibátlanul elkészült! A problémát ezzel a hihetetlenül ostoba módszerrel oldották meg. Nagyon örültem, hogy ennyi kudarc után végre megoldást találtam!
Köszönetnyilvánítás és következtetés
Ez volt az első élményem egy igazi Postgres adatbázis visszaállításában. Sokáig emlékezni fogok erre az élményre.
Végül pedig szeretném megköszönni a PostgresPro-nak a dokumentáció orosz nyelvre fordítását és a , amelyek nagyon hasznosak voltak a probléma elemzése során.
Forrás: will.com
