Min första erfarenhet av att återställa en Postgres-databas efter ett fel (ogiltig sida i block 4123007 av relatton base/16490)

Jag skulle vilja dela med mig av min första framgångsrika erfarenhet av att återställa en Postgres-databas till full funktionalitet. Jag bekantade mig med Postgres DBMS för ett halvår sedan, innan dess hade jag ingen erfarenhet av databasadministration alls.

Min första erfarenhet av att återställa en Postgres-databas efter ett fel (ogiltig sida i block 4123007 av relatton base/16490)

Jag arbetar som semi-DevOps-ingenjör i ett stort IT-företag. Vårt företag utvecklar mjukvara för högbelastningstjänster och jag ansvarar för prestanda, underhåll och driftsättning. Jag fick en standarduppgift: att uppdatera en applikation på en server. Applikationen är skriven i Django, under uppdateringen utförs migrering (ändringar i databasstrukturen), och innan denna process tar vi en fullständig databasdump genom standardprogrammet pg_dump, för säkerhets skull.

Ett oväntat fel inträffade när en dumpning togs (Postgres version 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

Fel "ogiltig sida i block" talar om problem på filsystemsnivå, vilket är mycket dåligt. På olika forum föreslogs det att göra HELT VAKUUM med option noll_skadade_sidor för att lösa det här problemet. Nåväl, låt oss försöka...

Förbereder för återhämtning

FÖRSIKTIGHET Se till att ta en Postgres-säkerhetskopia innan du försöker återställa din databas. Om du har en virtuell maskin, stoppa databasen och ta en ögonblicksbild. Om det inte är möjligt att ta en ögonblicksbild, stoppa databasen och kopiera innehållet i Postgres-katalogen (inklusive wal-filer) till en säker plats. Huvudsaken i vår verksamhet är att inte göra saker värre. Läsa detta.

Eftersom databasen i allmänhet fungerade för mig begränsade jag mig till en vanlig databasdump, men uteslöt tabellen med skadad data (alternativ -T, --exclude-table=TABELL i pg_dump).

Servern var fysisk, det var omöjligt att ta en ögonblicksbild. Säkerhetskopieringen har tagits bort, låt oss gå vidare.

Filsystemkontroll

Innan vi försöker återställa databasen måste vi se till att allt är i ordning med själva filsystemet. Och i händelse av misstag, korrigera dem, för annars kan du bara göra saken värre.

I mitt fall var filsystemet med databasen monterat in "/srv" och typen var ext4.

Stoppa databasen: systemctl stoppa [e-postskyddad] och kontrollera att filsystemet inte används av någon och kan avmonteras med kommandot lsof:
lsof +D /srv

Jag var också tvungen att stoppa redis-databasen, eftersom den också använde "/srv". Därefter avmonterade jag / srv (umount).

Filsystemet kontrollerades med hjälp av verktyget e2fsck med omkopplaren -f (Tvinga kontroll även om filsystemet är markerat som rent):

Min första erfarenhet av att återställa en Postgres-databas efter ett fel (ogiltig sida i block 4123007 av relatton base/16490)

Därefter använder du verktyget dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep kontrollerat) kan du verifiera att kontrollen faktiskt utfördes:

Min första erfarenhet av att återställa en Postgres-databas efter ett fel (ogiltig sida i block 4123007 av relatton base/16490)

e2fsck säger att inga problem hittades på ext4 filsystemnivå, vilket innebär att du kan fortsätta att försöka återställa databasen, eller snarare återgå till vakuum full (naturligtvis måste du montera tillbaka filsystemet och starta databasen).

Om du har en fysisk server, se till att kontrollera statusen för diskarna (via smartctl -a /dev/XXX) eller RAID-kontroller för att se till att problemet inte är på hårdvarunivå. I mitt fall visade sig RAID vara "hårdvara", så jag bad den lokala administratören att kontrollera statusen för RAID (servern var flera hundra kilometer ifrån mig). Han sa att det inte fanns några fel, vilket betyder att vi definitivt kan påbörja restaureringen.

Försök 1: zero_damaged_pages

Vi ansluter till databasen via psql med ett konto som har superanvändarrättigheter. Vi behöver en superanvändare, för... alternativ noll_skadade_sidor bara han kan ändra sig. I mitt fall är det postgres:

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

alternativ noll_skadade_sidor behövs för att ignorera läsfel (från postgrespro-webbplatsen):

När PostgreSQL upptäcker ett korrupt sidhuvud rapporterar det vanligtvis ett fel och avbryter den aktuella transaktionen. Om zero_damaged_pages är aktiverat utfärdar systemet istället en varning, nollställer den skadade sidan i minnet och fortsätter bearbetningen. Detta beteende förstör data, nämligen alla rader på den skadade sidan.

Vi aktiverar alternativet och försöker göra ett helt vakuum av tabellerna:

VACUUM FULL VERBOSE

Min första erfarenhet av att återställa en Postgres-databas efter ett fel (ogiltig sida i block 4123007 av relatton base/16490)
Tyvärr otur.

Vi stötte 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 – en mekanism för att lagra "lång data" i Poetgres om den inte får plats på en sida (8kb som standard).

Försök 2: återindexera

Det första rådet från Google hjälpte inte. Efter några minuters letande hittade jag det andra tipset – att göra renindex skadat bord. Jag såg det här rådet på många ställen, men det väckte inte förtroende. Låt oss återindexera:

reindex table ws_log_smevlog

Min första erfarenhet av att återställa en Postgres-databas efter ett fel (ogiltig sida i block 4123007 av relatton base/16490)

renindex klarat utan problem.

Detta hjälpte dock inte, VAKUUM FULL kraschade med ett liknande fel. Eftersom jag är van vid misslyckanden började jag leta vidare efter råd på Internet och kom över en ganska intressant Artikel.

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

Artikeln ovan föreslog att man tittade på tabellen rad för rad och tog bort problematiska data. Först behövde vi titta på alla raderna:

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

I mitt fall innehöll tabellen 1 628 991 rader! Det var nödvändigt att ta väl hand om datapartitionering, men detta är ett ämne för en separat diskussion. Det var lördag, jag körde det här kommandot i tmux och gick och la mig:

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

På morgonen bestämde jag mig för att kolla hur det gick. Till min förvåning upptäckte jag att efter 20 timmar hade bara 2% av datan skannats! Jag ville inte vänta 50 dagar. Ännu ett fullständigt misslyckande.

Men jag gav inte upp. Jag undrade varför skanningen tog så lång tid. Från dokumentationen (igen på postgrespro) fick jag reda på:

OFFSET anger att det angivna antalet rader ska hoppa över innan man börjar mata ut rader.
Om både OFFSET och LIMIT är specificerade, hoppar systemet först över OFFSET-raderna och börjar sedan räkna raderna för LIMIT-begränsningen.

När du använder LIMIT är det viktigt att även använda en ORDER BY-sats så att resultatraderna returneras i en specifik ordning. Annars kommer oförutsägbara delmängder av rader att returneras.

Uppenbarligen var kommandot ovan fel: för det första fanns det nej beställ av, kan resultatet bli felaktigt. För det andra var Postgres först tvungen att skanna och hoppa över OFFSET-rader, och med ökande OFFSET produktiviteten skulle sjunka ytterligare.

Försök 4: ta en dumpning i textform

Då kom en till synes briljant idé till mig: ta en dumpning i textform och analysera den senast inspelade raden.

Men låt oss först ta en titt på tabellens struktur. ws_log_smevlog:

Min första erfarenhet av att återställa en Postgres-databas efter ett fel (ogiltig sida i block 4123007 av relatton base/16490)

I vårt fall har vi en kolumn "Id", som innehöll den unika identifieraren (räknaren) för raden. Planen var så här:

  1. Vi börjar ta en dump i textform (i form av sql-kommandon)
  2. Vid en viss tidpunkt skulle dumpningen avbrytas på grund av ett fel, men textfilen skulle fortfarande sparas på disken
  3. Vi tittar i slutet av textfilen, därigenom hittar vi identifieraren (id) för den sista raden som togs bort framgångsrikt

Jag började ta en dumpning i textform:

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

Dumpningen avbröts som väntat med samma fel:

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

Längre igenom svans Jag tittade på slutet av soptippen (svans -5 ./min_dump.dump) upptäckte att dumpningen avbröts på linjen med id 186 525. "Så problemet är i linje med id 186 526, det är trasigt och måste raderas!" - Jag trodde. Men gör en fråga till databasen:
«välj * från ws_log_smevlog där id=186529”Det visade sig att allt var bra med den här linjen... Rader med index 186 530 - 186 540 fungerade också utan problem. En annan "briljant idé" misslyckades. Senare förstod jag varför detta hände: när man raderar och ändrar data från en tabell raderas de inte fysiskt, utan markeras som "döda tuplar", sedan kommer de autovakuum och markerar dessa rader som raderade och tillåter att dessa rader kan återanvändas. För att förstå, om data i tabellen ändras och autovakuum är aktiverat, så lagras det inte sekventiellt.

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

Misslyckanden gör oss starkare. Du ska aldrig ge upp, du måste gå till slutet och tro på dig själv och dina förmågor. Så jag bestämde mig för att prova ett annat alternativ: bara titta igenom alla poster i databasen en efter en. Genom att känna till strukturen i min tabell (se ovan), har vi ett id-fält som är unikt (primärnyckel). Vi har 1 628 991 rader i tabellen och id är i ordning, vilket innebär att vi bara kan gå igenom dem en efter en:

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

Om någon inte förstår fungerar kommandot enligt följande: det skannar tabellen rad för rad och skickar stdout till / Dev / null, men om kommandot SELECT misslyckas, skrivs feltexten ut (stderr skickas till konsolen) och en rad som innehåller felet skrivs ut (tack vare ||, vilket betyder att select hade problem (kommandots returkod) är inte 0)).

Jag hade tur, jag hade index skapade på fältet id:

Min första erfarenhet av att återställa en Postgres-databas efter ett fel (ogiltig sida i block 4123007 av relatton base/16490)

Det betyder att det inte bör ta lång tid att hitta en rad med önskat id. I teorin borde det fungera. Nåväl, låt oss köra kommandot in tmux och låt oss gå och lägga oss.

På morgonen upptäckte jag att cirka 90 000 bidrag hade setts, vilket är drygt 5%. Ett utmärkt resultat jämfört med den tidigare metoden (2%)! Men jag ville inte vänta 20 dagar...

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

Kunden hade en utmärkt server dedikerad till databasen: dubbelprocessor Intel Xeon E5-2697 v2, det fanns så många som 48 trådar på vår plats! Belastningen på servern var genomsnittlig, vi kunde ladda ner cirka 20 trådar utan problem. Det fanns också tillräckligt med RAM: så mycket som 384 gigabyte!

Därför behövde kommandot parallelliseras:

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

Här var det möjligt att skriva ett vackert och elegant manus, men jag valde den snabbaste parallelliseringsmetoden: manuellt dela upp området 0-1628991 i intervall om 100 000 poster och kör separat 16 kommandon av formen:

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

Men det är inte allt. I teorin tar det också lite tid och systemresurser att ansluta till en databas. Att koppla ihop 1 628 991 var inte särskilt smart, det håller ni med om. Låt oss därför hämta 1000 rader istället för en på en anslutning. Som ett resultat förvandlades teamet till detta:

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

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

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

En dag senare fick jag de första resultaten! Nämligen (värdena XXX och ZZZ är inte längre bevarade):

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

Det betyder att tre rader innehåller ett fel. Id:n för den första och andra problemposten var mellan 829 000 och 830 000, id:n för den tredje var mellan 146 000 och 147 000. Därefter var vi bara tvungna att hitta det exakta id-värdet för problemposterna. För att göra detta tittar vi igenom vårt sortiment med problematiska poster med steg 1 och identifierar ID: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

Lyckligt slut

Vi hittade de problematiska linjerna. 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 inläggen utan problem även utan möjlighet noll_skadade_sidor.

Sedan kopplade jag till databasen, gjorde det VAKUUM FULL (Jag tror att det inte var nödvändigt att göra detta), och slutligen tog jag bort säkerhetskopian med hjälp av pg_dump. Soptippen togs utan några fel! Problemet löstes på ett så dumt sätt. Glädjen visste inga gränser, efter så många misslyckanden lyckades vi hitta en lösning!

Tack och slutsatser

Så här blev min första erfarenhet av att återställa en riktig Postgres-databas. Jag kommer att minnas denna upplevelse länge.

Och till sist vill jag tacka PostgresPro för att du översatt dokumentationen till ryska och för helt gratis onlinekurser, vilket hjälpte mycket under analysen av problemet.

Källa: will.com

Lägg en kommentar