Acelerar el cifrado de discos en Linux

El cifrado de datos en reposo es imprescindible para cualquier empresa moderna de Internet. Sin embargo, muchas empresas no cifran sus discos porque temen la posible penalización de rendimiento causada por la sobrecarga de cifrado.

El cifrado de datos en reposo es vital para Cloudflare, con más de 200 centros de datos en todo el mundo. En este post, investigaremos el rendimiento del cifrado de disco en Linux y explicaremos cómo lo hemos hecho al menos dos veces más rápido para nosotros y nuestros clientes.

Cifrar los datos en reposo

Cuando se trata de cifrar los datos en reposo hay varias formas de implementarlo en un sistema operativo (SO) moderno. Las técnicas disponibles están estrechamente acopladas a una pila de almacenamiento típica del SO. Una versión simplificada de la pila de almacenamiento y de las soluciones de cifrado puede encontrarse en el siguiente diagrama:

En la parte superior de la pila están las aplicaciones, que leen y escriben datos en archivos (o flujos). El sistema de archivos en el kernel del sistema operativo hace un seguimiento de qué bloques del dispositivo de bloque subyacente pertenecen a qué archivos y traduce estas lecturas y escrituras de archivos en lecturas y escrituras de bloques, sin embargo, los detalles del hardware del dispositivo de almacenamiento subyacente se abstraen del sistema de archivos. Finalmente, el subsistema de bloques pasa las lecturas y escrituras de bloques al hardware subyacente utilizando los controladores de dispositivo apropiados.

El concepto de la pila de almacenamiento es en realidad similar al conocido modelo OSI de red, donde cada capa tiene una visión de más alto nivel de la información y los detalles de implementación de las capas inferiores se abstraen de las capas superiores. Y, al igual que en el modelo OSI, se puede aplicar el cifrado en diferentes capas (piense en TLS frente a IPsec o una VPN).

Para los datos en reposo podemos aplicar el cifrado en las capas de bloques (ya sea en el hardware o en el software) o en el nivel de archivos (ya sea directamente en las aplicaciones o en el sistema de archivos).

Cifrado de bloques frente a cifrado de archivos

En general, cuanto más arriba en la pila apliquemos el cifrado, más flexibilidad tendremos. Con el cifrado a nivel de aplicación, los responsables de la aplicación pueden aplicar el código de cifrado que deseen a cualquier dato concreto que necesiten. La desventaja de este enfoque es que tienen que implementarlo ellos mismos y el cifrado en general no es muy fácil para los desarrolladores: uno tiene que conocer los entresijos de un algoritmo criptográfico específico, generar adecuadamente claves, nonces, IVs, etc. Además, el cifrado a nivel de aplicación no aprovecha la caché a nivel de sistema operativo y la caché de páginas de Linux en particular: cada vez que la aplicación necesita utilizar los datos, tiene que descifrarlos de nuevo, desperdiciando ciclos de CPU, o implementar su propia «caché» descifrada, lo que introduce más complejidad en el código.

El cifrado a nivel de sistema de archivos hace que el cifrado de datos sea transparente para las aplicaciones, porque el propio sistema de archivos cifra los datos antes de pasarlos al subsistema de bloques, por lo que los archivos se cifran independientemente de si la aplicación tiene soporte criptográfico o no. Además, los sistemas de archivos pueden configurarse para cifrar sólo un directorio concreto o tener diferentes claves para distintos archivos. Esta flexibilidad, sin embargo, tiene el coste de una configuración más compleja. El cifrado del sistema de archivos también se considera menos seguro que el de los dispositivos de bloques, ya que sólo se cifra el contenido de los archivos. Los archivos también tienen metadatos asociados, como el tamaño del archivo, el número de archivos, la disposición del árbol de directorios, etc., que siguen siendo visibles para un adversario potencial.

El cifrado a nivel de bloque (a menudo denominado cifrado de disco o cifrado de disco completo) también hace que el cifrado de datos sea transparente para las aplicaciones e incluso para los sistemas de archivos completos. A diferencia del cifrado a nivel de sistema de archivos, cifra todos los datos del disco, incluidos los metadatos de los archivos e incluso el espacio libre. Sin embargo, es menos flexible: sólo se puede encriptar todo el disco con una única clave, por lo que no existe una configuración por directorio, por archivo o por usuario. Desde el punto de vista criptográfico, no se pueden utilizar todos los algoritmos criptográficos, ya que la capa de bloques no tiene una visión general de alto nivel de los datos, por lo que necesita procesar cada bloque de forma independiente. La mayoría de los algoritmos comunes requieren algún tipo de encadenamiento de bloques para ser seguros, por lo que no son aplicables al cifrado de discos. En su lugar, se desarrollaron modos especiales sólo para este caso de uso específico.

