Notas sobre el rendimiento de disco de un servidor
Medir el rendimiento de los discos, y de cualquier otra cosa, mola. Sobre todo mola si puedes decir que este dispositivo es más rápido que el otro o si sirve para valorar cómo afecta al rendimiento cierta configuración.
Afortunadamente resulta muy sencillo utilizar una herramienta como fio
para torturar nuestros discos y obtener unas medidas. Pero lamentablemente resulta muchísimo más complejo entender qué está pasando, qué limita el rendimiento y de qué manera se puede preparar un test que modele con más o menos acierto la principal carga de trabajo de la máquina.
En este artículo solo pretendo mostrar los resultados obtenidos por un servidor dedicado fijándonos en el rendimiento con la peor carga de trabajo posible.
Conceptos básicos
Todos sabemos que los discos son dispositivos de bloques en los que se crean sistemas de ficheros con los que trabajan las aplicaciones. Además del tipo de disco duro (si es mecánico o de estado sólido) va a condicionar el rendimiento la interfaz utilizada para la conexión (SATA, SAS, NVMe), la controladora utilizada, la velocidad de la RAM y por supuesto la CPU del equipo.
¿Y qué decir de la carga de trabajo? como mínimo que la infinita variedad de cargas de trabajo se puede catalogar en secuencial y aleatoria, siendo la secuencial mucho menos exigente y la aleatoria mucho más puñetera, sobre todo en los pobres discos duros mecánicos. Y en lecturas y escrituras, siendo las lecturas menos costosas y además mucho más fáciles de eludir con una cache.
Las escrituras siempre son más complejas e interesantes. ¿Cuándo se deben dar por completadas? ¿al llegar al dispositivo o cuando realmente se han escrito los datos en el dispositivo?. Si existen varios dispositivos con réplicas de los datos las lecturas se pueden obtener desde cualquiera de ellos aumentando el rendimiento pero las escrituras deben realizarse en cada dispositivo.
¿Y en qué métricas nos podemos fijar? Por ejemplo:
- Throughput: cantidad de información transferida por unidad de tiempo.
- Latencia: tiempo transcurrido para completar la operación.
- IOPS: número de operaciones por segundo completadas.
Quiero medir ya. Pásame el comando a ejecutar.
De acuerdo, en este artículo el test elegido va a ser:
fio --name=random-write --ioengine=posixaio --rw=randwrite --bs=64k --size=256m --numjobs=16 --iodepth=16 --runtime=60 --time_based --fsync=1 --end_fsync=1 --group_reporting
De esta manera fio
únicamente generará escrituras aleatorias síncronas que suponen la carga más costosa para el dispositivo de almacenamiento. Normalmente ninguna aplicación se dedica únicamente a generar escrituras aleatorias síncronas, así que en cualquier caso real se obtendrá una velocidad superior. Valoraremos la velocidad de escritura mínima con la que contamos en el peor escenario.
Los parámetros utilizados son:
--name=random-write
fio
creará unos ficheros para realizar los tests. En este caso los ficheros tendrán por nombre 'random-write'. Una vez acabado el test se pueden borrar.
--ioengine=posixaio
Los sistemas operativos soportan diferentes maneras de comunicarse con los dispositivos. Esta es común a todos los sistemas POSIX, pero en GNU/Linux como alternativa se podría utilizar libaio. Si el test se ejecuta en windows deberá utilizarse windowsaio.
--rw=randwrite
Se pueden hacer test de lectura, de escritura, con operaciones secuenciales y aleatorias. Incluso se pueden combinar los distintos tipos de operaciones pero con diferencia el peor de los casos son las escrituras aleatorias.
--bs=64k
No es lo mismo realizar 1000 operaciones de escritura de 4KiB que 1000 operaciones de escritura de 64KiB, cada aplicación puede realizar operaciones de un tamaño diferente pero en nuestro caso se utilízan operaciones de 64KiB para modelar los accesos que realiza KVM a los ficheros .qcow2 con el valor cluster_size
por defecto.
--size=256m
El tamaño para los ficheros utilizados durante el test. Como se lanzarán 16 procesos en paralelo todo el test consumirá 4GiB.
--numjobs=16
Número de procesos concurrentes generando operaciones IO, cada uno utilizará su propio fichero.
--iodepth=16
Número de operaciones IO que se admitirán en la cola del sistema operativo.
--runtime=60 --time_based
Estas opciones especifican que el test debe durar 1 minuto. Si el trabajo se acaba antes se volverá a lanzar hasta que acumule el tiempo especificado.
--fsync=1
Para que cada escritura sea síncrona.
--end_fsync=1
Especifica que el test no se completará hasta que no se hayan escrito los datos en disco. Si no se utiliza este parámetro, ni ninguna otra opción para hacer escrituras síncronas, el test puede acabar y los discos pueden seguir trabajando en segundo plano mucho tiempo mientras se completan las operaciones que han quedado en el buffer de escritura.
--group_reporting
Cuando se utilizan varios trabajos vale la pena obtener un informe de grupo para no abrumarse con los detalles de cada trabajo.
Los resultados
Durante la ejecución del test fio
muestra resultados preliminares y al acabar muestra los resultados definitivos. En el portátil en el que trabajo el disco duro es un SSD M.2 Micron MTFDDAV256TBN-1A de 256GB. La hoja de especificaciones del dispositivo indica que con escrituras aleatorias síncronas de 4KB alcanza 83000 IOPS. Pero en el test se obtienen 4500 IOPS utilizando escrituras de 4K y un valor menor utilizando las de 64KB.
Hay mucha información relevante pero aquí destaco los valores básicos:
vcarceler@luna:~/prueba$ time fio --name=random-write --ioengine=posixaio --rw=randwrite --bs=64k --size=256m --numjobs=16 --iodepth=16 --runtime=60 --time_based --fsync=1 --end_fsync=1 --group_reporting random-write: (g=0): rw=randwrite, bs=(R) 64.0KiB-64.0KiB, (W) 64.0KiB-64.0KiB, (T) 64.0KiB-64.0KiB, ioengine=posixaio, iodepth=16 ... fio-3.1 Starting 16 processes random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) random-write: Laying out IO file (1 file / 256MiB) Jobs: 16 (f=16): [w(16)][100.0%][r=0KiB/s,w=90.9MiB/s][r=0,w=1454 IOPS][eta 00m:00s] random-write: (groupid=0, jobs=16): err= 0: pid=3774: Sun Apr 26 20:06:02 2020 write: IOPS=2102, BW=131MiB/s (138MB/s)(7898MiB/60093msec) slat (nsec): min=1433, max=9499.4k, avg=12817.71, stdev=102966.66 clat (usec): min=5, max=254436, avg=25938.93, stdev=24204.42 lat (usec): min=132, max=254445, avg=25951.75, stdev=24203.31 clat percentiles (usec): | 1.00th=[ 1090], 5.00th=[ 2008], 10.00th=[ 3392], 20.00th=[ 6652], | 30.00th=[ 10028], 40.00th=[ 13960], 50.00th=[ 18220], 60.00th=[ 23200], | 70.00th=[ 31065], 80.00th=[ 42206], 90.00th=[ 60031], 95.00th=[ 81265], | 99.00th=[100140], 99.50th=[106431], 99.90th=[139461], 99.95th=[166724], | 99.99th=[229639] bw ( KiB/s): min= 2056, max=21767, per=6.32%, avg=8504.99, stdev=5197.98, samples=1920 iops : min= 32, max= 340, avg=132.48, stdev=81.23, samples=1920 lat (usec) : 10=0.01%, 100=0.01%, 250=0.01%, 500=0.08%, 750=0.32% lat (usec) : 1000=0.41% lat (msec) : 2=4.15%, 4=7.07%, 10=17.89%, 20=23.86%, 50=31.85% lat (msec) : 100=13.40%, 250=0.96%, 500=0.01% cpu : usr=0.38%, sys=0.21%, ctx=141178, majf=0, minf=660 IO depths : 1=0.4%, 2=1.0%, 4=3.5%, 8=106.1%, 16=91.3%, 32=0.0%, >=64=0.0% submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0% complete : 0=0.0%, 4=95.6%, 8=2.3%, 16=2.1%, 32=0.0%, 64=0.0%, >=64=0.0% issued rwt: total=0,126368,0, short=0,0,0, dropped=0,0,0 latency : target=0, window=0, percentile=100.00%, depth=16 Run status group 0 (all jobs): WRITE: bw=131MiB/s (138MB/s), 131MiB/s-131MiB/s (138MB/s-138MB/s), io=7898MiB (8282MB), run=60093-60093msec Disk stats (read/write): sda: ios=0/250246, merge=0/52179, ticks=0/826042, in_queue=410196, util=84.62% real 1m0,644s user 0m4,963s sys 0m29,715s vcarceler@luna:~/prueba$
Los datos más relevantes son:
- Velocidad de escritura (throughput): 131MiB/s o, lo que es lo mismo, 138MB/s.
- IOPS: 2102
Los competidores:
Todo esto viene al caso de la valoración de un servidor dedicado para alojar algunos servicios web del centro y aumentar así su disponibilidad ante cualquier avería que no se pueda arreglar en el servidor del centro.
El servidor dedicado de OVH es:
- RISE-1
- Intel Xeon E3-1230v6 (con HT 8 núcleos)
- RAM: 32GB DDR4 ECC 2133MHz
- 2 x HDD SATA 2TB Enterprise Class (HGST HUS724020AL)
- 2 x SSD NVMe 450GB Intel Datacenter Class
En cada uno de los dos discos HDD hay una partición de 99GiB dedicado a un RAID 1 para el sistema operativo, el resto del espacio está libre y se utilizará para crear un pool ZFS en el que guardar backups o archivos grandes. Los discos NVMe se van a dedicar a un pool ZFS en el que se dará soporte a /var/lib/libvirt
para que allí residan las máquinas virtuales con los servicios web.
Pero puestos a obtener resultados se probará el rendimiento de:
- Un disco NVMe con un sistema de archivos EXT4.
- Un pool ZFS con un disco NVMe.
- Un pool ZFS con dos discos NVMe sin redundancia.
- Un pool ZFS con dos discos NVMe en un mirror.
- Una partición en un disco HDD con una partición EXT4.
- Un pool ZFS con una partición de un disco HDD.
- Un pool ZFS con dos particiones HDD.
- Un pool ZFS con las dos particiones HDD en un mirror.
- Un disco SSD en el portátil de trabajo con EXT4.
- El pool ZFS utilizado en Edoras (el servidor del instituto).
En la creación de los sistemas de ficheros EXT4 se han añadido los parámetros que desactivan la inicialización tardía del sistema de archivos para evitar que el test se realice mientras continúa la inicialización en segundo plano. De esta manera la inicialización se completa durante la creación del sistema de archivos.
mkfs.ext4 -E lazy_itable_init=0,lazy_journal_init=0 <dispositivo>
En la definición del pool ZFS se utilizarán las opciones habituales activando la compresión LZ4, la propiedad recordsize=64k para que coincida con el valor cluster_size de QCOW2 y el resto de opciones habituales.
Ejemplo de creación del pool ZFS:
zpool create -f -o ashift=12 -O recordsize=64k -O acltype=posixacl -O compression=lz4 -O dnodesize=auto -O normalization=formD -O relatime=on -O xattr=sa -m none <pool> <VDEVS...>
Y los resultados obtenidos son:
Configuración | Throughput | IOPS |
---|---|---|
EXT4. Partición NVMe. | 619MiB/s (649MB/s) | 9905 |
ZFS. Disco NVMe. | 825MiB/s (865MB/s) | 13.2k |
ZFS. 2 discos NVMe. | 1282MiB/s (1345MB/s) | 20.5k |
ZFS. Mirror con discos NVMe. | 787MiB/s (825MB/s) | 12.6k |
EXT4. Partición HDD. | 16.9MiB/s (17.8MB/s) | 271 |
ZFS. Partición HDD. | 64.8MiB/s (67.9MB/s) | 1036 |
ZFS. 2 particiones HDD. | 94.8MiB/s (99.4MB/s) | 1516 |
ZFS. Mirror dos particiones HDD. | 59.5MiB/s (62.4MB/s) | 951 |
Portátil. SSD EXT4. | 131MiB/s (138MB/s) | 2102 |
Edoras. ZFS mirror SSD. | 242MiB/s (253MB/s) | 3866 |
¿Y qué decir de los resultados? Pues lo normal que se podía decir sin hacer ningún test.
- Que los discos se pueden ordenar de más rápido a más lento así: NVMe, SSD y HDD.
- Que combinar varios dispositivos en un pool ZFS sin rendundancia —el equivalente a RAID 0— aumenta la capacidad de escritura con el número de dispositivos miembros.
- Que un mirror tiene la capacidad aproximada del más lento de sus componentes.
- Que entre los discos de estado sólido también hay claras categorías: NVMe Intel para datacenter, SSD Samsung 860 EVO y finalmente, sin ser nada malo, M.2 Micron de 256GB para portátil.
Por supuesto los tests permiten cuantificar estas diferencias y comprobar el rendimiento del sistema, dentro de la simplicidad del test se intenta medir el rendimiento de escritura del dispositivo sin valorar la velocidad de la RAM y la CPU que intervienen mucho al realizar lecturas (cache). Pero estos números valorarse después de entender un detalle muy importante: la compresión LZ4 del pool ZFS.
Sobre la compresión LZ4 de OpenZFS
EXT4 y OpenZFS son dos herramientas muy distintas. El primero es el sistema de archivos nativo de GNU/Linux, es un sistema de archivos tradicional que utiliza una única partición o dispositivo y está afinadísimo.
OpenZFS proviene de un desarrollo ajeno a GNU/Linux y tiene por objetivo gestionar de manera eficiente y efectiva grandes pools de almacenamiento. No se parece en nada a un sistema de archivos tradicional pues incorpora capacidades equivalentes a RAID y a un gestor lógico de volúmenes.
De los números mostrados en la tabla de resultados se podría extraer la conclusión falsa de que OpenZFS es más rápido que EXT4 y quiero aclarar aquí que eso no es así. O no lo es de un modo absoluto.
No se puede decir que OpenZFS sea un sistema de ficheros lento, características como su cache ARC van a proporcionar en las cargas reales un gran rendimiento. Pero no se suele escoger OpenZFS porque sea más rápido que EXT4 utilizando un solo dispositivo, se suele escoger esta herramienta por todas las otras características que ofrece al trabajar con un conjunto de discos. De hecho, con un solo dispositivo y en igualdad de condiciones con toda seguridad OpenZFS no va a superar la velocidad de EXT4.
Sin embargo en este test se han realizado escrituras aleatorias síncronas así que ARC no ha alterado los resultados. ¿Qué ha pasado?
Lo que ha pasado se llama compresión transparente con LZ4. LZ4 es un algoritmo de compresión sin pérdida —desarrollado por Yan Collet que también es autor de Zstandard— que se caracteriza por una alta velocidad en la compresión y en la descompresión de datos. Además cuando se activa la compresión transparente con cualquiera de los algoritmos soportados ZFS monitoriza el resultado para desactivar la compresión en aquellos datos sin redundancia. Por esta razón activar la compresión transparente con LZ4 suele ser una ventaja significativa en cualquier carga de trabajo sin ninguna contrapartida (más allá de un mayor uso de CPU).
Gracias a la compresión transparente se escriben menos datos en disco lo que quiere decir que desde el punto de vista de la aplicación aumenta el rendimiento. Si se utiliza un único dispositivo en el pool ZFS y se desactiva la compresión transparente se obtendrá una velocidad menor que con EXT4. Si se utilizan varios dispositivos en el pool ZFS seguirá contando con la capacidad de distribuir la carga.
Por ejemplo, utilizando el dispositivo NVMe
Configuración | Throughput | IOPS |
---|---|---|
EXT4. Partición NVMe. | 619MiB/s (649MB/s) | 9905 |
ZFS. Disco NVMe. Compresión LZ4. | 825MiB/s (865MB/s) | 13.2k |
ZFS. Disco NVMe. Sin compresión. | 490MiB/s (514MB/s) | 7837 |
Naturalmente esto ocurre porque el conjunto de datos que escribe fio
tiene cierta redundancia, si los datos escritos fueran incompresibles se obtendría un resultado similar al que se obtiene sin compresión.
root@ns3084654:/mnt/test# zfs get compressratio test NAME PROPERTY VALUE SOURCE test compressratio 1.46x - root@ns3084654:/mnt/test#
Al realizar los test he decidido dejar activa la compresión en ZFS porque esta es la configuración habitual que utilizo en los sevidores para KVM, no tiene contrapartida apreciable y las imágenes de las máquinas virtuales también obtienen un ratio de compresión similar.
Enlaces: