Borrar snapshots ZFS de forma automática y selectiva

Lo fácil y barato que resulta crear snapshots en ZFS tiene el efecto secundario de que los usemos constantemente y que, tarde o temprano, llenemos el disco duro y tengamos que hacer limpieza.

Limpieza...

Decidir qué borrar y qué conservar siempre es complicado. En mi caso hago snapshots varias veces por semana, al menos cada vez que hago un backup diario y cuando instalo software nuevo o reconfiguro un servicio en profundidad. A la hora de hacer limpieza mi criterio personal es el siguiente:

  • Salvo casos extremos, mantengo todos los snapshots hechos este año.
  • Para años previos, mantengo un snapshot al mes. No elijo cual conservo sino que lo hago al revés: De todos los snapshots de un mes dado, elijo cual me cargo. Repito el proceso hasta que solo me queda uno.
  • Puntualmente puedo conservar snapshots estratégicos. Por ejemplo, un snapshot inmediatamente previo a una reestructuración masiva del sistema de correo electrónico. Esto sirve tanto como medida de protección (siempre puedo volver a ese snapshot con un comando si las cosas se complican de verdad) como para conservar indefinidamente una copia del viejo estado del sistema como referencia futura.

¿Qué snapshot elimino? Intento borrar el snapshot que va a liberar más espacio. Esto no es trivial de evaluar en ZFS cuando vamos a borrar más de un snapshot.

Cuando visualizas la información de un snapshot ZFS puedes ver algo de este estilo:

$ sudo zfs list -r -t snapshot sistema
NAME                                 USED  AVAIL  REFER  MOUNTPOINT
sistema@20110813-06:18                51K      -   170M  -
sistema@20110928-06:38                 1K      -   182M  -
sistema@20111003-02:08                 1K      -   182M  -
sistema@20111101-01:12               552K      -  12.6M  -
sistema@20111201-00:49               550K      -  12.6M  -

La columna REFER indica cuántos datos son accesibles a través del snapshot, pero lo que nos interesa es USED. USED nos muestra cuántos datos son exclusivos a este snapshot. Es decir, cuánto disco recuperamos si borramos ese snapshot.

El problema es que USED indica cuántos datos son exclusivos de ese snapshot en concreto, pero esta cifra puede variar a medida que borramos otros snapshots.

Veamos un ejemplo: supongamos que tenemos un pool ZFS. Copiamos un fichero grande dentro y hacemos un snapshot. Si ahora borramos ese fichero, el campo USED del snapshot medirá lo mismo que el fichero. Es decir, el fichero está referenciado por el snapshot, retenido ahí. Ese espacio estará ocupado por el snapshot y el campo USED nos indica claramente que si borramos el snapshot recuperaremos ese espacio. Hasta aquí, bien.

Veamos un segundo caso: tenemos un pool ZFS y copiamos un fichero grande dentro. Vale. Ahora hacemos DOS snapshots. ¿Cuánto miden sus campos USED? Pues cero. Esto nos indica que si borramos uno de los snapshots el espacio ocupado por el fichero no se liberará porque seguirá presente en el otro snapshots. La cosa será algo así:

$ sudo zfs list -r -t snapshot sistema
NAME                        USED  AVAIL  REFER  MOUNTPOINT
[..]
sistema@snapshot_1             0      -  3.66G  -
sistema@snapshot_2             0      -  3.66G  -

Obsérvese que el campo USED es cero. Recordemos que esto es así porque USED nos indica cuántos datos están retenidos exclusivamente por este snapshot. El fichero largo está en dos snapshots diferentes.

Supongamos ahora que borramos uno de los dos snapshots al azar. Por ejemplo:

$ sudo zfs destroy sistema@snapshot_1
$ sudo zfs list -r -t snapshot sistema
NAME                        USED  AVAIL  REFER  MOUNTPOINT
[..]
sistema@snapshot_2         66.1M      -  3.66G  -

Dado que ahora el fichero solo está en un snapshot, el campo USED refleja el espacio que se liberará al borrar ese snapshot. El efecto hubiera sido el mismo si en vez de haber borrado el snapshot sistema@snapshot_1 hubiéramos borrado el snapshot sistema@snapshot_2.

El detalle importante que hay que recordar es que USED indica el espacio que se va a liberar inmediatamente si se borrase ese snapshot. Borrándolo podemos afectar los valores del campo USED del resto de snapshots, como ha sido el caso en el ejemplo.

Es decir, si lo que nos interesa es recuperar el mayor espacio posible borrando snapshots, no podemos revisar sus campos USED y quedarnos solo con el snapshot que tenga el valor más pequeño. Lo que hay que hacer es eliminar primero el snapshot que tenga el campo USED más grande y luego volver a revisar todos los campos USED otra vez porque sus valores habrán cambiado (posiblemente).

Esta estrategia no es perfecta porque cambiar el orden de borrado puede modificar el snapshot con el que nos quedamos y cuánto espacio recuperamos finalmente. Pero la única forma de hacerlo bien sería analizar los metadatos del disco con comandos como zdb y eso es bastante overkill para lo que queremos hacer. Digamos que se trata de una solución de compromiso.

El código que yo uso es el siguiente:

 #!/bin/sh

 # (c)2015 Jesús Cea Avión - jcea@jcea.es - https://www.jcea.es/
 # This code is released as PUBLIC DOMAIN.

 snapshots=`zfs list -H -s used -r "$1" | fgrep "$1"@ | grep "$2"`

 echo "$snapshots"

 num_snapshots=`echo "$snapshots" | wc -l`

 if [ $num_snapshots = "1" ]
 then
   exit 0
 fi

 to_delete=`echo "$snapshots" | tail -1 | cut -f1`
 space=`echo "$snapshots" | tail -1 | cut -f2`
 echo
 echo Pulsa ENTER para borrar \"$to_delete\" \($space\) \(Control+C para abortar\)
 read ack
 if [ $? != 0 ]
 then
   exit 0
 fi

 zfs destroy "$to_delete"

El código se llama más o menos así:

$ sudo ./borrar_snapshots_menos_uno.sh FILESYSTEM GRUPO

donde FILESYSTEM es un sistema de ficheros ZFS y GRUPO es una búsqueda con la que agrupamos los snapshots que queremos analizar y de los cuales nos quedaremos solo uno. Por ejemplo, si nuestros snapshots tienen el formato @AñoMesDia, tendríamos que usar un GRUPO de @AñoMes si queremos quedarnos con uno al mes.

La línea 6 lista los snapshots del FILESYSTEM que queremos, ordenados por campo USED y limitados al GRUPO especificado. La línea 8 nos proporciona la lista por pantalla. En las líneas 10-15 miramos cuántos snapshots nos quedan y finalizamos la ejecución si solo hay uno.

Si no es así, mostramos por pantalla qué snapshot vamos a borrar y cuánto espacio recuperaremos (líneas 17-20). Damos al administrador la oportunidad de que confirme o cancele la operación en las líneas 21-25.

Si el administrador ha confirmado el borrado, lo hacemos efectivo en la línea 27.

Cada ejecución de este script borra el snapshots que va a liberar más espacio (el que tiene el campo USED más grande). Lo ejecutamos varias veces hasta que solo nos queda un snapshot. Se podría hacer un bucle automático [1], pero yo prefiero saber qué está pasando y poder intervenir en cada ciclo. Tú puedes modificar el código como mejor te convenga.

[1]

Puedes meter todo el script dentro de un

while [ true ]
do
[ EL SCRIPT ACTUAL ]
done

y eliminar la confirmación por teclado.

Una forma más limpia sería añadir un parámetro en la línea de comando.

Veamos un caso real: tengo un dataset ZFS llamado datos/backups/diario/datos con cientos de snapshots tomados en 2013 y queremos quedarnos con uno solo como referencia para el año entero [2]. Podríamos hacer algo así:

$ sudo ./borrar_snapshots_menos_uno.sh datos/backups/diario/datos @2013
[...]
datos/backups/diario/datos@20130205-03:13       240M    -       2.22G   -
datos/backups/diario/datos@20130302-15:33       346M    -       2.52G   -
datos/backups/diario/datos@20130219-10:20       1.40G   -       3.59G   -
datos/backups/diario/datos@20130214-03:39       6.65G   -       8.70G   -

Pulsa ENTER para borrar "datos/backups/diario/datos@20130214-03:39" (6.65G) (Control+C para abortar)

$ sudo ./borrar_snapshots_menos_uno.sh datos/backups/diario/datos @2013
[...]
datos/backups/diario/datos@20131210-16:09       149M    -       2.17G   -
datos/backups/diario/datos@20130205-03:13       240M    -       2.22G   -
datos/backups/diario/datos@20130302-15:33       346M    -       2.52G   -
datos/backups/diario/datos@20130219-10:20       1.40G   -       3.59G   -

Pulsa ENTER para borrar "datos/backups/diario/datos@20130219-10:20" (1.40G) (Control+C para abortar)

$ sudo ./borrar_snapshots_menos_uno.sh datos/backups/diario/datos @2013
[...]
datos/backups/diario/datos@20130702-23:22       103M    -       2.37G   -
datos/backups/diario/datos@20131210-16:09       149M    -       2.17G   -
datos/backups/diario/datos@20130205-03:13       240M    -       2.22G   -
datos/backups/diario/datos@20130302-15:33       346M    -       2.52G   -

Pulsa ENTER para borrar "datos/backups/diario/datos@20130302-15:33" (346M) (Control+C para abortar)

$ sudo ./borrar_snapshots_menos_uno.sh datos/backups/diario/datos @2013
[...]
datos/backups/diario/datos@20130702-23:22       103M    -       2.37G   -
datos/backups/diario/datos@20131210-16:09       149M    -       2.17G   -
datos/backups/diario/datos@20130205-03:13       240M    -       2.22G   -

Pulsa ENTER para borrar "datos/backups/diario/datos@20130205-03:13" (240M) (Control+C para abortar)

$ sudo ./borrar_snapshots_menos_uno.sh datos/backups/diario/datos @2013
[...]
datos/backups/diario/datos@20130702-23:22       103M    -       2.37G   -
datos/backups/diario/datos@20130208-12:58       141M    -       2.11G   -  <=*OJO*
datos/backups/diario/datos@20131210-16:09       149M    -       2.17G   -

Pulsa ENTER para borrar "datos/backups/diario/datos@20131210-16:09" (149M) (Control+C para abortar)

El patrón de borrado depende de las características exactas de los cambios del sistema de ficheros ZFS, pero observa, por ejemplo, lo que ocurre entre la penúltima y la última ejecución mostradas. De repente, entre los snapshots datos/backups/diario/datos@20130702-23:22 y datos/backups/diario/datos@20131210-16:09 aparece uno nuevo, el datos/backups/diario/datos@20130208-12:58 con 141 Megabytes a liberar. En el paso anterior ese snapshot tenía un campo USED de 77Mbytes, pero al borrar el snapshot datos/backups/diario/datos@20130205-03:13 se modificó el espacio de los otros snapshots y el orden de borrado localmente óptimo cambió.

Por eso es preferible ejecutar el script varias veces y liberar el snapshot con el campo USED más alto en cada ocasión, en vez de simplemente listar todos los snapshots en orden de campo USED, quedarnos con el primero (el más pequeño) y eliminar todos los demás.

De hecho este reajuste de campos USED al ir borrando snapshots suele ser más frecuente y llamativo a medida que nos vamos quedando con menos. Por ejemplo, si tenemos cuatro snapshots con un campo USED de 50Megabytes cada uno, es bastante frecuente liberar bastante más de 4*50=200 Megabytes si borramos los cuatro. Entendiendo cómo funciona, es evidente que el campo USED se queda igual o crece cuando se borran otros snapshots. Nunca decrece.

Si ahora quisiéramos quedarnos con un único snapshot de enero de 2014, podríamos usar un GRUPO de @201401. Algo como:

$ sudo ./borrar_snapshots_menos_uno.sh datos/backups/diario/datos @201401
[...]
datos/backups/diario/datos@20140108-23:48       11.9M   -       2.32G   -
datos/backups/diario/datos@20140122-01:04       11.9M   -       2.35G   -
datos/backups/diario/datos@20140110-03:13       14.2M   -       2.32G   -
datos/backups/diario/datos@20140122-01:18       20.1M   -       2.36G   -
datos/backups/diario/datos@20140113-20:02       48.4M   -       2.33G   -
datos/backups/diario/datos@20140115-00:39       49.4M   -       2.33G   -
datos/backups/diario/datos@20140108-00:54       50.1M   -       2.32G   -
datos/backups/diario/datos@20140129-00:50       68.3M   -       2.28G   -
datos/backups/diario/datos@20140104-22:13       73.0M   -       2.32G   -

Pulsa ENTER para borrar "datos/backups/diario/datos@20140104-22:13" (73.0M) (Control+C para abortar)

Es de notar que agrupaciones como @2013 o @201401 funcionan debido a la forma (¡cuidadosa!) en que nombro los snapshots. La búsqueda no se realiza por fecha sino por nombre de los snapshots. Por ejemplo, tengo nombres como:

  • datos/backups/diario/datos@20130204-15:56-ANTES_MIGRACION_UBUNTU_12_04_2.
  • datos/usr_local@20150720-15:33-antes_de_GCC_5.2.0.
  • datos/usr_local@20140915-03:31_tras_migrar_a_imap4.
  • datos/usr_local@20150417-antes_de_postfix_2.10.

¡¡El nombre que das a los snapshots es importante!!

[2]

En realidad me he quedado con dos snapshots para 2013:

$ sudo zfs list -r datos/backups/diario/datos | grep -i 2013
datos/backups/diario/datos@20130204-15:56-ANTES_MIGRACION_UBUNTU_12_04_2  90.6M      -  1.64G  -
datos/backups/diario/datos@20130425-18:36                                  982M      -  2.14G  -

Me quedo con un snapshot del sistema justo antes de haberlo actualizado a Ubuntu 12.04.2 por si en algún momento me interesase echarle un vistazo a cómo tenía todo antes.

Luego me quedo con otro snapshot posterior como una foto representativa del sistema durante 2013.

Por supuesto tengo snapshots posteriores de 2014 y de 2015.