Sala 4

Grupos de transacciones

La copia en escritura mantiene los discos coherentes en todo momento, pero solo si el paso del árbol antiguo al nuevo es una sola escritura. Las escrituras no dejan de llegar, desde muchos hilos a la vez. Si cada una terminara con su propia confirmación, el pool volcaría la caché de cada disco y reescribiría sus etiquetas y su uberblock por cada escritura. Y una confirmación tiene que abarcar un estado en el que ningún cambio esté a medias.

ZFS responde con lotes. Cada cambio entra en un grupo de transacciones numerado, un txg, y ZFS asserts consistency at the granularity of these transaction groups: la coherencia se garantiza grupo a grupo.

1 Tres txg, uno por etapa

Un txg está abierto, en cierre o en sincronización, y nunca hay más de un txg en cada estado: TXG_CONCURRENT_STATES vale 3. Las escrituras entran en el txg abierto. Para confirmarlo, un hilo lo cierra y abre el siguiente de inmediato, así que quien escribe nunca espera a un disco. Después espera a que terminen las escrituras aún en curso en el txg cerrado. Un segundo hilo escribe el txg cerrado en los discos y termina con el uberblock. Añade escrituras, inicia la animación o avanza evento a evento.

Versión en texto de esta figura
  1. Cada cambio en el pool entra en un grupo de transacciones (txg) numerado. Puede haber tres txg activos a la vez, uno en cada estado: abierto, en cierre, en sincronización.
  2. Una escritura retiene el txg abierto: txg_hold_open incrementa un contador de la CPU en la que se ejecuta, para ese txg. Cuando la escritura termina, txg_rele_to_sync lo decrementa.
  3. El hilo de sincronización espera una de tres cosas: que pasen zfs_txg_timeout (5 segundos) desde el comienzo de la última sincronización, que alguien espere una sincronización (zpool sync, o txg_kick cuando el txg abierto contiene más datos modificados que el zfs_dirty_data_sync_percent de zfs_dirty_data_max), o un txg ya cerrado.
  4. Después pide el txg abierto al hilo de cierre. El hilo de cierre lo cierra, abre de inmediato el txg siguiente para que las escrituras nunca esperen, y espera a que el contador de cada CPU para el txg cerrado sea 0. Entonces entrega el txg al hilo de sincronización.
  5. El hilo de sincronización escribe el txg en pasadas: la primera escribe los datos de usuario y los metadatos que hay por encima; cada pasada asigna espacio, lo que modifica space maps en el MOS, así que se repite hasta que una pasada deja el MOS limpio. Después vdev_config_sync escribe las etiquetas y el uberblock nuevo: esa única escritura confirma el txg. Un txg en el que nada ha cambiado no escribe nada en absoluto.
  6. Normalmente el txg siguiente sigue abierto mientras otro se sincroniza: el hilo de cierre solo cierra un txg cuando alguien lo pide. Un llamante que necesita un txg nuevo de inmediato, como dmu_free_long_range tras un txg lleno de liberaciones, llama a txg_wait_open, y entonces los tres estados están ocupados. Nunca hay más de un txg por estado.

La idea, a tres profundidades

En una frase

ZFS reúne cada cambio en un grupo de transacciones numerado, lo cierra cuando ya no queda dentro ninguna escritura en curso, y lo escribe entero, de modo que una sola escritura de uberblock confirma miles de cambios en un momento en que ninguno está a medias.

En un párrafo

