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.
Abierto
tx_open_txg
En cierre
tx_quiescing_txg
En sincronización
tx_syncing_txg
txg_sync_threadtxg_quiesce_thread
Eventos
Versión en texto de esta figura
- 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.
- 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.
- 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.
- 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.
- 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.
- 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);
}-
L533
El plazo es de
zfs_txg_timeoutsegundos, 5 por defecto, en ticks de reloj. -
L542
timercuenta desdestart, fijado cuando empezó elspa_syncanterior: los 5 segundos corren desde el comienzo de la última sincronización, no desde su final. - 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);
}-
L398–L399
Tomar el
tc_open_lockde cada CPU impide que entren escrituras nuevas en este txg:txg_hold_opentoma el cerrojo de su CPU antes de leertx_open_txg. - L402 El número del txg abierto aumenta en uno: a partir de aquí, las escrituras nuevas entran en el txg siguiente.
- 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.
- 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);
}- 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.
-
L646–L654
Toma el txg abierto, lo anota en
tx_quiescing_txgy ejecutatxg_quiescecon el cerrojo de sincronización del pool liberado. -
L662–L666
La entrega: el txg pasa a
tx_quiesced_txgy 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);-
L588–L593
El hilo de sincronización toma el txg cerrado, lo anota como
tx_syncing_txgy despierta al hilo de cierre, que ahora puede cerrar otro txg si se ha pedido uno. -
L601–L603
spa_synchace toda la E/S del txg.startes el momento desde el que cuenta el plazo de la espera siguiente. -
L607–L610
El txg está sincronizado:
tx_synced_txg, y se despierta a todos los que esperan entxg_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));
}- 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.
- 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.