Entonces, ¿qué capa elegir? Como siempre, depende… El cifrado a nivel de aplicación y de sistema de archivos suele ser la opción preferida para los sistemas cliente debido a su flexibilidad. Por ejemplo, cada usuario de un escritorio multiusuario puede querer cifrar su directorio personal con una clave que le pertenece y dejar sin cifrar algunos directorios compartidos. Por el contrario, en los sistemas de servidor, gestionados por empresas SaaS/PaaS/IaaS (incluida Cloudflare) la opción preferida es la simplicidad de configuración y la seguridad: con el cifrado de disco completo activado, cualquier dato de cualquier aplicación se cifra automáticamente sin excepciones ni anulaciones. Creemos que todos los datos deben estar protegidos sin clasificarlos en cubos «importantes» frente a «no importantes», por lo que la flexibilidad selectiva que proporcionan las capas superiores no es necesaria.

Encriptación de disco por hardware frente a software

Cuando se encriptan datos en la capa de bloques es posible hacerlo directamente en el hardware de almacenamiento, si el hardware lo soporta. Hacerlo así suele dar un mejor rendimiento de lectura/escritura y consume menos recursos del host. Sin embargo, dado que la mayoría del firmware del hardware es propietario, no recibe tanta atención y revisión por parte de la comunidad de seguridad. En el pasado, esto provocó fallos en algunas implementaciones de cifrado de disco por hardware, que hicieron que todo el modelo de seguridad fuera inútil. Microsoft, por ejemplo, empezó a preferir el cifrado de disco basado en software desde entonces.

No queríamos poner nuestros datos y los de nuestros clientes en riesgo de utilizar soluciones potencialmente inseguras y creemos firmemente en el código abierto. Por eso confiamos únicamente en el cifrado de discos por software en el núcleo de Linux, que es abierto y ha sido auditado por muchos profesionales de la seguridad en todo el mundo.

Rendimiento del cifrado de discos en Linux

Nuestro objetivo no es sólo ahorrar costes de ancho de banda a nuestros clientes, sino entregar el contenido a los usuarios de Internet lo más rápido posible.

En un momento dado nos dimos cuenta de que nuestros discos no eran tan rápidos como nos gustaría. Algunos perfiles, así como una rápida prueba A/B, apuntaron a la encriptación del disco de Linux. Debido a que no cifrar los datos (incluso si se supone que es una caché pública de Internet) no es una opción sostenible, decidimos echar un vistazo más de cerca en el rendimiento de cifrado de disco de Linux.

Device mapper y dm-crypt

Linux implementa el cifrado de disco transparente a través de un módulo dm-crypt y dm-crypt en sí mismo es parte del marco del kernel device mapper. En pocas palabras, el mapeador de dispositivos permite pre/post-procesar las solicitudes de IO mientras viajan entre el sistema de archivos y el dispositivo de bloque subyacente.

dm-cryptEn particular, cifra las solicitudes de IO de «escritura» antes de enviarlas más abajo en la pila al dispositivo de bloque real y descifra las solicitudes de IO de «lectura» antes de enviarlas al controlador del sistema de archivos. Simple y fácil. ¿O no lo es?

Configuración de pruebas

Para que conste, los números de este post se obtuvieron ejecutando los comandos especificados en un servidor Cloudflare G9 inactivo fuera de producción. Sin embargo, la configuración debería ser fácilmente reproducible en cualquier portátil x86 moderno.

En general, la evaluación comparativa de cualquier cosa en torno a una pila de almacenamiento es difícil debido al ruido introducido por el propio hardware de almacenamiento. No todos los discos son iguales, por lo que para el propósito de este post vamos a utilizar los discos más rápidos disponibles por ahí – es decir, sin discos.

En su lugar, Linux tiene una opción para emular un disco directamente en la memoria RAM. Dado que la RAM es mucho más rápida que cualquier almacenamiento persistente, debería introducir poco sesgo en nuestros resultados.

El siguiente comando crea un ramdisk de 4GB:

$ sudo modprobe brd rd_nr=1 rd_size=4194304$ ls /dev/ram0

Ahora podemos configurar una instancia dm-crypt sobre ella permitiendo así el cifrado del disco. En primer lugar, tenemos que generar la clave de cifrado del disco, «formatear» el disco y especificar una contraseña para desbloquear la clave recién generada.

$ fallocate -l 2M crypthdr.img$ sudo cryptsetup luksFormat /dev/ram0 --header crypthdr.imgWARNING!========This will overwrite data on crypthdr.img irrevocably.Are you sure? (Type uppercase yes): YESEnter passphrase:Verify passphrase:

Los que están familiarizados con LUKS/dm-crypt pueden haber notado que utilizamos una cabecera separada LUKS aquí. Normalmente, LUKS almacena la clave de cifrado del disco con contraseña en el mismo disco que los datos, pero como queremos comparar el rendimiento de lectura/escritura entre dispositivos cifrados y no cifrados, podríamos sobrescribir accidentalmente la clave cifrada durante nuestra evaluación comparativa posterior. Mantener la clave encriptada en un archivo separado evita este problema para los propósitos de este post.

Ahora, podemos realmente «desbloquear» el dispositivo encriptado para nuestras pruebas:

$ sudo cryptsetup open --header crypthdr.img /dev/ram0 encrypted-ram0Enter passphrase for /dev/ram0:$ ls /dev/mapper/encrypted-ram0/dev/mapper/encrypted-ram0

En este punto podemos ahora comparar el rendimiento del ramdisk encriptado frente al no encriptado: si leemos/escribimos datos en /dev/ram0, se almacenarán en texto plano. Del mismo modo, si leemos/escribimos datos en /dev/mapper/encrypted-ram0, serán descifrados/cifrados en el camino por dm-crypt y almacenados en texto cifrado.

Cabe destacar que no estamos creando ningún sistema de archivos en la parte superior de nuestros dispositivos de bloque para evitar sesgar los resultados con una sobrecarga del sistema de archivos.

Medir el rendimiento

Cuando se trata de pruebas de almacenamiento/benchmarking Flexible I/O tester es la solución habitual. Vamos a simular una simple carga de lectura/escritura secuencial con un tamaño de bloque de 4K en el ramdisk sin encriptación:

$ sudo fio --filename=/dev/ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=plainplain: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=21013MB, aggrb=1126.5MB/s, minb=1126.5MB/s, maxb=1126.5MB/s, mint=18655msec, maxt=18655msec WRITE: io=21023MB, aggrb=1126.1MB/s, minb=1126.1MB/s, maxb=1126.1MB/s, mint=18655msec, maxt=18655msecDisk stats (read/write): ram0: ios=0/0, merge=0/0, ticks=0/0, in_queue=0, util=0.00%

El comando anterior se ejecutará durante mucho tiempo, así que simplemente lo detenemos después de un tiempo. Como podemos ver en las estadísticas, somos capaces de leer y escribir aproximadamente con el mismo rendimiento alrededor de 1126 MB/s. Repitamos la prueba con el ramdisk encriptado:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=1693.7MB, aggrb=150874KB/s, minb=150874KB/s, maxb=150874KB/s, mint=11491msec, maxt=11491msec WRITE: io=1696.4MB, aggrb=151170KB/s, minb=151170KB/s, maxb=151170KB/s, mint=11491msec, maxt=11491msec

¡Qué caída! Ahora sólo obtenemos ~147 MB/s, lo que es más de 7 veces más lento. Y esto es en una máquina totalmente inactiva!

Tal vez, crypto es simplemente lento

Lo primero que consideramos es asegurarnos de usar el crypto más rápido. cryptsetup nos permite comparar todas las implementaciones de cripto disponibles en el sistema para seleccionar la mejor:

$ sudo cryptsetup benchmark# Tests are approximate using memory only (no storage IO).PBKDF2-sha1 1340890 iterations per second for 256-bit keyPBKDF2-sha256 1539759 iterations per second for 256-bit keyPBKDF2-sha512 1205259 iterations per second for 256-bit keyPBKDF2-ripemd160 967321 iterations per second for 256-bit keyPBKDF2-whirlpool 720175 iterations per second for 256-bit key# Algorithm | Key | Encryption | Decryption aes-cbc 128b 969.7 MiB/s 3110.0 MiB/s serpent-cbc 128b N/A N/A twofish-cbc 128b N/A N/A aes-cbc 256b 756.1 MiB/s 2474.7 MiB/s serpent-cbc 256b N/A N/A twofish-cbc 256b N/A N/A aes-xts 256b 1823.1 MiB/s 1900.3 MiB/s serpent-xts 256b N/A N/A twofish-xts 256b N/A N/A aes-xts 512b 1724.4 MiB/s 1765.8 MiB/s serpent-xts 512b N/A N/A twofish-xts 512b N/A N/A

