Backup con snapshot utilizando un script Bash y Rsync

La gestión de backup siempre me ha llevado mucho tiempo. He probado muchos programas sin terminar de encontrar ninguno que responda a mis necesidades y que sea sencillo de utilizar. En general, los programas que he utilizado suelen utilizar rsync por debajo, por lo que tenía ganas de trastear con esta herramienta.

Requisitos

Mis requisitos son:

  • Que cada backup sea un snapshot de los datos en el momento en que se realizó, pero sin tener que realizar todos los backups completos.
  • Backup totalmente automatizado
  • Posibilidad de especificar cada cuantos backups se hace uno completo.
  • Posibilidad de especificar un número de backups a almacenar y que se borren automáticamente los backups antiguos.
  • Volcado de información a un log para que sea sencillo comprobar que todo funciona correctamente.

Tipos de backup

Como recordatorio, indicar que existen que los tipos de backup más conocidos son completo, incremental y diferencial.

Un backup completo almacena toda la información que se desea salvaguardar. Lógicamente la restauración es sencilla, pero tiene el inconveniente de que cada vez que se realiza un backup el espacio necesario en disco es igual al de los datos originales, con lo que es muy poco eficiente.

Un backup incremental copia únicamente los archivos que han cambiado desde el anterior backup. Por ello es muy eficiente en términos de espacio en disco, pero tiene la desventaja de que la restauración de datos es más complicada, ya que hay que restaurar el último backup completo y a continuación cada backup incremental hasta llegar al último.

El backup diferencial trata de minimizar este problema. Un backup diferencial almacena los archivos que han cambiado desde el último backup completo. De este modo, si los últimos backup son éstos:

  • Completo (lunes).
  • Diferencial (martes).
  • Diferencial (miércoles).
  • Diferencial (jueves).

Y el viernes se desea restaurar el backup más reciente, basta con restaurar el del lunes y a continuación el del jueves.

El backup diferencial está bastante bien, pero como he indicado antes el objetivo era tener una sola carpeta con el estado de los datos cada vez que se hizo una copia (aunque sin el coste en espacio que tiene el backup completo).

Herramientas: bash, rsync y enlaces duros

Tras probar herramientas como Deja Dup o Simple Backup, me decidí a dedicar unas horas a programar un script que se comporte exactamente como quiero. Decidí usar Bash como lenguaje de scripting y rsync como herramienta principal por su uso extendido para este tipo de tareas.

Investigando rsync descubrí que tiene una opción "--link-dest" que permite al hacer un backup incremental añadir enlaces duros a los archivos que no han cambiado. Así, el backup incremental tiene la apariencia de backup completo. Por lo tanto, basta con indicar el último backup completo a la hora de hacer un backup incremental y se obtendrá un snapshot perfecto de los datos.

Script

El boceto de la funcionalidad es la siguiente:

  1. Obtener los directorios de origen y destino via argumentos.
  2. Examinar backups previos.
  3. Si procede, eliminar backups antiguos.
  4. Decidir si el próximo backup es completo o incremental.
  5. Realizar el backup.

Y aquí está el código. Puede que siga puliéndolo en el futuro, por lo que si lo utilizáis echadle un vistazo a mi github por si he hecho algún cambio.

#!/bin/bash

############################################################################
# Simple backup script. It uses complete and incremental backups, with #
# hard links to simulate snapshots. $FULL_BACKUP_LIMIT controls the #
# frecuency of full backups.It accepts at least one source directory and a #
# single destination directory as arguments. Usage: #
# #
# incremental_backup.sh SOURCE_DIRECTORY_1 [SOURCE_DIRECTORY_2..N] #
# DESTINATION_DIRECTORY #
# #
# #
# Author: Álvaro Reig González #
# Licence: GNU GLPv3 # #
# www.alvaroreig.com #
# https://github.com/alvaroreig #
############################################################################

DATE=`date +%Y%m%d`
TIMESTAMP=$(date +%m%d%y%H%M%S)
FULL_BACKUP_STRING=backup-full-$DATE-$TIMESTAMP
INC_BACKUP_STRING=backup-inc-$DATE-$TIMESTAMP
FULL_BACKUP_LIMIT=6
BACKUPS_TO_KEEP=21
EXCLUSSIONS="--exclude .cache/ --exclude .thumbnails/ --exclude .gvfs"
OPTIONS="-h -ab --stats"
#To test the script, include "-n" to perform a 'dry' rsync
LOG=/var/log/backup.log

############################################################################
# Arguments processing. The last argument is the destination directory, the#
# previous arguments are the source[s] directory[ies] #
############################################################################

ARGS=("$@")

if [ ${#ARGS[*]} -lt 2 ]; then
echo "At least two arguments are needed" >> $LOG
  echo "Usage: bash incremental_backup [SOURCE_DIR_1]...[SOURCE_DIR_N] [DESTINATION_DIR]" >> $LOG
  exit;
else

  #Store the destination directory
  DEST_DIR=${ARGS[${#ARGS[*]}-1]}

  #Store the first source directory
  SOURCE_DIRS=${ARGS[0]}
  let LAST_SOURCE_POSITION=${#ARGS[*]}-2
  SOURCE_COUNTER=1
  
  #Store the next source directories
  while [ $SOURCE_COUNTER -le $LAST_SOURCE_POSITION ]; do
CURRENT_SOURCE_DIR=${ARGS[$SOURCE_COUNTER]-1]}
    let SOURCE_COUNTER=SOURCE_COUNTER+1
    SOURCE_DIRS=$SOURCE_DIRS" "$CURRENT_SOURCE_DIR
  done

echo "" >> $LOG
  echo "" >> $LOG
  echo "[" `date +%Y-%m-%d_%R` "]" "###### Starting backup #######" >> $LOG
  echo "[" `date +%Y-%m-%d_%R` "]" "Directories to backup" $SOURCE_DIRS >> $LOG
  echo "[" `date +%Y-%m-%d_%R` "]" "Destination directory" $DEST_DIR >> $LOG
  echo "[" `date +%Y-%m-%d_%R` "]" "Limit to full backup:" $FULL_BACKUP_LIMIT >> $LOG
  echo "[" `date +%Y-%m-%d_%R` "]" "Backups to keep:" $BACKUPS_TO_KEEP >> $LOG
  echo "[" `date +%Y-%m-%d_%R` "]" "Exclussions:" $EXCLUSSIONS >> $LOG
  echo "[" `date +%Y-%m-%d_%R` "]" "###### Browsing previous backups ######" >> $LOG
fi

############################################################################
# Browse previous backups #
############################################################################
BACKUPS=`ls -t $DEST_DIR |grep backup-`
BACKUP_COUNTER=0
BACKUPS_LIST=()

for x in $BACKUPS
do
BACKUPS_LIST[$BACKUP_COUNTER]="$x"
    echo "[" `date +%Y-%m-%d_%R` "]" "backup detected:" ${BACKUPS_LIST[$BACKUP_COUNTER]} >> $LOG
    let BACKUP_COUNTER=BACKUP_COUNTER+1
    
done

############################################################################
# Delete old backups, if necessary #
############################################################################

echo "[" `date +%Y-%m-%d_%R` "]" "###### Deleting old backups ######" >> $LOG
echo "[" `date +%Y-%m-%d_%R` "]" "Number of previous backups: " ${#BACKUPS_LIST[*]} >> $LOG
echo "[" `date +%Y-%m-%d_%R` "]" "Backups to keep:" $BACKUPS_TO_KEEP >> $LOG

###
if [ $BACKUPS_TO_KEEP -lt ${#BACKUPS_LIST[*]} ]; then
let BACKUPS_TO_DELETE=${#BACKUPS_LIST[*]}-$BACKUPS_TO_KEEP
  echo "[" `date +%Y-%m-%d_%R` "]" "Need to delete" $BACKUPS_TO_DELETE" backups" $BACKUPS_TO_DELETE >> $LOG

  while [ $BACKUPS_TO_DELETE -gt 0 ]; do
BACKUP=${BACKUPS_LIST[${#BACKUPS_LIST[*]}-1]}
    unset BACKUPS_LIST[${#BACKUPS_LIST[*]}-1]
    echo "[" `date +%Y-%m-%d_%R` "]" "Backup to delete:" $BACKUP >> $LOG
    rm -rf $DEST_DIR"/"$BACKUP >> $LOG
    if [ $? -ne 0 ]; then
echo "[" `date +%Y-%m-%d_%R` "]" "####### Error while deleting backup #######" >> $LOG
    else
echo "[" `date +%Y-%m-%d_%R` "]" "Backup correctly deleted" >> $LOG
    fi
let BACKUPS_TO_DELETE=BACKUPS_TO_DELETE-1
  done
else
echo "[" `date +%Y-%m-%d_%R` "]" "No need to delete backups" >> $LOG
fi


############################################################################
# The next backup will be complete if there is no full backup in the last #
# FULL_BACKUP_LIMIT backups. If it is incremental, the last full backup #
# will be used as a reference for the "--link-dest" option #
############################################################################

NEXT_BACKUP_FULL=true
COUNTER=0
LAST_FULL_BACKUP=

echo "[" `date +%Y-%m-%d_%R` "]" "###### Performing the backup ######" >> $LOG

while [[ $COUNTER -lt $FULL_BACKUP_LIMIT && $COUNTER -lt ${#BACKUPS_LIST[*]} ]]; do
if [[ ${BACKUPS_LIST[$COUNTER]} == *full* ]]; then
NEXT_BACKUP_FULL=false;
   LAST_FULL_BACKUP=${BACKUPS_LIST[$COUNTER]}
    echo "[" `date +%Y-%m-%d_%R` "]" "A full backup was performed" $COUNTER "backups ago which is less that the specified limit of" $FULL_BACKUP_LIMIT>> $LOG
   break;
  fi
let COUNTER=COUNTER+1
done

############################################################################
# Finally, the backup is performed #
############################################################################

if [ $NEXT_BACKUP_FULL == true ]; then
echo "[" `date +%Y-%m-%d_%R` "]" "The backup will be full" >> $LOG
rsync $OPTIONS $EXCLUSSIONS $SOURCE_DIRS $DEST_DIR/$FULL_BACKUP_STRING >> $LOG
else
echo "[" `date +%Y-%m-%d_%R` "]" "The backup will be incremental" >> $LOG
rsync $OPTIONS $EXCLUSSIONS --link-dest=$DEST_DIR/$LAST_FULL_BACKUP $SOURCE_DIRS $DEST_DIR/$INC_BACKUP_STRING >> $LOG
  $COM >> $LOG
fi

############################################################################
# Log the backup status #
############################################################################

if [ $? -ne 0 ]; then
echo "[" `date +%Y-%m-%d_%R` "]" "####### Error during the backup. Please execute the script with the -v flag #######" >> $LOG
  echo "" >> $LOG
  echo "" >> $LOG
else
echo "[" `date +%Y-%m-%d_%R` "]" "####### Backup correct #######" >> $LOG
  echo "" >> $LOG
  echo "" >> $LOG
fi

Recursos

www.mikerubel.org
Publicado en Herramientas Etiquetado con: , , ,
3 Comentarios en “Backup con snapshot utilizando un script Bash y Rsync
  1. Antonio dice:

    Muchas gracias
    Script estupendo. Con lo difícil que resulta Rsync para los que no tenemos mucha idea, has conseguido que sea fácil su uso con este script.

    La modificación que has hecho en github añadiendo notify, en mi servidor Centos no funciona, a pesar de tener instaladas las dependencias, así que he vuelto a esta versión con el archivo log.

    Como mi idea inicial era tener una copia funcional del sistema, leyendo otros artículos mis opciones finales han sido “rsync -abhxAXE –stats / /Backup”. Con -H me da errores por lo que lo quité (como he dicho, no soy muy sabio en la materia)

    Pero quería hacer una aportación que leí sobre las bases de datos (siento no poner la referencia ya que no guardé el enlace en favoritos):
    “Como Rsync es una copia de seguridad a nivel de archivo y los motores de bases de datos están constantemente haciendo cambios en los archivos de base de datos a nivel de bloque, la copia de seguridad podría ser incoherente y posiblemente incluso inutilizable.
    Para evitarlo, después de hacer el backup re-sincronizamos los archivos de bases de datos apagando previamente el motor de base de datos y volviéndolo a encenderlo, para que los archivos no están cambiando durante la copia de seguridad.

    Por tanto después de acabada la copia detén mysql en el equipo de origen y sincronizar las bases de datos para prevenir la corrupción y vuelve a encender mysql:
    Service mysqld stop
    rsync -abhxAXE –stats /var/lib/mysql/* /backup/var/lib/mysql
    Service mysqld start

    Seguro que a ti se te ocurre algo mejor, así que estaré atento a si haces algún cambio en el script.
    MUCHAS GRACIAS

    • Hola Antonio,

      Disculpa el retraso, tenía un problema con el envío de notificaciones y no he visto tu comentario hasta ahora.

      La parte de notify la metí para disponer de mensajes emergentes en un equipo de sobremesa (Linux Mint). No conozco muy bien la herramienta, pero creo que requiere de un entorno gráfico para funcionar, por lo que tiene sentido que no te funciuone en un servidor.

      Gracias por el aporte de los índices de BBDD, la verdad es que no tenía ni idea.

      Un saludo,

  2. C C C dice:

    Buenos Dias Alvaro, he instalado tu Script en un servidor CentOS 6.2 y ha funcionado perfectamente. Queria agradecer tu esfuerzo y que la licencia del mismo sea GPL.

    Mucha suerte y gracias una vez mas.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*