Mi primera experiencia recuperando una base de datos Postgres después de un error (página no válida en el bloque 4123007 de relatton base/16490)

Me gustaría compartir con ustedes mi primera experiencia exitosa al restaurar una base de datos Postgres a su funcionalidad completa. Me familiaricé con el DBMS Postgres hace medio año; antes no tenía ninguna experiencia en administración de bases de datos.

Mi primera experiencia recuperando una base de datos Postgres después de un error (página no válida en el bloque 4123007 de relatton base/16490)

Trabajo como ingeniero semi-DevOps en una gran empresa de TI. Nuestra empresa desarrolla software para servicios de alta carga y yo soy responsable del rendimiento, el mantenimiento y la implementación. Me asignaron una tarea estándar: actualizar una aplicación en un servidor. La aplicación está escrita en Django, durante la actualización se realizan migraciones (cambios en la estructura de la base de datos), y antes de este proceso realizamos un volcado completo de la base de datos a través del programa estándar pg_dump, por si acaso.

Se produjo un error inesperado al realizar un volcado (Postgres versión 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

Bug "página no válida en bloque" habla de problemas a nivel de sistema de archivos, lo cual es muy malo. En varios foros se sugirió hacer VACÍO COMPLETO con opción páginas_cero_dañadas para resolver este problema. Bueno, intentemos...

Preparándose para la recuperación

ADVERTENCIA! Asegúrese de realizar una copia de seguridad de Postgres antes de cualquier intento de restaurar su base de datos. Si tiene una máquina virtual, detenga la base de datos y tome una instantánea. Si no es posible tomar una instantánea, detenga la base de datos y copie el contenido del directorio de Postgres (incluidos los archivos wal) a un lugar seguro. Lo principal en nuestro negocio es no empeorar las cosas. Leer это.

Como la base de datos generalmente funcionó para mí, me limité a un volcado de base de datos normal, pero excluí la tabla con datos dañados (opción -T, --excluir-tabla=TABLA en pg_dump).

El servidor era físico, era imposible tomar una instantánea. La copia de seguridad ha sido eliminada, sigamos adelante.

Comprobación del sistema de archivos

Antes de intentar restaurar la base de datos, debemos asegurarnos de que todo esté en orden con el sistema de archivos. Y en caso de errores, corrígelos, porque de lo contrario sólo podrás empeorar las cosas.

En mi caso, el sistema de archivos con la base de datos se montó en "/srv" y el tipo era ext4.

Deteniendo la base de datos: parada systemctl [email protected] y verifique que el sistema de archivos no esté siendo utilizado por nadie y pueda desmontarse usando el comando lsof:
lsof +D /srv

También tuve que detener la base de datos de Redis, ya que también estaba usando "/srv". Luego lo desmonté / srv (desmontar).

El sistema de archivos se verificó usando la utilidad. e2fsck con el interruptor -f (Forzar la verificación incluso si el sistema de archivos está marcado como limpio):

Mi primera experiencia recuperando una base de datos Postgres después de un error (página no válida en el bloque 4123007 de relatton base/16490)

A continuación, usando la utilidad dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep comprobado) puede verificar que la verificación se realizó realmente:

Mi primera experiencia recuperando una base de datos Postgres después de un error (página no válida en el bloque 4123007 de relatton base/16490)

e2fsck dice que no se encontraron problemas en el nivel del sistema de archivos ext4, lo que significa que puede continuar intentando restaurar la base de datos, o más bien volver a vacío lleno (Por supuesto, debe volver a montar el sistema de archivos e iniciar la base de datos).

Si tiene un servidor físico, asegúrese de verificar el estado de los discos (a través de smartctl -a /dev/XXX) o controlador RAID para asegurarse de que el problema no esté en el nivel de hardware. En mi caso, el RAID resultó ser "hardware", así que le pedí al administrador local que verificara el estado del RAID (el servidor estaba a varios cientos de kilómetros de mí). Dijo que no hubo errores, lo que significa que definitivamente podemos comenzar la restauración.

Intento 1: cero_páginas_dañadas

Nos conectamos a la base de datos vía psql con una cuenta que tiene derechos de superusuario. Necesitamos un superusuario, porque... opción páginas_cero_dañadas sólo él puede cambiar. En mi caso es postgres:

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

Opcion páginas_cero_dañadas necesario para ignorar los errores de lectura (del sitio web de postgrespro):

Cuando PostgreSQL detecta un encabezado de página corrupto, normalmente informa un error y cancela la transacción actual. Si zero_damged_pages está habilitado, el sistema emite una advertencia, pone a cero la página dañada en la memoria y continúa procesando. Este comportamiento destruye los datos, es decir, todas las filas de la página dañada.

Activamos la opción e intentamos hacer un vacío completo de las mesas:

VACUUM FULL VERBOSE

Mi primera experiencia recuperando una base de datos Postgres después de un error (página no válida en el bloque 4123007 de relatton base/16490)
Lamentablemente, mala suerte.

Encontramos 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_tostada – un mecanismo para almacenar “datos largos” en Poetgres si no caben en una página (8kb por defecto).

Intento 2: reindexar

El primer consejo de Google no sirvió de nada. Después de unos minutos de búsqueda, encontré el segundo consejo: hacer reindexar mesa dañada. Vi este consejo en muchos lugares, pero no me inspiró confianza. Reindexemos:

reindex table ws_log_smevlog

Mi primera experiencia recuperando una base de datos Postgres después de un error (página no válida en el bloque 4123007 de relatton base/16490)

reindexar completado sin problemas.

Sin embargo, esto no ayudó, VACÍO LLENO se estrelló con un error similar. Como estoy acostumbrado a los fracasos, comencé a buscar consejos en Internet y encontré una solución bastante interesante. Artículo.

Intento 3: SELECCIONAR, LIMITAR, COMPENSAR

El artículo anterior sugería mirar la tabla fila por fila y eliminar los datos problemáticos. Primero necesitábamos mirar todas las líneas:

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 mi caso, la tabla contenía 1 628 991 ¡líneas! Era necesario cuidar bien partición de datos, pero este es un tema para una discusión separada. Era sábado, ejecuté este comando en tmux y me fui a la cama:

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

Por la mañana decidí comprobar cómo iban las cosas. Para mi sorpresa, descubrí que después de 20 horas, ¡solo se había escaneado el 2% de los datos! No quería esperar 50 días. Otro completo fracaso.

Pero no me rendí. Me pregunté por qué el escaneo tomó tanto tiempo. De la documentación (nuevamente en postgrespro) descubrí:

OFFSET especifica omitir el número especificado de filas antes de comenzar a generar filas.
Si se especifican tanto OFFSET como LIMIT, el sistema primero omite las filas OFFSET y luego comienza a contar las filas para la restricción LIMIT.

Cuando se utiliza LIMIT, es importante utilizar también una cláusula ORDER BY para que las filas de resultados se devuelvan en un orden específico. De lo contrario, se devolverán subconjuntos de filas impredecibles.

Obviamente, el comando anterior era incorrecto: en primer lugar, no había ordenar por, el resultado podría ser erróneo. En segundo lugar, Postgres primero tuvo que escanear y omitir filas OFFSET, y con el aumento OFFSET la productividad disminuiría aún más.

Intento 4: realizar un volcado en forma de texto

Entonces se me ocurrió una idea aparentemente brillante: hacer un volcado en forma de texto y analizar la última línea grabada.

Pero primero, echemos un vistazo a la estructura de la tabla. ws_log_smevlog:

Mi primera experiencia recuperando una base de datos Postgres después de un error (página no válida en el bloque 4123007 de relatton base/16490)

En nuestro caso tenemos una columna. "identificación", que contenía el identificador único (contador) de la fila. El plan era así:

  1. Comenzamos a realizar un volcado en forma de texto (en forma de comandos SQL)
  2. En cierto momento, el volcado se interrumpiría debido a un error, pero el archivo de texto aún se guardaría en el disco.
  3. Miramos el final del archivo de texto, allí encontramos el identificador (id) de la última línea que se eliminó con éxito.

Empecé a hacer un volcado en forma de texto:

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

El volcado, como era de esperarse, fue interrumpido con el mismo error:

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

Más adelante cola Miré al final del vertedero (cola -5 ./my_dump.dump) descubrió que el volcado fue interrumpido en la línea con id 186 525. "Entonces, el problema está en la línea con el ID 186 526, está roto y debe eliminarse". - Pensé. Pero, haciendo una consulta a la base de datos:
«seleccione * de ws_log_smevlog donde id=186529"Resultó que todo estaba bien con esta línea... Las filas con índices 186 - 530 también funcionaron sin problemas. Otra “idea brillante” fracasó. Luego entendí por qué pasaba esto: al borrar y cambiar datos de una tabla, no se borran físicamente, sino que se marcan como “tuplas muertas”, luego viene autovacío y marca estas líneas como eliminadas y permite que estas líneas se reutilicen. Para entenderlo, si los datos de la tabla cambian y el vacío automático está habilitado, entonces no se almacena de forma secuencial.

Intento 5: SELECCIONAR, DESDE, DONDE id=

Los fracasos nos hacen más fuertes. Nunca debes rendirte, debes llegar hasta el final y creer en ti mismo y en tus capacidades. Así que decidí probar otra opción: simplemente revisar todos los registros de la base de datos uno por uno. Conociendo la estructura de mi tabla (ver arriba), tenemos un campo de identificación que es único (clave principal). Tenemos 1 filas en la tabla y id están en orden, lo que significa que podemos revisarlos uno por 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

Si alguien no entiende, el comando funciona de la siguiente manera: escanea la tabla fila por fila y envía la salida estándar a / dev / null, pero si el comando SELECT falla, entonces se imprime el texto del error (se envía stderr a la consola) y se imprime una línea que contiene el error (gracias a ||, lo que significa que el select tuvo problemas (el código de retorno del comando no es 0)).

Tuve suerte, tuve índices creados en el campo. id:

Mi primera experiencia recuperando una base de datos Postgres después de un error (página no válida en el bloque 4123007 de relatton base/16490)

Esto significa que encontrar una línea con la identificación deseada no debería llevar mucho tiempo. En teoría debería funcionar. Bueno, ejecutemos el comando en tmux y vámonos a la cama.

Por la mañana descubrí que se habían visto unas 90 entradas, lo que supone poco más del 000%. ¡Un resultado excelente en comparación con el método anterior (5%)! Pero no quería esperar 2 días...

Intento 6: SELECCIONAR, DESDE, DONDE id >= e id

El cliente disponía de un excelente servidor dedicado a la base de datos: doble procesador Intel Xeon E5-2697 v2, ¡había hasta 48 hilos en nuestra ubicación! La carga en el servidor fue media, pudimos descargar unos 20 hilos sin ningún problema. También había suficiente RAM: ¡hasta 384 gigabytes!

Por lo tanto, era necesario paralelizar el comando:

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í fue posible escribir un script hermoso y elegante, pero elegí el método de paralelización más rápido: dividir manualmente el rango 0-1628991 en intervalos de 100 registros y ejecutar por separado 000 comandos del formulario:

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

Pero eso no es todo. En teoría, conectarse a una base de datos también requiere algo de tiempo y recursos del sistema. Conectar 1 no fue muy inteligente, estarás de acuerdo. Por lo tanto, recuperemos 628 filas en lugar de una conexión uno a uno. Como resultado, el equipo se transformó en esto:

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

Abra 16 ventanas en una sesión tmux y ejecute los comandos:

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 día después recibí los primeros resultados! Es decir (los valores XXX y ZZZ ya no se conservan):

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

Esto significa que tres líneas contienen un error. Las identificaciones del primer y segundo registros del problema estaban entre 829 000 y 830 000, las identificaciones del tercero estaban entre 146 000 y 147 000. A continuación, simplemente teníamos que encontrar el valor de identificación exacto de los registros del problema. Para hacer esto, revisamos nuestro rango con registros problemáticos con un paso de 1 e identificamos la identificación:

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

Feliz final

Encontramos las líneas problemáticas. Entramos en la base de datos vía psql e intentamos eliminarlos:

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

Para mi sorpresa, las entradas se eliminaron sin problemas incluso sin la opción páginas_cero_dañadas.

Luego me conecté a la base de datos, ¿no? VACÍO LLENO (Creo que no era necesario hacer esto), y finalmente eliminé con éxito la copia de seguridad usando pg_dump. ¡El volcado se realizó sin ningún error! El problema se resolvió de una manera tan estúpida. ¡La alegría no tuvo límites, después de tantos fracasos logramos encontrar una solución!

Agradecimientos y Conclusión

Así resultó mi primera experiencia de restauración de una base de datos Postgres real. Recordaré esta experiencia durante mucho tiempo.

Y finalmente, me gustaría agradecer a PostgresPro por traducir la documentación al ruso y por cursos online completamente gratis, lo que ayudó mucho durante el análisis del problema.

Fuente: habr.com

Añadir un comentario