Parece que aes-xts con una clave de cifrado de datos de 256 bits es la más rápida aquí. Pero, ¿cuál estamos usando realmente para nuestro ramdisk encriptado?

$ sudo dmsetup table /dev/mapper/encrypted-ram00 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0

Usamos aes-xts con una clave de encriptación de datos de 256 bits (cuenta todos los ceros convenientemente enmascarados por la herramienta dmsetup – si quieres ver los bytes reales, añade la opción --showkeys al comando anterior). Sin embargo, los números no suman: cryptsetup benchmark nos dice arriba que no nos fiemos de los resultados, ya que «Las pruebas son aproximadas utilizando sólo la memoria (sin IO de almacenamiento)», pero así es exactamente como hemos configurado nuestro experimento utilizando el ramdisk. En un caso un poco peor (asumiendo que estamos leyendo todos los datos y luego encriptando/desencriptando secuencialmente sin paralelismo) haciendo un cálculo de vuelta de la hoja deberíamos estar obteniendo alrededor de (1126 * 1823) / (1126 + 1823) =~696 MB/s, que todavía está bastante lejos del real 147 * 2 = 294 MB/s (total para lecturas y escrituras).

Banderas de rendimiento de dm-crypt

Mientras leíamos la página man de cryptsetup nos dimos cuenta de que tiene dos opciones prefijadas con --perf-, que probablemente están relacionadas con el ajuste de rendimiento. La primera es --perf-same_cpu_crypt con una descripción bastante críptica:

Perform encryption using the same cpu that IO was submitted on. The default is to use an unbound workqueue so that encryption work is automatically balanced between available CPUs. This option is only relevant for open action.

Así que habilitamos la opción

$ sudo cryptsetup close encrypted-ram0$ sudo cryptsetup open --header crypthdr.img --perf-same_cpu_crypt /dev/ram0 encrypted-ram0

Nota: según la última página de manual también hay un comando cryptsetup refresh, que puede usarse para habilitar estas opciones en vivo sin tener que «cerrar» y «reabrir» el dispositivo encriptado. Nuestro cryptsetup sin embargo no lo soportaba todavía.

Verificando si la opción ha sido realmente habilitada:

$ sudo dmsetup table encrypted-ram00 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0 1 same_cpu_crypt

Sí, ahora podemos ver same_cpu_crypt en la salida, que es lo que queríamos. Volvamos a ejecutar el benchmark:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=1596.6MB, aggrb=139811KB/s, minb=139811KB/s, maxb=139811KB/s, mint=11693msec, maxt=11693msec WRITE: io=1600.9MB, aggrb=140192KB/s, minb=140192KB/s, maxb=140192KB/s, mint=11693msec, maxt=11693msec

Hmm, ahora es ~136 MB/s que es ligeramente peor que antes, así que no sirve. ¿Qué pasa con la segunda opción --perf-submit_from_crypt_cpus:

Disable offloading writes to a separate thread after encryption. There are some situations where offloading write bios from the encryption threads to a single thread degrades performance significantly. The default is to offload write bios to the same thread. This option is only relevant for open action.

Tal vez, estamos en la «alguna situación» aquí, así que vamos a probarlo:

$ sudo cryptsetup close encrypted-ram0$ sudo cryptsetup open --header crypthdr.img --perf-submit_from_crypt_cpus /dev/ram0 encrypted-ram0Enter passphrase for /dev/ram0:$ sudo dmsetup table encrypted-ram00 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0 1 submit_from_crypt_cpus

Y ahora el punto de referencia:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=2066.6MB, aggrb=169835KB/s, minb=169835KB/s, maxb=169835KB/s, mint=12457msec, maxt=12457msec WRITE: io=2067.7MB, aggrb=169965KB/s, minb=169965KB/s, maxb=169965KB/s, mint=12457msec, maxt=12457msec

~166 MB/s, que es un poco mejor, pero todavía no es bueno …

Preguntando a la comunidad

Estando desesperados decidimos buscar apoyo en Internet y publicamos nuestros hallazgos en la lista de correo dm-crypt, pero la respuesta que obtuvimos no fue muy alentadora:

Si los números te molestan, entonces es por falta de comprensión por tu parte. Probablemente no seas consciente de que el cifrado es una operación muy pesada…

Decidimos hacer una investigación científica sobre este tema escribiendo «¿es caro el cifrado?» en la búsqueda de Google y uno de los primeros resultados, que realmente contiene mediciones significativas, es… nuestro propio post sobre el coste del cifrado, ¡pero en el contexto de TLS! Es una lectura fascinante por sí misma, pero lo esencial es que el cifrado moderno en el hardware moderno es muy barato incluso a escala de Cloudflare (haciendo millones de peticiones HTTP cifradas por segundo). De hecho, es tan barato que Cloudflare fue el primer proveedor en ofrecer SSL/TLS gratis para todo el mundo.

Hurgando en el código fuente

Cuando intentamos utilizar las opciones personalizadas de dm-crypt descritas anteriormente tuvimos curiosidad por saber por qué existen en primer lugar y de qué se trata esa «descarga». Originalmente esperábamos que dm-crypt fuera un simple «proxy», que sólo encripta/desencripta los datos a medida que fluyen a través de la pila. Resulta que dm-crypt hace más que encriptar los búferes de memoria y un diagrama (simplificado) de la ruta transversal de IO se presenta a continuación:

Cuando el sistema de archivos emite una solicitud de escritura, dm-crypt no la procesa inmediatamente – en su lugar la pone en una cola de trabajo llamada «kcryptd». En pocas palabras, una cola de trabajo del kernel simplemente programa algún trabajo (cifrado en este caso) para ser realizado en algún momento posterior, cuando sea más conveniente. Cuando llega «el momento», dm-crypt envía la solicitud a la API Crypto de Linux para el cifrado real. Sin embargo, la moderna Crypto API de Linux también es asíncrona, por lo que dependiendo de la implementación particular que utilice tu sistema, lo más probable es que no se procese inmediatamente, sino que se ponga en cola para «más adelante». Cuando la API Crypto de Linux finalmente haga el cifrado, dm-crypt puede intentar ordenar las solicitudes de escritura pendientes poniendo cada solicitud en un árbol rojo-negro. A continuación, un hilo del kernel separado de nuevo en «algún momento más tarde» realmente toma todas las solicitudes de IO en el árbol y los envía hacia abajo de la pila.

Ahora para las solicitudes de lectura: esta vez tenemos que obtener los datos cifrados en primer lugar desde el hardware, pero dm-crypt no sólo pide el controlador de los datos, pero las colas de la solicitud en una cola de trabajo diferente llamado «kcryptd_io». En algún momento posterior, cuando tengamos realmente los datos encriptados, los programamos para su descifrado utilizando la ya conocida cola de trabajo «kcryptd». «kcryptd» enviará la solicitud a Linux Crypto API, que puede descifrar los datos de forma asíncrona también.

Para ser justos, la solicitud no siempre atraviesa todas estas colas, pero la parte importante aquí es que las solicitudes de escritura pueden estar en cola hasta 4 veces en dm-crypt y las solicitudes de lectura hasta 3 veces. En este punto nos preguntamos si toda esta cola adicional puede causar algún problema de rendimiento. Por ejemplo, hay una buena presentación de Google sobre la relación entre las colas y la latencia de cola. Una de las claves de la presentación es:

Una cantidad significativa de latencia de cola se debe a los efectos de las colas

Entonces, ¿por qué están todas estas colas ahí y podemos eliminarlas?

Arqueología de Git

Nadie escribe código más complejo sólo por diversión, especialmente para el núcleo del sistema operativo. Así que todas estas colas deben haber sido puestas ahí por una razón. Por suerte, el código fuente del kernel de Linux está gestionado por git, así que podemos intentar rastrear los cambios y las decisiones en torno a ellos.

La cola de trabajo «kcryptd» estaba en el código fuente desde el principio de la historia disponible con el siguiente comentario:

Se necesita porque sería muy imprudente hacer el descifrado en un contexto de interrupción, así que las bios que vuelven de las peticiones de lectura se ponen en cola aquí.

Así que era sólo para lecturas, pero incluso entonces – ¿por qué nos importa si es contexto de interrupción o no, si la API Crypto de Linux probablemente utilizará un hilo/cola dedicado para el cifrado de todos modos? Bueno, en 2005 la API Crypto no era asíncrona, así que esto tenía mucho sentido.

En 2006 dm-crypt comenzó a utilizar la cola de trabajo «kcryptd» no sólo para el cifrado, sino para enviar solicitudes de IO:

Este parche está diseñado para ayudar a dm-crypt a cumplir con las nuevas restricciones impuestas por el siguiente parche en -mm: md-dm-reduce-stack-usage-with-stacked-block-devices.patch

Parece que el objetivo aquí no era añadir más concurrencia, sino reducir el uso de la pila del kernel, lo que tiene sentido de nuevo ya que el kernel tiene una pila común en todo el código, por lo que es un recurso bastante limitado. Vale la pena señalar, sin embargo, que la pila del kernel de Linux se ha ampliado en 2014 para las plataformas x86, por lo que esto podría dejar de ser un problema.

