La meva primera experiència recuperant una base de dades Postgres després d'un error (pàgina no vàlida al bloc 4123007 de relatton base/16490)

M'agradaria compartir amb vosaltres la meva primera experiència exitosa de restaurar una base de dades Postgres a la funcionalitat completa. Em vaig familiaritzar amb el SGBD Postgres fa mig any; abans no tenia cap experiència en l'administració de bases de dades.

La meva primera experiència recuperant una base de dades Postgres després d'un error (pàgina no vàlida al bloc 4123007 de relatton base/16490)

Treballo com a enginyer semi-DevOps en una gran empresa informàtica. La nostra empresa desenvolupa programari per a serveis d'alta càrrega, i jo sóc responsable del rendiment, manteniment i desplegament. Em van donar una tasca estàndard: actualitzar una aplicació en un servidor. L'aplicació està escrita en Django, durant l'actualització es fan migracions (canvis en l'estructura de la base de dades), i abans d'aquest procés fem un bolcat complet de la base de dades a través del programa estàndard pg_dump, per si de cas.

S'ha produït un error inesperat en fer un bolcat (Postgres versió 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

Error "pàgina no vàlida al bloc" parla de problemes a nivell del sistema de fitxers, que és molt dolent. En diversos fòrums es va suggerir fer-ho BUIT COMPLET amb opció zero_pàgines_danyades per resoldre aquest problema. Bé, provem...

Preparant-se per a la recuperació

ADVERTÈNCIA! Assegureu-vos de fer una còpia de seguretat de Postgres abans de qualsevol intent de restaurar la vostra base de dades. Si teniu una màquina virtual, atureu la base de dades i feu una instantània. Si no és possible fer una instantània, atureu la base de dades i copieu el contingut del directori Postgres (inclosos els fitxers wal) a un lloc segur. El més important en el nostre negoci és no empitjorar les coses. Llegeix aquest.

Com que la base de dades generalment funcionava per a mi, em vaig limitar a un bolcat de base de dades normal, però vaig excloure la taula amb dades danyades (opció -T, --exclude-table=TAULA a pg_dump).

El servidor era físic, era impossible fer una instantània. La còpia de seguretat s'ha eliminat, continuem.

Comprovació del sistema de fitxers

Abans d'intentar restaurar la base de dades, hem d'assegurar-nos que tot està en ordre amb el propi sistema de fitxers. I en cas d'error, corregiu-los, perquè sinó només podeu empitjorar les coses.

En el meu cas, es va muntar el sistema de fitxers amb la base de dades "/srv" i el tipus era ext4.

Atura de la base de dades: aturar systemctl [protegit per correu electrònic] i comproveu que el sistema de fitxers no està en ús per ningú i es pot desmuntar mitjançant l'ordre lsof:
lsof +D /srv

També vaig haver d'aturar la base de dades redis, ja que també estava utilitzant "/srv". A continuació, vaig desmuntar / srv (desmuntar).

El sistema de fitxers es va comprovar mitjançant la utilitat e2fsck amb l'interruptor -f (Força la comprovació encara que el sistema de fitxers estigui marcat com a net):

La meva primera experiència recuperant una base de dades Postgres després d'un error (pàgina no vàlida al bloc 4123007 de relatton base/16490)

A continuació, utilitzant la utilitat dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep comprovat) podeu comprovar que la comprovació s'ha realitzat realment:

La meva primera experiència recuperant una base de dades Postgres després d'un error (pàgina no vàlida al bloc 4123007 de relatton base/16490)

e2fsck diu que no s'han trobat problemes al nivell del sistema de fitxers ext4, el que significa que podeu continuar intentant restaurar la base de dades, o més aviat tornar a buit ple (per descomptat, heu de tornar a muntar el sistema de fitxers i iniciar la base de dades).

Si teniu un servidor físic, assegureu-vos de comprovar l'estat dels discs (a través de smartctl -a /dev/XXX) o el controlador RAID per assegurar-vos que el problema no és a nivell de maquinari. En el meu cas, el RAID va resultar ser "maquinari", així que vaig demanar a l'administrador local que comprové l'estat del RAID (el servidor estava a diversos centenars de quilòmetres de mi). Va dir que no hi havia errors, la qual cosa significa que definitivament podem començar la restauració.

Intent 1: zero_damaged_pages

Ens connectem a la base de dades via psql amb un compte que té drets de superusuari. Necessitem un superusuari, perquè... opció zero_pàgines_danyades només ell pot canviar. En el meu cas és postgres:

psql -h 127.0.0.1 -U postgres -s [nom_base de dades]

Opció zero_pàgines_danyades necessari per ignorar els errors de lectura (del lloc web de postgrespro):

Quan PostgreSQL detecta una capçalera de pàgina corrupta, normalment informa d'un error i avorta la transacció actual. Si zero_damaged_pages està habilitat, el sistema emet un avís, posa a zero la pàgina danyada a la memòria i continua el processament. Aquest comportament destrueix les dades, és a dir, totes les files de la pàgina danyada.

Activem l'opció i intentem fer un buit complet de les taules:

VACUUM FULL VERBOSE

La meva primera experiència recuperant una base de dades Postgres després d'un error (pàgina no vàlida al bloc 4123007 de relatton base/16490)
Malauradament, mala sort.

Hem trobat un error similar:

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_torrada – un mecanisme per emmagatzemar “dades llargues” a Poetgres si no caben en una pàgina (8kb per defecte).

Intent 2: reindexar

El primer consell de Google no va ajudar. Després d'uns minuts de recerca, vaig trobar el segon consell: fer reindexar taula danyada. Vaig veure aquest consell en molts llocs, però no em va inspirar confiança. Reindexem:

reindex table ws_log_smevlog

La meva primera experiència recuperant una base de dades Postgres després d'un error (pàgina no vàlida al bloc 4123007 de relatton base/16490)

reindexar completat sense problemes.

Tanmateix, això no va ajudar, BUIT COMPLET es va estavellar amb un error similar. Com que estic acostumat als fracassos, vaig començar a buscar més consells a Internet i em vaig trobar amb un assessorament força interessant un article.

Intent 3: SELECT, LIMIT, OFFSET

L'article anterior suggeria mirar la taula fila per fila i eliminar les dades problemàtiques. Primer hem de mirar totes les línies:

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

En el meu cas, la taula continguda 1 628 991 línies! Calia cuidar-ho bé partició de dades, però aquest és un tema per a una discussió a part. Era dissabte, vaig executar aquesta comanda a tmux i vaig anar al llit:

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

Al matí vaig decidir comprovar com anaven les coses. Per a la meva sorpresa, vaig descobrir que després de 20 hores, només s'havien escanejat el 2% de les dades! No volia esperar 50 dies. Un altre fracàs total.

Però no em vaig rendir. Em vaig preguntar per què l'escaneig trigava tant. A partir de la documentació (de nou a postgrespro) em vaig assabentar:

OFFSET especifica saltar el nombre especificat de files abans de començar a sortir les files.
Si s'especifiquen tant OFFSET com LIMIT, el sistema primer omet les files OFFSET i després comença a comptar les files per a la restricció LIMIT.

Quan utilitzeu LIMIT, és important utilitzar també una clàusula ORDER BY perquè les files del resultat es tornin en un ordre específic. En cas contrari, es retornaran subconjunts impredictibles de files.

Òbviament, l'ordre anterior era incorrecta: en primer lloc, no n'hi havia demanat per, el resultat podria ser erroni. En segon lloc, Postgres primer va haver d'escanejar i saltar les files OFFSET, i amb l'augment OFFSET la productivitat disminuiria encara més.

Intent 4: fer un abocador en forma de text

Llavors em va venir al cap una idea aparentment brillant: fer un abocador en forma de text i analitzar l'última línia gravada.

Però primer, fem una ullada a l'estructura de la taula. ws_log_smevlog:

La meva primera experiència recuperant una base de dades Postgres després d'un error (pàgina no vàlida al bloc 4123007 de relatton base/16490)

En el nostre cas tenim una columna «Id», que contenia l'identificador únic (comptador) de la fila. El pla era així:

  1. Comencem a fer un abocador en forma de text (en forma d'ordres SQL)
  2. En un moment determinat, l'abocament s'interrompria a causa d'un error, però el fitxer de text encara es desarà al disc
  3. Observem el final del fitxer de text, així trobem l'identificador (id) de l'última línia que es va eliminar correctament

Vaig començar a fer un abocador en forma de text:

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

L'abocament, com s'esperava, es va interrompre amb el mateix error:

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

Més endavant cua Vaig mirar al final de l'abocador (cua -5 ./my_dump.dump) va descobrir que l'abocador estava interromput a la línia amb id 186 525. "Així que el problema està en la línia de l'identificador 186 526, està trencat i s'ha d'eliminar!" - Vaig pensar. Però, fent una consulta a la base de dades:
«seleccioneu * de ws_log_smevlog on id=186529“Va resultar que tot anava bé amb aquesta línia... Les files amb índexs 186 - 530 també van funcionar sense problemes. Una altra "idea brillant" va fracassar. Més tard vaig entendre per què passava això: quan s'esborren i es canvien dades d'una taula, no s'esborren físicament, sinó que es marquen com a "tuples mortes", després autobuit i marca aquestes línies com a suprimides i permet que aquestes línies es reutilitzin. Per entendre-ho, si les dades de la taula canvien i l'autobuit està activat, no s'emmagatzemen seqüencialment.

Intent 5: SELECT, FROM, WHERE id=

Els fracassos ens fan més forts. No t'has de rendir mai, has d'anar fins al final i creure en tu mateix i en les teves capacitats. Així que vaig decidir provar una altra opció: només mirar tots els registres de la base de dades un per un. Coneixent l'estructura de la meva taula (vegeu més amunt), tenim un camp d'identificació que és únic (clau primària). Tenim 1 files a la taula i id estan en ordre, el que significa que només podem repassar-los un per un:

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

Si algú no entén, l'ordre funciona de la següent manera: escaneja la taula fila per fila i envia stdout a / dev / null, però si l'ordre SELECT falla, s'imprimeix el text d'error (stderr s'envia a la consola) i s'imprimeix una línia que conté l'error (gràcies a ||, la qual cosa significa que la selecció va tenir problemes (el codi de retorn de l'ordre). no és 0)).

Vaig tenir sort, vaig tenir índexs creats al camp id:

La meva primera experiència recuperant una base de dades Postgres després d'un error (pàgina no vàlida al bloc 4123007 de relatton base/16490)

Això vol dir que trobar una línia amb l'identificador desitjat no hauria de prendre gaire temps. En teoria hauria de funcionar. Bé, anem a executar l'ordre tmux i anem a dormir.

Al matí vaig trobar que s'havien vist unes 90 entrades, que és una mica més del 000%. Un resultat excel·lent en comparació amb el mètode anterior (5%)! Però no volia esperar 2 dies...

Intent 6: SELECT, FROM, WHERE id >= i id <

El client disposava d'un excel·lent servidor dedicat a la base de dades: doble processador Intel Xeon E5-2697 v2, hi havia fins a 48 fils a la nostra ubicació! La càrrega al servidor era mitjana; vam poder descarregar uns 20 fils sense cap problema. També hi havia prou memòria RAM: fins a 384 gigabytes!

Per tant, calia paral·lelitzar l'ordre:

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

Aquí va ser possible escriure un script bonic i elegant, però vaig triar el mètode de paral·lelització més ràpid: dividir manualment el rang 0-1628991 en intervals de 100 registres i executar per separat 000 ordres del formulari:

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

Però això no és tot. En teoria, connectar-se a una base de dades també requereix temps i recursos del sistema. Connectar 1 no va ser gaire intel·ligent, estaràs d'acord. Per tant, recuperem 628 files en lloc de la connexió una a una. Com a resultat, l'equip es va transformar en això:

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

Obriu 16 finestres en una sessió tmux i executeu les ordres:

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

Un dia després vaig rebre els primers resultats! És a dir (els valors XXX i ZZZ ja no es conserven):

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

Això vol dir que tres línies contenen un error. Els identificadors del primer i segon registres de problemes estaven entre 829 i 000, els identificadors del tercer entre 830 i 000. A continuació, simplement havíem de trobar el valor d'identificació exacte dels registres de problemes. Per fer-ho, examinem el nostre rang amb registres problemàtics amb un pas d'146 i identifiquem l'identificador:

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

Final feliç

Hem trobat les línies problemàtiques. Entrem a la base de dades mitjançant psql i intentem eliminar-los:

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

Per a la meva sorpresa, les entrades es van suprimir sense cap problema, fins i tot sense l'opció zero_pàgines_danyades.

Llavors em vaig connectar a la base de dades, ho vaig fer BUIT COMPLET (Crec que no era necessari fer-ho) i finalment vaig eliminar la còpia de seguretat amb èxit pg_dump. L'abocador es va fer sense cap error! El problema es va resoldre d'una manera tan estúpida. L'alegria no va tenir límits, després de tants fracassos vam aconseguir trobar una solució!

Agraïments i Conclusió

Així va resultar la meva primera experiència de restaurar una base de dades de Postgres real. Recordaré aquesta experiència durant molt de temps.

I finalment, m'agradaria donar les gràcies a PostgresPro per traduir la documentació al rus i per cursos en línia totalment gratuïts, que va ajudar molt durant l'anàlisi del problema.

Font: www.habr.com

Afegeix comentari