lunedì 26 agosto 2013

Un efficiente backup incrementale con il TAR

Pulizie d'autunno

Si avvicina il tempo dell'autunno ed è il momento di pulizie dei dischi e dell'ambiente circostante i dischi, cioè lo spazio vitale.

Tra i meandri più oscuri di tale spazio vitale, ho ritrovato decine di obsoleti CD e DVD e addirittura dei floppy disk, con tracce di antico codice TurboC scritto da giovane e di altissimo valore sentimentale, ormai semi irrecuperabili.

Ripensandoci mi è venuto in mente di salvare il salvabile da qualche parte, che non significa semplicemente salvarlo su un disco o una chiavetta a sua volta da dimenticare da qualche parte, in attesa che venga perso o cancellato, ma di organizzare un sistema di backup più modulare di quello normalmente usato per il disco e ampiamente discusso in passati articoli.

L'archiviatore ufficiale del mondo Linux come sappiamo è il Tape ARchiver che, dopo il passaggio per diverse strutture e versioni, è e sarà universalmente riconosciuto da tutti i sistemi operativi come uno standard.

Abbiamo anche visto come col tar , grazie al fatto che non si tratta di un semplice archiviatore ma di un serializzatore di oggetti del File System, sia praticamente possibile far di tutto, compreso far volare un pinguino da un computer all'altro.

Qui analizzeremo il tar dal punto di vista del backup incrementale superando alcune sostanziali inadeguatezze e costruendo uno script particolarmente interessante per chiunque voglia memorizzare i propri progetti, anche in modo incrementale, in un singolo file portabile e standard.

Come funziona il tar incrementale ?

Non è mia intenzione causare reazioni eccessive e rabbiose da parte dei puristi dei comandi *nix e nemmeno elevarmi al di sopra delle parti e giudicare severamente strumenti in uso da decenni, ma mi sento di dire che l'utilizzo di tar come sistema di backup incrementale è tecnicamente "un casino".

Sia chiaro che questo è dovuto all'estrema versatilità del comando tar e delle infinite possibilità che ci offre e grazie alle quali vedremo come risolvere parte dei nostri problemi, ma ciò non toglie che il suo comportamento standard sia piuttosto complesso da capire, tanto che molti articoli su Internet, pretendono di spiegarlo ma non sono del tutto corretti.

Vediamo allora di spiegarlo correttamente, cercando di capirne il funzionamento.

Il tar come ho detto, è un semplice serializzatore di oggetti del File System, quindi scansiona i rami del File System e scrive in uno stream una sequenza, che può essere recuperata in modo da ricostruire altrove e/o poi gli stessi rami. L'archiviatore in sé, non comprime ma passa lo stream ad un semplice filtro.

Questo porta ad innumerevoli vantaggi in fatto di versatilità ma anche come fattore di compressione, specie quando si ha a che fare con molti files di testo in quanto, a differenza per esempio da un semplice zip, dove i files sono compressi singolarmente e poi aggiunti, qui i files sono compressi nella globalità e considerato che il buffer di un bzip2 (opzione best) è di circa 1M, consente di scendere ad un entropia infinitesimale.

Per contro si deve dire che a differenza di uno zip, una volta che si ha il prodotto compresso, non è possibile modificarlo (aggiungere, togliere o fare update), senza prima decomprimerlo e ciò può essere molto scomodo se non si ha abbastanza spazio.

Per quanto riguarda il backup incrementale il tar offre un semplice flag denominato --listed-incremental, da non confondersi con --incremental che è riferito al vecchio sistema, e già il fatto che siano due fa capire che tale vicenda è molto travagliata.

Capire cosa fa esattamente questo flag che deve essere usato con un nome file, cioè come :

--listed-incremental=<file>

è importante, perché molti lo trattano come se compisse mirabili operazioni nascoste.

In realtà produce una semplice lista, che non è nemmeno in formato testo ma è una specie di dump binario delle strutture indice, e non dà quasi nessuna indicazione sul file salvato.

È in sostanza una lista simile a quella che si potrebbe ricavare col un semplice find anche se con qualche controllo in più.

Fase 1 - Il primo tarball 

Consideriamo l'ipotetica cartella "saveme".

Per eseguire un tar incrementale si può procedere in questo modo :

tar cvfz saveme-20130826123545.tgz \
     --listed-incremental=saveme-20130826123545.idx \
     saveme


Il prodotto della suddetta operazione saranno ben due files :

saveme-20130826123545.tgz
saveme-20130826123545.idx

Il primo contenente il vero tarball e il secondo con l'indice dei files o lo stato del file system o in qualsiasi altro modo volete chiamarlo.

Per chi se lo chiedesse il valore 20130826123545 si ricava così :

date +%Y%m%d%H%M%S

ed è una comodissima "data ordinabile".

Fase 2 - Aggiornamento

Ipotizziamo di fare alcune modifiche alla cartella "saveme", aggiungendo, spostando e togliendo files.

Per aggiornare il backup ci serve il vecchio indice, cioè l'ultimo che abbiamo salvato.

In caso non volessimo tenere una storia di tutto il file system, ma usare un semplice backup incrementale, allora potremmo usare un indice unico, ma non conviene, in quanto gli indici sono di dimensioni molto ridotte, e con essi è possibile tenersi l'effettiva storia di una cartella e ripristinarla in qualsiasi momento del suo passato

La scelta più semplice è quindi trovare la nuova data e copiare l'indice :

cp saveme-20130826123545.idx saveme-20130826152632.idx 

e quindi

tar cvfz saveme-20130826152632.tgz \
     --listed-incremental=saveme-20130826
152632.idx \
     saveme


Quest'ultima operazione, prenderà in considerazione l'indice appena copiato ed eseguirà il backup dei soli files modificati in base al timestamp, quindi aggiornerà l'indice con il nuovo stato (di tutti i files), con i files in più e omettendo chiaramente quelli cancellati.

E i files presenti diventeranno quattro :

saveme-20130826123545.tgz
saveme-20130826123545.idx
saveme-20130826152632.tgz
saveme-20130826152632.idx

Nonostante la considerazione che i tarball sono comunque semplici tarball e possono essere estratti indipendentemente dagli indici, il fatto che files aumentino a dismisura e che tali indici restino separati, non è affatto una cosa positiva e introduce evidenti svantaggi dal punto di vista della gestibilità del backup e nella sua archiviazione.

Fase 3 - Restore

Per ripristinare lo stato di un progetto in un certo istante temporale, per esempio le 15.26.32 del 26/08/2013 (cioè l'ultimo), dobbiamo procedere solo sull'indice interessato ed estrarre tutti i backup fino a questo indice, in questo modo :

tar xvfz saveme-20130826123545.tgz \
     --listed-incremental=saveme-20130826
152632.idx \
     saveme

tar xvfz saveme-20130826152632.tgz \
     --listed-incremental=saveme-20130826
152632.idx \
     saveme


La logica di questa operazione è molto particolare, infatti il tar usa l'indice, solo per cancellare dal file system i files che non si trovano nell'indice stesso.

Per spiegarci meglio, immaginiamo che al primo backup in saveme, ci fossero tre files , A , B  e C .

Immaginiamo che prima di fare il secondo backup, avessimo tolto B , aggiornato C e aggiunto un file D.

L'indice 20130826152632 a questo punto, conterrebbe l'elenco : A, C, D .

Durante l'estrazione del primo tarball, che conterrebbe A,B e C , tutti i files sarebbero estratti, in quanto non ci sarebbe nulla da cancellare, curiosamente anche B che non si trova nell'indice.

Al secondo passaggio B sarebbe cancellato, e sarebbero estratti sempre tutti i files contenuti nel secondo tarball e cioè C che sarebbe così sovrascritto e come file nuovo, ripristinando lo stato del sistema.


Quindi l'archiviatore, non ricostruisce lo stato del sistema ma cerca di ripristinarlo con strani passaggi cancellando e sovrascrivendo più volte, con la scusa che l'hard disk di solito è trasparente a queste azioni (grazie alle fantastiche buffer cache).

Bisognerebbe chiedere a quelli che usano i DOM o in generale le Flash, magari con appositi file systems a rotazione se sono d'accordo, ma come al solito non si può pretendere tutto dalla vita.

Un po`d'ordine nel chaos

Cercando di comprendere il tutto e di fare un po`d'ordine nel chaos, mi rifaccio a uno dei primi progetti che ho descritto tra queste pagine, cioè Time Archive che usa un sistema di patch differenziali, sui tar binari di progetto e che si è rivelato ottimo per piccole modifiche, ma piuttosto dispendioso su grosse variazioni.

L'idea è quella di introdurre praticamente gli stessi comandi, anzi volendo di creare una nuova versione di Time Archive , e di chiamare quella vecchia per esempio DiffArchive o qualcosa del genere, usando semplicemente e solamente il concetto di tar incrementale .

I comandi da implementare sono i 4 di Time Archive , cioè U per update, R per rollback , X per extract e T per trace.

Gli scopi di questo progetto sono interessanti :
  1. Definire uno standard per i backup.
  2. Automatizzare le procedure.
  3. Mantenere l'affidabilità e il formato del tar con compressione bzip2.
  4. Usare un solo file per ogni progetto che mantenga la storia e le versioni.
In particolare per il punto 4, useremo un semplice tar non compresso che conterrà tutti i tar compressi, in ordine con indici e descrizioni. Il fatto che non sia compresso implicherà la sua facile modificabilità e affidabilità.

TimeArchive II

Il comando Update

Il comando update per questo timearchive si definisce come nella precedente versione :
timearchive U <archive> <folder> [ "<description>" ]
dove archive è il nome dell'archivio arbitrario che dovrà ospitare tutti gli altri archivi, folder è la cartella da archiviare e description è una breve descrizione opzionale della versione corrente.

Dal punto di vista logico, si individuano innanzi tutto il folder e l'archivio in termini assoluti, poi si può procedere con un codice come questo :

 tmpdir=`mktemp -d`
 todate=`date +%Y%m%d%H%M%S`

 if [ -e "$archfull" ]
 then
    lastidx=`tar tf "$archfull" | sort | grep idx | tail -n 1`
    tar xf $archfull $lastidx -O >$tmpdir/$todate.idx
 fi
 

 tar cfj $tmpdir/$todate.tb2 -C "${dirfull%/*}" --listed-incremental=$tmpdir/$todate.idx "${dirfull##*/}"
 

 # Update archive
 echo $4 >$tmpdir/$todate.txt
 tar -C $tmpdir -rf $archfull $todate.tb2
 tar -C $tmpdir -rf $archfull $todate.txt
 tar -C $tmpdir -rf $archfull $todate.idx
 rm $tmpdir/*
 rmdir $tmpdir

In sostanza si crea una cartella temporanea di lavoro e si determina la data. Se l'archivio esiste già, è estratto direttamente dal tar l'ultimo indice salvato, in questo modo :

    lastidx=`tar tf "$archfull" | sort | grep idx | tail -n 1`

Se invece l'archivio non esiste, l'indice sarà creato.

Poi c'è un tar che esegue il backup incrementale e alla fine i due files tarball , indice e un file contenente semplicemente l'echo della description, sono appesi al file (-rf) archivio principale, che se non c'è è ovviamente creato sul momento.

Quindi per ogni versione avremo l'aggiunta di tre files ad un file archivio principale, facilmente verificabile con :
tar tvf archive

Il comando eXtract

La sintassi del comando è :

timearchive X <archive> <folder> [ <date> ]
Dove archive è il solito archivio, folder  è il folder dove sarà estratta la cartella del progetto indicata prima, cioè se la cartella era per esempio Pictures indicando /tmp , troveremo in /tmp la Picures. Per ripristinarla nella cartella corrente basta indicare come folder il "." .

Infine date è la data che si vuol ripristinare ed è opzionale.

Estrarre il contenuto dell'archivio richiede l'estrazione di tutti i tarball fino ad una data specifica ed è espressa dal seguente blocco di codice :

  tmpfile=`mktemp`
  if [ -z "$4" ]
  then
    lastidx=`tar tf "$archfull" | sort | grep idx | tail -n 1`
  else
    lastidx=`tar tf "$archfull" | grep "$4.idx"`
    if [ -z "$lastidx" ]
    then
       echo "Date does not match"
       exit
    fi
  fi
  tar xf $archfull $lastidx -O >$tmpfile
  cd "$dirfull"
  tar tf $archfull | grep tb2 | tarext "$archfull" "$tmpfile" "$4"
  rm $tmpfile
A parte la creazione del file temporaneo che ci servirà per l'indice, con  -z "$4" si verifica se il quarto parametro e cioè la Date è vuota o meno.

Se è vuota si ricava l'ultimo indice, come nel caso dell'update, altrimenti l'indice è cercato esplicitamente (e se non si trova si ha un errore).

Estratto l'indice nel file temporaneo si analizza semplicemente il contenuto dell'archivio, si ordina in ordine crescente di data e si passano i nomi dei relativi tarball ad una funzione chiamata tarext .

Il tarext fa questo :
function tarext
{
  while read a
  do
    tar xfp "$1" "$a" -O | tar xj --listed-incremental="$2"
    if [ ! -z "$3" ]
    then
      if [ "$3.tb2" == $a ]
      then
        return
      fi
    fi
  done
}
ossia prosegue estraendo coerentemente tutti i tar sull'indice indicato fino a quando ha estratto quello giusto (che può essere anche l'ultimo), ricostruendo così lo stato in un preciso istante di tempo.

Notare che il tarball, in questo caso non è copiato su disco ma estratto in una pipe e il folder ricostruito senza occupare ulteriore spazio.

Il comando Rollback

Il comando rollback ripristina l'archivio ad una data prescelta che significa la cancellazione di tutti le successive versioni. È un comando che si usa raramente,  per esempio quando si sbaglia a fare un update.

La sintassi è semplicemente:
timearchive R <archive> <date>
Il funzionamento è piuttosto simile a extract :
if [ -z "$3" ]
  then
     echo "Missing rollback date"
     exit
  fi   
lastidx=`tar tf "$archfull" | grep "$3.idx"`
if [ -z "$lastidx" ]
then
   echo "Date does not match"
   exit
fi
tar tf $archfull | sort -r | rollback "$archfull" "$3" 

 

Si noti però che il sort è rovesciato, quindi sono cancellati tutti i files a partire dal fondo fino alla data scelta.  Chi li cancella è la seguente procedura :

function rollback
{
  while read a
  do       
    if [ "$a" == "$2.txt" ]
    then
      return
    fi     
    tar --delete --file "$1" "$a"
  done
}

Anche qui simile alla precedente ma tenendo conto dell'ordine inverso e dell'uscita prima e non dopo la data prescelta.

Non si può ovviamente fare il rollback ad una data inesistente, e cancellare il primo record, per fare quello basta cancellare l'archivio.

Il comando Trace

Mostra il contenuto dell'archivio. La sua sintassi è :
timearchive T <archive>
e il suo funzionamento è basato su una riga di codice  :
tar tf $2 | sort | grep txt | showcontent "$2"
e una funzione :

function showcontent
{
  while read l
  do
    echo -n ${l%%\.*}"  "
    tar xf $1 $l -O
  done
}

la quale, come possiamo osservare, mostra la data e il commento opzionale inserito durante l'update.

Se il commento non è stato inserito, il file è comunque presente in quanto echo, come una stringa vuota.

Lo script finale


Ed eccoci al consueto script complessivo che implementa il comando :


#!/bin/bash

function showcontent
{
  while read l
  do
    echo -n ${l%%\.*}"  "
    tar xf $1 $l -O
  done
}


function rollback
{
  while read a
  do      
    if [ "$a" == "$2.txt" ]
    then
      return
    fi    
    tar --delete --file "$1" "$a"
  done
}


function tarext
{
  while read a
  do
    tar xfp "$1" "$a" -O | tar xj --listed-incremental="$2"
    if [ ! -z "$3" ]
    then
      if [ "$3.tb2" == $a ]
      then
        return
      fi
    fi
  done
}


case $1 in

U)
  dirfull=$3
  if [ "${dirfull:0:1}" != "/" ]
  then
    dirfull=`pwd`/$dirfull
  fi
  if [ ! -d  "$dirfull" ]
  then
    echo "$3 must be an existing directory"
    exit
  fi
  archfull=$2
  if [ "${archfull:0:1}" != "/" ]
  then
    archfull=`pwd`/$archfull
  fi
  tmpdir=`mktemp -d`
  todate=`date +%Y%m%d%H%M%S`
  if [ -e "$archfull" ]
  then
    lastidx=`tar tf "$archfull" | sort | grep idx | tail -n 1`
    tar xf $archfull $lastidx -O >$tmpdir/$todate.idx
  fi
  tar cfj $tmpdir/$todate.tb2 -C "${dirfull%/*}" --listed-incremental=$tmpdir/$todate.idx "${dirfull##*/}"
  # Update archive
  echo $4 >$tmpdir/$todate.txt
  tar -C $tmpdir -rf $archfull $todate.tb2
  tar -C $tmpdir -rf $archfull $todate.txt
  tar -C $tmpdir -rf $archfull $todate.idx
  rm $tmpdir/*
  rmdir $tmpdir
;;

X)
  dirfull=$3
  if [ "${dirfull:0:1}" != "/" ]
  then
    dirfull=`pwd`/$dirfull
  fi
  if [ ! -d  $dirfull ]
  then
    echo "$3 must be an existing directory"
    exit
  fi
  archfull=$2
  if [ "${archfull:0:1}" != "/" ]
  then
    archfull=`pwd`/$archfull
  fi
  if [ ! -e $archfull ]
  then
     echo "Archive not found"
     exit
  fi
  tmpfile=`mktemp`
  if [ -z "$4" ]
  then
    lastidx=`tar tf "$archfull" | sort | grep idx | tail -n 1`
  else
    lastidx=`tar tf "$archfull" | grep "$4.idx"`
    if [ -z "$lastidx" ]
    then
       echo "Date does not match"
       exit
    fi
  fi
  tar xf $archfull $lastidx -O >$tmpfile
  cd "$dirfull"
  tar tf $archfull | grep tb2 | tarext "$archfull" "$tmpfile" "$4"
  rm $tmpfile
;;

R)
  archfull=$2
  if [ "${archfull:0:1}" != "/" ]
  then
    archfull=`pwd`/$archfull
  fi
  if [ ! -e $archfull ]
  then
     echo "Archive not found"
     exit
  fi
  if [ -z "$3" ]
  then
     echo "Missing rollback date"
     exit
  fi   
  lastidx=`tar tf "$archfull" | grep "$3.idx"`
  if [ -z "$lastidx" ]
  then
     echo "Date does not match"
     exit
  fi
  tar tf $archfull | sort -r | rollback "$archfull" "$3" 
;;

T)
    tar tf $2 | sort | grep txt | showcontent "$2"
;;

*)
  echo Bad paramenter, please use U,T,R or X.
;;
esac

con questa evoluzione del timearchive, ho ottenuto ottimi risultati specie su backup significativi e complessi e il risultato è comunque un tar, facilmente gestibile in caso di emergenza o trasferimento su altri sistemi.

Nessun commento:

Posta un commento