Cada cambio en el pool es una transacción que entra en el txg abierto. Dos hilos del núcleo por pool hacen avanzar los txg. El hilo de sincronización se despierta cuando han pasado zfs_txg_timeout (5 segundos) desde el comienzo de la última sincronización, o antes si alguien necesita una sincronización: zpool sync, o un txg que contiene demasiados datos modificados. Pide al hilo de cierre el txg abierto. El hilo de cierre abre el txg siguiente, espera a que ninguna transacción retenga el antiguo, y se lo entrega. El hilo de sincronización lo escribe entonces en pasadas hasta que no queda nada por escribir, y lo confirma con el uberblock. Mientras tanto, el txg siguiente ya está recibiendo escrituras. Un txg en el que nada ha cambiado no escribe nada, ni siquiera un uberblock. Un programa que necesita sus datos en el disco de inmediato, con fsync, normalmente no espera a un txg: pasa por el ZIL (sala 10).

En detalle

Entrar en el txg abierto. dmu_tx_assign llama a txg_hold_open, que lee tx_open_txg e incrementa un contador de la CPU en la que se ejecuta, tc_count[txg & TXG_MASK]. El estado de cada txg se guarda en arrays de TXG_SIZE = 4, indexados por los dos bits menos significativos del número de txg, así que cuatro txg consecutivos nunca comparten una posición. Cuando la transacción termina, dmu_tx_commit llama a txg_rele_to_sync, que decrementa el contador y despierta al hilo de cierre cuando llega a 0. Los contadores son por CPU porque uno solo would result in heavy lock contention (provocaría una fuerte contención en un cerrojo); la figura dibuja estos contadores para 4 CPU y, encima, los números de txg de cada estado.

Cuándo termina un txg. Lo decide el hilo de sincronización, en su bucle de espera.

