una - se puede hacer mal uso de copia de escrituras
Escribir programas para hacer frente a errores de E/S que causan escrituras perdidas en Linux (5)
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
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 se han enviado correctamente mediante
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(...)
en
fs/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
Como 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 kernel, pero
creo
que establece
AS_EIO
en 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 está claro para mí si la aplicación puede averiguarlo o cómo hacerlo cuando más tarde
fsync()
s el archivo para confirmar que está en el disco.
Parece que
wait_on_page_writeback_range(...)
en
mm/filemap.c
podría ser
do_sync_mapping_range(...)
en
fs/sync.c
que a su vez es llamado por
sys_sync_file_range(...)
.
Devuelve
-EIO
si no se pueden escribir uno o más buffers.
Si, como supongo, esto se propaga al resultado de
fsync()
, entonces si la aplicación entra en pánico y se rescata si obtiene un error de E / S de
fsync()
y sabe cómo volver a hacer su trabajo cuando se reinicia, eso debería ser suficiente salvaguarda?
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 la última
fsync()
del archivo, y eso reescribe cualquier búfer de kernel sucio correspondiente a escrituras perdidas en el archivo, eso debería borrar cualquier indicador de error de E / S en las páginas perdidas y permitir que se complete el siguiente
fsync()
, ¿verdad?
¿Hay alguna otra circunstancia inofensiva en la que
fsync()
pueda devolver
-EIO
donde 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 valores predeterminados del controlador
dm-multipath
y el código de detección utilizado por la SAN para informar sobre fallas en la asignación del almacenamiento de aprovisionamiento delgado.
Pero esta no es la única circunstancia en la que
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.
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.
write
puede 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, eso significa en la próxima
fsync
, posiblemente en una escritura siguiente si el sistema decide vaciar la caché y al 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 la última
fsync
exitosa
puede
haber fallado y que, al menos, algo ha fallado.
fsync()
devuelve
-EIO
si 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)
set_bit(AS_EIO, &page->mapping->flags);
set_buffer_write_io_error(bh);
clear_buffer_uptodate(bh);
SetPageError(page);
que luego es detectado por
wait_on_page_writeback_range(...)
como lo llama
do_sync_mapping_range(...)
como lo llama
sys_sync_file_range(...)
como lo llama
sys_sync_file_range2(...)
para implementar la biblioteca C llamada
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
fsync()
devuelve
-EIO
o (no documentado en la página de
-ENOSPC
)
-ENOSPC
,
borrará el estado de error,
por lo que un
fsync()
posterior informará de éxito aunque las páginas nunca se hayan escrito.
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 intentar
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
fsync()
y cree que todo estará bien cuando tenga éxito.
¿Esto está permitido?
Los documentos
POSIX / SuS en
fsync()
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 Linux para
fsync()
simplemente no dice nada sobre lo que sucede en caso de falla.
Por lo tanto, parece que el significado de los errores de
fsync()
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_write
establece
-EIO
en 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_errors
en
mm/filemap.c
ahora hace:
if (test_bit(AS_EIO, &mapping->flags) &&
test_and_clear_bit(AS_EIO, &mapping->flags))
ret = -EIO;
que tiene el mismo efecto
Las comprobaciones de errores parecen pasar por
filemap_check_errors
que realiza una prueba y borrado:
if (test_bit(AS_EIO, &mapping->flags) &&
test_and_clear_bit(AS_EIO, &mapping->flags))
ret = -EIO;
return ret;
Estoy usando
btrfs
en mi computadora portátil, pero cuando creo un loopback
ext4
para probar en
/mnt/tmp
y configuro 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 el
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()
) devuelve
-EIO
, el archivo está en un estado indefinido entre la última vez que
fsync()
d o
close()
d se
close()
última vez
write()
diez estados.
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 página del
fsync()
man
fsync()
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
- https://bugzilla.kernel.org/show_bug.cgi?id=194755
- https://bugzilla.kernel.org/show_bug.cgi?id=194757
Otras lecturas
lwn.net tocó esto en el artículo "Manejo mejorado de errores en la capa de bloques" .
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.
Verifique el valor de retorno de close. cerrar puede fallar mientras que las escrituras almacenadas en memoria intermedia parecen tener éxito.
write
(2) proporciona menos de lo que espera.
La página de manual es muy abierta sobre la semántica de una llamada exitosa de
write()
:
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 manera de estar seguro es llamar afsync
(2) después de que haya terminado de escribir todos sus datos.
Podemos concluir que una
write()
exitosa
write()
simplemente significa 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 estar
close()
.
La página de manual de la llamada al sistema
close
(2) contiene la siguiente oración:
Es muy posible que los errores en una operación anterior de
write
(2) se informen primero en elclose
final ().
Si su aplicación necesita conservar datos, debe usar
fsync
/
fsyncdata
de forma regular:
fsync()
transfiere ("enjuaga") todos los datos internos modificados de (es decir, páginas de 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 toda la información modificada se puede recuperar 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.