Vogliu sparte cun voi a mo prima sperienza riescita di restaurà una basa di dati Postgres à piena funziunalità. Aghju cunnisciutu u DBMS Postgres un annu fà prima chì ùn aghju avutu micca sperienza in l'amministrazione di basa di dati.

U travagliu cum'è un ingegnere semi-DevOps in una grande cumpagnia di IT. A nostra sucietà sviluppa software per servizii di alta carica, è sò rispunsevuli di u rendiment, mantenimentu è implementazione. Mi sò datu un compitu standard: aghjurnà una applicazione in un servitore. L'applicazione hè scritta in Django, durante l'aghjurnà e migrazioni sò realizati (cambiamenti in a struttura di a basa di dati), è prima di stu prucessu pigliamu un dump di basa di dati cumpletu attraversu u prugramma standard pg_dump, in casu.
Un errore imprevisu hè accadutu mentre pigliate un dump (versione Postgres 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 errore "pagina invalida in bloccu" parla di prublemi à u livellu di u sistema di schedari, chì hè assai male. In diversi fori hè stata suggerita di fà VU PIENU cù opzione zero_pagine_dannate per risolve stu prublema. Ebbè, pruvemu...
Preparazione per a ricuperazione
ATENCION! Assicuratevi di piglià una copia di salvezza Postgres prima di qualsiasi tentativu di restaurà a vostra basa di dati. Se tenete una macchina virtuale, fermate a basa di dati è pigliate una snapshot. Se ùn hè micca pussibule di piglià una snapshot, ferma a basa di dati è copià u cuntenutu di u repertoriu Postgres (inclusi i schedari wal) in un locu sicuru. A cosa principal in a nostra attività ùn hè micca di peghju. Leghjite .
Siccomu a basa di dati hà generalmente travagliatu per mè, aghju limitatu à un dump di basa di dati regulare, ma escluditu a tavula cù dati dannatu (opzione -T, --exclude-table=TABLE in pg_dump).
U servitore era fisicu, era impussibile di piglià una snapshot. A copia di salvezza hè stata eliminata, andemu avanti.
Verificate u sistema di file
Prima di pruvà à restaurà a basa di dati, avemu bisognu di assicurà chì tuttu hè in ordine cù u sistema di fugliale stessu. È in casu d'errore, corregge, perchè altrimente pudete solu peghju.
In u mo casu, u sistema di fugliale cù a basa di dati hè stata muntata "/srv" è u tipu era ext4.
Arresta a basa di dati: systemctl ferma postgresql@9.5-main.service è verificate chì u sistema di fugliale ùn hè micca in usu di nimu è pò esse unmounted usendu u cumandamentu lsof:
lsof +D /srv
Aghju avutu ancu à piantà a basa di dati redis, postu chì era ancu usu "/srv". Dopu aghju smontatu / srv (smontare).
U sistema di schedari hè stata verificata cù l'utilità e2fsck cù l'interruttore -f (Forza a verificazione ancu s'è u sistema di file hè marcatu pulitu):

Dopu, usendu l'utilità dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep verificatu) pudete verificà chì a verificazione hè stata veramente realizata:

e2fsck dice chì ùn sò micca stati trovati prublemi à u nivellu di u sistema di file ext4, chì significa chì pudete cuntinuà à pruvà à restaurà a basa di dati, o piuttostu vultà à vacuum full (Di sicuru, avete bisognu di muntà u sistema di fugliale è principià a basa di dati).
Sì avete un servitore fisicu, assicuratevi di verificà l'statu di i dischi (via smartctl -a /dev/XXX) o controller RAID per assicurà chì u prublema ùn hè micca à u livellu di hardware. In u mo casu, u RAID hè diventatu "hardware", cusì aghju dumandatu à l'amministratore lucale per verificà u statutu di u RAID (u servitore era à parechji centu chilometri da mè). Ellu disse chì ùn ci era micca errore, chì significa chì pudemu definitivamente principià a risturazione.
Tentativu 1: zero_damaged_pages
Cunnettamu à a basa di dati via psql cun un contu chì hà diritti di superuser. Avemu bisognu di un superuser, perchè... opzione zero_pagine_dannate solu ellu pò cambià. In u mo casu hè postgres:
psql -h 127.0.0.1 -U postgres -s [nome_database]
Opzione zero_pagine_dannate necessariu per ignurà l'errori di lettura (da u situ web postgrespro):
Quandu PostgreSQL detecta un intestazione di pagina corrupta, tipicamente informa un errore è aborta a transazzione attuale. Se zero_damaged_pages hè attivatu, u sistema invece emette un avvisu, zeros a pagina dannata in memoria, è cuntinueghja a trasfurmazioni. Stu cumpurtamentu distrugge i dati, vale à dì tutte e fila in a pagina dannata.
Abilitemu l'opzione è pruvate à fà un vacu sanu di e tavule:
VACUUM FULL VERBOSE 
Sfurtunatamente, mala furtuna.
Avemu scontru un errore simili:
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- un mecanismu per almacenà "dati longu" in Poetgres s'ellu ùn si mette micca in una pagina (8kb per difettu).
Tentativu 2 : reindexà
U primu cunsigliu da Google ùn hà micca aiutu. Dopu qualchì minutu di ricerca, aghju trovu a seconda punta - per fà reindexà tavula dannata. Aghju vistu stu cunsigliu in parechji posti, ma ùn hà micca inspiratu cunfidenza. Reindexemu:
reindex table ws_log_smevlog 
reindexà cumpletu senza prublemi.
Tuttavia, questu ùn hà micca aiutu, VACUUM FULL crash cù un errore simili. Siccomu sò abituatu à i fallimenti, aghju cuminciatu à circà più cunsiglii nantu à Internet è scontru un piuttostu interessante .
Tentativu 3: SELECT, LIMIT, OFFSET
L'articulu sopra suggeriu di guardà a tavula fila per fila è di sguassà e dati problematiche. Prima avemu bisognu di guardà tutte e 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; doneIn u mo casu, a tavula cuntene 1 628 991 linee! Era necessariu di piglià bè cura , ma questu hè un tema per una discussione separata. Era sabbatu, aghju currettu stu cumandamentu in tmux è andò à lettu:
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; doneA matina aghju decisu di verificà cumu andavanu e cose. À a mo sorpresa, aghju scupertu chì dopu à l'ora di 20, solu 2% di e dati sò stati scannati! Ùn vulia micca aspittà 50 ghjorni. Un altru fallimentu cumpletu.
Ma ùn aghju micca rinunciatu. Mi dumandu perchè u scanning hà pigliatu tantu tempu. Da a documentazione (di novu nantu à postgrespro) aghju scupertu:
OFFSET specifica di saltà u numeru specificatu di fila prima di inizià a produzzione di fila.
Se sia OFFSET sia LIMIT sò specificati, u sistema prima salta e fila OFFSET è poi principia à cuntà e fila per a limitazione LIMIT.Quandu si usa LIMIT, hè impurtante ancu aduprà una clause ORDER BY per chì e fila di u risultatu sò tornati in un ordine specificu. Altrimenti, subsets imprevisible di fila seranu tornati.
Ovviamente, u cumandamentu sopra era sbagliatu: prima, ùn ci era micca ordine da, u risultatu pò esse sbagliatu. Siconda, Postgres hà avutu prima scansà è saltà e fila OFFSET, è cù l'aumentu OFFSET a produttività diminuirà ancu di più.
Tentativu 4: piglià un dump in forma di testu
Allora una idea apparentemente brillanti hè vinuta à a mo mente: piglià un dump in forma di testu è analizà l'ultima linea arregistrata.
Ma prima, fighjemu un ochju à a struttura di a tavula. ws_log_smevlog:

In u nostru casu avemu una colonna "Id", chì cuntene l'identificatore unicu (contatore) di a fila. U pianu era cusì:
- Cuminciamu à piglià un dump in forma di testu (in forma di cumandamenti sql)
- À un certu puntu in u tempu, u dump hè stata interrotta per un errore, ma u schedariu di testu serà sempre salvatu nantu à u discu.
- Fighjemu à a fine di u schedariu di testu, cusì truvamu l'identificatore (id) di l'ultima linea chì hè stata eliminata bè.
Aghju cuminciatu à piglià un dump in forma di testu:
pg_dump -U my_user -d my_database -F p -t ws_log_smevlog -f ./my_dump.dumpU dump, cum'è previstu, hè stata interrotta cù u listessu errore:
pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989 Più in là cuda Aghju guardatu à a fine di u dump (coda -5 ./my_dump.dump) hà scupertu chì u dump hè stata interrotta nantu à a linea cù id 186 525. "Allora u prublema hè in linea cù l'id 186 526, hè rottu, è deve esse sguassatu!" - Aghju pensatu. Ma, fendu una dumanda à a basa di dati:
«selezziunate * da ws_log_smevlog induve id=186529"Hè risultatu chì tuttu era bè cù questa linea... E fila cù indici 186 - 530 anu travagliatu ancu senza prublemi. Un'altra "idea brillanti" hà fallutu. In seguitu aghju capitu perchè questu hè accadutu: quandu sguassate è cambiate dati da una tavula, ùn sò micca sguassati fisicamente, ma sò marcati cum'è "tuple morti", dopu vene. autovacuum è marca queste linee cum'è sguassate è permette à queste linee per esse riutilizate. Per capiscenu, se i dati in a tavula cambianu è l'autovacuum hè attivatu, allora ùn hè micca almacenatu in sequenza.
Tentativu 5: SELECT, FROM, WHERE id=
I fallimenti ci facenu più forti. Ùn deve mai rinunzià, avete bisognu à andà à a fine è crede in sè stessu è in e vostre capacità. Allora aghju decisu di pruvà una altra opzione: basta à circà tutti i registri in a basa di dati unu per unu. Sapendu a struttura di a mo tavula (vede sopra), avemu un campu id chì hè unicu (chjave primaria). Avemu 1 fila in a tavula è id sò in ordine, chì significa chì pudemu solu passà per elli unu per unu:
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; doneSe qualchissia ùn capisce micca, u cumandimu funziona cusì: scansa a tavola fila per fila è manda stdout à / dev / null, ma se u cumandamentu SELECT falla, u testu di l'errore hè stampatu (stderr hè mandatu à a cunsola) è una linea chì cuntene l'errore hè stampata (grazzi à ||, chì significa chì a selezzione hà avutu prublemi (u codice di ritornu di u cumandimu). ùn hè micca 0)).
Eru furtunatu, aghju avutu indici creati nantu à u campu id:

Questu significa chì truvà una linea cù l'id desideratu ùn deve micca piglià assai tempu. In teoria, deve travaglià. Ebbè, lanciamu u cumandamentu tmux è andemu à lettu.
A matina aghju trovu chì circa 90 000 entrate sò state viste, chì hè pocu più di 5%. Un risultatu eccellente quandu paragunatu cù u metudu precedente (2%)! Ma ùn vulia micca aspittà 20 ghjorni ...
Tentativu 6: SELECT, FROM, WHERE id >= è id
U cliente avia un servitore eccellente dedicatu à a basa di dati: dual-processor Intel Xeon E5-2697 v2, ci era quant'è 48 fili in u nostru locu ! A carica nantu à u servitore era mediu, pudemu scaricà circa 20 fili senza prublemi. Ci era ancu abbastanza RAM: finu à 384 gigabyte !
Dunque, u cumandamentu deve esse parallelizatu:
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; doneQuì era pussibule di scrive un scrittu bellu è eleganti, ma aghju sceltu u metudu di parallelizazione più veloce: split manually the range 0-1628991 in intervalli di 100 records è eseguite separatamente 000 cumandamenti di a forma:
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; doneMa ùn hè micca tuttu. In teoria, a cunnessione à una basa di dati piglia ancu qualchì tempu è risorse di u sistema. Cunnessu 1 ùn era micca assai intelligente, vi d'accordu. Dunque, ritruvemu 628 fila invece di una cunnessione unu à unu. In u risultatu, a squadra si trasformò in questu:
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; doneAprite 16 Windows in una sessione tmux è eseguite i cumandamenti:
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 ghjornu dopu aghju ricevutu i primi risultati! Vale à dì (i valori XXX è ZZZ ùn sò più cunservati):
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
146000Questu significa chì trè linii cuntenenu un errore. L'ids di u primu è u sicondu prublemu records eranu trà 829 è 000, l'ids di u terzu eranu trà 830 è 000 In seguitu, avemu avutu solu truvà u valore d'identificazione esatta di i registri di u prublema. Per fà questu, guardemu à traversu a nostra gamma cù registri problematici cù un passu di 146 è identificà 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
Felice finale
Avemu trovu e linee problematiche. Andemu in a basa di dati via psql è pruvate à sguassà:
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À a mo sorpresa, e voci sò state sguassate senza prublemi ancu senza l'opzione zero_pagine_dannate.
Allora aghju cunnessu à a basa di dati, hà fattu VACUUM FULL (Pensu chì ùn era micca necessariu di fà questu), è infine aghju sguassatu successu a copia di salvezza usendu pg_dump. U dump hè statu pigliatu senza errore! U prublema hè stata risolta in modu cusì stupidu. L'alegria ùn cunnisciava cunfini, dopu à tanti fallimenti simu riesciutu à truvà una suluzione !
Riconoscimenti è cunclusioni
Hè cusì chì a mo prima sperienza di restaurà una vera basa di dati Postgres hè stata. Ricurdaraghju sta sperienza per un bellu pezzu.
È infine, vogliu ringrazià PostgresPro per a traduzzione di a documentazione in russo è per , chì hà aiutatu assai durante l'analisi di u prublema.
Source: www.habr.com
