mercoledì 16 dicembre 2009

ZFS: always consistent on disk (parte 1)

Ieri sera (15 dicembre), durante la LinuxNight di dicembre (foto), organizzata dal Linux User Group di Perugia ho avuto modo di presentare alcune caratteristiche di OpenSolaris, nell'ambito degli incontri organizzati dal Gruppo Italiano degli Utenti OpenSolaris (ITL-OSUG).

Si è parlato di numerosissimi aspetti di OpenSolaris: DTrace, SMF, Time Slider e sopratutto ZFS. Proprio riguardo al motto "always consistent on disk" di ZFS si sono concentrate alcune demo (non tutte portate a termine per mancanza di tempo), per chi non ha potuto seguire "in diretta" la presentazione ecco riportati i vari passaggi dei test effettuati.

ATTENZIONE: i test comportano l'alterazione dei dati presenti nei dischi utilizzati nei pool, quindi effettuate questi test su filesystem di prova, che non contengono dati e sopratutto A VOSTRO RISCHIO E PERICOLO!!!!



Cominciamo creando un nuovo pool (denominato "testPool") senza nessuna ridondanza dei dati attiva in cui creeremo un filesystem (denominato "fs1"), come dischi ho utilizzato un file (chiamato "disk1"):
$ mkfile 150m disk1
$ pfexec zpool create testPool `pwd`/disk1
$ pfexec zfs create testPool/fs1
$ zpool status testPool
pool: testPool
state: ONLINE
scrub: none requested
config:

NAME STATE READ WRITE CKSUM
testPool ONLINE 0 0 0
/export/home/luca/itl-osug/zfs/disk1 ONLINE 0 0 0

ora copiamo un file qualsiasi nel nostro filesystem e andiamo a leggere l'i-node attribuito al file (per essere precisi sotto ZFS dovremmo usare il termine ObjectID al posto di i-node):
$ pfexec cp /etc/bash/bash_completion /testPool/fs1/
$ ls -li /testPool/fs1/
total 259
5 -rwxr-xr-x 1 root root 217434 2009-12-16 11:21 bash_completion*

ora, grazie al tool di diagnostica zdb(1M) andiamo a leggere in quali blocchi fisici è stato memorizzato il nostro file (con Object-ID uguale a 5):
$ pfexec zdb -ddddd testPool/fs1 5
Dataset testPool/fs1 [ZPL], ID 30, cr_txg 6, 280K, 5 objects,
rootbp DVA[0]=<0:58200:200> DVA[1]=<0:1c18200:200> [L0 DMU objset]
fletcher4 lzjb LE contiguous unique double size=800L/200P
birth=8L/8P fill=5
cksum=15525a36dc:7f4d019c8fa:18a35fbbba8d3:349cb86bc37272

Object lvl iblk dblk dsize lsize %full type
5 2 16K 128K 258K 256K 100.00 ZFS plain file
264 bonus ZFS znode
dnode flags: USED_BYTES USERUSED_ACCOUNTED
dnode maxblkid: 1
path /bash_completion
uid 0
gid 0
atime Wed Dec 16 11:21:45 2009
mtime Wed Dec 16 11:21:45 2009
ctime Wed Dec 16 11:21:45 2009
crtime Wed Dec 16 11:21:45 2009
gen 8
mode 100755
size 217434
parent 3
links 1
xattr 0
rdev 0x0000000000000000
Indirect blocks:
0 L1 0:53200:400 4000L/400P F=2 B=8/8
0 L0 0:13000:20000 20000L/20000P F=1 B=8/8
20000 L0 0:33000:20000 20000L/20000P F=1 B=8/8

segment [0000000000000000, 0000000000040000) size 256K

tra le tante informazioni visualizzate andiamo ad estrapolare i blocchi che ci interessano, ovvero quelli di livello 0 (L0): il nostro file è stato scritto usando due blocchi con i seguenti DVA (Data Virtual Address): 0:13000:20000 e 0:33000:20000. Il significato dei tre numeri (in esadecimale) è: device sul quale è scritto il blocco (nel nostro caso ne abbiamo uno solo, quindi troveremo sempre zero), offset del blocco rispetto all'inizio del disco (a cui dovremmo aggiungere 4 MegaByte di spazio riservato ai metadati e al boot block) e per finire la grandezza del blocco.

Ora che conosciamo le "coordinate" del nostro file possiamo divertirci a fare qualche danno, prima però esportiamo il nostro pool per poter accedere senza problemi al disco da rovinare!
$ pfexec zpool export testPool

ora, per agevolare la demo ho creato uno script che prende il disco e lo divide in tre parti: la prima conterrà tutto il disco fino al blocco di inizio del file (escluso), la seconda con il blocco richiesto e la terza parte tutto il resto del disco. Lo script richiede due parametri: il nome del disco e l'offset del file (ricavato precedentemente con zdb), lo script è così composto:
$ cat zsplit.sh
#!/bin/sh

DISK=$1

# converte da esadecimale a decimale
START=`echo $2 | awk --non-decimal-data '{print ($1)+0}'`
# aggiunge i 4M di offset (fissi) iniziali e divide per 512 (block size)
START=$(((4194304+$START)/512))
# se non specificato "estrae" 256 blocchi
SIZE=${3:-256}
END=$(($START+$SIZE))

# esegue la "frammentazione"
dd if=$DISK of=$DISK-p1 bs=512 count=$START
dd if=$DISK of=$DISK-p2 bs=512 skip=$START count=$SIZE
dd if=$DISK of=$DISK-p3 bs=512 skip=$END

ora "sezioniamo" il nostro disco e controlliamo che la parte 2 contegga effettivamente il file presente sul nostro disco:
$ ./zsplit.sh disk1 0x13000
8344+0 records in
8344+0 records out
4272128 bytes (4.3 MB) copied, 0.104004 s, 41.1 MB/s
256+0 records in
256+0 records out
131072 bytes (131 kB) copied, 0.00671322 s, 19.5 MB/s
298600+0 records in
298600+0 records out
152883200 bytes (153 MB) copied, 3.6703 s, 41.7 MB/s
$ head disk1-p2
#
# This file contains an example set of shell completions that can be used with
# bash(1).  These completions allow a user to complete filenames, commands
# name, command line options, and command line arguments using the [tab] key.
# The completions defined here are specific to the GNU command set, as a result
# they will provide the choice of GNU command line options in response to the
# [tab] key.  For the completed options to match the command implementation,
# you may have to have /usr/gnu/bin at the head of your PATH.
#
# These completions are not included in the default bash(1) environment.  To

per simulare una "rottura" sporcherò il file con sed(1) mettendo in maiuscolo tutte le occorrenze della parola "file":
$ sed "s/ file / FILE /g" disk1-p2 > disk1-p2-bad

ora "ricomponiamo" il disco, facciamo di nuovo l'import del nostro pool e controlliamo il suo stato:
$ cat disk1-p1 disk1-p2-bad disk1-p3 > disk1
$ pfexec zpool import -d `pwd` testPool
$ zpool status testPool
pool: testPool
state: ONLINE
scrub: none requested
config:

NAME STATE READ WRITE CKSUM
testPool ONLINE 0 0 0
/export/home/luca/itl-osug/zfs/disk1 ONLINE 0 0 0

come vedete zfs ancora non si è accorto della presenza di eventuali errori, ma se proviamo ad accedere al nostro file ci accorgiamo che qualcosa non và:
$ head /testPool/fs1/bash_completion
head: error reading `/testPool/fs1/bash_completion': I/O error
$ pfexec zpool status -v testPool
pool: testPool
state: ONLINE
status: One or more devices has experienced an error resulting in data
corruption. Applications may be affected.
action: Restore the file in question if possible. Otherwise restore the
entire pool from backup.
see: http://www.sun.com/msg/ZFS-8000-8A
scrub: none requested
config:

NAME STATE READ WRITE CKSUM
testPool ONLINE 0 0 1
/export/home/luca/itl-osug/zfs/disk1 ONLINE 0 0 2

errors: Permanent errors have been detected in the following files:

/testPool/fs1/bash_completion

ZFS ci avverte che il file è inutilizzabile e ci "invita" a ripristinarlo da un eventuale backup, visto che non ha la possibilità di farlo lui!!! E' importante eseguire ad intervalli regolari uno "scrub" dei nostri pool, altrimenti ci accorgeremmo di eventuali problemi solo al momento dell'accesso a potenziali file danneggiati; invece con lo scrub (comando zpool scrub nome_pool) sarà ZFS che andrà a controllorare i dati presenti su disco e la loro consistenza!

Bene, nella seconda parte di questo articolo vedremo invece come si comporterà ZFS in caso di ridondanza dei dati, a presto!