TL; DR: si el kernel de Linux pierde una escritura de E/S almacenada en búfer , ¿hay alguna forma de que la aplicación lo descubra?
Sé que tiene que fsync() el archivo (y su directorio principal) para mayor durabilidad . La pregunta es si el kernel 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 recuperarse o abortar?
Piense en aplicaciones de bases de datos, etc., donde el orden de las escrituras y la durabilidad de las escrituras pueden ser cruciales.
La capa de bloque del kernel de Linux puede, en algunas circunstancias, perder las 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 Dado que la write() de la aplicación ya habrá regresado sin error, parece que no hay forma de informar un error a la aplicación.
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 me queda claro si o cómo la aplicación puede averiguarlo cuando 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 pudieron escribir uno o más búferes.
Si, como supongo, esto se propaga al resultado de fsync() , entonces si la aplicación entra en pánico y se recupera si recibe un error de E/S de fsync() y sabe cómo volver a hacer su trabajo cuando se reinicia, eso debería ser suficiente salvaguardia?
Presumiblemente, no hay forma de que la aplicación sepa qué desplazamientos de bytes en un archivo corresponden a las páginas perdidas para que pueda reescribirlas si sabe cómo, pero si la aplicación repite todo su trabajo pendiente desde la última fsync() exitosa 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 entonces alguna otra circunstancia inofensiva en la que fsync() pueda devolver -EIO en la que rescatar y rehacer el trabajo sería demasiado drástico?
Por supuesto, tales errores no deberían ocurrir. En este caso, el error surgió de una interacción desafortunada entre los valores predeterminados del controlador dm-multipath y el código de sentido utilizado por la SAN para informar la falla en la asignación de almacenamiento de aprovisionamiento delgado. Pero esta no es la única circunstancia en la que pueden ocurrir: 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 un kernel panic, las aplicaciones tienen que encontrar una manera de hacerle frente.
El impacto práctico es que encontré un caso en el que un problema de rutas múltiples con una SAN causó escrituras perdidas que terminaron causando daños en la base de datos porque el DBMS no sabía que sus escrituras habían fallado. No es divertido.
Use el indicador O_SYNC cuando abra el archivo. Garantiza que los datos se escriban en el disco.
Si esto no te satisface, no habrá nada.
write (2) proporciona menos de lo que espera. La página del manual es muy abierta sobre la semántica de una llamada exitosa a write() :
Un retorno exitoso de
write()no garantiza que los datos se hayan enviado al 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 afsync(2) una vez que haya terminado de escribir todos sus datos.
Podemos concluir que una write() exitosa simplemente significa que los datos han llegado a las instalaciones de almacenamiento en búfer del kernel. Si falla la persistencia del búfer, un acceso posterior al descriptor de archivo devolverá el código de error. Como último recurso, puede ser close() . La página del manual de la llamada al sistema close (2) contiene la siguiente oración:
Es muy posible que los errores en una operación de
writeanterior (2) se notifiquen primero en elclosefinal ().
Si su aplicación necesita conservar la escritura de datos, debe usar fsync / fsyncdata regularmente:
fsync()transfiere ("descarga") 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 fallara o se reiniciara. 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.
Compruebe el valor de retorno de close. close puede fallar mientras que las escrituras almacenadas en búfer parecen tener éxito.
fsync() devuelve -EIO si el kernel perdió una escritura(Nota: la primera parte hace referencia a núcleos más antiguos; se actualiza 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 llamada de la biblioteca C fsync() .
Este comentario sobre 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 manual) -ENOSPC , borrará el estado de error para que un fsync() posterior informe el é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 intentar fsync() y piensa que todo estará bien cuando tenga éxito.
Los documentos POSIX/SuS en fsync() realmente no especifican esto de ninguna manera:
Si la función fsync() falla, no se garantiza que se hayan completado las operaciones de E/S pendientes.
La página de manual de Linux para fsync() simplemente no dice nada sobre lo que sucede en caso de falla.
Entonces parece que el significado de los errores de fsync() es "No sé qué pasó con tus escrituras, podría haber funcionado o no, mejor inténtalo de nuevo para estar seguro".
En 4.9, end_buffer_async_write establece -EIO en la página, solo a través 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 todas por filemap_check_errors , que hace una prueba y borra:
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 bucle invertido 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 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_errorsUna lectura sugiere que sí, los núcleos modernos se comportan de la misma manera.
Esto parece significar que si fsync() (o presumiblemente write() o close() ) devuelve -EIO , el archivo se encuentra en un estado indefinido entre la última vez que fsync() d o close() y su fecha más reciente write() diez estados.
He implementado un caso de prueba para demostrar este comportamiento .
Un DBMS puede hacer frente a esto ingresando a la recuperación de fallas. ¿Cómo diablos se supone que una aplicación de usuario normal puede hacer frente a esto? La página de manual de fsync() no advierte que significa "fsync-if-you-feels-like-it" y espero que muchas aplicaciones no se adapten bien a este comportamiento.
lwn.net se refirió a esto en el artículo "Manejo de errores de capa de bloque mejorado" .
Dado que la escritura () de la aplicación ya habrá regresado sin error, parece que no hay forma de informar un error a la aplicación.
No estoy de acuerdo. La 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 el próximo fsync , posiblemente en una siguiente escritura si el sistema decide vaciar el caché. y al menos en el cierre del último archivo.
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 asumir que todo lo que se escribió desde la última fsync exitosa puede haber fallado y que, en todo eso, al menos algo ha fallado.