Una primera versión de la cola de trabajo «kcryptd_io» fue añadida en 2007 con la intención de evitar:

la inanición causada por muchas peticiones en espera de la asignación de memoria…

El procesamiento de peticiones se estaba embotellando en una sola cola de trabajo aquí, por lo que la solución fue añadir otra. Tiene sentido.

Definitivamente no somos los primeros que experimentan una degradación del rendimiento debido a la extensa cola de trabajo: en 2011 se introdujo un cambio para revertir condicionalmente algunas de las colas de trabajo para las solicitudes de lectura:

Si hay suficiente memoria, el código puede enviar directamente bio en lugar de poner en cola esta operación en un hilo separado.

Desgraciadamente, en ese momento los mensajes de commit del kernel de Linux no eran tan verboso como hoy en día, por lo que no hay datos de rendimiento disponibles.

En 2015 dm-crypt comenzó a ordenar las escrituras en un hilo separado «dmcrypt_write» antes de enviarlas a la pila:

En una máquina multiprocesadora, las solicitudes de cifrado terminan en un orden diferente al que fueron enviadas. En consecuencia, las peticiones de escritura se enviarían en un orden diferente y podría causar una severa degradación del rendimiento.

Tiene sentido ya que el acceso secuencial al disco solía ser mucho más rápido que el aleatorio y dm-crypt estaba rompiendo el patrón. Pero esto se aplica sobre todo a los discos giratorios, que todavía eran dominantes en 2015. Puede que no sea tan importante con las modernas y rápidas SSDs (incluyendo las SSDs NVME).

Otra parte del mensaje de commit es digna de mención:

…en particular, permite a los programadores de IO como CFQ clasificar de forma más efectiva…

Menciona los beneficios de rendimiento para el programador de IO CFQ, pero los programadores de Linux han mejorado desde entonces hasta el punto de que el programador CFQ ha sido eliminado del kernel en 2018.

El mismo conjunto de parches reemplaza la lista de ordenación con un árbol rojo-negro:

En teoría la ordenación debería ser realizada por el programador de disco subyacente, sin embargo, en la práctica el programador de disco sólo acepta y ordena un número finito de peticiones. Para permitir la ordenación de todas las peticiones, dm-crypt necesita implementar su propia ordenación.

La sobrecarga asociada a la ordenación basada en rbtree se considera insignificante, por lo que no se utiliza de forma condicional.

Todo eso tiene sentido, pero estaría bien tener algunos datos de respaldo.

Interesantemente, en el mismo conjunto de parches vemos la introducción de nuestra familiar opción «submit_from_crypt_cpus»:

Hay algunas situaciones en las que la descarga de bios de escritura de los hilos de encriptación a un solo hilo degrada el rendimiento significativamente

En general, podemos ver que cada cambio era razonable y necesario, sin embargo las cosas han cambiado desde entonces:

  • el hardware se ha vuelto más rápido e inteligente
  • Se ha revisado la asignación de recursos de Linux
  • se han rearticulado los subsistemas Linux acoplados

Y muchas de las opciones de diseño anteriores pueden no ser aplicables al Linux moderno.

La «limpieza»

Basado en la investigación anterior decidimos intentar eliminar todas las colas adicionales y el comportamiento asíncrono y revertir dm-crypta su propósito original: simplemente encriptar/desencriptar las peticiones IO a medida que pasan. Pero en aras de la estabilidad y la evaluación comparativa, terminamos no eliminando el código real, sino añadiendo otra opción dm-crypt, que omite todas las colas/hilos, si está activada. La bandera nos permite cambiar entre el comportamiento actual y el nuevo en tiempo de ejecución bajo plena carga de producción, por lo que podemos revertir fácilmente nuestros cambios si vemos cualquier efecto secundario. El parche resultante se puede encontrar en el repositorio de Cloudflare GitHub Linux.

Synchronous Linux Crypto API

Del diagrama anterior recordamos que no todas las colas están implementadas en dm-crypt. La moderna Crypto API de Linux también puede ser asíncrona y, por el bien de este experimento, queremos eliminar las colas allí también. Sin embargo, ¿qué significa «puede ser»? El sistema operativo puede contener diferentes implementaciones del mismo algoritmo (por ejemplo, AES-NI acelerado por hardware en plataformas x86 e implementaciones genéricas de AES en código C). Por defecto, el sistema elige el «mejor» basándose en la prioridad del algoritmo configurado. dm-crypt permite anular este comportamiento y solicitar una implementación de cifrado concreta utilizando el prefijo capi:. Sin embargo, hay un problema. Comprobemos las implementaciones disponibles de AES-XTS (este es nuestro cifrado de disco, ¿recuerdas?) en nuestro sistema:

$ grep -A 11 'xts(aes)' /proc/cryptoname : xts(aes)driver : xts(ecb(aes-generic))module : kernelpriority : 100refcnt : 7selftest : passedinternal : notype : skcipherasync : noblocksize : 16min keysize : 32max keysize : 64--name : __xts(aes)driver : cryptd(__xts-aes-aesni)module : cryptdpriority : 451refcnt : 1selftest : passedinternal : yestype : skcipherasync : yesblocksize : 16min keysize : 32max keysize : 64--name : xts(aes)driver : xts-aes-aesnimodule : aesni_intelpriority : 401refcnt : 1selftest : passedinternal : notype : skcipherasync : yesblocksize : 16min keysize : 32max keysize : 64--name : __xts(aes)driver : __xts-aes-aesnimodule : aesni_intelpriority : 401refcnt : 7selftest : passedinternal : yestype : skcipherasync : noblocksize : 16min keysize : 32max keysize : 64

Queremos seleccionar explícitamente un cifrado síncrono de la lista anterior para evitar efectos de cola en los hilos, pero los únicos dos soportados son xts(ecb(aes-generic)) (la implementación genérica en C) y __xts-aes-aesni (la implementación acelerada por hardware x86). Definitivamente queremos esta última ya que es mucho más rápida (aquí buscamos rendimiento), pero está sospechosamente marcada como interna (ver internal: yes). Si revisamos el código fuente:

Marca un cifrado como una implementación de servicio sólo utilizable por otro cifrado y nunca por un usuario normal de la API Crypto del núcleo

Así que este cifrado está destinado a ser utilizado sólo por otro código envolvente en la API Crypto y no fuera de ella. En la práctica esto significa, que quien llama a la Crypto API necesita especificar explícitamente esta bandera, cuando solicita una implementación de cifrado particular, pero dm-crypt no lo hace, porque por diseño no es parte de la Crypto API de Linux, sino un usuario «externo». Ya hemos parcheado el módulo dm-crypt, así que también podríamos añadir la bandera correspondiente. Sin embargo, hay otro problema con AES-NI en particular: la FPU x86. ¿Dices «punto flotante»? ¿Por qué necesitamos matemáticas de punto flotante para hacer un cifrado simétrico que sólo debería consistir en desplazamientos de bits y operaciones XOR? No necesitamos las matemáticas, pero las instrucciones AES-NI utilizan algunos de los registros de la CPU, que están dedicados a la FPU. Desafortunadamente el kernel de Linux no siempre preserva estos registros en contexto de interrupción por razones de rendimiento (guardar/restaurar la FPU es caro). Pero dm-cryptpuede ejecutar código en contexto de interrupción, por lo que corremos el riesgo de corromper algún otro dato del proceso y volvemos a la afirmación «sería muy imprudente hacer el descifrado en un contexto de interrupción» del código original.

Nuestra solución para abordar lo anterior fue crear otro módulo de la API Crypto algo «inteligente». Este módulo es síncrono y no rueda su propio crypto, sino que es sólo un «router» de peticiones de encriptación:

  • si podemos usar la FPU (y por tanto AES-NI) en el contexto de ejecución actual, simplemente reenviamos la petición de cifrado a la implementación «interna» más rápida __xts-aes-aesni (y podemos usarla aquí, porque ahora somos parte de la API Crypto)
  • de lo contrario, sólo reenviamos la solicitud de cifrado a la más lenta, genérica basada en C xts(ecb(aes-generic)) implementación

Usando todo el lote

Vamos a caminar a través del proceso de usar todo junto. El primer paso es tomar los parches y recompilar el kernel (o simplemente compilar dm-crypt y nuestros módulos xtsproxy).

A continuación, vamos a reiniciar nuestra carga de trabajo IO en un terminal separado, para asegurarnos de que podemos reconfigurar el kernel en tiempo de ejecución bajo carga:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...

En el terminal principal asegúrate de que nuestro nuevo módulo Crypto API está cargado y disponible:

$ sudo modprobe xtsproxy$ grep -A 11 'xtsproxy' /proc/cryptodriver : xts-aes-xtsproxymodule : xtsproxypriority : 0refcnt : 0selftest : passedinternal : notype : skcipherasync : noblocksize : 16min keysize : 32max keysize : 64ivsize : 16chunksize : 16

