La mia prima esperienza nel recupero di un database Postgres dopo un errore (pagina non valida nel blocco 4123007 di relatton base/16490)

Vorrei condividere con voi la mia prima esperienza di successo nel ripristino della piena funzionalità di un database Postgres. Ho conosciuto il DBMS Postgres sei mesi fa; prima non avevo alcuna esperienza nell'amministrazione di database.

La mia prima esperienza nel recupero di un database Postgres dopo un errore (pagina non valida nel blocco 4123007 di relatton base/16490)

Lavoro come ingegnere semi-DevOps in una grande azienda IT. La nostra azienda sviluppa software per servizi ad alto carico e io sono responsabile delle prestazioni, della manutenzione e dell'implementazione. Mi è stato assegnato un compito standard: aggiornare un'applicazione su un server. L'applicazione è scritta in Django, durante l'aggiornamento vengono eseguite le migrazioni (modifiche nella struttura del database) e prima di questo processo, per ogni evenienza, eseguiamo un dump completo del database tramite il programma pg_dump standard.

Si è verificato un errore imprevisto durante l'esecuzione di un dump (Postgres versione 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

Insetto "pagina non valida nel blocco" parla di problemi a livello di file system, il che è pessimo. Su vari forum è stato suggerito di farlo VUOTO COMPLETO con opzione zero_pagine_danneggiate risolvere questo problema. Bene, proviamo...

Prepararsi al recupero

ATTENZIONE! Assicurati di eseguire un backup di Postgres prima di qualsiasi tentativo di ripristinare il database. Se disponi di una macchina virtuale, arresta il database e acquisisci uno snapshot. Se non è possibile acquisire uno snapshot, arresta il database e copia il contenuto della directory Postgres (inclusi i file wal) in un luogo sicuro. La cosa principale nella nostra attività è non peggiorare le cose. Leggere essa.

Poiché il database generalmente ha funzionato per me, mi sono limitato a un normale dump del database, escludendo però la tabella con dati danneggiati (opzione -T, --exclude-table=TABELLA in pg_dump).

Il server era fisico, era impossibile scattare uno snapshot. Il backup è stato rimosso, andiamo avanti.

Controllo del file system

Prima di tentare di ripristinare il database, dobbiamo assicurarci che tutto sia in ordine con il file system stesso. E in caso di errori correggeteli, perché altrimenti non potrete che peggiorare le cose.

Nel mio caso, è stato montato il file system con il database "/srv" e il tipo era ext4.

Arresto del database: sistemactl stop [email protected] e controlla che il file system non sia utilizzato da nessuno e possa essere smontato utilizzando il comando lsof:
lsof +D /srv

Ho anche dovuto interrompere il database Redis, poiché anch'esso stava utilizzando "/srv". Successivamente ho smontato / srv (smontare).

Il file system è stato controllato utilizzando l'utilità e2fsck con l'interruttore -f (Forza il controllo anche se il filesystem è contrassegnato come pulito):

La mia prima esperienza nel recupero di un database Postgres dopo un errore (pagina non valida nel blocco 4123007 di relatton base/16490)

Successivamente, utilizzando l'utility dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep controllato) è possibile verificare che il controllo sia stato effettivamente effettuato:

La mia prima esperienza nel recupero di un database Postgres dopo un errore (pagina non valida nel blocco 4123007 di relatton base/16490)

e2fsck dice che non sono stati riscontrati problemi a livello di file system ext4, il che significa che puoi continuare a provare a ripristinare il database, o meglio tornare a vuoto pieno (ovviamente è necessario rimontare il file system e avviare il database).

Se disponi di un server fisico, assicurati di controllare lo stato dei dischi (tramite smartctl -a /dev/XXX) o il controller RAID per accertarsi che il problema non sia a livello hardware. Nel mio caso il RAID si è rivelato “hardware”, quindi ho chiesto all'amministratore locale di verificare lo stato del RAID (il server era a diverse centinaia di chilometri da me). Ha detto che non ci sono stati errori, il che significa che possiamo sicuramente iniziare il restauro.

Tentativo 1: zero_damaged_pages

Ci colleghiamo al database tramite psql con un account che dispone dei diritti di superutente. Abbiamo bisogno di un superutente, perché... opzione zero_pagine_danneggiate solo lui può cambiare. Nel mio caso è Postgres:

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

Opzione zero_pagine_danneggiate necessario per ignorare gli errori di lettura (dal sito web postgrespro):

Quando PostgreSQL rileva un'intestazione di pagina danneggiata, in genere segnala un errore e interrompe la transazione corrente. Se zero_damaged_pages è abilitato, il sistema emette invece un avviso, azzera la pagina danneggiata in memoria e continua l'elaborazione. Questo comportamento distrugge i dati, vale a dire tutte le righe nella pagina danneggiata.

Abilitiamo l'opzione e proviamo a fare un vuoto completo delle tabelle:

VACUUM FULL VERBOSE

La mia prima esperienza nel recupero di un database Postgres dopo un errore (pagina non valida nel blocco 4123007 di relatton base/16490)
Sfortunatamente, sfortuna.

Abbiamo riscontrato un errore simile:

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_brindisi – un meccanismo per archiviare “dati lunghi” in Poetgres se non rientrano in una pagina (8kb per impostazione predefinita).

Tentativo 2: reindicizzare

Il primo consiglio di Google non ha aiutato. Dopo alcuni minuti di ricerca, ho trovato il secondo consiglio: farlo reindicizzare tavolo danneggiato. Ho visto questo consiglio in molti posti, ma non ispirava fiducia. Reindicizziamo:

reindex table ws_log_smevlog

La mia prima esperienza nel recupero di un database Postgres dopo un errore (pagina non valida nel blocco 4123007 di relatton base/16490)

reindicizzare completato senza problemi.

Ciò però non ha aiutato, VUOTO PIENO si è bloccato con un errore simile. Dato che sono abituato ai fallimenti, ho iniziato a cercare consigli su Internet e mi sono imbattuto in qualcosa di piuttosto interessante Articolo.

Tentativo 3: SELEZIONE, LIMITE, OFFSET

L'articolo precedente suggeriva di esaminare la tabella riga per riga e di rimuovere i dati problematici. Per prima cosa dovevamo guardare tutte le linee:

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

Nel mio caso, la tabella conteneva 1 628 991 linee! Era necessario prendersene cura partizionamento dei dati, ma questo è un argomento per una discussione separata. Era sabato, ho eseguito questo comando in tmux e sono andato a letto:

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 mattino ho deciso di verificare come stavano andando le cose. Con mia sorpresa, ho scoperto che dopo 20 ore solo il 2% dei dati era stato scansionato! Non volevo aspettare 50 giorni. Un altro completo fallimento.

Ma non mi sono arreso. Mi chiedevo perché la scansione richiedesse così tanto tempo. Dalla documentazione (sempre su postgrespro) ho scoperto:

OFFSET specifica di saltare il numero specificato di righe prima di iniziare a generare righe.
Se vengono specificati sia OFFSET che LIMIT, il sistema salta prima le righe OFFSET e poi inizia a contare le righe per il vincolo LIMIT.

Quando si utilizza LIMIT, è importante utilizzare anche una clausola ORDER BY in modo che le righe dei risultati vengano restituite in un ordine specifico. In caso contrario, verranno restituiti sottoinsiemi di righe imprevedibili.

Ovviamente il comando sopra era sbagliato: in primo luogo non c'era order by, il risultato potrebbe essere errato. In secondo luogo, Postgres ha dovuto prima scansionare e saltare le righe OFFSET e con aumento COMPENSARE la produttività diminuirebbe ulteriormente.

Tentativo 4: fai una discarica in forma di testo

Poi mi è venuta in mente un'idea apparentemente geniale: fare un dump sotto forma di testo e analizzare l'ultima riga registrata.

Ma prima diamo un'occhiata alla struttura della tabella. ws_log_smevlog:

La mia prima esperienza nel recupero di un database Postgres dopo un errore (pagina non valida nel blocco 4123007 di relatton base/16490)

Nel nostro caso abbiamo una colonna "Id", che conteneva l'identificatore univoco (contatore) della riga. Il piano era questo:

  1. Iniziamo a fare un dump in forma testuale (sotto forma di comandi sql)
  2. Ad un certo punto il dump verrebbe interrotto a causa di un errore, ma il file di testo verrebbe comunque salvato su disco
  3. Guardiamo la fine del file di testo, quindi troviamo l'identificatore (id) dell'ultima riga che è stata rimossa con successo

Ho iniziato a fare un dump in forma di testo:

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

Il dump, come previsto, è stato interrotto con lo stesso errore:

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

Più avanti coda Ho guardato la fine della discarica (coda -5 ./mio_dump.dump) ha scoperto che il dump è stato interrotto sulla linea con id 186/525. "Quindi il problema è nella linea con l'ID 186 526, è rotto e deve essere eliminato!" - Ho pensato. Ma, facendo una query al database:
«seleziona * da ws_log_smevlog dove id=186529"Si è scoperto che con questa linea andava tutto bene... Anche le righe con gli indici 186 - 530 hanno funzionato senza problemi. Un’altra “idea geniale” è fallita. Più tardi ho capito perché ciò accadeva: quando si eliminano e si modificano i dati da una tabella, questi non vengono eliminati fisicamente, ma vengono contrassegnati come "tuple morte", quindi arriva autovuoto e contrassegna queste righe come cancellate e consente il riutilizzo di queste righe. Per intenderci, se i dati nella tabella cambiano e l'autovacuum è abilitato, allora non vengono memorizzati in modo sequenziale.

