"fsync" y LD_PRELOAD

Como comento en Migrar Thunderbird de "mbox" a "IMAP" (III): ¡Migración!, estoy moviendo cuatro mensajes por segundo. No es mala velocidad en abstracto, pero sí lo es si pensamos que para mover un millón de mensajes necesito 70 horas ininterrumpidas.

Una vez que quedó claro que las extensiones de Thunderbird y el propio programa no estaban a la altura de las circunstancias, la migración manual copiando carpeta a carpeta empezó a ser una opción válida, si bien poco deseable. Tengo carpetas como Python-DEV con 80.000 mensajes. Mi propio Inbox tiene 42.000 mensajes. Carpetas de correo privado con 8.000 mensajes, etc. Copiar a mano es un coñazo y es fácil meter la pata. Y a 4 mensajes por segundo tambien es una tortura lenta y dolorosa.

¿Por qué 4 mensajes por segundo?. El correo electrónico es una aplicación atípica. Una de las máximas de todos los sistemas de correo electrónico serios es que un mensaje no debe perderse nunca. Si un sistema acepta un mensaje es responsable del mismo y no debe perderlo por causas pueriles como un disco duro lleno o que se vaya la luz en mal momento. Y ahí está el problema.

Mucha gente piensa que leer de un disco duro es rápido y escribir en él es lento. Al contrario, leer de un disco es lento porque mientras no tengamos la información el programa no puede continuar. En cambio cuando un programa escribe, sus escrituras se almacenan en la caché del sistema operativo y el programa continua inmediatamente, sin ninguna pausa. El sistema operativo acumula cambios en RAM y los vuelca todos a disco en una larga ráfaga en un momento futuro. Se trata de escrituras asíncronas.

Pero a veces necesitamos que un dato se grabe en el disco AHORA. Por ejemplo, cuando entra un mensaje de correo electrónico y debemos asegurarnos de que se ha grabado en disco duro de verdad antes de darle el OK al servidor que nos lo manda.

Todo esto lo explico muy bien en un artículo anterior: ZFS y SSD's. Lee ese artículo y luego continua aquí.

¿Ya?

¿De verdad?

Vale, seguimos.

La cuestión es que mi portátil tiene un disco duro relativamente rápido, pero Dovecot necesita varias escrituras síncronas a disco por cada mensajes de correo electrónico nuevo: grabar el mensaje en el disco, actualizar el índice general y actualizar el índice de la carpeta afectada. Además, las escrituras síncronas están serializadas y nos garantizan el orden de las grabaciones en disco duro (en este caso, que no se va a actualizar el índice hasta que el mensaje no se ha grabado realmente en el disco). Total, nos quedamos en cuatro mensajes por segundo [1].

[1] Como las escrituras síncronas paran el proceso, suele ser beneficioso abrir varias conexiones IMAP4 en paralelo, trabajando con varias carpetas a la vez. Al menos hasta cierto nivel de concurrencia.

El fastidio de las escrituras síncronas es que se trata de un caso de informática defensiva con un coste muy elevado. Estamos pagando un precio significativo en cada operación a cambio de tranquilidad. Las escrituras síncronas nos protegen de caídas del ordenador (corte de luz) o del sistema operativo (Kernel Panic). Es de señalar que una caída en la propia aplicación no nos importa, porque esos datos ya están bajo el control del sistema operativo. El sistema operativo volcará los datos a disco eventualmente, aunque la aplicación responsable haya muerto.

Si pudiésemos estar seguros de que el ordenador no se va a colgar o a apagar, si todos los reinicios fuesen controlados, no tendríamos necesidad de escrituras síncronas.

En el caso concreto de migración de correo, acelerar la operación es muy interesante. Más importante aún, si el ordenador se apaga a lo bruto y se corrompen datos, podemos borrar lo migrado al IMAP4 y volver a empezar otra vez. Al ser una operación repetible no tenemos riesgos. Así que sería muy interesante que, durante la migración, pudiésemos desactivar las escrituras síncronas en Dovecot.

¿Cómo?. Viendo la operación de Dovecot con herramientas como strace comprobamos que las operaciones síncronas se solicitan a través de una variante concreta llamada fdatasync. Podríamos parchear Dovecot para eliminar esas llamadas (últimamente sé más de las tripas de Dovecot de lo que nunca he deseado :-).

Una posibilidad que funciona en todos los sistemas UNIX consiste en precargar una librería dinámica cuando se arranca un proceso, de forma que llame a funciones de esa librería en vez de a las funciones de la librería habitual. En nuestro caso queremos que Dovecot llame a una función fdatasync nuestra en vez de a la versión "buena" de la libc. Interceptando esa llamada podemos hacer lo que queramos. En nuestro caso lo que queremos hacer es... nada.

Para ello escribimos un fichero con el siguiente contenido:

int fdatasync(int fd) {
    return 0;
}

Ahora generamos una librería compartida con:

$ gcc -shared -o z-fdatasync.so z-fdatasync.c

Así obtenemos una librería compartida z-fdatasync.so con una rutina fdatasync que termina inmediatamente en vez de hacer una llamada fdatasync al sistema operativo.

Para conseguir que se cargue nuestra librería en Dovecot vamos al directorio /usr/local/libexec/dovecot, renombramos el fichero imap a imap2 y creamos un fichero Shell con el siguiente contenido (y permisos de ejecución):

#!/bin/sh

LD_PRELOAD=/home/jcea/z-fdatasync.so
/usr/local/libexec/dovecot/imap.2 "$@"

Paramos Dovecot y lo relanzamos otra vez.

ATENCIÓN: Si el ordenador se para de forma súbita (corte de luz, cuelgue) corromperemos datos, potencialmente de forma catastrófica. Las escrituras síncronas están ahí por un motivo.

En este caso concreto, sólo desactivo el fdatasync durante la migración, y si hay cualquier problema (como, de hecho, lo hubo), borro todo el trabajo y vuelvo a empezar otra vez.

Esto no debe dejarse en producción bajo ninguna circunstancia. Avisados estáis.

No entro en los detalles finos de cómo funciona LD_PRELOAD porque hay muchas páginas web por ahí que lo explican a la perfección. Es una técnica limpia, simple y que funciona en cualquier UNIX.

Como desventaja señalaría que Dovecot es un programa multiproceso y ha sido algo laborioso encontrar el punto de inyección exacto. Otra desventaja es que requiere parar el programa y lanzarlo con LD_PRELOAD, lo que provoca una pequeña pérdida de servicio y que el cambio afecte a todos los usuarios IMAP4. Además, cuando terminamos hay que parar el programa y quitarle el LD_PRELOAD, lo que supone otra pequeña interrupción.

Con este cambio pasamos de grabar 4 mensajes por segundo a grabar 22 mensajes por segundo. Migrar a mano empieza a ser sino deseable, al menos aceptable.

[2] A toro pasado habría que haber evaluado la conveniencia de mover mis buzones de correo a mis servidores con ZFS y SSD y realizar la migración allí. No se me ocurrió en ese momento.