Sala 7
Instantáneas y clones
Una instantánea tiene que congelar de golpe todo un sistema de archivos, un terabyte tan fácilmente como un kilobyte, y no debe ralentizar las escrituras que siguen. Tomarla es la mitad fácil. La mitad difícil llega con cada sobrescritura y cada borrado posteriores en el sistema de archivos vivo: ¿sigue necesitando el bloque antiguo alguna instantánea, o puede reutilizarse su espacio? Preguntar a cada instantánea supondría recorrerlas todas. Un contador de referencias en cada bloque supondría actualizar contadores para cada bloque en cada instantánea.
ZFS no hace ninguna de las dos cosas. Cada puntero de bloque registra el txg en el que nació el bloque. El sistema de archivos vivo recuerda el txg de su última instantánea. Un bloque nacido después de esa instantánea no puede estar en ninguna instantánea, así que se libera; un bloque nacido en ese txg o antes puede estarlo, así que se guarda, en una deadlist. Una sola comparación de dos números.
1 Escribir, tomar una instantánea, sincronizar
fs es un sistema de archivos de cuatro bloques de datos, de A a D, bajo un bloque raíz, el objset al
que apunta su ds_bp. Cada bloque muestra su txg de nacimiento: A8 nació en el txg 8. La
figura empieza tras tres txg: A, B y C escritos en el txg 8, la instantánea fs@s1 tomada en el txg 9,
A sobrescrito en el txg 10. Prepara cambios en el txg abierto, toma una instantánea, clona una o destruye una, y
luego sincroniza: cada bloque que la sincronización suelta muestra la comparación y adónde va. Selecciona
cualquier bloque para ver qué instantáneas lo retienen.
Txg abierto: 11. Instantáneas: fs@s1. 6 bloques en el disco.
fs
cabeza, modificableds_bp →R10A10B8C8D: vacíodeadlist- > 0vacía
- > 1A8R8
fs@s1
instantánea del txg 9ds_bp →R8A8B8C8D: vacíodeadlist- > 0vacía
¿Qué instantánea es dueña de este bloque?
Selecciona un bloque para ver su txg de nacimiento y quién lo retiene.
Lo que ha hecho la sincronización
Versión en texto de esta figura
El txg abierto es el 11. Los datasets, por orden:
fs, cabeza, modificable: ds_prev_snap_txg 9; ds_bp apunta a R10, la raíz, nacida en el txg 10; bloques de datos: A10, nacido en el txg 10, B8, nacido en el txg 8, C8, nacido en el txg 8, D: vacío. Deadlist: entrada > 0: vacía; entrada > 1: A8, nacido en el txg 8 y R8, la raíz, nacida en el txg 8.fs@s1, instantánea del txg 9: ds_prev_snap_txg 1; ds_bp apunta a R8, la raíz, nacida en el txg 8; bloques de datos: A8, nacido en el txg 8, B8, nacido en el txg 8, C8, nacido en el txg 8, D: vacío. Deadlist: entrada > 0: vacía.
Liberados hasta ahora: R7, la raíz, nacida en el txg 7 (en el txg 8).
La idea, a tres profundidades
En una frase
Cada bloque lleva el txg de su nacimiento, así que cuando el sistema de archivos vivo suelta un bloque, una sola comparación con el txg de su última instantánea decide: liberarlo, o guardarlo en una deadlist para las instantáneas que aún lo retienen.
En un párrafo
Una instantánea es un nuevo registro de dataset que copia un solo puntero de bloque, ds_bp, la raíz
del árbol del sistema de archivos, y nada más: todos los bloques que cuelgan de él pasan a estar compartidos. La
copia en escritura nunca vuelve a modificar esos bloques; escribe
otros nuevos, nacidos en txg posteriores, y mata los antiguos. dsl_dataset_block_kill
compara el nacimiento del bloque antiguo con el ds_prev_snap_txg del sistema de archivos, el txg de su
última instantánea. Nacido después, el bloque pertenece solo al sistema de archivos vivo y se libera. Nacido en ese
txg o antes, la última instantánea lo retiene, y va a la deadlist. La deadlist está dividida en entradas por los txg
de las instantáneas anteriores, de modo que indica entre qué dos instantáneas nació cada bloque muerto. Eso hace
barato destruir una instantánea: los bloques que solo ella retenía son entradas enteras de la deadlist del dataset
siguiente, liberadas sin leer un solo puntero de bloque. Un clon es un dataset modificable que parte del
ds_bp de una instantánea, con el txg de esa instantánea como su propio ds_prev_snap_txg:
la misma comparación conserva los bloques que comparte con su origen y libera los suyos propios.
En detalle
El nacimiento. El txg de nacimiento de un bloque se fija al asignarlo:
metaslab_alloc termina con BP_SET_BIRTH(bp, txg, 0), y la
escritura le pasa el txg al que pertenece. Un puntero de bloque tiene
dos palabras de nacimiento, lógica y física; solo difieren en
la deduplicación, la clonación de bloques y los bloques reescritos, y el valor que se
compara más abajo es BP_GET_BIRTH, el nacimiento lógico para un bloque
ordinario.
Liberar ya, o la deadlist. Esta es toda la decisión.
dsl_dataset_block_kill
Extracto de module/zfs/dsl_dataset.c, líneas
294–316, OpenZFS zfs-2.4.4.
Ver en GitHub
if (BP_GET_BIRTH(bp) > dsl_dataset_phys(ds)->ds_prev_snap_txg) {
int64_t delta;
/*
* Put blocks that would create IO on the pool's deadlist for
* dsl_process_async_destroys() to find. This is to prevent
* zio_free() from creating a ZIO_TYPE_FREE IO for them, which
* are very heavy and can lead to out-of-memory conditions if
* something tries to free millions of blocks on the same txg.
*/
boolean_t defer = spa_version(spa) >= SPA_VERSION_DEADLISTS &&
(BP_IS_GANG(bp) || BP_GET_DEDUP(bp) ||
brt_maybe_exists(spa, bp));
if (defer) {
dprintf_bp(bp, "putting on free list: %s", "");
bpobj_enqueue(&ds->ds_dir->dd_pool->dp_free_bpobj,
bp, B_FALSE, tx);
} else {
dprintf_bp(bp, "freeing ds=%llu",
(u_longlong_t)ds->ds_object);
dsl_free(tx->tx_pool, tx->tx_txg, bp);
}-
L294
La comparación.
ds_prev_snap_txges el txg de la última instantánea del dataset. La prueba es estricta: un bloque nacido en ese mismo txg lo retiene esa instantánea. - L304–L311 Los bloques gang, deduplicados o posiblemente clonados se ponen en la lista de bloques por liberar del pool en lugar de liberarse en el acto, porque liberarlos cuesta E/S. El resultado es el mismo: ninguna instantánea los retiene.
-
L312–L315
El caso ordinario:
dsl_free, en este txg. La figura pone el bloque en gris bajo «Liberados».
Si no, en la rama else, el bloque va a la deadlist. Tres funciones la llaman
para matar bloques, todas mientras se sincroniza un txg: dbuf_write_done cuando se ha
reescrito un bloque de datos o un bloque indirecto, dmu_objset_write_done
para el bloque del objset al que apunta ds_bp, reescrito en cada txg en el que cambia el dataset, y
free_blocks para un borrado o un truncamiento. La figura dibuja ese objset
y los bloques indirectos como una sola raíz, que muere según la misma regla. Los dos callbacks de la
escritura dejan un bloque destinado a la deadlist en una lista pendiente que
dsl_dataset_sync_done vacía antes de las tareas de sincronización del txg:
el mismo resultado, unas líneas más adelante.
Una instantánea copia un puntero de bloque.
dsl_dataset_snapshot_sync_impl
Extracto de module/zfs/dsl_dataset.c, líneas
1760–1785, OpenZFS zfs-2.4.4.
Ver en GitHub
dsobj = dmu_object_alloc(mos, DMU_OT_DSL_DATASET, 0,
DMU_OT_DSL_DATASET, sizeof (dsl_dataset_phys_t), tx);
VERIFY0(dmu_bonus_hold(mos, dsobj, FTAG, &dbuf));
dmu_buf_will_dirty(dbuf, tx);
dsphys = dbuf->db_data;
memset(dsphys, 0, sizeof (dsl_dataset_phys_t));
dsphys->ds_dir_obj = ds->ds_dir->dd_object;
dsphys->ds_fsid_guid = unique_create();
(void) random_get_pseudo_bytes((void*)&dsphys->ds_guid,
sizeof (dsphys->ds_guid));
dsphys->ds_prev_snap_obj = dsl_dataset_phys(ds)->ds_prev_snap_obj;
dsphys->ds_prev_snap_txg = dsl_dataset_phys(ds)->ds_prev_snap_txg;
dsphys->ds_next_snap_obj = ds->ds_object;
dsphys->ds_num_children = 1;
dsphys->ds_creation_time = gethrestime_sec();
dsphys->ds_creation_txg = crtxg;
dsphys->ds_deadlist_obj = dsl_dataset_phys(ds)->ds_deadlist_obj;
dsphys->ds_referenced_bytes = dsl_dataset_phys(ds)->ds_referenced_bytes;
dsphys->ds_compressed_bytes = dsl_dataset_phys(ds)->ds_compressed_bytes;
dsphys->ds_uncompressed_bytes =
dsl_dataset_phys(ds)->ds_uncompressed_bytes;
dsphys->ds_flags = dsl_dataset_phys(ds)->ds_flags;
rrw_enter(&ds->ds_bp_rwlock, RW_READER, FTAG);
dsphys->ds_bp = dsl_dataset_phys(ds)->ds_bp;
rrw_exit(&ds->ds_bp_rwlock, FTAG);
dmu_buf_rele(dbuf, FTAG);- L1760–L1761 Un objeto nuevo en el MOS, los metadatos propios del pool: el registro de dataset de la instantánea.
-
L1770–L1773
Toma de la cabeza el enlace a la instantánea anterior y su
ds_prev_snap_txg, apunta hacia delante a la cabeza, y empieza con un hijo, su sucesor. - L1775 Su txg de creación es el txg que se está sincronizando.
- L1776 La deadlist de la cabeza pasa a ser la de la instantánea, tal cual.
- L1783 Se copia el puntero de bloque raíz. Esa única copia es lo que hace que la instantánea comparta todos los bloques con la cabeza. No se lee ni se escribe nada en el árbol: la figura no muestra ningún bloque nuevo para una instantánea.
La nueva deadlist de la cabeza. Más abajo en la misma función, la cabeza recibe una deadlist propia.
dsl_dataset_snapshot_sync_impl
Extracto de module/zfs/dsl_dataset.c, líneas
1830–1839, OpenZFS zfs-2.4.4.
Ver en GitHub
dmu_buf_will_dirty(ds->ds_dbuf, tx);
dsl_dataset_phys(ds)->ds_deadlist_obj =
dsl_deadlist_clone(&ds->ds_deadlist, UINT64_MAX,
dsl_dataset_phys(ds)->ds_prev_snap_obj, tx);
dsl_deadlist_close(&ds->ds_deadlist);
VERIFY0(dsl_deadlist_open(&ds->ds_deadlist, mos,
dsl_dataset_phys(ds)->ds_deadlist_obj));
dsl_deadlist_add_key(&ds->ds_deadlist,
dsl_dataset_phys(ds)->ds_prev_snap_txg, tx);
dsl_bookmark_snapshotted(ds, tx);-
L1831–L1833
dsl_deadlist_clonesin límite: una entrada vacía por cada clave de la deadlist que la instantánea acaba de tomar. -
L1837–L1838
Más una clave: el
ds_prev_snap_txgde la cabeza, que aquí todavía es el txg de la instantánea anterior. Solo toma el txg de esta instantánea después.
Así, tras instantáneas en los txg t1 < t2 < … < tn, la deadlist de la cabeza tiene sus claves iniciales
más las claves de t1 a t(n−1), y su ds_prev_snap_txg es tn. Cada instantánea conserva la
deadlist que tenía la cabeza hasta que se tomó: los bloques matados entre la instantánea anterior y esta. Es decir,
bloques que la instantánea anterior retiene y esta no; la deadlist de una instantánea no es «los bloques
que solo le pertenecen a ella».
Adónde va un bloque muerto.
dsl_deadlist_insert
Extracto de module/zfs/dsl_deadlist.c, líneas
487–501, OpenZFS zfs-2.4.4.
Ver en GitHub
dle_tofind.dle_mintxg = BP_GET_BIRTH(bp);
dle = avl_find(&dl->dl_tree, &dle_tofind, &where);
if (dle == NULL)
dle = avl_nearest(&dl->dl_tree, where, AVL_BEFORE);
else
dle = AVL_PREV(&dl->dl_tree, dle);
if (dle == NULL) {
zfs_panic_recover("blkptr at %p has invalid BLK_BIRTH %llu",
bp, (longlong_t)BP_GET_BIRTH(bp));
dle = avl_first(&dl->dl_tree);
}
ASSERT3P(dle, !=, NULL);
dle_enqueue(dl, dle, bp, bp_freed, tx);- L487–L492 Una deadlist es un árbol de entradas, cada una una lista de punteros de bloque guardada bajo una clave, un txg que «no es inclusivo». Se busca el nacimiento: si coincide exactamente con una clave, se toma la entrada anterior; si no, la entrada más próxima por debajo. En ambos casos, la entrada con la mayor clave estrictamente inferior al nacimiento.
- L494–L498 Un nacimiento menor o igual que todas las claves no puede darse en un pool sano: se notifica, y el bloque va a la primera entrada.
Dónde empiezan las claves. Un sistema de archivos se crea a partir de la
instantánea $ORIGIN del pool, creada a partir de un dataset vacío
con el txg de creación 1. Siguiendo los extractos, un sistema de archivos nuevo empieza
con un ds_prev_snap_txg de 1 y una deadlist con una clave, 0, y su primera instantánea añade la clave
1. Todo bloque que escribe un usuario nace después del txg 1: la entrada 0 se queda vacía, la entrada 1 recoge lo
que nació antes de la primera instantánea. Son las entradas > 0 y > 1 de la
figura.
El mismo txg. Una instantánea es una tarea de sincronización, y el txg escribe primero sus datos.
dsl_pool_sync
Extracto de module/zfs/dsl_pool.c, líneas
747–760, OpenZFS zfs-2.4.4.
Ver en GitHub
/*
* Sync the datasets again to push out the changes due to
* userspace updates. This must be done before we process the
* sync tasks, so that any snapshots will have the correct
* user accounting information (and we won't get confused
* about which blocks are part of the snapshot).
*/
rio = zio_root(dp->dp_spa, NULL, NULL, ZIO_FLAG_MUSTSUCCEED);
while ((ds = txg_list_remove(&dp->dp_dirty_datasets, txg)) != NULL) {
objset_t *os = ds->ds_objset;
ASSERT(list_link_active(&ds->ds_synced_link));
dmu_buf_rele(ds->ds_dbuf, ds);
dsl_dataset_sync(ds, rio, tx);-
L747–L753
Cada dataset modificado se escribe antes de que se ejecuten las tareas de sincronización, para no confundirse sobre qué bloques forman parte de la instantánea (
we won’t get confused about which blocks are part of the snapshot
). Las tareas se ejecutan al final.
Dos consecuencias, ambas en la figura. Un bloque escrito en el mismo txg que una instantánea está en esa
instantánea: nacido en el txg T, lo retiene la instantánea del txg T, y por eso la prueba es estricta. Y un bloque
matado en el txg de una instantánea no está en ella: su muerte se decidió respecto a la instantánea anterior. Una
sola instantánea de un dataset por txg: una segunda se rechaza, EAGAIN.
Un clon parte del puntero de bloque de una instantánea.
dsl_dataset_create_sync_dd
Extracto de module/zfs/dsl_dataset.c, líneas
1213–1227, OpenZFS zfs-2.4.4.
Ver en GitHub
} else {
dsl_dataset_t *ohds; /* head of the origin snapshot */
dsphys->ds_prev_snap_obj = origin->ds_object;
dsphys->ds_prev_snap_txg =
dsl_dataset_phys(origin)->ds_creation_txg;
dsphys->ds_referenced_bytes =
dsl_dataset_phys(origin)->ds_referenced_bytes;
dsphys->ds_compressed_bytes =
dsl_dataset_phys(origin)->ds_compressed_bytes;
dsphys->ds_uncompressed_bytes =
dsl_dataset_phys(origin)->ds_uncompressed_bytes;
rrw_enter(&origin->ds_bp_rwlock, RW_READER, FTAG);
dsphys->ds_bp = dsl_dataset_phys(origin)->ds_bp;
rrw_exit(&origin->ds_bp_rwlock, FTAG);-
L1216–L1218
La instantánea anterior del clon es su origen, y su
ds_prev_snap_txg, el txg de creación del origen. -
L1225–L1227
Su
ds_bpes el del origen: como una instantánea, un clon empieza siendo un puntero de bloque copiado.
Después, el origen gana un hijo, la deadlist del clon
empieza con las claves inferiores al txg del origen, y el origen se registra
en el directorio del clon (en memoria, dd_origin_txg).
Una escritura en el clon pasa por el mismo dsl_dataset_block_kill, frente al propio
ds_prev_snap_txg del clon: un bloque que escribió el clon nació después del origen y se libera cuando
el clon lo suelta; un bloque nacido en el txg del origen o antes se comparte con él y va a la deadlist del clon. El
clon diverge por txg de nacimiento, y no se copia nada.
Destruir una instantánea.
dsl_destroy_snapshot_sync_impl
Extracto de module/zfs/dsl_destroy.c, líneas
387–424, OpenZFS zfs-2.4.4.
Ver en GitHub
dmu_buf_will_dirty(ds_next->ds_dbuf, tx);
dsl_dataset_phys(ds_next)->ds_prev_snap_obj =
dsl_dataset_phys(ds)->ds_prev_snap_obj;
dsl_dataset_phys(ds_next)->ds_prev_snap_txg =
dsl_dataset_phys(ds)->ds_prev_snap_txg;
ASSERT3U(dsl_dataset_phys(ds)->ds_prev_snap_txg, ==,
ds_prev ? dsl_dataset_phys(ds_prev)->ds_creation_txg : 0);
if (ds_next->ds_deadlist.dl_oldfmt) {
process_old_deadlist(ds, ds_prev, ds_next,
after_branch_point, tx);
} else {
/* Adjust prev's unique space. */
if (ds_prev && !after_branch_point) {
dsl_deadlist_space_range(&ds_next->ds_deadlist,
dsl_dataset_phys(ds_prev)->ds_prev_snap_txg,
dsl_dataset_phys(ds)->ds_prev_snap_txg,
&used, &comp, &uncomp);
dsl_dataset_phys(ds_prev)->ds_unique_bytes += used;
}
/* Adjust snapused. */
dsl_deadlist_space_range(&ds_next->ds_deadlist,
dsl_dataset_phys(ds)->ds_prev_snap_txg, UINT64_MAX,
&used, &comp, &uncomp);
dsl_dir_diduse_space(ds->ds_dir, DD_USED_SNAP,
-used, -comp, -uncomp, tx);
/* Move blocks to be freed to pool's free list. */
dsl_deadlist_move_bpobj(&ds_next->ds_deadlist,
&dp->dp_free_bpobj, dsl_dataset_phys(ds)->ds_prev_snap_txg,
tx);
dsl_dir_diduse_space(tx->tx_pool->dp_free_dir,
DD_USED_HEAD, used, comp, uncomp, tx);
/* Merge our deadlist into next's and free it. */
dsl_deadlist_merge(&ds_next->ds_deadlist,
dsl_dataset_phys(ds)->ds_deadlist_obj, tx);-
L387–L391
El dataset siguiente, la instantánea siguiente o la cabeza, hereda la instantánea anterior y el
ds_prev_snap_txgde la instantánea destruida. - L395–L397 Los pools anteriores a las deadlists con claves aplican la misma prueba a cada bloque. La figura sigue el camino con claves.
- L399–L413 Contabilidad del espacio, no dibujada.
-
L415–L418
Cada entrada de la deadlist siguiente a partir del
ds_prev_snap_txgde la instantánea va a la lista de bloques por liberar del pool. Esos bloques nacieron después de la instantánea anterior y murieron antes de la siguiente: solo esta instantánea los retenía. Basta con las claves para saberlo. El hilo de sincronización libera esa lista en los txg siguientes; la figura los libera en el acto. - L422–L424 La deadlist propia de la instantánea, bloques que la instantánea anterior aún retiene, se une a la siguiente entrada por entrada, y después se libera.
Por último, la clave del txg de la instantánea se elimina, y su entrada se fusiona con la
anterior: en la cabeza cuando el dataset siguiente es una instantánea, y, mediante
dsl_dir_remove_clones_key, en cada clon
cuyo origen es más reciente que la instantánea. Una instantánea con un clon tiene
más de un hijo y no puede destruirse,
EEXIST: no se puede borrar un punto de bifurcación (Can’t delete a branch point
).
Qué instantánea es dueña de este bloque. Nada en ZFS guarda esa respuesta: se deduce de los txg
de nacimiento. Un bloque de fs nacido en el txg B y matado en el txg K lo retienen las instantáneas
de fs tomadas desde el txg B hasta el txg K − 1: las tomadas antes de B se congelaron antes de que
existiera, las tomadas en K o después, cuando ya había desaparecido. Un bloque todavía vivo lo retienen todas las
instantáneas tomadas desde B. La figura calcula ese intervalo a partir de los números, y sus pruebas lo comparan
con el árbol del dataset que realmente contiene el bloque, en miles de secuencias aleatorias.
Lo que la figura deja fuera
La figura ejecuta las funciones anteriores sobre un sistema de archivos con cuatro bloques de datos y hasta cuatro
instantáneas, y un clon del que no se pueden tomar instantáneas y que no se puede destruir. El objset, los dnodes y
los bloques indirectos son un solo bloque raíz. Las liberaciones diferidas de bloques gang, deduplicados y clonados,
la contabilidad del espacio y la livelist del clon quedan fuera; nada de ello cambia que un
bloque se libere o se guarde. Los bloques de la lista de bloques por liberar del pool se dibujan como liberados en el
acto. No se muestran las retenciones (holds), la destrucción diferida ni las deadlists del formato antiguo. Los
marcadores (dsl_bookmark.c) y zfs promote, que intercambia un clon y el
sistema de archivos de su origen, son asunto de otras salas.