Tentativo 5: SELECT, FROM, WHERE id=

I fallimenti ci rendono più forti. Non bisogna mai arrendersi, bisogna arrivare fino in fondo e credere in se stessi e nelle proprie capacità. Quindi ho deciso di provare un'altra opzione: esaminare tutti i record nel database uno per uno. Conoscendo la struttura della mia tabella (vedi sopra), abbiamo un campo id che è unico (chiave primaria). Abbiamo 1 righe nella tabella e id sono in ordine, il che significa che possiamo esaminarli uno per uno:

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

Se qualcuno non capisce, il comando funziona così: scansiona la tabella riga per riga e invia lo stdout a / Dev / null, ma se il comando SELECT fallisce, allora viene stampato il testo dell'errore (stderr viene inviato alla console) e viene stampata una riga contenente l'errore (grazie a ||, il che significa che la select ha avuto problemi (il codice di ritorno del comando non è 0)).

Sono stato fortunato, ho fatto creare degli indici sul campo id:

La mia prima esperienza nel recupero di un database Postgres dopo un errore (pagina non valida nel blocco 4123007 di relatton base/16490)

Ciò significa che trovare una linea con l'ID desiderato non dovrebbe richiedere molto tempo. In teoria dovrebbe funzionare. Bene, eseguiamo il comando tmux e andiamo a letto.

Al mattino ho scoperto che erano state visualizzate circa 90 voci, ovvero poco più del 000%. Un risultato eccellente se confrontato con il metodo precedente (5%)! Ma non volevo aspettare 2 giorni...

Tentativo 6: SELECT, FROM, WHERE id >= e id

Il cliente disponeva di un ottimo server dedicato al database: doppio processore Intel Xeon E5-2697 v2, c'erano ben 48 thread nella nostra posizione! Il carico sul server era nella media; abbiamo potuto scaricare circa 20 thread senza problemi. C'era anche abbastanza RAM: ben 384 gigabyte!

Pertanto, il comando doveva essere parallelizzato:

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

Qui è stato possibile scrivere uno script bello ed elegante, ma ho scelto il metodo di parallelizzazione più veloce: dividere manualmente l'intervallo 0-1628991 in intervalli di 100 record ed eseguire separatamente 000 comandi del modulo:

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

Ma non è tutto. In teoria, anche la connessione a un database richiede tempo e risorse di sistema. Collegare 1 non è stato molto intelligente, sarai d'accordo. Pertanto, recuperiamo 628 righe anziché una su una connessione. Di conseguenza, il team si è trasformato in questo:

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

Apri 16 finestre in una sessione tmux ed esegui i comandi:

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

Il giorno dopo ho ricevuto i primi risultati! Ovvero (i valori XXX e ZZZ non vengono più conservati):

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

Ciò significa che tre righe contengono un errore. Gli ID del primo e del secondo record del problema erano compresi tra 829 e 000, gli ID del terzo erano compresi tra 830 e 000. Successivamente, dovevamo semplicemente trovare il valore ID esatto dei record del problema. Per fare ciò, esaminiamo il nostro intervallo con record problematici con un passaggio pari a 146 e identifichiamo l'ID:

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

Lieto fine

Abbiamo trovato le linee problematiche. Entriamo nel database tramite psql e proviamo a cancellarli:

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

Con mia sorpresa, le voci sono state cancellate senza problemi anche senza l'opzione zero_pagine_danneggiate.

Poi mi sono connesso al database, l'ho fatto VUOTO PIENO (Penso che non fosse necessario farlo) e alla fine ho rimosso con successo il backup utilizzando pg_dump. Il dump è stato eseguito senza errori! Il problema è stato risolto in un modo così stupido. La gioia non conosceva limiti, dopo tanti insuccessi siamo riusciti a trovare una soluzione!

Ringraziamenti e Conclusione

Ecco come è andata a finire la mia prima esperienza di ripristino di un vero database Postgres. Ricorderò questa esperienza per molto tempo.

E infine, vorrei ringraziare PostgresPro per aver tradotto la documentazione in russo e per corsi online completamente gratuiti, che ha aiutato molto durante l'analisi del problema.

Fonte: habr.com

Aggiungi un commento