Escribir programas para hacer frente a errores de E / S que causan escrituras perdidas en Linux

138

TL; DR: Si el kernel de Linux pierde una escritura de E / S almacenada en un búfer , ¿hay alguna forma de que la aplicación se entere?

Sé que tiene que tener fsync()el archivo (y su directorio principal) para mayor durabilidad . La pregunta es si el núcleo pierde buffers sucios que están pendientes de escritura debido a un error de E / S, ¿cómo puede la aplicación detectar esto y recuperarlo o cancelarlo?

Piense en aplicaciones de bases de datos, etc., donde el orden de las escrituras y la durabilidad de la escritura pueden ser cruciales.

Perdido escribe? ¿Cómo?

La capa de bloque del kernel de Linux puede, en algunas circunstancias, perder solicitudes de E / S almacenadas en búfer que hayan sido enviadas con éxito write(), pwrite()etc., con un error como:

Buffer I/O error on device dm-0, logical block 12345
lost page write due to I/O error on dm-0

(Ver end_buffer_write_sync(...)y end_buffer_async_write(...)enfs/buffer.c ).

En los núcleos más nuevos, el error contendrá "escritura de página asíncrona perdida" , como:

Buffer I/O error on dev dm-0, logical block 12345, lost async page write

Dado que la aplicación write()ya habrá regresado sin error, parece que no hay forma de informar un error a la aplicación.

¿Detectarlos?

No estoy tan familiarizado con las fuentes del núcleo, pero creo que se establece AS_EIOen el búfer que no se pudo escribir si está haciendo una escritura asíncrona:

    set_bit(AS_EIO, &page->mapping->flags);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

pero no me queda claro si la aplicación puede enterarse de esto o cómo puede hacerlo cuando más tarde sea fsync()el archivo para confirmar que está en el disco.

Parece que wait_on_page_writeback_range(...)enmm/filemap.c poder por do_sync_mapping_range(...)enfs/sync.c que se llama turno sys_sync_file_range(...). Regresa -EIOsi no se pueden escribir uno o más buffers.

Si, como supongo, esto se propaga a fsync() resultado, entonces si la aplicación entra en pánico y se rescata si recibe un error de E / S fsync()y sabe cómo volver a hacer su trabajo cuando se reinicia, ¿debería ser suficiente protección?

Presumiblemente no hay forma de que la aplicación sepa qué desplazamientos de bytes en un archivo corresponden a las páginas perdidas, por lo que puede reescribirlas si lo sabe, pero si la aplicación repite todo su trabajo pendiente desde el último éxito fsync()del archivo, y eso reescribe cualquier almacenamiento intermedio de kernel sucio correspondiente a escrituras perdidas en el archivo, que debería borrar cualquier indicador de error de E / S en las páginas perdidas y permitir fsync()que se complete la siguiente , ¿verdad?

¿Existen entonces otras circunstancias inofensivas donde fsync() pueda regresar -EIOdonde rescatar y rehacer el trabajo sería demasiado drástico?

¿Por qué?

Por supuesto, tales errores no deberían suceder. En este caso, el error surgió de una desafortunada interacción entre los dm-multipathvalores predeterminados del controlador y el código de detección utilizado por la SAN para informar la falla en la asignación del almacenamiento de aprovisionamiento delgado. Pero esta no es la única circunstancia donde pueden suceder; también he visto informes de LVM de aprovisionamiento delgado, por ejemplo, como lo usan libvirt, Docker y más. Una aplicación crítica como una base de datos debería tratar de hacer frente a tales errores, en lugar de continuar ciegamente como si todo estuviera bien.

Si el kernel piensa que está bien perder escrituras sin morir con el pánico del kernel, las aplicaciones tienen que encontrar una manera de hacer frente.

El impacto práctico es que encontré un caso en el que un problema de múltiples rutas con una SAN causó escrituras perdidas que terminaron causando corrupción en la base de datos porque el DBMS no sabía que sus escrituras habían fallado. No es divertido.

Craig Ringer
fuente
1
Me temo que esto necesitaría campos adicionales en la SystemFileTable para almacenar y recordar estas condiciones de error. Y una posibilidad para que el proceso del espacio de usuario los reciba o los inspeccione en llamadas posteriores. (¿fsync () y close () devuelven este tipo de información histórica ?)
joop
@joop Gracias. Acabo de publicar una respuesta con lo que creo que está sucediendo, importa tener un control de cordura ya que parece saber más sobre lo que está sucediendo que las personas que han publicado variantes obvias de "write () necesitan close () o fsync ( ) para durabilidad "sin leer la pregunta?
Craig Ringer
Por cierto: creo que realmente debería profundizar en las fuentes del núcleo. Los sistemas de archivos con diario probablemente sufrirían el mismo tipo de problemas. Sin mencionar el manejo de la partición de intercambio. Dado que estos viven en el espacio del kernel, el manejo de estas condiciones probablemente será un poco más rígido. writev (), que es visible desde el espacio de usuario, también parece un lugar para buscar. [en Craig: sí, porque sé tu nombre y sé que no eres un completo idiota; -]
joop
1
Estoy de acuerdo, no fui tan justo. Por desgracia, su respuesta no es muy satisfactoria, quiero decir que no hay una solución fácil (¿sorprendente?).
Jean-Baptiste Yunès
1
@ Jean-BaptisteYunès True. Para el DBMS con el que estoy trabajando, "bloquear e ingresar rehacer" es aceptable. Para la mayoría de las aplicaciones, esa no es una opción y es posible que tengan que tolerar el horrible rendimiento de las E / S sincrónicas o simplemente aceptar comportamientos mal definidos y corrupción en los errores de E / S.
Craig Ringer

Respuestas:

91

fsync()regresa -EIOsi el núcleo perdió una escritura

(Nota: la primera parte hace referencia a núcleos más antiguos; actualizada a continuación para reflejar los núcleos modernos)

Parece que la escritura de búfer asíncrono en end_buffer_async_write(...)fallas establece un -EIOindicador en la página de búfer sucio fallido para el archivo :

set_bit(AS_EIO, &page->mapping->flags);
set_buffer_write_io_error(bh);
clear_buffer_uptodate(bh);
SetPageError(page);

que es detectada a continuación, por wait_on_page_writeback_range(...)como llamado por do_sync_mapping_range(...)como llamado por sys_sync_file_range(...)como llamado por sys_sync_file_range2(...)para implementar la función de biblioteca C fsync().

¡Pero solo una vez!

Este comentario en sys_sync_file_range

168  * SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER will detect any
169  * I/O errors or ENOSPC conditions and will return those to the caller, after
170  * clearing the EIO and ENOSPC flags in the address_space.

sugiere que cuando se fsync()devuelve -EIOo (sin documentar en la página de manual) -ENOSPC, se borrará el estado de error, por lo que un fsync()informe posterior informará el éxito a pesar de que las páginas nunca se escribieron.

Efectivamente wait_on_page_writeback_range(...) borra los bits de error cuando los prueba :

301         /* Check for outstanding write errors */
302         if (test_and_clear_bit(AS_ENOSPC, &mapping->flags))
303                 ret = -ENOSPC;
304         if (test_and_clear_bit(AS_EIO, &mapping->flags))
305                 ret = -EIO;

Entonces, si la aplicación espera que pueda volver a intentarlo fsync()hasta que tenga éxito y confíe en que los datos están en el disco, está terriblemente mal.

Estoy bastante seguro de que esta es la fuente de la corrupción de datos que encontré en el DBMS. Vuelve a intentarlo fsync()y piensa que todo estará bien cuando tenga éxito.

¿Esto está permitido?

Los documentos POSIX / SuS enfsync() realmente no especifican esto de ninguna manera:

Si la función fsync () falla, no se garantiza que las operaciones de E / S pendientes se hayan completado.

La página de manual de Linuxfsync() simplemente no dice nada sobre lo que sucede en caso de falla.

Por lo tanto, parece que el significado de los fsync()errores es "no sé qué sucedió con sus escritos, podría haber funcionado o no, mejor intente nuevamente para estar seguro".

Núcleos más nuevos

En 4.9 end_buffer_async_writeconjuntos -EIOen la página, solo a través de mapping_set_error.

    buffer_io_error(bh, ", lost async page write");
    mapping_set_error(page->mapping, -EIO);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

En el lado de la sincronización, creo que es similar, aunque la estructura ahora es bastante compleja de seguir. filemap_check_errorsen mm/filemap.cahora hace:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;

que tiene el mismo efecto Parece que todas las comprobaciones de errores pasan, lo filemap_check_errorsque hace una prueba y borra:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
    return ret;

Lo estoy usando btrfsen mi computadora portátil, pero cuando creo un ext4loopback para probar /mnt/tmpy configurar una sonda de rendimiento en él:

sudo dd if=/dev/zero of=/tmp/ext bs=1M count=100
sudo mke2fs -j -T ext4 /tmp/ext
sudo mount -o loop /tmp/ext /mnt/tmp

sudo perf probe filemap_check_errors

sudo perf record -g -e probe:end_buffer_async_write -e probe:filemap_check_errors dd if=/dev/zero of=/mnt/tmp/test bs=4k count=1 conv=fsync

Encuentro la siguiente pila de llamadas en perf report -T:

        ---__GI___libc_fsync
           entry_SYSCALL_64_fastpath
           sys_fsync
           do_fsync
           vfs_fsync_range
           ext4_sync_file
           filemap_write_and_wait_range
           filemap_check_errors

Una lectura completa sugiere que sí, los núcleos modernos se comportan igual.

Esto parece significar que si fsync()(o, presumiblemente, write()o close()retornos) -EIO, el archivo está en un estado no definido entre el último éxito cuando fsync()D o close()D y su más reciente write()estado de diez.

Prueba

He implementado un caso de prueba para demostrar este comportamiento .

Trascendencia

Un DBMS puede hacer frente a esto ingresando la recuperación de fallas. ¿Cómo se supone que una aplicación de usuario normal debe hacer frente a esto? La fsync()página del manual no advierte que significa "fsync-if-you-like-it-it" y espero que muchas aplicaciones no se adapten bien a este comportamiento.

Informes de errores

Otras lecturas

lwn.net tocó esto en el artículo "Manejo mejorado de errores de capa de bloque" .

hilo de la lista de correo postgresql.org .

Craig Ringer
fuente
3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 es una carrera posible, porque espera {E / S pendiente y programada}, no {E / S aún no programada}. Obviamente, esto es para evitar viajes de ida y vuelta adicionales al dispositivo. (Supongo que las escrituras del usuario () no regresan hasta que la E / S esté programada, para mmap (), esto es diferente)
joop
3
¿Es posible que la llamada de otro proceso a fsync para algún otro archivo en el mismo disco obtenga el error devuelto?
Random832
3
@ Random832 Muy relevante para una base de datos de procesamiento múltiple como PostgreSQL, muy buena pregunta. Parece probable, pero no conozco el código del núcleo lo suficientemente bien como para entenderlo. Sin embargo, será mejor que tus procs cooperen si ambos tienen el mismo archivo abierto.
Craig Ringer
1
@DavidFoerster: las llamadas al sistema devuelven fallas utilizando códigos de error negativos; errnoes completamente una construcción de la biblioteca C del espacio de usuario. Es común ignorar las diferencias de valor de retorno entre las llamadas al sistema y la biblioteca C de esta manera (como lo hace Craig Ringer, más arriba), ya que el valor de retorno de error identifica confiablemente a cuál (función de la biblioteca syscall o C) se hace referencia a: " -1con errno==EIO"se refiere a una función de biblioteca C, mientras que" -EIO"se refiere a una llamada al sistema. Finalmente, las páginas de manual de Linux en línea son la referencia más actualizada para las páginas de manual de Linux.
Nominal Animal
2
@CraigRinger: Para responder a su pregunta final: "Al usar E / S de bajo nivel y fsync()/ fdatasync()cuando el tamaño de la transacción es un archivo completo; al usar mmap()/ msync()cuando el tamaño de la transacción es un registro alineado con la página; y al usar I de bajo nivel / O, fdatasync()y múltiples descriptores de archivos concurrentes (un descriptor y un hilo por transacción) al mismo archivo de otra manera " . Los bloqueos de descripción de archivo abierto específicos de Linux ( fcntl(), F_OFD_) son muy útiles con el último.
Nominal Animal
22

Como la aplicación write () ya habrá regresado sin error, parece que no hay forma de informar un error a la aplicación.

No estoy de acuerdo. writepuede regresar sin error si la escritura simplemente se pone en cola, pero el error se informará en la próxima operación que requerirá la escritura real en el disco, es decir, en la próxima fsync, posiblemente en una escritura siguiente si el sistema decide vaciar la caché y en menos en el último archivo cerrado.

Esa es la razón por la cual es esencial que la aplicación pruebe el valor de retorno de close para detectar posibles errores de escritura.

Si realmente necesita poder realizar un procesamiento inteligente de errores, debe suponer que todo lo que se escribió desde el último éxito fsync puede haber fallado y que, al menos, algo ha fallado.

Serge Ballesta
fuente
44
Sí, creo que eso lo clava. Esto de hecho sugieren que la aplicación debe volver a hacer todo su trabajo desde la última confirmada éxito fsync()o close()del archivo si se pone una -EIOde write(), fsync()o close(). Bueno, eso es divertido.
Craig Ringer
1

write(2) proporciona menos de lo que espera. La página de manual es muy abierta sobre la semántica de una write()llamada exitosa :

Un retorno exitoso de write()no garantiza que los datos se hayan confirmado en el disco. De hecho, en algunas implementaciones con errores, ni siquiera garantiza que el espacio se haya reservado con éxito para los datos. La única forma de estar seguro es llamar al fsync(2) después de que haya terminado de escribir todos sus datos.

Podemos concluir que un éxito write()significa simplemente que los datos han llegado a las instalaciones de almacenamiento en búfer del núcleo. Si la persistencia del búfer falla, un acceso posterior al descriptor de archivo devolverá el código de error. Como último recurso que puede ser close(). La página de manual de la closellamada al sistema (2) contiene la siguiente oración:

Es muy posible que los errores en una writeoperación anterior (2) se informen primero en el final close().

Si su aplicación necesita persistir en la escritura de datos, debe usar fsync/ fsyncdataregularmente:

fsync()transfiere ("descarga") todos los datos internos modificados de (es decir, páginas de memoria caché de búfer modificadas para) el archivo al que hace referencia el descriptor de archivo fd al dispositivo de disco (u otro dispositivo de almacenamiento permanente) para que se pueda recuperar toda la información modificada incluso después de que el sistema se bloqueó o se reinició. Esto incluye escribir o vaciar un caché de disco si está presente. La llamada se bloquea hasta que el dispositivo informa que la transferencia se ha completado.

fzgregor
fuente
44
Sí, soy consciente de que fsync()es obligatorio. Pero en el caso específico en el que el núcleo pierde las páginas debido a un error de E / S se fsync()fallará? ¿En qué circunstancias puede tener éxito después?
Craig Ringer
Tampoco conozco la fuente del núcleo. Asumamos fsync()retornos -EIOen problemas de E / S (¿para qué sería bueno de lo contrario?). Por lo tanto, la base de datos sabe que algo de una escritura anterior falló y podría entrar en modo de recuperación. ¿No es esto lo que quieres? ¿Cuál es la motivación de tu última pregunta? ¿Desea saber qué escritura falló o recuperar el descriptor de archivo para su uso posterior?
fzgregor
Idealmente, un DBMS preferirá no ingresar en la recuperación de fallas (iniciar a todos los usuarios y volverse temporalmente inaccesible o al menos de solo lectura) si posiblemente puede evitarlo. Pero incluso si el núcleo pudiera decirnos "bytes 4096 a 8191 de fd X", sería difícil averiguar qué (re) escribir allí sin hacer una recuperación de bloqueo. Así que supongo que la pregunta principal es si hay más circunstancias inocentes en las que fsync()pueda volver a -EIOdonde sea seguro volver a intentarlo, y si es posible notar la diferencia.
Craig Ringer
La recuperación segura es el último recurso. Pero como ya dijo, se espera que estos problemas sean muy, muy raros. Por lo tanto, no veo ningún problema con la recuperación en ninguno -EIO. Si cada descriptor de archivo solo es utilizado por un hilo a la vez, este hilo podría volver al último fsync()y rehacer las write()llamadas. Pero aún así, si esos write()solo escriben parte de un sector, la parte no modificada aún puede estar corrupta.
fzgregor
1
Tienes razón en que entrar en recuperación de accidentes es probablemente razonable. En cuanto a los sectores parcialmente corruptos, el DBMS (PostgreSQL) almacena una imagen de toda la página la primera vez que la toca después de un punto de control dado por esa misma razón, por lo que debería estar bien :)
Craig Ringer
0

Use el indicador O_SYNC cuando abra el archivo. Asegura que los datos se escriban en el disco.

Si esto no te satisface, no habrá nada.

hombre fuerte
fuente
17
O_SYNCEs una pesadilla para el rendimiento. Significa que la aplicación no puede hacer nada más mientras está ocurriendo la E / S del disco a menos que genere hilos de E / S. También podría decir que la interfaz de E / S almacenada no es segura y todos deberían usar AIO. ¿Seguramente las escrituras perdidas en silencio no pueden ser aceptables en E / S almacenadas?
Craig Ringer
3
( O_DATASYNCes solo un poco mejor en ese sentido)
Craig Ringer
@CraigRinger Debería usar AIO si tiene esta necesidad y necesita algún tipo de rendimiento. O simplemente use un DBMS; maneja todo por ti.
Demi
10
@Demi La aplicación aquí es un dbms (postgresql). Estoy seguro de que puede imaginarse que reescribir toda la aplicación para usar AIO en lugar de E / S almacenadas no es práctico. Tampoco debería ser necesario.
Craig Ringer
-5

Verifique el valor de retorno de close. cerrar puede fallar mientras que las escrituras almacenadas en el búfer parecen tener éxito.

Malcolm McLean
fuente
8
Bueno, casi no queremos ser open()ing y close()ing del archivo cada pocos segundos. es por eso que tenemos fsync()...
Craig Ringer