Notas sobre el rendimiento de disco de un servidor

per Victor Carceler darrera modificació 2021-01-07T10:28:09+02:00

https://commons.wikimedia.org/wiki/File:Laptop-hard-drive-exposed.jpgMedir 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

https://commons.wikimedia.org/wiki/File:Openzfs.svgEXT4 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: