Member of The Internet Defense League Últimos cambios
Últimos Cambios
Blog personal: El hilo del laberinto Geocaching

ZFS y SSD's

Última Actualización: 10 de enero de 2012

Hasta donde yo sé, ZFS es el único sistema de ficheros de uso mínimamente popular que permite combinar el uso de discos duros normales con memorias FLASH (SSD = Solid-State Disk) de forma transparente y combinando lo mejor de ambos mundos.

Lo discos duros son razonablemente rápidos para acceso "streaming" (acceso a datos contiguos, no acceso aleatorio) y tienen una gran capacidad a un coste reducido.

Aunque tecnológicamente los SSD puedan proporcionar un ancho de banda arbitrariamente alto (simplemente juntando más chips en el paquete y accediéndolos en paralelo) y que habitualmente su consumo energético sea inferior a un disco duro equivalente, su principal ventaja es que carecen de "seek time". Es decir, que a la hora de acceder al SSD, da lo mismo que los datos estén contiguos a que estén repartidos de forma aleatoria.

Un disco duro es un dispositivo mecánico sujeto a fatiga y a inercias físicas. Por mucha ingeniería que metamos, son límites físicos que pueden mejorarse pero no romperse.

Eventualmente, el coste por gigabyte de los SSD bajará por debajo del de los discos duros, momento en que éstos quedarán obsoletos. Mientras tanto casi todos los sistemas te obligan a elegir: usar disco duro o usar SSD. Muchos servidores incorporan ambos dispositivos, y el administrador debe particionarlos y decidir qué aloja en cada uno. Por ejemplo, la base de datos en el SSD, porque la velocidad de acceso aleatorio es importante, y lo que se usa poco o cuya velocidad de acceso es menos crítica, en el disco duro, que es más barato y tiene mucha más capacidad.

En todo caso, es una decisión explícita del administrador de sistemas. Si la base de datos crece hasta superar la capacidad de memoria del SSD, no podemos sino comprar otra más grande, ya que alojarla en el disco duro no nos proporciona el rendimiento que necesitamos.

En ZFS no es así. En ZFS podemos unir un "pool" de discos duros (puede ser un disco o docenas) a un número cualquiera (uno o docenas) de SSD's. Y no asignamos cada recurso de forma estática para un "dataset", sino que pertenecen a todo el "pool" y su uso es automático y transparente.

Básicamente las SSD se utilizarán como sistema de caché transparente. Veamos los dos casos: lectura y escritura.

Caché de lectura

ZFS utiliza un sistema de caché en RAM llamado ARC (Adaptive Replacement Cache). Este algoritmo es bastante más avanzado y complejo que el LRU, por ejemplo, pero se comporta mejor que éste ante situaciones patológicas típicas, como que copiar ficheros grandes de un sitio a otro desaloje de la caché objetos de uso frecuente, que tendrán que leerse de nuevo del disco duro enseguida.

Pero por muy bueno que sea el algoritmo, y aunque que el sistema operativo utilice los 24 gigabytes del servidor como caché, lo cierto es que si estamos manejando datos de forma activa con un tamaño superior a nuestra capacidad de RAM al final tendremos que "tirar" datos de la memoria que, tarde o temprano, habrá que volver a leer del disco duro.

Pero podemos definir un dispositivo del "pool" ZFS como "L2ARC". Es decir, "ARC de nivel 2". La idea es que los objetos que ya no caben en la RAM (el ARC), en vez de eliminarlos de memoria y recargarlos de disco de nuevo cuando sean necesarios, se envían al dispositivo (o dispositivos) L2ARC. Si la lectura del L2ARC es más rápida que acceder al disco original, tenemos una ganancia de rendimiento.

Normalmente el dispositivo L2ARC será una SSD, pero no es necesario. Por ejemplo, si los datos originales residen en un dispositivo iSCSI remoto, usar un disco duro local rápido como L2ARC puede ser una ganancia sustancial en ancho de banda y en velocidad. ZFS usará como L2ARC el dispositivo o dispositivos que le indiquemos.

Algunas características de L2ARC en las versiones actuales de ZFS:

  • Las páginas almacenadas en L2ARC mantienen una referencia en memoria, así que esos objetos siguen consumiendo RAM, aunque menos de lo que supondría mantener el objeto completo en memoria. Es decir, usar un dispositivo L2ARC de gran tamaño tienen una penalización en memoria. En casos extremos, la ocupación de memoria del L2ARC puede competir con el ARC, siendo contraproducente. De hecho, como ZFS utiliza un tamaño de bloque variable, la ocupación de memoria es difícil de calcular, aunque lo cierto es que no suelo ver problemas en producción. Además, Solaris proporciona estadísticas detalladas sobre todo esto.

  • L2ARC no es persistente. Es decir, cuando reiniciamos el ordenador, el contenido del L2ARC se pierde. Aparentemente esto es un problema, pero en la práctica no lo es y evita muchas dificultades. La penalización de rendimiento es "relativa", porque en realidad los objetos más frecuentemente utilizados no están en la L2ARC, sino en la ARC (la RAM), que perdemos del todas maneras al reiniciar. Aunque los datos de L2ARC sobreviviesen el reinicio (no lo hacen porque su índice se mantienen RAM, como se ha explicado en el punto anterior), si nuestro servidor tiene 24 GB de RAM y usa la mitad para caché, tendríamos que leer 12 GB "calientes y muy demandados" del disco duro, por mucha L2ARC que tengamos. Tal vez en sistemas con poca RAM compensaría tener un L2ARC persistente, a pesar de su mayor complejidad y un menor rendimiento (habría que mantener un índice en el L2ARC).

    Este detalle hace también que asignar un dispositivo "grande" como L2ARC puede ser un desperdicio que no aporta ningún beneficio. Por ejemplo, si creamos un L2ARC en un disco de un terabyte, tardaríamos mucho (posiblemente, nunca) en llenarlo. No compensa. Por no olvidar el consumo de memoria RAM que supondría. Por otro lado, contendría datos de uso tan infrecuente que ir al disco duro "real" a por ellos no afecta al rendimiento.

    La regla habitual es que 100GB de L2ARC ocupan 1-2GB de RAM real. Pero la cifra exacta depende del tamaño de bloque efectivo de ZFS.

  • Si ocurre un error de lectura al intentar leer un dato del L2ARC, o éste queda inaccesible por cualquier problema (por ejemplo, alguien tira del cable), no pasa nada. El error se ignora y se considera un "cache miss", así que se irá al disco duro original a leer los datos, con una pérdida de rendimiento mínima.

    Debido a esto y al punto anterior, no es inteligente usar "mirrors" como L2ARC. En general, sobre todo con el tamaño actual de las SSD, es preferible utilizar dos SSD como "stripping", duplicando la capacidad y el ancho de banda del L2ARC.

    Cuando se guarda un dato en el L2ARC, se le calcula una suma de control que se mantienen en RAM, de forma que podemos verificar su integridad cuando se lee, aunque el dispositivo no nos indique ningún error.

  • Lo mismo se aplica cuando hay problemas con un dispositivo L2ARC al importar su "zpool". Se marcará como erróneo y perdemos la mejora del rendimiento que nos proprocionaba, pero el "zpool" sigue estando accesible.

  • En las versiones de ZFS que soportan cifrado, los datos correspondientes a "datasets" cifrados no se envían nunca al L2ARC, donde estarían en texto plano. Esto supone una pérdida de rendimiento para dichos "datasets", ya que no utilizarán el L2ARC.

Solaris 10 y derivados proporcionan estadísticas interesantes y detalladas sobre ARC y L2ARC. Por ejemplo, en un servidor sin L2ARC puedo ver lo siguiente:

[root@XXX /]# kstat zfs::arcstats
module: zfs                             instance: 0     
name:   arcstats                        class:    misc
        c                               10212435301
        c_max                           24678334464
        c_min                           3084791808
        crtime                          77.32556706
        data_size                       8770825216
        deleted                         19154548
        demand_data_hits                4415206368
        demand_data_misses              5057241
        demand_metadata_hits            9148538352
        demand_metadata_misses          3213951
        evict_l2_cached                 0
        evict_l2_eligible               739645837312
        evict_l2_ineligible             42101163520
        evict_skip                      2159701
        hash_chain_max                  15
        hash_chains                     305112
        hash_collisions                 326041988
        hash_elements                   1026344
        hash_elements_max               1587374
        hdr_size                        202097704
        hits                            13596352582
        l2_abort_lowmem                 0
        l2_cksum_bad                    0
        l2_evict_lock_retry             0
        l2_evict_reading                0
        l2_feeds                        0
        l2_free_on_write                0
        l2_hdr_size                     0
        l2_hits                         0
        l2_io_error                     0
        l2_misses                       0
        l2_read_bytes                   0
        l2_rw_clash                     0
        l2_size                         0
        l2_write_bytes                  0
        l2_writes_done                  0
        l2_writes_error                 0
        l2_writes_hdr_miss              0
        l2_writes_sent                  0
        memory_throttle_count           0
        mfu_ghost_hits                  4091225
        mfu_hits                        13314858371
        misses                          14457327
        mru_ghost_hits                  3114547
        mru_hits                        249049566
        mutex_miss                      40351
        other_size                      1236206768
        p                               9193395515
        prefetch_data_hits              5728937
        prefetch_data_misses            5098302
        prefetch_metadata_hits          26878925
        prefetch_metadata_misses        1087833
        recycle_miss                    857241
        size                            10209129688
        snaptime                        8599864.1085979

La máquina tiene un "uptime" de tres meses. No tiene configurado L2ARC (aún), pero vemos que Solaris hubiera movido al L2ARC unos 700 gigabytes, de haber podido. Eso no significa que necesitemos 700GB para el L2ARC, por dos motivos:

  1. Cuando se descarta un objeto del ARC y se decide que es candidato para ser volcado a un L2ARC (de tenerlo), no se guarda memoria de ello. Es decir, que si más tarde volvemos a pedir el mismo objeto (que cargamos del disco duro, porque no tenemos L2ARC) y luego lo descartamos de nuevo, volverá a contabilizarse. Es decir, los 700 GB son muy pesimistas. Los capacidad real que se hubiera utilizado sería inferior. ¿Cuánto?. Toca hacer pruebas y simulaciones.

  2. No vale la pena cachear objetos con un acceso muy esporádico. Un L2ARC grande puede cachear más de lo necesario, en el sentido de que la diferencia de rendimiento entre ese L2ARC y otro de menor tamaño sea inapreciable. Un caso claro de la ley de retornos decrecientes.

Caché de escritura

Acelerar las lecturas supone un aumento de rendimiento inmediato porque casi ninguna aplicación puede realizar ningún tipo de progreso hasta que recibe el dato leído. A la hora de escribir, en cambio, lo importante es disponer de ancho de banda suficiente al disco para nuestra aplicación. Pero la latencia de escritura no importa en la mayoría de los casos. De hecho cualquier sistema operativo moderno decente acumulará unos segundos de actividad en RAM y luego los volcará en una ráfaga al disco duro, sin que las aplicaciones sean conscientes de estos detalles. Esta acumulación permite muchas optimizaciones, como evitar toda actividad de disco para ficheros creados, modificados, leídos y borrados en rápida sucesión (por ejemplo, ficheros de intercambio entre programas), o colapsar muchas escrituras cortas en una larga y eficiente escritura larga. O planificar con detalle la asignación de posiciones de los datos en el disco duro. Y todo automático y transparente a las aplicaciones, que dan la orden de grabación al sistema operativo y se olvidan.

Pero hay un caso donde la velocidad de escritura es importante, crucial: las escrituras síncronas.

Las escrituras "cacheadas" en memoria, las normales, son escrituras asíncronas. Los programas piden que se grabe un dato y el sistema operativo lo hará "cuando le parezca". Un tiempo normal de "cacheo" es, por ejemplo, 30 segundos. El problema es que si se va la luz, el sistema se cuelga, el ordenador empieza a echar humo, etc., podemos perder esos 30 segundos de grabaciones pendientes. Es decir, podemos estar escribiendo nuestra tesis doctoral, grabarla y tirar del cable del ordenador a los diez segundos... y haber perdido el fichero que grabamos "hace" diez segundos.

La mayor parte del tiempo no importa mucho. Un servidor se reinicia una vez al mes (hay que parchearlo :) de forma controlada, y permitiendo que las escrituras sean asíncronas la mejora de velocidad es apabullante.

Pero existen casos donde debemos garantizar que los datos se han grabado en el disco duro de forma fehaciente. Por ejemplo, cuando llega un mensaje de correo electrónico a nuestro servidor, no debería darle el OK al remitente HASTA haberse asegurado de que el mensaje se ha grabado realmente en el disco duro. No podemos permitir que el ordenador se cuelgue y se pierda un correo electrónico. Eso es intolerable. O no queremos que nuestro balance bancario quede inconsistente. Otro ejemplo donde las escrituras síncronas son importantes es el caso de un servidor NFS.

En el caso del procesador de texto, las copias de seguridad periódicas que hace el programa automáticamente pueden ser grabaciones asíncronas (y que mantenga un histórico, porque la última puede estar incompleta), pero estaría bien que cuando el usuario pulsa sobre "save", el fichero se "grabe de verdad". Ahora.

En el mundo de las bases de datos se habla de ACID. La "D" significa "Durabilidad". Es decir, que una vez que la base de datos se hace cargo de un dato, este dato no se va a perder por "chorraditas" menores que un incendio en el datacenter. O una bomba atómica. Pero, desde luego, no porque alguien tropiece con el enchufe.

El problema es, nuevamente, que los discos duros son dispositivos mecánicos sujetos a leyes físicas. Son lentos, comparados con la electrónica, sobre todo cuando empezamos a mover el cabezal del disco duro de un lado para otro. ZFS utiliza ZIL (ZFS Intention Log) para completar las grabaciones síncronas "rápido", a poder ser sin mover el cabezal del disco duro (gracias a que ZFS es un sistema CoW, "Copy on Write"). Comparado con otros sistemas de ficheros, ZFS es muy rápido en escrituras síncronas, siendo muy fácil obtener 3-4 veces más transacciones por segundo que otros sistemas de ficheros modernos, si los datos a escribir son pocos. 120 transacciones por segundo en un disco "normal" de 7200 RPM es rutina.

Pero un SSD "normal" me permite hacer más de 5000 escrituras síncronas por segundo. Eso son casi dos órdenes de magnitud por encima. Y esto es con la tecnología actual, que no está limitada por restricciones mecánicas como mover el cabezal a la otra punta del disco duro, o esperar a que el disco rote hasta que el sector que nos interesa pase por debajo del cabezal.

ZFS permite poner el ZIL en el dispositivo separado. Puede ser una SSD o un disco duro pequeño pero rápido (digamos, un disco de 72GB de 15.000 RPM). Las escrituras asíncronas, que son la mayoría, se guardan en el disco duro como siempre, tras un pequeño tiempo en memoria. Las escrituras síncronas se guardan en el ZIL, en un dispositivo rápido, pero permanecen en memoria como siempre y serán volcados al disco duro de forma normal tras ese período de cacheo temporal en RAM. De hecho el ZIL es prácticamente escrituras en exclusiva. Solo se lee al reiniciar el sistema, para comprobar si hay transacciones pendientes de grabar en el disco duro. El resto del tiempo solo se escribe.

Algunos detalles:

  1. El tamaño del ZIL solo depende del volumen de escrituras síncronas que tengamos, normalmente muy inferior al volumen de escrituras totales (a menos que ese servidor se utilice exclusivamente como base de datos ACID). De hecho, gracias a DTRACE, existe un script que nos ayuda a ver el tráfico de grabaciones al ZIL:
    [root@XXX z-dtrace]# ./zilstat.ksh -t -p datos txg
    waiting for txg commit...
    TIME                        txg    N-Bytes  N-Bytes/s N-Max-Rate    B-Bytes  B-Bytes/s B-Max-Rate    ops  <=4kB 4-32kB >=32kB
    2012 Jan  5 19:30:48     619068     476512      17648     299024    1347584      49910     524288     11      0      0     11
    2012 Jan  5 19:31:18     619069     445616      14853     119152    4775936     159197     598016     41      1      0     40
    2012 Jan  5 19:31:48     619070    2907760      96925    2860440    3575808     119193    3014656     28      0      0     28
    2012 Jan  5 19:32:18     619071     666240      22208     409688    2527232      84241    1122304     26      1      0     25
    2012 Jan  5 19:32:48     619072     278248       9274     142248    1212416      40413     430080     15      0      0     15
    2012 Jan  5 19:33:18     619073     803400      26780     363296    3067904     102263     524288     33      1      0     32
    2012 Jan  5 19:33:48     619074      82280       2742      45136     466944      15564     262144      5      0      0      5
    2012 Jan  5 19:34:18     619075    1139568      37985     306712    3584000     119466     917504     47      1      0     46
    2012 Jan  5 19:34:48     619076      49656       1655      49656     503808      16793     503808      6      0      0      6
    2012 Jan  5 19:35:18     619077    4127464     137582    2807424    6348800     211626    2883584     62      1      2     59
    2012 Jan  5 19:35:48     619078     181048       6034      80864     634880      21162     262144      7      0      0      7
    2012 Jan  5 19:36:18     619079     786864      26228     544536    3895296     129843    1122304     35      1      0     34
    2012 Jan  5 19:36:48     619080     850440      28348     715624    2002944      66764     917504     16      0      0     16
    2012 Jan  5 19:37:18     619081    1198856      39961     404856    5578752     185958     823296     50      1      0     49
    2012 Jan  5 19:37:48     619082     305360      10178     117160    2134016      71133     786432     17      0      0     17
    2012 Jan  5 19:38:18     619083     816240      27208     294320    4993024     166434    1179648     45      1      1     43
    2012 Jan  5 19:38:48     619084    2915080      97169    2820680    3432448     114414    2883584     28      0      2     26
    2012 Jan  5 19:39:18     619085     625152      20838     302808    2576384      85879     786432     39      1      6     32
    2012 Jan  5 19:39:48     619086     198328       6610      48400    2396160      79872     671744     32      0      8     24
    2012 Jan  5 19:40:18     619087    1120288      37342     775032    2473984      82466    1048576     37      1     11     25
    2012 Jan  5 19:40:48     619088     194720       6490     135560     692224      23074     262144      8      0      3      5
    2012 Jan  5 19:41:18     619089   58749672    1958322   58147816   69685248    2322841   65966080    540      1      1    538
    2012 Jan  5 19:41:48     619090     340008      11333      67136    2785280      92842     536576     33      0      9     24
    2012 Jan  5 19:42:18     619091    3623800     120793    2612088    6569984     218999    2752512     68      1      2     65
    2012 Jan  5 19:42:48     619092    1039200      34640     903920    1347584      44919    1179648     11      0      0     11
    2012 Jan  5 19:43:18     619093     672104      22403     358896    2379776      79325     524288     31      1      2     28
    2012 Jan  5 19:43:48     619094      47096       1569      47096     167936       5597     167936      2      0      0      2
    ^C
    

    Vemos que ZFS genera una transaccion ZFS nueva cada 30 segundos (en algunos casos el tiempo puede ser menor, como cuando se hace un "snapshot" ZFS), y las escrituras síncronas durante cada período se canalizan al ZIL, que es lo que estamos midiendo con ese script DTrace. Como no tengo configurado aún un ZIL en la SSD, el ZIL (en realidad uno por cada "dataset") se guarda en los discos duros del propio ZPOOL.

    En este caso la transacción más "cargada" generó unos 70MB en 30 segundos, aunque el tráfico medio sea muy inferior. Y 540 transacciones. La flash puede absorber fácilmente 5.000 transacciones por segundo, mientras que el disco duro, con suerte, llega a 120 transacciones por segundo (y eso si usas ZFS, que es muy eficiente al respecto).

    Digamos que nuestro tráfico medio son 70MB por 30 segundos. ¿Qué tamaño necesitamos para el ZIL?. Pues del orden de 2-3 veces más que el tráfico medio (estamos recopilando una transacción nueva mientras estamos grabando la anterior). En este caso iríamos sobradísimos con 200MB.

    Como puede verse, el tamaño del ZIL puede ser pequeño, pero la diferencia de rendimiento es brutal.

  2. La pérdida del ZIL no es crítico para la estabilidad del ZPOOL, pero nos hará perder las últimas transacciones si apagamos el ordenador de forma no controlada. Es más, como los datos del ZIL solo se leen al reiniciar el ordenador, podemos tener un SSD defectuoso (o "gastado") sin saberlo y descubriéndolo demasiado tarde (o no saberlo, porque el ZIL "termina" en el punto donde la suma de control no sea correcta, que puede ser porque el ZIL ha terminado realmente o porque los datos están corruptos). Debido a ello, es buena idea que el ZIL esté duplicado. De esta forma, un fallo en una SSD podría ser detectado y, además, subsanado con su pareja. Se pierde un poco de rendimiento, pero se gana en seguridad y tranquilidad de espíritu.

  3. Si hay un problema con el ZIL al importar un "zpool", Solaris se negará a importarlo "por las buenas", dado que podríamos tener transacciones comprometidas en el ZIL pero inexistentes en los discos duros. No obstante se puede forzar una importación mediante "-f" ("force"), descartando el ZIL (y perdiendo esas transacciones, pero manteniendo la integridad y disponibilidad del "zpool"). Luego podemos eliminar el ZIL de la configuración, o reemplazarlo por dispositivos nuevos. Por ello, que el ZIL esté en "mirror" es interesante y recomendable.

  4. Si perdemos el dispositivo ZIL en producción, ZFS lo detectará y usará el disco duro como ZIL. Como las transacciones en el ZIL también se almacenan en memoria hasta que se produce el flush ZFS (cada 30 segundos, en situaciones típicas), al cabo de 30-60 segundos de perder el ZIL, lo que pudiese contener ya no tendrá importancia, porque los datos ya están en el disco duro. Sólo perderíamos transacciones si perdemos el ZIL y la máquina falla durante ese período de 30-60 segundos. En este caso perderíamos transacciones ya comprometidas, pero el "zpool" se mantendría estable y consistente.

  5. No he visto el código fuente, y preguntando a la gente no me ha quedado claro si un ZIL al que se le añade un mirror realiza "silvering" del nuevo dispositivo o simplemente guarda los datos nuevos en ambos. Esto no es muy importante, ya que el contenido del ZIL se recicla a los 30 segundos (la duración de las transacciones ZFS), así que tras ese tiempo ambos "mirrors" del ZIL ya estarán sincronizados AUNQUE no haya habido un "resilvering". De todas formas es algo que me gustaría tener tiempo para investigar, por puro masoquismo :-).

  6. Cuando se hacen escrituras síncronas a un "dataset" cifrado, los datos correspondientes que se graban en el ZIL están cifrados.

  7. Si tenemos escrituras síncronas que superan el tamaño de la SSD, no pasa nada. Se usarán los ZIL en los discos duros. Por tanto, un "overflow" esporádico no debería causar problemas.

El caso práctico

En mi servidor tengo dos SSD's de 40GB. Los particiono de la siguiente forma, como explico en otro documento: 8 GB como partición de recuperación (con OpenIndiana), 2 GB como ZIL y 27GB para el L2ARC.

Lo primero que hago es comparar el tráfico medio sostenido en lecturas aleatorias, en el disco duro y en el SSD. Usaré el siguiente programa en Python:

#!/usr/bin/env python

import sys, threading, Queue, time, random

timeout = 10
done = False

if len(sys.argv) != 4+1 :
  print >>sys.stderr, "%s device maxsize num_threads blocksize" %sys.argv[0]
  sys.exit(1)

device, maxsize, num_threads, blocksize = sys.argv[1:]
maxsize = int(maxsize)*1024*1024*1024
num_threads = int(num_threads)
blocksize = int(blocksize)

q = Queue.Queue()

def worker() :
    f = open(device, "rb")
    rnd = random.Random()
    num_blocks = maxsize//blocksize
    n = 0
    while not done :
        f.seek(blocksize*rnd.randrange(0, num_blocks))
        a = f.read(blocksize)
        n += 1
    q.put(n)

t = []
for i in xrange(num_threads) :
    i = threading.Thread(target = worker)
    i.setDaemon(True)
    i.start()
    t.append(i)

time.sleep(timeout)
done = True
transfered = 0
for i in t :
    i.join()
    transfered += q.get()

bps = transfered/(timeout+0.0)
print "%.1f bloques/s, %.1f bytes/s" %(bps, blocksize*bps)

Para comprobar el disco duro, quito una unidad del "mirror", para que el tráfico "normal" no afecte a la medida. Ejecuto el programa sobre el disco duro "offline":

[root@XXX datos]# zpool offline -t datos c4t3d0s0
[root@XXX datos]# zpool status
  pool: datos
 state: DEGRADED
status: One or more devices has been taken offline by the administrator.
        Sufficient replicas exist for the pool to continue functioning in a
        degraded state.
action: Online the device using 'zpool online' or replace the device with
        'zpool replace'.
 scan: scrub repaired 0 in 8h7m with 0 errors on Sun Jan  1 02:34:18 2012
config:

        NAME          STATE     READ WRITE CKSUM
        datos         DEGRADED     0     0     0
          mirror-0    DEGRADED     0     0     0
            c4t2d0s0  ONLINE       0     0     0
            c4t3d0s0  OFFLINE      0     0     0

errors: No known data errors

Una vez que terminamos con el disco duro, restauro el "mirror", que ya me estaba poniendo nervioso:

[root@XXX datos]# zpool online datos c4t3d0s0
[root@XXX datos]# zpool status
  pool: datos
 state: ONLINE
 scan: resilvered 258M in 0h1m with 0 errors on Thu Jan  5 21:26:19 2012
config:

        NAME          STATE     READ WRITE CKSUM
        datos         ONLINE       0     0     0
          mirror-0    ONLINE       0     0     0
            c4t2d0s0  ONLINE       0     0     0
            c4t3d0s0  ONLINE       0     0     0

errors: No known data errors

Obsérverse como ZFS realiza una sincronización incremental de los discos en espejo, muy eficiente.

  512 bytes 1 Kbyte 2 Kbytes 4 Kbytes 8 Kbytes 16 Kbytes 32 Kbytes 64 Kbytes 128 Kbytes 256 Kbytes 512 Kbytes 1 Mbytes
Disco duro 32/62 65/127 128/258 261/516 526/1035 1001/1946 1986/3185 3867/4862 7209/- 13029/- 22073/- 32506/-
Unidad SSD 3907/14853 7800/29733 15606/59474 30818/119419 61095/239162 67066/238969 67777/239016 76454/239069 89129/239298 98382/239206 102813/239337 104858/240543

La tabla muestra dos valores por casilla: la velocidad de transferencia con un único hilo, y la velocidad de transferencia óptima lanzando varios hilos para proporcionar varias peticiones al sistema operativo y al disco duro, para que pueda procesarlas y reordenarlas como les parezca. No indico el número de hilos para llegar a ese valor óptimo, pero suele rondar los 32 hilos en paralelo. Si indico "-" significa que lanzar más de un hilo es contraproducente, porque las peticiones se hacen competencia entre sí. Es el caso de las transferencias largas, donde si hay dos peticiones concurrentes, el disco duro mueve el cabezal entre ellas, en vez de realizar una larga transferencia sin movimiento mecánico.

En el caso de la memoria SSD la penalización de las transferencias cortas es muy evidente también, aunque se recupera muy rápido. Asimismo, lanzando unos pocos hilos es muy fácil llegar al límite de transferencia de 240MB/s. Otro detalle interesante es que muchas peticiones concurrentes no afectan demasiado al rendimiento, lo que es consistente con el hecho de que las SSD no tengan "seek time".

El disco duro tiene una velocidad de transferencia que varía por la posición de la cabeza (en la parte exterior del disco, la transferencia es más rápida). Además, para obtener la velocidad máxima de transferencia es necesario que la cabeza no se mueva, así que la ráfaga a transferir debe ser muy larga. En cuanto hay competencia por el acceso a disco, el rendimiento cae. Con un tamaño de bloque de 128KB en ZFS, en el peor de los casos estaríamos utilizando apenas el 9% del ancho de banda del disco duro.

Quiero señalar, no obstante, que estas cifras representan el peor de los casos para el disco duro, y que no son representativas del rendimiento típico. En la práctica suele existir bastante localidad de referencia y el rendimiento es mucho mejor. Estas cifras representan el caso pesimista de tráfico aleatorio de disco, que solo se cumpliría en la práctica en casos extremos, como una base de datos inmensa (cientos de gigabytes) con un acceso aleatorio a la misma. De hecho en condiciones normales se llega fácilmente a los 100-120MB/s por disco. 240MB/s con dos discos.

Activemos ahora L2ARC. Como he comentado en la sección correspondiente, no utilizo redundancia porque no aporta nada y es preferible incrementar su capacidad y ancho de banda:

[root@XXX datos]# zpool add datos cache c4t0d0s2
[root@XXX datos]# zpool add datos cache c4t1d0s2
[root@XXX datos]# 
  pool: datos
 state: ONLINE
 scan: resilvered 258M in 0h1m with 0 errors on Thu Jan  5 21:26:19 2012
config: 

        NAME          STATE     READ WRITE CKSUM
        datos         ONLINE       0     0     0
          mirror-0    ONLINE       0     0     0
            c4t2d0s0  ONLINE       0     0     0
            c4t3d0s0  ONLINE       0     0     0
        cache
          c4t0d0s2    ONLINE       0     0     0
          c4t1d0s2    ONLINE       0     0     0

errors: No known data errors

[root@XXX /]# kstat zfs::arcstats
module: zfs                             instance: 0     
name:   arcstats                        class:    misc
        c                               9981439403
        c_max                           24678334464
        c_min                           3084791808
        crtime                          77.32556706
        data_size                       8532319744
        deleted                         19217837
        demand_data_hits                4442792691
        demand_data_misses              5064197
        demand_metadata_hits            9178662256
        demand_metadata_misses          3217339
        evict_l2_cached                 723968
        evict_l2_eligible               741436190720
        evict_l2_ineligible             42276615680
        evict_skip                      2159701
        hash_chain_max                  15
        hash_chains                     305196
        hash_collisions                 327132917
        hash_elements                   1025709
        hash_elements_max               1587374
        hdr_size                        201643584
        hits                            13654205277
        l2_abort_lowmem                 0
        l2_cksum_bad                    0
        l2_evict_lock_retry             0
        l2_evict_reading                0
        l2_feeds                        388
        l2_free_on_write                0
        l2_hdr_size                     0
        l2_hits                         0
        l2_io_error                     0
        l2_misses                       289
        l2_read_bytes                   0
        l2_rw_clash                     0
        l2_size                         54472192
        l2_write_bytes                  54947328
        l2_writes_done                  30
        l2_writes_error                 0
        l2_writes_hdr_miss              0
        l2_writes_sent                  30
        memory_throttle_count           0
        mfu_ghost_hits                  4095269
        mfu_hits                        13371779494
        misses                          14487113
        mru_ghost_hits                  3116107
        mru_hits                        249839090
        mutex_miss                      40442
        other_size                      1229508512
        p                               9103037846
        prefetch_data_hits              5758589
        prefetch_data_misses            5117053
        prefetch_metadata_hits          26991741
        prefetch_metadata_misses        1088524
        recycle_miss                    859124
        size                            9963471840
        snaptime                        8630741.47083925

Vemos que Solaris detecta el L2ARC y que está empezando a usarlo, poco a poco, a medida que debe desalojar elementos del ARC en RAM para hacer sitio a datos nuevos. Ahora hay que darle tiempo para que se vaya llenando.

Para ocuparnos del ZIL, primero vemos la velocidad de escritura síncrona actual. El programa en Python que utilizaré es el siguiente:

import os, time

for i in xrange(21) :
  l = 2**i
  m = 2<<20
  d = open("/dev/urandom").read(m)
  assert len(d) == m
  f = open("zzz", "w")
  t = time.time()
  fd = f.fileno()
  p = 0
  count = 0
  while time.time()-t < 10 :
    count += 1
    if p >= m : p = 0
    f.write(d[p:p+l])
    f.flush()
    os.fsync(fd)
    p += l
  t = time.time()-t
  print "%d bytes: %.1f bytes/s, %.1f transacciones por segundo" \
          %(l, count*l/t, count/t)
Este programa nos indica el número de transacciones por segundo y bytes por segundo que nos permite realizar el ZIL. El juego con "urandom" es para evitar suspicacias debido a la compresión transparente que realiza ZFS, otra de sus muchas ventajas. Se observa que para tamaños pequeños de escritura, de menos de 64Kbytes, las escrituras son muy rápidas, en los límites físicos de una única escritura en el disco duro. Para tamaños de escritura altos, la estrategia del ZIL cambia y el rendimiento desciende bastante, aunque sigue siendo bastante superior a otros sistemas operativos.

Con mi configuración actual, el programa me ofrece la siguiente salida:

1 bytes: 114.4 bytes/s, 114.4 transacciones por segundo
2 bytes: 196.6 bytes/s, 98.3 transacciones por segundo
4 bytes: 473.8 bytes/s, 118.4 transacciones por segundo
8 bytes: 953.1 bytes/s, 119.1 transacciones por segundo
16 bytes: 1579.0 bytes/s, 98.7 transacciones por segundo
32 bytes: 3813.3 bytes/s, 119.2 transacciones por segundo
64 bytes: 7467.6 bytes/s, 116.7 transacciones por segundo
128 bytes: 12806.7 bytes/s, 100.1 transacciones por segundo
256 bytes: 29325.7 bytes/s, 114.6 transacciones por segundo
512 bytes: 61007.7 bytes/s, 119.2 transacciones por segundo
1024 bytes: 107706.5 bytes/s, 105.2 transacciones por segundo
2048 bytes: 243773.0 bytes/s, 119.0 transacciones por segundo
4096 bytes: 478810.5 bytes/s, 116.9 transacciones por segundo
8192 bytes: 799350.4 bytes/s, 97.6 transacciones por segundo
16384 bytes: 1854843.4 bytes/s, 113.2 transacciones por segundo
32768 bytes: 3710380.4 bytes/s, 113.2 transacciones por segundo
65536 bytes: 2916061.9 bytes/s, 44.5 transacciones por segundo
131072 bytes: 6803879.4 bytes/s, 51.9 transacciones por segundo
262144 bytes: 9944467.1 bytes/s, 37.9 transacciones por segundo
524288 bytes: 16451763.6 bytes/s, 31.4 transacciones por segundo
1048576 bytes: 27946452.0 bytes/s, 26.7 transacciones por segundo

Por comparación, esto es lo que veo en un ordenador moderno con Ubuntu 10.04 y sistema de ficheros EXT4:

1 bytes: 20.1 bytes/s, 20.1 transacciones por segundo
2 bytes: 40.6 bytes/s, 20.3 transacciones por segundo
4 bytes: 81.1 bytes/s, 20.3 transacciones por segundo
8 bytes: 155.5 bytes/s, 19.4 transacciones por segundo
16 bytes: 289.3 bytes/s, 18.1 transacciones por segundo
32 bytes: 639.3 bytes/s, 20.0 transacciones por segundo
64 bytes: 1207.0 bytes/s, 18.9 transacciones por segundo
128 bytes: 2372.5 bytes/s, 18.5 transacciones por segundo
256 bytes: 4921.7 bytes/s, 19.2 transacciones por segundo
512 bytes: 10682.4 bytes/s, 20.9 transacciones por segundo
1024 bytes: 19443.0 bytes/s, 19.0 transacciones por segundo
2048 bytes: 40720.4 bytes/s, 19.9 transacciones por segundo
4096 bytes: 78898.7 bytes/s, 19.3 transacciones por segundo
8192 bytes: 154574.1 bytes/s, 18.9 transacciones por segundo
16384 bytes: 282820.3 bytes/s, 17.3 transacciones por segundo
32768 bytes: 570933.1 bytes/s, 17.4 transacciones por segundo
65536 bytes: 1266590.2 bytes/s, 19.3 transacciones por segundo
131072 bytes: 2300879.1 bytes/s, 17.6 transacciones por segundo
262144 bytes: 4195885.0 bytes/s, 16.0 transacciones por segundo
524288 bytes: 6912373.8 bytes/s, 13.2 transacciones por segundo
1048576 bytes: 12640044.4 bytes/s, 12.1 transacciones por segundo

Además de que ZFS duplica el rendimiento en el peor de los casos, el movimiento de cabezal se ve también reducido (por el diseño del ZIL y por la arquitectura CoW de ZFS), así que en caso de competencia por el acceso a disco duro la ventaja del ZFS será incluso mayor.

Voy ahora a activar el ZIL en las SSD. Por lo ya explicado en su sección, lo haré como "mirror":

[root@XXX datos]# zpool add datos log mirror c4t0d0s1 c4t1d0s1
cannot add to 'datos': root pool can not have multiple vdevs or separate logs
[root@XXX datos]# zpool upgrade
This system is currently running ZFS pool version 29.

All pools are formatted using this version.

Vaya, esto sí que es completamente inesperado. No puedo activar un ZIL separado en el "zpool" de arranque del sistema operativo. Tiene su lógica, porque mientras arranca el sistema operativo los recursos de los que disponemos son escasos y frágiles, y este soporte supone complejidad adicional. Pero no deja de ser decepcionante. Mi gozo en un pozo... de momento.

En aras de la demostración voy a crear un "zpool" nuevo en un ZVOLUME del "zpool" actual. A ver si funciona:

[root@XXX datos]# zfs create -V 10G datos/prueba
[root@XXX datos]# zpool create datos2 /dev/zvol/dsk/datos/prueba
[root@XXX datos]# mv rendimiento_ZIL.py /datos2
[root@XXX datos]# cd /datos2
[root@XXX datos2]# zpool status datos2
  pool: datos2
 state: ONLINE
 scan: none requested
config:

        NAME                          STATE     READ WRITE CKSUM
        datos2                        ONLINE       0     0     0
          /dev/zvol/dsk/datos/prueba  ONLINE       0     0     0

errors: No known data errors
[root@XXX datos2]# python rendimiento_ZIL.py
1 bytes: 111.7 bytes/s, 111.7 transacciones por segundo
2 bytes: 202.5 bytes/s, 101.2 transacciones por segundo
4 bytes: 459.4 bytes/s, 114.9 transacciones por segundo
8 bytes: 910.7 bytes/s, 113.8 transacciones por segundo
16 bytes: 1547.2 bytes/s, 96.7 transacciones por segundo
32 bytes: 3647.4 bytes/s, 114.0 transacciones por segundo
64 bytes: 7339.9 bytes/s, 114.7 transacciones por segundo
128 bytes: 12616.5 bytes/s, 98.6 transacciones por segundo
256 bytes: 28840.0 bytes/s, 112.7 transacciones por segundo
512 bytes: 60024.9 bytes/s, 117.2 transacciones por segundo
1024 bytes: 103763.8 bytes/s, 101.3 transacciones por segundo
2048 bytes: 230377.5 bytes/s, 112.5 transacciones por segundo
4096 bytes: 475515.8 bytes/s, 116.1 transacciones por segundo
8192 bytes: 750557.2 bytes/s, 91.6 transacciones por segundo
16384 bytes: 1655206.8 bytes/s, 101.0 transacciones por segundo
32768 bytes: 3387881.2 bytes/s, 103.4 transacciones por segundo
65536 bytes: 1716648.9 bytes/s, 26.2 transacciones por segundo
131072 bytes: 5804253.9 bytes/s, 44.3 transacciones por segundo
262144 bytes: 7798288.7 bytes/s, 29.7 transacciones por segundo
524288 bytes: 4254491.2 bytes/s, 8.1 transacciones por segundo
1048576 bytes: 12577264.9 bytes/s, 12.0 transacciones por segundo
[root@XXX /]# zpool status datos2
  pool: datos2
 state: ONLINE
 scan: resilvered 0 in 0h0m with 0 errors on Fri Jan  6 03:53:59 2012
config:

        NAME                          STATE     READ WRITE CKSUM
        datos2                        ONLINE       0     0     0
          /dev/zvol/dsk/datos/prueba  ONLINE       0     0     0
        logs
          c4t0d0s1                    ONLINE       0     0     0

errors: No known data errors
[root@XXX datos2]# python rendimiento_ZIL.py
1 bytes: 5135.9 bytes/s, 5135.9 transacciones por segundo
2 bytes: 10797.5 bytes/s, 5398.7 transacciones por segundo
4 bytes: 21472.7 bytes/s, 5368.2 transacciones por segundo
8 bytes: 43205.3 bytes/s, 5400.7 transacciones por segundo
16 bytes: 87860.8 bytes/s, 5491.3 transacciones por segundo
32 bytes: 168635.6 bytes/s, 5269.9 transacciones por segundo
64 bytes: 342234.8 bytes/s, 5347.4 transacciones por segundo
128 bytes: 693206.5 bytes/s, 5415.7 transacciones por segundo
256 bytes: 1346381.5 bytes/s, 5259.3 transacciones por segundo
512 bytes: 2737607.1 bytes/s, 5346.9 transacciones por segundo
1024 bytes: 5483587.9 bytes/s, 5355.1 transacciones por segundo
2048 bytes: 10894478.8 bytes/s, 5319.6 transacciones por segundo
4096 bytes: 10068762.0 bytes/s, 2458.2 transacciones por segundo
8192 bytes: 28521978.3 bytes/s, 3481.7 transacciones por segundo
16384 bytes: 2235868.9 bytes/s, 136.5 transacciones por segundo
32768 bytes: 34653658.7 bytes/s, 1057.5 transacciones por segundo
65536 bytes: 4002900.2 bytes/s, 61.1 transacciones por segundo
131072 bytes: 12414994.3 bytes/s, 94.7 transacciones por segundo
262144 bytes: 33842062.6 bytes/s, 129.1 transacciones por segundo
524288 bytes: 12736360.5 bytes/s, 24.3 transacciones por segundo
1048576 bytes: 11944777.9 bytes/s, 11.4 transacciones por segundo
[root@XXX /]# zpool attach datos2 c4t0d0s1 c4t1d0s1
[root@XXX /]# zpool status datos2
  pool: datos2
 state: ONLINE
 scan: resilvered 0 in 0h0m with 0 errors on Fri Jan  6 04:10:11 2012
config:

        NAME                          STATE     READ WRITE CKSUM
        datos2                        ONLINE       0     0     0
          /dev/zvol/dsk/datos/prueba  ONLINE       0     0     0
        logs
          mirror-1                    ONLINE       0     0     0
            c4t0d0s1                  ONLINE       0     0     0
            c4t1d0s1                  ONLINE       0     0     0

errors: No known data errors
[root@XXX datos2]# python rendimiento_ZIL.py
1 bytes: 4510.6 bytes/s, 4510.6 transacciones por segundo
2 bytes: 8916.0 bytes/s, 4458.0 transacciones por segundo
4 bytes: 18884.8 bytes/s, 4721.2 transacciones por segundo
8 bytes: 37616.0 bytes/s, 4702.0 transacciones por segundo
16 bytes: 73822.1 bytes/s, 4613.9 transacciones por segundo
32 bytes: 140470.9 bytes/s, 4389.7 transacciones por segundo
64 bytes: 284437.2 bytes/s, 4444.3 transacciones por segundo
128 bytes: 601880.1 bytes/s, 4702.2 transacciones por segundo
256 bytes: 1108514.7 bytes/s, 4330.1 transacciones por segundo
512 bytes: 2371158.3 bytes/s, 4631.2 transacciones por segundo
1024 bytes: 4550019.3 bytes/s, 4443.4 transacciones por segundo
2048 bytes: 9164521.8 bytes/s, 4474.9 transacciones por segundo
4096 bytes: 13783347.7 bytes/s, 3365.1 transacciones por segundo
8192 bytes: 25303757.2 bytes/s, 3088.8 transacciones por segundo
16384 bytes: 5225476.4 bytes/s, 318.9 transacciones por segundo
32768 bytes: 34207540.9 bytes/s, 1043.9 transacciones por segundo
65536 bytes: 4882781.9 bytes/s, 74.5 transacciones por segundo
131072 bytes: 36812441.0 bytes/s, 280.9 transacciones por segundo
262144 bytes: 4338026.7 bytes/s, 16.5 transacciones por segundo
524288 bytes: 20352716.7 bytes/s, 38.8 transacciones por segundo
1048576 bytes: 17280271.9 bytes/s, 16.5 transacciones por segundo

Advertimos varias cosas:

  • ¡La mejora del rendimiento con el ZIL en una SSD es de 50:1!.

  • Cuando el tamaño de los datos grabados de forma síncrona es grande, la estrategia ZIL cambia y el rendimiento baja mucho, aunque sigue siendo bastante mejor que el caso sin ZIL en una flash.

  • El hecho de poner el ZIL en un "mirror" penaliza el rendimiento de forma apreciable, en torno al 10% para un "mirror" de dos elementos.

  • En todas las medidas de prestaciones del ZIL que he mostrado en este documento, los resultados son un tanto erráticos. Ello es porque se trata de un sistema en producción, con bastante actividad de disco, incluyendo escrituras síncronas. Las medidas no pretenden ser exactas.

Ahora toca pensar en separar el ZPOOL en dos: "sistema" y "datos", para poder beneficiarme al 100% de esta tecnología...

Problema solucionado: Solaris 10 y ZFS: Separación de un ZPOOL unificado en producción, en ZPOOLs de"sistema" y "datos".

La adición del ZIL en "mirror" en simple:

[root@XXX /]# zpool add datos log mirror c4t0d0s1 c4t1d0s1
Veamos el rendimiento. Recordemos que la máquina está en producción con bastante actividad, así que el resultado es errático pero representativo:
1 bytes: 4329.8 bytes/s, 4329.8 transacciones por segundo
2 bytes: 8959.7 bytes/s, 4479.9 transacciones por segundo
4 bytes: 16496.5 bytes/s, 4124.1 transacciones por segundo
8 bytes: 33868.1 bytes/s, 4233.5 transacciones por segundo
16 bytes: 63228.8 bytes/s, 3951.8 transacciones por segundo
32 bytes: 126826.2 bytes/s, 3963.3 transacciones por segundo
64 bytes: 245802.5 bytes/s, 3840.7 transacciones por segundo
128 bytes: 509778.8 bytes/s, 3982.6 transacciones por segundo
256 bytes: 1089404.4 bytes/s, 4255.5 transacciones por segundo
512 bytes: 1870344.2 bytes/s, 3653.0 transacciones por segundo
1024 bytes: 3845308.7 bytes/s, 3755.2 transacciones por segundo
2048 bytes: 8500152.5 bytes/s, 4150.5 transacciones por segundo
4096 bytes: 12163273.7 bytes/s, 2969.5 transacciones por segundo
8192 bytes: 19263422.3 bytes/s, 2351.5 transacciones por segundo
16384 bytes: 24046256.0 bytes/s, 1467.7 transacciones por segundo
32768 bytes: 20473232.0 bytes/s, 624.8 transacciones por segundo
65536 bytes: 11248985.6 bytes/s, 171.6 transacciones por segundo
131072 bytes: 17431207.5 bytes/s, 133.0 transacciones por segundo
262144 bytes: 21972560.3 bytes/s, 83.8 transacciones por segundo
524288 bytes: 25369483.4 bytes/s, 48.4 transacciones por segundo
1048576 bytes: 25659356.7 bytes/s, 24.5 transacciones por segundo

Más información:

ERRATA: El L2ARC no se llena con lo que desborda del ARC de forma síncrona, sino que hay un hilo "feeder" que se ejecuta de vez en cuando y revisa los extremos de las colas ARC, es decir los objetos que posiblemente se tiren pronto, y los copia en el L2ARC. Por tanto, no todo lo que sale del ARC acaba en el L2ARC, y bajo extrema presión en el ARC, el L2ARC no se ve saturado. Controlando la frecuencia del "feeder", controlamos todo. Los detalles son interesantes.


Historia

  • 10/ene/12: Publicación del documento.

  • 07/ene/12: Documento cómo separar el "zpool" unificado en dos: "sistema" y "datos".

  • 05/ene/12: Primera versión de esta página.



Python Zope ©2012 jcea@jcea.es

Más información sobre los OpenBadges

Donación BitCoin: 19niBN42ac2pqDQFx6GJZxry2JQSFvwAfS