txg_sync_thread Extracto de module/zfs/txg.c, líneas 532–553, OpenZFS zfs-2.4.4. Ver en GitHub
	for (;;) {
		clock_t timeout = zfs_txg_timeout * hz;
		clock_t timer;
		uint64_t txg;

		/*
		 * We sync when we're scanning, there's someone waiting
		 * on us, or the quiesce thread has handed off a txg to
		 * us, or we have reached our timeout.
		 */
		timer = (delta >= timeout ? 0 : timeout - delta);
		while (!dsl_scan_active(dp->dp_scan) &&
		    !tx->tx_exiting && timer > 0 &&
		    tx->tx_synced_txg >= tx->tx_sync_txg_waiting &&
		    !txg_has_quiesced_to_sync(dp)) {
			dprintf("waiting; tx_synced=%llu waiting=%llu dp=%p\n",
			    (u_longlong_t)tx->tx_synced_txg,
			    (u_longlong_t)tx->tx_sync_txg_waiting, dp);
			txg_thread_wait(tx, &cpr, &tx->tx_sync_more_cv, timer);
			delta = ddi_get_lbolt() - start;
			timer = (delta > timeout ? 0 : timeout - delta);
		}
  1. L533 El plazo es de zfs_txg_timeout segundos, 5 por defecto, en ticks de reloj.
  2. L542 timer cuenta desde start, fijado cuando empezó el spa_sync anterior: los 5 segundos corren desde el comienzo de la última sincronización, no desde su final.
  3. L543–L546 El hilo duerme mientras se cumple todo esto: no hay ningún recorrido activo (un scrub, un resilver (la resincronización de un disco) o una destrucción o liberación en segundo plano), el temporizador no se ha agotado, nadie espera un txg posterior al último sincronizado, y no espera ningún txg cerrado. Después pide al hilo de cierre el txg abierto.

Dos tipos de llamantes aumentan tx_sync_txg_waiting. zpool sync termina en txg_wait_synced(dp, 0), que espera al txg abierto más TXG_DEFER_SIZE, 2: en la figura, sincroniza tres txg seguidos. Y cada cambio suma su tamaño a los datos modificados de su txg; al superar zfs_dirty_data_sync_percent, el 20 %, de zfs_dirty_data_max (por defecto el 10 % de la memoria, con un tope), dsl_pool_dirty_space lo detecta y llama a txg_kick, así que un pool con mucha actividad sincroniza mucho antes de 5 segundos.

Cerrarlo. El hilo de cierre ejecuta txg_quiesce.

txg_quiesce Extracto de module/zfs/txg.c, líneas 395–428, OpenZFS zfs-2.4.4. Ver en GitHub
	/*
	 * Grab all tc_open_locks so nobody else can get into this txg.
	 */
	for (c = 0; c < max_ncpus; c++)
		mutex_enter(&tx->tx_cpu[c].tc_open_lock);

	ASSERT(txg == tx->tx_open_txg);
	tx->tx_open_txg++;
	tx->tx_open_time = tx_open_time = gethrtime();

	DTRACE_PROBE2(txg__quiescing, dsl_pool_t *, dp, uint64_t, txg);
	DTRACE_PROBE2(txg__opened, dsl_pool_t *, dp, uint64_t, tx->tx_open_txg);

	/*
	 * Now that we've incremented tx_open_txg, we can let threads
	 * enter the next transaction group.
	 */
	for (c = 0; c < max_ncpus; c++)
		mutex_exit(&tx->tx_cpu[c].tc_open_lock);

	spa_txg_history_set(dp->dp_spa, txg, TXG_STATE_OPEN, tx_open_time);
	spa_txg_history_add(dp->dp_spa, txg + 1, tx_open_time);

	/*
	 * Quiesce the transaction group by waiting for everyone to
	 * call txg_rele_to_sync() for their open transaction handles.
	 */
	for (c = 0; c < max_ncpus; c++) {
		tx_cpu_t *tc = &tx->tx_cpu[c];
		mutex_enter(&tc->tc_lock);
		while (tc->tc_count[g] != 0)
			cv_wait(&tc->tc_cv[g], &tc->tc_lock);
		mutex_exit(&tc->tc_lock);
	}
  1. L398–L399 Tomar el tc_open_lock de cada CPU impide que entren escrituras nuevas en este txg: txg_hold_open toma el cerrojo de su CPU antes de leer tx_open_txg.
  2. L402 El número del txg abierto aumenta en uno: a partir de aquí, las escrituras nuevas entran en el txg siguiente.
  3. L412–L413 Los cerrojos se liberan de inmediato. El txg siguiente está abierto antes de que el antiguo se vacíe, así que las escrituras no esperan.
  4. L422–L428 Para cada CPU por turno, esperar a que su contador para este txg sea 0. Las escrituras que entraron antes del cambio terminan aquí. Cuando acaba el bucle, ya nada puede cambiar el txg.

Un txg por estado. El bucle del hilo de cierre es donde se mantiene la regla.

txg_quiesce_thread Extracto de module/zfs/txg.c, líneas 628–667, OpenZFS zfs-2.4.4. Ver en GitHub
	for (;;) {
		uint64_t txg;

		/*
		 * We quiesce when there's someone waiting on us.
		 * However, we can only have one txg in "quiescing" or
		 * "quiesced, waiting to sync" state.  So we wait until
		 * the "quiesced, waiting to sync" txg has been consumed
		 * by the sync thread.
		 */
		while (!tx->tx_exiting &&
		    (tx->tx_open_txg >= tx->tx_quiesce_txg_waiting ||
		    txg_has_quiesced_to_sync(dp)))
			txg_thread_wait(tx, &cpr, &tx->tx_quiesce_more_cv, 0);

		if (tx->tx_exiting)
			txg_thread_exit(tx, &cpr, &tx->tx_quiesce_thread);

		txg = tx->tx_open_txg;
		dprintf("txg=%llu quiesce_txg=%llu sync_txg=%llu\n",
		    (u_longlong_t)txg,
		    (u_longlong_t)tx->tx_quiesce_txg_waiting,
		    (u_longlong_t)tx->tx_sync_txg_waiting);
		tx->tx_quiescing_txg = txg;

		mutex_exit(&tx->tx_sync_lock);
		txg_quiesce(dp, txg);
		mutex_enter(&tx->tx_sync_lock);

		/*
		 * Hand this txg off to the sync thread.
		 */
		dprintf("quiesce done, handing off txg %llu\n",
		    (u_longlong_t)txg);
		tx->tx_quiescing_txg = 0;
		tx->tx_quiesced_txg = txg;
		DTRACE_PROBE2(txg__quiesced, dsl_pool_t *, dp, uint64_t, txg);
		cv_broadcast(&tx->tx_sync_more_cv);
		cv_broadcast(&tx->tx_quiesce_done_cv);
	}
  1. L631–L641 Duerme hasta que alguien pide un txg posterior al abierto, y mientras el hilo de sincronización no haya tomado el último txg que cerró. Como mucho un txg está en cierre o cerrado.
  2. L646–L654 Toma el txg abierto, lo anota en tx_quiescing_txg y ejecuta txg_quiesce con el cerrojo de sincronización del pool liberado.
  3. L662–L666 La entrega: el txg pasa a tx_quiesced_txg y se despierta al hilo de sincronización. Si este aún está escribiendo el txg anterior, el nuevo espera ahí, cerrado.

¿Quién lo pide? Normalmente el propio hilo de sincronización, cuando está listo para más trabajo. Así, mientras un txg se sincroniza, el siguiente suele seguir abierto y recibiendo escrituras. Un llamante lo pide antes de tiempo: txg_wait_open(dp, 0, B_TRUE), al que en esta versión dmu_free_long_range llama to avoid filling up a TXG with just frees, para evitar que un txg se llene solo de liberaciones. Entonces el txg siguiente se cierra durante la sincronización, y los tres estados están ocupados a la vez: en la figura, «Liberar un archivo grande» mientras un txg se sincroniza.

La entrega al hilo de sincronización.

txg_sync_thread Extracto de module/zfs/txg.c, líneas 583–610, OpenZFS zfs-2.4.4. Ver en GitHub
		/*
		 * Consume the quiesced txg which has been handed off to
		 * us.  This may cause the quiescing thread to now be
		 * able to quiesce another txg, so we must signal it.
		 */
		ASSERT(tx->tx_quiesced_txg != 0);
		txg = tx->tx_quiesced_txg;
		tx->tx_quiesced_txg = 0;
		tx->tx_syncing_txg = txg;
		DTRACE_PROBE2(txg__syncing, dsl_pool_t *, dp, uint64_t, txg);
		cv_broadcast(&tx->tx_quiesce_more_cv);

		dprintf("txg=%llu quiesce_txg=%llu sync_txg=%llu\n",
		    (u_longlong_t)txg, (u_longlong_t)tx->tx_quiesce_txg_waiting,
		    (u_longlong_t)tx->tx_sync_txg_waiting);
		mutex_exit(&tx->tx_sync_lock);

		txg_stat_t *ts = spa_txg_history_init_io(spa, txg, dp);
		start = ddi_get_lbolt();
		spa_sync(spa, txg);
		delta = ddi_get_lbolt() - start;
		spa_txg_history_fini_io(spa, ts);

		mutex_enter(&tx->tx_sync_lock);
		tx->tx_synced_txg = txg;
		tx->tx_syncing_txg = 0;
		DTRACE_PROBE2(txg__synced, dsl_pool_t *, dp, uint64_t, txg);
		cv_broadcast(&tx->tx_sync_done_cv);
  1. L588–L593 El hilo de sincronización toma el txg cerrado, lo anota como tx_syncing_txg y despierta al hilo de cierre, que ahora puede cerrar otro txg si se ha pedido uno.
  2. L601–L603 spa_sync hace toda la E/S del txg. start es el momento desde el que cuenta el plazo de la espera siguiente.
  3. L607–L610 El txg está sincronizado: tx_synced_txg, y se despierta a todos los que esperan en txg_wait_synced.

Escribir hasta que nada cambie. Escribir un bloque asigna espacio, lo que modifica un space map, que es un metadato del MOS, que a su vez hay que escribir. Así que spa_sync ejecuta pasadas. La primera escribe todos los datos de usuario modificados; las siguientes solo escriben metadatos, lo que la pasada anterior modificó. Para que converja, a partir de la pasada 2 un bloque nacido en este txg se reescribe en su sitio en lugar de reasignarse (la sala de la copia en escritura muestra dónde), y a partir de la pasada 2 las liberaciones se aplazan a un txg posterior (sin la funcionalidad log spacemap).

spa_sync_iterate_to_convergence Extracto de module/zfs/spa.c, líneas 10743–10766, OpenZFS zfs-2.4.4. Ver en GitHub
		 * (e.g. if we have sync tasks but no dirty user data). We need
		 * to check the uberblock's rootbp because it is updated if we
		 * have synced out dirty data (though in this case the MOS will
		 * most likely also be dirty due to second order effects, we
		 * don't want to rely on that here).
		 */
		if (pass == 1 &&
		    BP_GET_LOGICAL_BIRTH(&spa->spa_uberblock.ub_rootbp) < txg &&
		    !dmu_objset_is_dirty(mos, txg)) {
			/*
			 * Nothing changed on the first pass, therefore this
			 * TXG is a no-op. Avoid syncing deferred frees, so
			 * that we can keep this TXG as a no-op.
			 */
			ASSERT(txg_list_empty(&dp->dp_dirty_datasets, txg));
			ASSERT(txg_list_empty(&dp->dp_dirty_dirs, txg));
			ASSERT(txg_list_empty(&dp->dp_sync_tasks, txg));
			ASSERT(txg_list_empty(&dp->dp_early_sync_tasks, txg));
			break;
		}

		spa_sync_deferred_frees(spa, tx);
	} while (dmu_objset_is_dirty(mos, txg));
}
  1. L10749–L10764 Si la pasada 1 no encuentra nada que escribir, el puntero de bloque raíz no nació en este txg y el MOS está limpio: el txg no tiene efecto, y el bucle se detiene.
  2. L10765 Si no, otra pasada mientras el MOS esté modificado en este txg. El bucle termina cuando una pasada no cambia nada.

Después spa_sync_rewrite_vdev_config llama a vdev_config_sync, que vuelca la caché de escritura de cada disco escrito en este txg y después escribe las etiquetas pares, el uberblock y las etiquetas impares. Las etiquetas solo se reescriben en los vdevs cuya configuración ha cambiado en este txg. El uberblock va a todos los vdevs de primer nivel si la configuración ha cambiado; si no, a tres vdevs de primer nivel como máximo, tomados en orden a partir de uno elegido al azar, y entonces es lo único nuevo en sus etiquetas. La escritura del uberblock es la confirmación, y su coste (un volcado por disco, escrituras de etiquetas) se paga una vez por txg, no una vez por escritura. Si nada ha cambiado en el txg, el uberblock no informa de ningún cambio y no se escribe nada (salvo que la protección multihost esté activada): un pool inactivo pasa por un txg cada 5 segundos sin escribir en sus discos. El Vestíbulo muestra dónde cae el uberblock en el anillo.

Lo que la figura deja fuera

La figura ejecuta las condiciones de espera de los dos hilos tal como las escribe el código, sobre las mismas variables. Dibuja 4 CPU; cada escritura es un bloque, y el umbral de datos modificados se reduce a 4 bloques (el 20 % de un zfs_dirty_data_max de 20 bloques). El tiempo está ralentizado, y la duración de una escritura o de una pasada es inventada. Deja fuera la regulación de escrituras (escrituras retrasadas o bloqueadas a medida que crecen los datos modificados), la espera en tc_open_lock mientras se cierra un txg, los scrubs y resilvers, las synctasks, el ZIL y la suspensión del pool. El tamaño de las pasadas es ilustrativo: aquí, un txg con datos siempre necesita tres pasadas.