Min första erfarenhet av att ÄterstÀlla en Postgres-databas efter ett fel (invalid page in block 4123007 of relatton base/16490)

Jag vill dela med mig av min första framgÄngsrika erfarenhet av att ÄterstÀlla den fullstÀndiga funktionaliteten i en Postgres-databas. Jag lÀrde kÀnna Postgres-databashanteraren för ett halvÄr sedan, och innan dess hade jag ingen erfarenhet av databasadministration.

Min första erfarenhet av att ÄterstÀlla en Postgres-databas efter ett fel (invalid page in block 4123007 of relatton base/16490)

Jag arbetar som delvis DevOps-ingenjör pÄ ett stort IT-företag. VÄrt företag utvecklar mjukvara för högbelastade tjÀnster, och jag ansvarar för drift, underhÄll och distribution. Jag fick en standarduppgift: att uppdatera en applikation pÄ en server. Applikationen Àr skriven i Django, och under uppdateringen genomförs migrationer (Àndringar i databasstrukturen). Innan denna process tar vi en fullstÀndig databasdump med hjÀlp av det vanliga programmet pg_dump, bara för sÀkerhets skull.

Under dumpningen uppstod ett oförutsett fel (Postgres-version – 9.5):

pg_dump: Att dumpa innehĂ„llet i tabellen “ws_log_smevlog” misslyckades: PQgetResult() misslyckades.
pg_dump: Felmeddelande frÄn servern: FEL: ogiltig sida i block 4123007 i relationen base/16490/21396989
pg_dump: Kommandot var: COPY public.ws_log_smevlog [...]
pg_dunp: [parallell arkivering] en arbetare process avbröts ovÀntat.

Fel «ogiltig sida i block» indikerar problem pÄ filsystemsnivÄ, vilket Àr mycket otrevligt. PÄ olika forum föreslogs att göra FULL VACUUM med alternativet zero_damaged_pages för att lösa detta problem. Vad ska vi göra, lÄt oss prova ...

Förberedelse för ÄterstÀllning

VARNING! Se till att du gör en sÀkerhetskopia av Postgres innan du gör nÄgot försök att ÄterstÀlla databasen. Om du har en virtuell maskin, stoppa databasen och ta en snapshot. Om det inte gÄr att ta en snapshot, stÀng av databasen och kopiera innehÄllet i Postgres-katalogen (inklusive wal-filer) till en sÀker plats. Det viktigaste i vÄrt fall Àr att inte förvÀrra situationen. LÀs igenom detta.

Eftersom i allmĂ€nhet databasen fungerade för mig, begrĂ€nsade jag mig till den vanliga databasdumpen men uteslöt tabellen med skadad data (alternativ -T, —exclude-table=TABLE i pg_dump).

Servern var fysisk, det var omöjligt att ta en snapshot. Backupen Àr tagen, vi gÄr vidare.

Kontroll av filsystemet

Innan du försöker ÄterstÀlla databasen mÄste du sÀkerstÀlla att vÄrt filsystem Àr i gott skick. Och om det finns fel, ÄtgÀrda dem, annars kan situationen förvÀrras.

I mitt fall var filsystemet med databasen monterat i «/srv» och typen var ext4.

Stoppa databasen: systemctl stop postgresql@9.5-main.service och kontrollerar att filsystemet inte anvÀnds av nÄgon och att det kan monteras av med kommandot lsof:
lsof +D /srv

Jag var tvungen att stoppa redis-databasen ocksÄ, eftersom den ocksÄ anvÀndes «/srv». DÀrefter monterade jag av /srv (umount).

Kontroller av filsystemet genomfördes med hjÀlp av verktyget e2fsck med nyckeln -f (Tvinga kontroll Àven om filsystemet Àr mÀrkt som rent):

Min första erfarenhet av att ÄterstÀlla en Postgres-databas efter ett fel (invalid page in block 4123007 of relatton base/16490)

DĂ€refter med hjĂ€lp av verktyget dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep checked) kan man vara sĂ€ker pĂ„ att kontrollen verkligen har genomförts:

Min första erfarenhet av att ÄterstÀlla en Postgres-databas efter ett fel (invalid page in block 4123007 of relatton base/16490)

e2fsck sÀger att inga problem pÄ filsystemsnivÄ ext4 har hittats, vilket betyder att vi kan fortsÀtta försöken att ÄterstÀlla databasen, eller snarare ÄtervÀnda till vacuum full (naturligtvis mÄste man montera filsystemet tillbaka och starta databasen).

Om du har en fysisk server, se till att kontrollera skivornas tillstÄnd (via smartctl -a /dev/XXX) eller RAID-kontrollern för att sÀkerstÀlla att problemet inte Àr hÄrdvarunivÄ. I mitt fall visade sig RAID vara "jÀrnigt", sÄ jag bad den lokala administratören att kontrollera RAID-status (servern var flera hundra kilometer bort). Han sa att det inte fanns nÄgra fel, vilket betyder att vi verkligen kan pÄbörja ÄterstÀllningen.

Försök 1: zero_damaged_pages

Vi ansluter till databasen via psql med ett konto som har superanvÀndarrÀttigheter. Vi behöver verkligen en superanvÀndare, eftersom alternativet zero_damaged_pages kan bara Àndras av honom. I mitt fall Àr det postgres:

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

Alternativ zero_damaged_pages behövs för att ignorera lÀsfel (frÄn postgrespro-sidan):

Vid identifiering av en skadad sidrubrik rapporterar Postgres Pro vanligtvis ett fel och avbryter den aktuella transaktionen. Om parametern zero_damaged_pages Àr aktiverad, ger systemet istÀllet ett varningsmeddelande, nollstÀller den skadade sidan i minnet och fortsÀtter behandlingen. Detta beteende förstör data, nÀmligen alla rader i den skadade sidan.

Vi slÄr pÄ alternativet och försöker göra full vacuum pÄ tabellen:

VACUUM FULL VERBOSE

Min första erfarenhet av att ÄterstÀlla en Postgres-databas efter ett fel (invalid page in block 4123007 of relatton base/16490)
TyvÀrr, det misslyckades.

Vi stöter pÄ ett liknande fel:

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 – lagringsmekanismen för "lĂ„nga data" i Postgres, om de inte fĂ„r plats pĂ„ en sida (standard Ă€r 8kB).

Försök 2: reindex

Det första rĂ„det frĂ„n Google hjĂ€lpte inte. Efter nĂ„gra minuters sökning hittade jag det andra rĂ„det – att göra reindex av den korrupta tabellen. Jag har stött pĂ„ detta rĂ„d pĂ„ mĂ„nga stĂ€llen, men det inspirerade inte förtroende. LĂ„t oss göra reindex:

reindex table ws_log_smevlog

Min första erfarenhet av att ÄterstÀlla en Postgres-databas efter ett fel (invalid page in block 4123007 of relatton base/16490)

reindex slutfördes utan problem.

Men det hjÀlpte inte, VACUUM FULL avbröts med ett liknande fel. Eftersom jag var van vid motgÄngar började jag leta efter rÄd pÄ internet och stötte pÄ en ganska intressant artikeln.

Försök 3: SELECT, LIMIT, OFFSET

I artikeln ovan föreslogs det att titta pÄ tabellen rad för rad och ta bort problematiska data. Först behövde jag gÄ igenom alla rader:

for ((i=0; i /dev/null || echo $i; done

I mitt fall innehöll tabellen 1 628 991 rader! I sjÀlva verket borde man ha tagit hand om partitionering av data, men det Àr ett Àmne för en separat diskussion. Det var lördag, jag körde just detta kommando i tmux och gick och la mig:

for ((i=0; i /dev/null || echo $i; done

PĂ„ morgonen beslutade jag mig för att kolla hur det gick. Till min förvĂ„ning upptĂ€ckte jag att endast 2% av data hade genomsökts under 20 timmar! Jag ville inte vĂ€nta i 50 dagar. Ännu ett totalt misslyckande.

Men jag tÀnkte inte ge upp. Jag blev nyfiken pÄ varför genomsökningen tog sÄ lÄng tid. FrÄn dokumentationen (Äterigen pÄ postgrespro) fick jag veta:

OFFSET anger hur mÄnga rader som ska hoppas över innan systemet börjar ge ut rader.
Om bÄde OFFSET och LIMIT anges, hoppar systemet först över OFFSET rader, och börjar sedan rÀkna rader för att begrÀnsa LIMIT.

NÀr man anvÀnder LIMIT Àr det viktigt att ocksÄ anvÀnda ORDER BY, sÄ att resultatet returneras i en viss ordning. Annars kommer oförutsÀgbara delmÀngder av rader att returneras.

Det Àr uppenbart att den ovanstÄende kommandon var felaktig: för det första fanns det ingen order by, resultatet kunde bli felaktigt. För det andra mÄste Postgres först genomsöka och hoppa över OFFSET-rader, och med ökad OFFSET minskade prestanda ytterligare.

Försök 4: ta en dump i textformat

DÀrefter kom jag pÄ en, vid första anblicken, genial idé: ta en dump i textformat och analysera den senaste registrerade raden.

Men först, lÄt oss se pÄ tabellstrukturen ws_log_smevlog:

Min första erfarenhet av att ÄterstÀlla en Postgres-databas efter ett fel (invalid page in block 4123007 of relatton base/16490)

I vÄrt fall har vi en kolumn "id", som innehöll ett unikt identifieringsnummer (rÀknare) för raden. Planen var följande:

  1. Vi börjar ta dumpen i textformat (i form av sql-kommandon)
  2. Vid en viss tidpunkt skulle dumpningen avbrytas pÄ grund av ett fel, men textfilen skulle ÀndÄ ha sparats pÄ disken.
  3. Vi kollar slutet av textfilen, och dÀrmed hittar vi identifieraren (id) för den sista rad som lyckades dumpas.

Jag började dumpa i textformat:

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

Dumpningen avbröts, som förvÀntat, med samma fel:

pg_dump: Felmeddelande frÄn servern: ERROR: invalid page in block 4123007 of relation base/16490/21396989

DĂ€refter, genom tail tittade jag pĂ„ slutet av dumpen (tail -5 ./my_dump.dump) och upptĂ€ckte att dumpningen avbröts pĂ„ raden med id 186 525. "SĂ„ problemet ligger i raden med id 186 526, den Ă€r korrupt, den mĂ„ste tas bort!" – tĂ€nkte jag. Men nĂ€r jag gjorde en frĂ„ga i databasen:
«select * from ws_log_smevlog where id=186529visade det sig att den hĂ€r raden var helt okej... Raderna med index 186 530 – 186 540 fungerade ocksĂ„ utan problem. Det nĂ€sta "geniala" idĂ© föll platt. Senare insĂ„g jag varför detta hĂ€nde: nĂ€r man tar bort/Ă€ndrar data i tabellen tas de inte bort fysiskt utan markeras som "döda tuple", dĂ€refter kommer autovacuum och markerar dessa rader som borttagna och tillĂ„ter att dessa rader anvĂ€nds igen. För att förstĂ„, om data i tabellen Ă€ndras och autovacuum Ă€r aktiverat, lagras de inte i ordning.

Försök 5: SELECT, FROM, WHERE id=

MotgÄngar gör oss starkare. Man ska aldrig ge upp, man mÄste gÄ till slutet och tro pÄ sig sjÀlv och sina möjligheter. DÀrför bestÀmde jag mig för att prova en annan metod: bara kolla igenom alla poster i databasen en i taget. Med vetskap om strukturen i min tabell (se ovan), har vi ett fÀlt id, som Àr unikt (primÀrnyckel). I tabellen har vi 1 628 991 rader och id de Àr i ordning, vilket innebÀr att vi enkelt kan gÄ igenom dem en i taget:

for ((i=1; i/dev/null || echo $i; done

Om nÄgon inte förstÄr fungerar kommandot pÄ följande sÀtt: det gÄr igenom tabellen rad för rad och skickar stdout till /dev/null, men om SELECT-kommandot misslyckas skrivs felmeddelandet ut (stderr skickas till konsolen) och raden som innehÄller felet skrivs ut (tack vare ||, vilket indikerar att select fick problem (returvÀrdet för kommandot Àr inte 0)).

Jag hade tur, jag hade skapat index pÄ fÀltet id:

Min första erfarenhet av att ÄterstÀlla en Postgres-databas efter ett fel (invalid page in block 4123007 of relatton base/16490)

Och det betyder att hitta raden med det nödvÀndiga id:t inte borde ta lÄng tid. I teorin borde det fungera. SÄ, vi kör kommandot. tmux och vi gÄr till sÀngs.

PÄ morgonen upptÀckte jag att cirka 90 000 poster hade granskats, vilket Àr lite mer Àn 5 %. Ett utmÀrkt resultat, jÀmfört med den tidigare metoden (2 %)! Men jag ville inte vÀnta i 20 dagar...

Försök 6: SELECT, FROM, WHERE id >= and id <

För kunden var en utmÀrkt server avsatt för databasen: dubbelprocessor Intel Xeon E5-2697 v2, i vÄr konfiguration hade vi hela 48 trÄdar! Belastningen pÄ servern var genomsnittlig, vi kunde utan nÄgra större problem hÀmta ungefÀr 20 trÄdar. Det fanns ocksÄ tillrÀckligt med arbetsminne: sÄ mÄnga som 384 gigabyte!

SÄ kommandot behövde parallelliseras:

for ((i=1; i/dev/null || echo $i; done

HÀr kunde man ha skrivit ett snyggt och elegant skript, men jag valde den snabbaste metoden för parallellisering: att manuellt dela intervallet 0-1628991 i segment om 100 000 poster och köra 16 kommandon av typen:

for ((i=N; i/dev/null || echo $i; done

Men det Àr inte allt. Egentligen tar anslutningen till databasen ocksÄ tid och systemresurser. Att ansluta 1 628 991 var inte sÄ klokt, eller hur? SÄ lÄt oss vid en anslutning hÀmta 1000 rader istÀllet för en. I slutÀndan blev kommandot sÄ hÀr:

for ((i=N; i=$i and id/dev/null || echo $i; done

Öppnar 16 fönster i en tmux-session och kör kommandona:

1) for ((i=0; i=$i and id/dev/null || echo $i; done
2) for ((i=100000; i=$i and id/dev/null || echo $i; done


15) for ((i=1400000; i=$i and id/dev/null || echo $i; done
16) for ((i=1500000; i=$i and id/dev/null || echo $i; done

Dagen efter fick jag de första resultaten! NÀmligen (vÀrdena XXX och ZZZ har redan inte sparats):

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

Detta betyder att tre rader innehĂ„ller ett fel. id för den första och andra problematiska posten lĂ„g mellan 829 000 och 830 000, id för den tredje – mellan 146 000 och 147 000. DĂ€refter mĂ„ste vi helt enkelt hitta det exakta vĂ€rdet id för de problematiska posterna. För detta granskar vi vĂ„rt intervall med problematiska poster steg för steg och identifierar id:

for ((i=829000; 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/dev/null || echo $i; done
829417
ERROR:  unexpected chunk number ZZZ (expected 0) for toast value XXX in pg_toast_106070
146911

Ett lyckligt slut

Vi har hittat problematiska rader. Vi gÄr in i databasen via psql och försöker ta bort dem:

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

Till min förvÄning raderades posterna utan nÄgra problem Àven utan alternativet zero_damaged_pages.

Sedan anslöt jag till databasen, utförde VACUUM FULL (jag tror att det var onödigt), och till sist lyckades jag ta en backup med hjÀlp av pg_dump. Dumpen gjordes utan nÄgra fel! Problemet löstes pÄ detta simpla sÀtt. Det fanns ingen grÀns för glÀdjen, efter sÄ mÄnga misslyckanden lyckades jag hitta en lösning!

Tack och slutsats

SÄ hÀr blev min första erfarenhet av att ÄterstÀlla en verklig Postgres-databas. Denna erfarenhet kommer jag att minnas lÀnge.

Och till sist skulle jag vilja tacka PostgresPro för den översatta dokumentationen till ryska och för helt gratis onlinekurser, som verkligen hjÀlpte mycket under analysen av problemet.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster