Cuidado al activar "userobj_accounting" en ZFS

Tras escribir Actualización de características en un sistema de ficheros ZFS (20200528) y decidir que todas mis máquinas con ZFS eran estables y no necesitaban la seguridad de poder volver a una versión previa del Sistema Operativo, me puse a actualizar mis ZPOOLs ZFS con todas las características opcionales disponibles.

La actualización fue simple, rápida e indolora salvo en uno de mis servidores. En ese servidor la operación se eternizó, con una carga de CPU muy elevada, a pesar de que la actividad de disco era relativamente baja.

De todas las características opcionales activadas, la única potencialmente costosa es userobj_accounting, porque requiere repasar los metadatos de todos los archivos y directorios en el ZPOOL.

La extrema carga de CPU en ese servidor es evidente:

top - 02:11:43 up 16 min,  1 user,  load average: 69.54, 61.05, 34.18
Tasks: 298 total,  66 running, 171 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us, 99.5 sy,  0.0 ni,  0.0 id,  0.3 wa,  0.0 hi,  0.2 si,  0.0 st
KiB Mem :  1994684 total,   240660 free,  1568512 used,   185512 buff/cache
KiB Swap:        0 total,        0 free,        0 used.   279708 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
18438 root      20   0       0      0      0 R   3.6  0.0   0:00.74 arc_prune
18603 root      20   0       0      0      0 R   3.6  0.0   0:00.15 arc_prune
18630 root      20   0       0      0      0 R   3.3  0.0   0:18.67 arc_prune
18461 root      20   0       0      0      0 R   3.3  0.0   0:00.63 arc_prune
18483 root      20   0       0      0      0 R   3.3  0.0   0:00.58 arc_prune
18507 root      20   0       0      0      0 R   3.3  0.0   0:00.42 arc_prune
18522 root      20   0       0      0      0 R   3.3  0.0   0:00.37 arc_prune
18528 root      20   0       0      0      0 R   3.3  0.0   0:00.36 arc_prune
18540 root      20   0       0      0      0 R   3.3  0.0   0:00.31 arc_prune
18548 root      20   0       0      0      0 R   3.3  0.0   0:00.28 arc_prune
18552 root      20   0       0      0      0 R   3.3  0.0   0:00.26 arc_prune
18553 root      20   0       0      0      0 R   3.3  0.0   0:00.26 arc_prune
18560 root      20   0       0      0      0 R   3.3  0.0   0:00.25 arc_prune
18563 root      20   0       0      0      0 R   3.3  0.0   0:00.24 arc_prune
18581 root      20   0       0      0      0 R   3.3  0.0   0:00.16 arc_prune
18589 root      20   0       0      0      0 R   3.3  0.0   0:00.15 arc_prune
18596 root      20   0       0      0      0 R   3.3  0.0   0:00.15 arc_prune
18599 root      20   0       0      0      0 R   3.3  0.0   0:00.15 arc_prune

Aquí vemos que la carga de CPU es muy elevada, casi 70, con más de un 99% del tiempo consumido en el Sistema Operativo por hilos ejecutando arc_prune. Buscando ese nombre en internet, aparece un enlace interesante: arc_prune: high load and soft lockups #6223.

Resumiendo: este servidor solo tiene dos gigabytes de RAM y revisar el ZPOOL para actualizar la información proporcionada con la característica opcional userobj_accounting necesita más memoria de trabajo.

Confirmemos que es el caso:

root@csi:~# cat /proc/spl/kstat/zfs/arcstats |grep -i dnode
dnode_size                      4    409194768
arc_dnode_limit                 4    76595865

Efectivamente, el uso de dnode_size supera con creces el valor de arc_dnode_limit. Los hilos arc_prune en el Sistema Operativo están esforzándose al máximo para mantener dnode_size bajo control. Eso consume casi toda la capacidad de la máquina, por lo que el progreso es penosamente lento.

Por defecto, esta caché está limitada al 10% de la RAM, pero podemos subirlo bastante (no demasiado, ya que pondría en peligro la estabilidad del Sistema Operativo). Veamos:

root@csi:~# cat /sys/module/zfs/parameters/zfs_arc_dnode_limit_percent
10
root@csi:~# echo 80 >/sys/module/zfs/parameters/zfs_arc_dnode_limit_percent
root@csi:~# cat /sys/module/zfs/parameters/zfs_arc_dnode_limit_percent
80

Ahora permitimos que esa caché ocupe hasta el 80% de la RAM.

A medida que la revisión del ZPOOL progresa, vamos viendo cómo va la cosa:

root@csi:~# cat /proc/spl/kstat/zfs/arcstats |i grep -i dnode
dnode_size                      4    421004864
arc_dnode_limit                 4    612766924
[..]
root@csi:~# cat /proc/spl/kstat/zfs/arcstats | grep -i dnode
dnode_size                      4    424327328
arc_dnode_limit                 4    612766924

Aquí vemos que ya no hay problemas con esta caché, pero el progreso sigue siendo desesperante. Veamos otras cachés ZFS:

root@csi:~# cat /proc/spl/kstat/zfs/arcstats |grep -i arc_meta
arc_meta_used                   4    1110475504
arc_meta_limit                  4    765958656
arc_meta_max                    4    1110984472
arc_meta_min                    4    16777216

Aquí estamos tropezándonos con el límite de otra caché. Intentemos subir su límite:

root@csi:~# cat /sys/module/zfs/parameters/zfs_arc_meta_limit_percent
75
root@csi:~# echo 90 >/sys/module/zfs/parameters/zfs_arc_meta_limit_percent

Aquí subimos el límite del tamaño de esta caché del 70% al 90% de la RAM.

La máquina colapsa. No tenemos suficiente RAM.

Segundo intento

Por suerte, los ZPOOLs en este servidor en concreto no se importan por defecto al reiniciar la máquina. Los importo manualmente cuando los necesito.

Revisando la documentación, podemos leer lo siguiente:

userobj_accounting

GUID        org.zfsonlinux:userobj_accounting
READ-ONLY COMPATIBLE        yes
DEPENDENCIES        extensible_dataset

This feature allows administrators to account the object usage
information by user and group.

This feature becomes active as soon as it is enabled and will
never return to being enabled. Each filesystem will be
upgraded automatically when remounted, or when new files are
created under that filesystem. The upgrade can also be started
manually on filesystems by running `zfs set version=current
<pool/fs>`. The upgrade process runs in the background and may
take a while to complete for filesystems containing a large
number of files.

En este texto hay un par de detalles de interés:

  1. La contabilidad de userobj_accounting se realiza dataset a dataset. Si pudiéramos actualizar los datasets uno por uno, evitamos la competencia de recursos entre ellos.
  2. Se puede actualizar un dataset dado montándolo o bien ejecutando el comando zfs set version=current <pool/fs>.

Los pasos, por lo tanto, resultan simples: Importamos los ZPOOL sin montar sus datasets y vamos realizando la contabilidad userobj_accounting uno a uno:

root@csi:~# zpool import -N blue
root@csi:~# zfs mount blue/blue_1
[.. esperamos a que la contabilidad termine ..]
root@csi:~# zfs mount blue/blue_2
[.. esperamos a que la contabilidad termine ..]
root@csi:~# zfs mount blue/blue_3
[.. esperamos a que la contabilidad termine ..]
root@csi:~# zfs mount blue/blue_4
[.. esperamos a que la contabilidad termine ..]

Cada uno de estos datasets contiene un cuarto de millón de ficheros. Podemos ir viendo cómo va el progreso en tiempo real:

root@csi:~# cat /proc/spl/kstat/zfs/arcstats | grep -e "arc_meta\|dnode"
dnode_size                      4    33766512
arc_meta_used                   4    116807216
arc_meta_limit                  4    765958656
arc_dnode_limit                 4    76595865
arc_meta_max                    4    122911392
arc_meta_min                    4    16777216

Aquí vemos que la máquina va cómoda. Examinemos la actividad de disco:

root@csi:~# zpool iostat -v 1
                capacity     operations     bandwidth
pool          alloc   free   read  write   read  write
------------  -----  -----  -----  -----  -----  -----
blue          3.51T   116G     22    100   144K   695K
  disco_blue  3.51T   116G     22    100   144K   695K
------------  -----  -----  -----  -----  -----  -----

Veamos cómo avanza la contabilidad de userobj_accounting:

root@csi:~# zfs groupspace blue
TYPE         NAME       USED  QUOTA  OBJUSED  OBJQUOTA
POSIX Group  root      2.24M   none      467      none
POSIX Group  www-data   512B   none        1      none

root@csi:~# zfs groupspace blue/blue_1
TYPE         NAME       USED  QUOTA  OBJUSED  OBJQUOTA
POSIX Group  www-data  1000G   none        -         -

Terminamos con el ZPOOL blue. Sin trashing, el proceso es bastante rápido (minutos). Perfecto.

Ahora nos queda hacer lo mismo con el ZPOOL datos. Los pasos son los mismos, pero aquí tenemos datasets con más ficheros y tendremos que subir los parámetros de las diferentes cachés como se indicó más arriba. Podemos ver si tenemos problemas con el comando iostat, por ejemplo, si tenemos muchas lecturas de disco pero pocas escrituras (o pocas lecturas pero sin escrituras). Estos son síntomas de trashing de las cachés.

Conclusiones

  1. Cuando se activa la característica opcional userobj_accounting en un ZPOOL ZFS, las necesidades de RAM durante el proceso de actualización son proporcionales al número de ficheros en el ZPOOL. En general, las necesidades son modestas, pero en mi caso tengo decenas de millones de ficheros en una máquina con solo dos gigabytes de RAM.

  2. En caso necesario, se puede migrar dataset a dataset. En ese caso, la necesidad de RAM es proporcional al número de ficheros en el dataset con más ficheros.

    No obstante si el ZPOOL forma parte del arranque del Sistema Operativo, en general no podremos importar el ZPOOL sin montar los datasets a menos que arranquemos la máquina en modo rescate o similar.

  3. En una situación de emergencia, configurar las cachés ZFS de forma más agresiva puede salvar el día. Tras el proceso podemos volver a dejarlas como estaban o mejor, si es posible, reiniciamos la máquina para asegurarnos de que todo va bien y la configuración vuelve a un estado "normal".

  4. En todo caso, todo esto solo es problema al activar esta característica opcional. Una vez que se completa la contabilidad de fondo, el coste de mantener los metadatos userobj_accounting actualizados es despreciable, por muchos ficheros que tengamos.