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_prev_snap_txg 9 (fs@s1)
ds_bp →R10A10B8C8D: vacío
deadlist
  • > 0vacía
  • > 1A8R8

fs@s1

instantánea del txg 9ds_prev_snap_txg 1 ($ORIGIN)
ds_bp →R8A8B8C8D: vacío
deadlist
  • > 0vacía
Versión en texto de esta figura

El txg abierto es el 11. Los datasets, por orden:

  1. 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.
  2. 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);
		}
  1. L294 La comparación. ds_prev_snap_txg es 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.
  2. 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.
  3. 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);
  1. L1760–L1761 Un objeto nuevo en el MOS, los metadatos propios del pool: el registro de dataset de la instantánea.
  2. 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.
  3. L1775 Su txg de creación es el txg que se está sincronizando.
  4. L1776 La deadlist de la cabeza pasa a ser la de la instantánea, tal cual.
  5. 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);
  1. L1831–L1833 dsl_deadlist_clone sin límite: una entrada vacía por cada clave de la deadlist que la instantánea acaba de tomar.
  2. L1837–L1838 Más una clave: el ds_prev_snap_txg de 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);
  1. 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.
  2. 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);
  1. 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);
  1. L1216–L1218 La instantánea anterior del clon es su origen, y su ds_prev_snap_txg, el txg de creación del origen.
  2. L1225–L1227 Su ds_bp es 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);
  1. L387–L391 El dataset siguiente, la instantánea siguiente o la cabeza, hereda la instantánea anterior y el ds_prev_snap_txg de la instantánea destruida.
  2. L395–L397 Los pools anteriores a las deadlists con claves aplican la misma prueba a cada bloque. La figura sigue el camino con claves.
  3. L399–L413 Contabilidad del espacio, no dibujada.
  4. L415–L418 Cada entrada de la deadlist siguiente a partir del ds_prev_snap_txg de 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.
  5. 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.