Salle 4

Groupes de transactions

La copie sur écriture garde les disques cohérents à chaque instant, mais seulement si le passage de l’ancien arbre au nouveau tient en une seule écriture. Or les écritures arrivent sans arrêt, de nombreux threads à la fois. Si chacune se terminait par sa propre validation, le pool viderait le cache de chaque disque et réécrirait ses labels et son uberblock à chaque écriture. Et une validation doit porter sur un état où aucune modification n’est à moitié faite.

ZFS répond par des lots. Chaque modification entre dans un groupe de transactions numéroté, un txg, et ZFS asserts consistency at the granularity of these transaction groups : la cohérence est garantie au niveau du groupe.

1 Trois txg, un par étape

Un txg est ouvert, en clôture ou en synchronisation, et il n’y a jamais plus d’un txg dans chaque état : TXG_CONCURRENT_STATES vaut 3. Les écritures entrent dans le txg ouvert. Pour le valider, un thread le ferme et ouvre aussitôt le suivant : les écritures n’attendent jamais un disque. Il attend ensuite que les écritures encore en cours dans le txg fermé se terminent. Un second thread écrit le txg fermé sur les disques et termine par l’uberblock. Ajoutez des écritures, lancez la lecture, ou avancez événement par événement.

Version texte de cette figure
  1. Chaque modification du pool entre dans un groupe de transactions numéroté (txg). Trois txg peuvent être actifs à la fois, un par état : ouvert, en clôture, en synchronisation.
  2. Une écriture prend une prise sur le txg ouvert : txg_hold_open incrémente un compteur du CPU sur lequel elle tourne, pour ce txg. Quand l’écriture est terminée, txg_rele_to_sync le décrémente.
  3. Le thread de synchronisation attend l’une de trois choses : zfs_txg_timeout (5 secondes) depuis le début de la dernière synchronisation, quelqu’un qui attend une synchronisation (zpool sync, ou txg_kick quand le txg ouvert contient plus de données modifiées que zfs_dirty_data_sync_percent de zfs_dirty_data_max), ou un txg déjà clos.
  4. Il demande alors le txg ouvert au thread de clôture. Celui-ci le ferme, ouvre aussitôt le txg suivant pour que les écritures n’attendent jamais, et attend que le compteur de chaque CPU pour le txg fermé soit à 0. Il remet alors le txg au thread de synchronisation.
  5. Le thread de synchronisation écrit le txg en passes : la première écrit les données et les métadonnées au-dessus d’elles ; chaque passe alloue de l’espace, ce qui modifie des space maps dans le MOS, et une nouvelle passe suit, jusqu’à ce qu’une passe laisse le MOS intact. vdev_config_sync écrit ensuite les labels et le nouvel uberblock : cette seule écriture valide le txg. Un txg où rien n’a changé n’écrit rien du tout.
  6. D’ordinaire, le txg suivant reste ouvert pendant qu’un autre se synchronise : le thread de clôture ne ferme un txg que si on le lui demande. Un appelant qui a besoin tout de suite d’un nouveau txg, comme dmu_free_long_range après un txg rempli de libérations, appelle txg_wait_open, et les trois états sont alors occupés. Il n’y a jamais plus d’un txg par état.

L’idée, à trois profondeurs

En une phrase

ZFS rassemble chaque modification dans un groupe de transactions numéroté, le ferme quand plus aucune écriture n’y est en cours, et l’écrit en une seule fois, si bien qu’une seule écriture d’uberblock valide des milliers de modifications à un moment où aucune n’est à moitié faite.

En un paragraphe

Chaque modification du pool est une transaction qui entre dans le txg ouvert. Deux threads du noyau par pool font avancer les txg. Le thread de synchronisation se réveille quand zfs_txg_timeout (5 secondes) s’est écoulé depuis le début de la dernière synchronisation, ou plus tôt si quelqu’un a besoin d’une synchronisation : zpool sync, ou un txg qui contient trop de données modifiées. Il demande le txg ouvert au thread de clôture. Celui-ci ouvre le txg suivant, attend qu’aucune transaction ne tienne plus l’ancien, et le lui remet. Le thread de synchronisation l’écrit alors en passes, jusqu’à ce qu’il ne reste rien à écrire, et le valide par l’uberblock. Pendant ce temps, le txg suivant reçoit déjà des écritures. Un txg où rien n’a changé n’écrit rien, pas même un uberblock. Un programme qui a besoin de ses données sur le disque tout de suite, avec fsync, n’attend pas, en temps normal, un txg : il passe par le ZIL (salle 10).

En détail

Entrer dans le txg ouvert. dmu_tx_assign appelle txg_hold_open, qui lit tx_open_txg et incrémente un compteur du CPU sur lequel il tourne, tc_count[txg & TXG_MASK]. L’état de chaque txg est rangé dans des tableaux de TXG_SIZE = 4 cases, indexés par les deux bits de poids faible du numéro de txg : quatre txg consécutifs ne partagent jamais une case. Quand la transaction est terminée, dmu_tx_commit appelle txg_rele_to_sync, qui décrémente le compteur et réveille le thread de clôture quand il atteint 0. Les compteurs sont par CPU parce qu’un compteur unique would result in heavy lock contention (provoquerait une forte contention sur un verrou) ; la figure dessine ces compteurs pour 4 CPU, et au-dessus les numéros de txg de chaque état.

Quand un txg se termine. C’est le thread de synchronisation qui décide, dans sa boucle d’attente.

txg_sync_thread Extrait de module/zfs/txg.c, lignes 532–553, OpenZFS zfs-2.4.4. Voir sur 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 Le délai est de zfs_txg_timeout secondes, 5 par défaut, compté en tops d’horloge.
  2. L542 timer part de start, fixé quand le précédent spa_sync a commencé : les 5 secondes courent depuis le début de la dernière synchronisation, pas depuis sa fin.
  3. L543–L546 Le thread dort tant que tout ceci est vrai : aucun parcours n’est actif (scrub, resilver, destruction ou libération en tâche de fond), le délai n’est pas écoulé, personne n’attend un txg plus récent que le dernier synchronisé, et aucun txg clos n’attend. Ensuite il demande le txg ouvert au thread de clôture.

Deux sortes d’appelants relèvent tx_sync_txg_waiting. zpool sync aboutit à txg_wait_synced(dp, 0), qui attend le txg ouvert plus TXG_DEFER_SIZE, 2 : dans la figure, trois txg se synchronisent à la suite. Et chaque modification ajoute sa taille aux données modifiées de son txg ; au-delà de zfs_dirty_data_sync_percent, 20 %, de zfs_dirty_data_max (par défaut 10 % de la mémoire, plafonné), dsl_pool_dirty_space le constate et appelle txg_kick : un pool chargé se synchronise bien avant 5 secondes.

Le fermer. Le thread de clôture exécute txg_quiesce.

txg_quiesce Extrait de module/zfs/txg.c, lignes 395–428, OpenZFS zfs-2.4.4. Voir sur 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 Prendre le tc_open_lock de chaque CPU empêche de nouvelles écritures d’entrer dans ce txg : txg_hold_open prend le verrou de son CPU avant de lire tx_open_txg.
  2. L402 Le numéro du txg ouvert augmente de un : désormais, les nouvelles écritures entrent dans le txg suivant.
  3. L412–L413 Les verrous sont rendus aussitôt. Le txg suivant est ouvert avant que l’ancien soit vidé : les écritures n’attendent pas.
  4. L422–L428 Pour chaque CPU tour à tour, attendre que son compteur pour ce txg soit à 0. Les écritures entrées avant le changement s’y terminent. À la fin de la boucle, plus rien ne peut modifier le txg.

Un txg par état. C’est la boucle du thread de clôture qui tient la règle.

txg_quiesce_thread Extrait de module/zfs/txg.c, lignes 628–667, OpenZFS zfs-2.4.4. Voir sur 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 Il dort jusqu’à ce qu’on demande un txg au-delà du txg ouvert, et tant que le dernier txg qu’il a clos n’a pas été pris par le thread de synchronisation. Un seul txg au plus est en clôture ou clos.
  2. L646–L654 Il prend le txg ouvert, le note dans tx_quiescing_txg et exécute txg_quiesce, verrou de synchronisation du pool relâché.
  3. L662–L666 La remise : le txg passe dans tx_quiesced_txg et le thread de synchronisation est réveillé. Si celui-ci écrit encore le txg précédent, celui-là attend ici, clos.

Qui le demande ? D’ordinaire, le thread de synchronisation lui-même, quand il est prêt pour la suite. Pendant qu’un txg se synchronise, le suivant reste donc en général ouvert et continue de recevoir des écritures. Un appelant demande plus tôt : txg_wait_open(dp, 0, B_TRUE), que dans cette version dmu_free_long_range appelle to avoid filling up a TXG with just frees, pour éviter qu’un txg ne se remplisse que de libérations. Le txg suivant entre alors en clôture pendant la synchronisation, et les trois états sont occupés à la fois : dans la figure, « Libérer un gros fichier » pendant qu’un txg se synchronise.

La remise au thread de synchronisation.

txg_sync_thread Extrait de module/zfs/txg.c, lignes 583–610, OpenZFS zfs-2.4.4. Voir sur 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 Le thread de synchronisation prend le txg clos, le note comme tx_syncing_txg et réveille le thread de clôture, qui peut désormais clore un autre txg si on en a demandé un.
  2. L601–L603 spa_sync fait toutes les entrées-sorties du txg. start est l’instant d’où partira le délai de la prochaine attente.
  3. L607–L610 Le txg est synchronisé : tx_synced_txg, et tous ceux qui attendent dans txg_wait_synced sont réveillés.

Écrire jusqu’à ce que rien ne change. Écrire un bloc alloue de l’espace, ce qui modifie une space map, qui est une métadonnée du MOS, qu’il faut donc écrire aussi. spa_sync procède donc par passes. La première écrit toutes les données utilisateur modifiées ; les suivantes n’écrivent que des métadonnées, ce que la passe précédente a modifié. Pour converger, à partir de la passe 2, un bloc né dans ce txg est réécrit sur place au lieu d’être réalloué (la salle de la copie sur écriture montre où), et à partir de la passe 2, les libérations sont reportées à un txg ultérieur (sans la fonctionnalité log spacemap).

spa_sync_iterate_to_convergence Extrait de module/zfs/spa.c, lignes 10743–10766, OpenZFS zfs-2.4.4. Voir sur 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 passe 1 ne trouve rien à écrire, le pointeur de bloc racine n’est pas né dans ce txg et le MOS est intact : le txg est sans effet, et la boucle s’arrête.
  2. L10765 Sinon, une nouvelle passe tant que le MOS est modifié dans ce txg. La boucle s’arrête quand une passe ne change plus rien.

Ensuite spa_sync_rewrite_vdev_config appelle vdev_config_sync, qui vide le cache d’écriture de chaque disque écrit dans ce txg, puis écrit les labels pairs, l’uberblock et les labels impairs. Les labels ne sont réécrits que sur les vdevs dont la configuration a changé dans ce txg. L’uberblock va sur tous les vdevs de premier niveau si la configuration a changé, sinon sur trois vdevs de premier niveau au plus, pris dans l’ordre à partir d’un vdev tiré au hasard, et il est alors la seule nouveauté de leurs labels. L’écriture de l’uberblock est la validation, et son coût (un vidage de cache par disque, des écritures de labels) se paie une fois par txg, pas une fois par écriture. Si rien n’a changé dans le txg, l’uberblock ne signale aucun changement et rien n’est écrit (sauf si la protection multihost est active) : un pool inactif passe un txg toutes les 5 secondes sans écrire sur ses disques. Le hall montre où l’uberblock se range dans l’anneau.

Ce que la figure laisse de côté

La figure exécute les conditions d’attente des deux threads telles que le code les écrit, sur les mêmes variables. Elle dessine 4 CPU ; chaque écriture fait un bloc, et le seuil de données modifiées est ramené à 4 blocs (20 % d’un zfs_dirty_data_max de 20 blocs). Le temps est ralenti, et la durée d’une écriture ou d’une passe est inventée. Elle laisse de côté la régulation des écritures (écritures retardées ou bloquées quand les données modifiées s’accumulent), l’attente sur tc_open_lock pendant la clôture d’un txg, les scrubs et resilvers, les synctasks, le ZIL et la suspension du pool. La taille des passes est indicative : un txg avec des données prend toujours trois passes ici.