Reconfiguremos el disco encriptado para que use nuestro módulo recién cargado y habilitemos nuestra bandera dm-crypt parcheada (tenemos que usar la herramienta de bajo nivel dmsetup ya que cryptsetup obviamente no está al tanto de nuestras modificaciones):

$ sudo dmsetup table encrypted-ram0 --showkeys | sed 's/aes-xts-plain64/capi:xts-aes-xtsproxy-plain64/' | sed 's/$/ 1 force_inline/' | sudo dmsetup reload encrypted-ram0

Acabamos de «cargar» la nueva configuración, pero para que tenga efecto, necesitamos suspender/reanudar el dispositivo encriptado:

$ sudo dmsetup suspend encrypted-ram0 && sudo dmsetup resume encrypted-ram0

Y ahora observe el resultado. Podemos volver al otro terminal que ejecuta el trabajo fio y mirar la salida, pero para hacer las cosas más agradables, aquí hay una instantánea del rendimiento de lectura/escritura observado en Grafana:


¡Vaya, hemos duplicado el rendimiento! Con el rendimiento total de ~640 MB/s ahora estamos mucho más cerca del esperado ~696 MB/s de arriba. ¿Qué pasa con la latencia IO? (La estadística await de la herramienta de informes iostat):

¡La latencia también se ha reducido a la mitad!

A producción

Hasta ahora hemos estado utilizando una configuración sintética con algunas partes de la pila de producción completa que faltan, como los sistemas de archivos, el hardware real y lo más importante, la carga de trabajo de producción. Para asegurarnos de que no estamos optimizando cosas imaginarias, aquí tenemos una instantánea del impacto en producción que estos cambios tienen en la parte de la caché de nuestra pila:

Este gráfico representa una comparación en tres direcciones de los peores tiempos de respuesta (percentil 99) para un golpe de caché en uno de nuestros servidores. La línea verde es de un servidor con discos no encriptados, que usaremos como línea de base. La línea roja corresponde a un servidor con discos cifrados con la implementación de cifrado de discos de Linux por defecto y la línea azul corresponde a un servidor con discos cifrados y nuestras optimizaciones activadas. Como podemos ver, la implementación de cifrado de disco de Linux por defecto tiene un impacto significativo en nuestra latencia de caché en los peores escenarios, mientras que la implementación parcheada es indistinguible de no usar el cifrado en absoluto. En otras palabras, la implementación de encriptación mejorada no tiene ningún impacto en la velocidad de respuesta de nuestra caché, por lo que básicamente nos sale gratis. Eso es una victoria!

Sólo estamos empezando

Este post muestra cómo una revisión de la arquitectura puede duplicar el rendimiento de un sistema. También reconfirmamos que la criptografía moderna no es cara y que no suele haber excusa para no proteger tus datos.

Vamos a presentar este trabajo para su inclusión en el árbol de fuentes del núcleo principal, pero lo más probable es que no en su forma actual. Aunque los resultados parecen alentadores, tenemos que recordar que Linux es un sistema operativo altamente portátil: se ejecuta en servidores potentes, así como en pequeños dispositivos IoT con recursos limitados y en muchas otras arquitecturas de CPU también. La versión actual de los parches sólo optimiza el cifrado del disco para una carga de trabajo concreta en una arquitectura determinada, pero Linux necesita una solución que se ejecute sin problemas en todas partes.

Dicho esto, si crees que tu caso es similar y quieres aprovechar las mejoras de rendimiento ahora, puedes hacerte con los parches y, con suerte, dar tu opinión. La bandera de tiempo de ejecución hace que sea fácil de cambiar la funcionalidad sobre la marcha y una simple prueba A / B se puede realizar para ver si beneficia a cualquier caso o configuración particular. Estos parches se han ejecutado en nuestra amplia red de más de 200 centros de datos en cinco generaciones de hardware, por lo que pueden considerarse razonablemente estables. Disfruta del rendimiento y la seguridad de Cloudflare para todos!

Actualización (11 de octubre de 2020)

El parche principal de este blog (en una forma ligeramente actualizada) se ha fusionado con el kernel de Linux de línea principal y está disponible desde la versión 5.9 en adelante. La principal diferencia es que la versión de línea principal expone dos banderas en lugar de una, que proporcionan la capacidad de eludir las colas de trabajo de dm-crypt para lecturas y escrituras de forma independiente. Para más detalles, consulte la documentación oficial de dm-crypt.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.