Room 2
The tree and the uberblock
ZFS has no fsck. After a crash, the pool must open either in the state before the last transaction group or in the state after it, never in a mixture, even though one txg writes thousands of blocks to several disks.
So the whole change of state has to come down to one small write that either happened or did not. And that write must not be able to destroy the previous state if the power goes in the middle of it. The code gets there with a tree that hangs from a single pointer, a rule that a live block is never overwritten, and an order of writes at the end of every txg.
1 Everything hangs from one pointer
The uberblock holds one block pointer, ub_rootbp. It points to the pool’s
meta objset, the MOS, which begins with a dnode, the meta-dnode, whose blocks are blocks
of dnodes. A dnode is the root of one object: its dn_blkptr[] point to
dn_nlevels − 1 levels of indirect blocks, and the last level points to the
data. Each pointer is a 128-byte blkptr_t that holds up to three addresses
and the checksum of the block below. From the uberblock down, nothing else is needed to find every block of the
pool. The figure draws a file with three levels and four data blocks under the objset.
- old copy
- in memory, not written
3D view of the same state. Drag, or use the arrow keys on the focused view, to turn it; + and − to zoom. Every control above works the same in 3D.
No WebGL in this browser: the figure stays in 2D, which shows the same state.
The commit
Txg 4211 is open: 5 blocks to write. Next step: spa_sync writes them.
- next spa_sync writes the new blocks of txg 4211, each to a new place. code
- to do Flush: every disk written in this txg commits its write cache. code
- to do Even labels: L0 and L2 get the config of txg 4211. code
- to do Uberblocks: txg 4211 goes to slot 115 of the four rings. code
- to do Odd labels: L1 and L3 get the config of txg 4211. code
Last import
No import yet. Pull the plug at any step, then import the pool.
On the disk
| Label | Starts at code | Config | Slot 114 (+242 KiB) | Slot 115 (+243 KiB) |
|---|---|---|---|---|
| L0 | 0 MiB | txg 4200 | txg 4210 | txg 4083 |
| L1 | 0.25 MiB | txg 4200 | txg 4210 | txg 4083 |
| L2 | 1023.5 MiB | txg 4200 | txg 4210 | txg 4083 |
| L3 | 1023.75 MiB | txg 4200 | txg 4210 | txg 4083 |
One disk of 1 GiB, ashift 9: each ring has 128 slots of 1 KiB. Not to scale: a label is 256 KiB, the boot area 3.5 MiB, the rest holds the blocks.
Text version of this figure
The newest uberblock on the disk is that of txg 4210, in slot 114. It points to the objset of txg 4210. The tree it reaches: objset (txg 4210), dnodes (txg 4210), L2 #0 (txg 4210), L1 #0 (txg 4190), L1 #1 (txg 4210), D0 (txg 4190), D1 (txg 4120), D2 (txg 4180), D3 (txg 4210).
Txg 4211 has new copies of objset, dnodes, L2 #0, L1 #1 and D2; Txg 4211 is open: 5 blocks to write. Next step: spa_sync writes them.
Labels: L0 config txg 4200, slot 115 txg 4083; L1 config txg 4200, slot 115 txg 4083; L2 config txg 4200, slot 115 txg 4083; L3 config txg 4200, slot 115 txg 4083.
2 A write copies its whole path
A changed block is never written in place: it gets a new address. Its parent holds its
pointer, address and checksum, so the parent changes too: dbuf_dirty dirties the
parent, and so on up to the dnode, marked dirty in its block of dnodes, up to the
objset. One changed data block gives a new copy of itself and of every block on its path to the root; every
other block is shared by the old tree and the new one. The old tree is untouched: the last synced uberblock still
reaches all of it. In the figure, write D2, then D3 in the same txg: their common ancestors are copied once.
The next room follows the checksums down that path.
3 The commit, in four beats
At the end of a txg, once spa_sync has written the new blocks, vdev_config_sync runs
four steps in a fixed order. First it flushes the write cache of every disk written in the
txg, so the new tree is on stable storage before anything points to it. Then it writes the even
labels, L0 and L2, with the new configuration. Then the uberblock, into slot
ub_txg mod the number of slots of all four labels, which the uberblock of the previous txg
does not occupy: that write is the commit, the moment the root pointer swings to the new tree. Last, the
odd labels, L1 and L3. Each disk carries four labels of 256 KiB, two at
its start and two at its end, and each label ends with a 128 KiB ring of uberblock slots: 128
slots on the figure’s 512-byte sectors, 32 on 4K-sector disks.
Labels are written only for vdevs whose configuration changed in this txg. In most txgs none did: beats 2 and 4 write nothing, and only the uberblock is new. Turn off “The vdev configuration changes in this txg” and step through again.
4 Pull the plug, then import
On import, ZFS reads every slot of every label. A slot counts only if its
checksum and its magic number are right, so a torn write is skipped; among the others, the
highest ub_txg wins. Then the configuration is read from the
label with the newest config not newer than that uberblock. Pull the plug at each step and import. Before beat
3, no uberblock of the new txg exists: the old one wins, the even labels already written are newer than it and
are passed over, the odd labels still match it. As soon as one copy of the new uberblock has landed, it wins, and
the even labels match it. The tree is old or new, never mixed. Set how many uberblock writes land before a cut in
beat 3: zero gives case (1) of the code’s comment, one or more gives case (2).
The idea, at three depths
In one sentence
Every block of the pool hangs from the uberblock’s one pointer, a write copies its path instead of overwriting it, and the new uberblock goes to a slot the old one does not hold, after the new tree is on disk: so the pool is either the old tree or the new one, whatever moment the power goes.
In one paragraph
ub_rootbp points to the MOS’s objset_phys_t; its meta-dnode’s blocks hold dnodes; a
dnode’s dn_blkptr[] lead through dn_nlevels − 1 levels of indirect blocks to the data.
dbuf_dirty dirties a block’s parent, recursively, so a changed leaf gives new copies of the leaf and
all its ancestors at sync, each at a new address. vdev_config_sync then flushes the disks written in
the txg, writes the config of the even labels, writes the uberblock into slot ub_txg %
VDEV_UBERBLOCK_COUNT of all four labels and flushes it, and writes the odd labels. On import,
vdev_uberblock_load keeps the valid uberblock with the highest txg, and
vdev_label_read_config takes the label whose config txg is the highest not above it. A cut before any
new uberblock lands leaves the old uberblock and the odd labels; a cut after one lands leaves the new uberblock and
the even labels. Either pair is consistent.
In detail
The top of the tree. The uberblock names a txg and one block pointer.
struct uberblock
Excerpt of include/sys/uberblock_impl.h, lines
122–139, OpenZFS zfs-2.4.4.
View on GitHub
struct uberblock {
uint64_t ub_magic; /* UBERBLOCK_MAGIC */
uint64_t ub_version; /* SPA_VERSION */
uint64_t ub_txg; /* txg of last sync */
uint64_t ub_guid_sum; /* sum of all vdev guids */
uint64_t ub_timestamp; /* UTC time of last sync */
blkptr_t ub_rootbp; /* MOS objset_phys_t */
/* highest SPA_VERSION supported by software that wrote this txg */
uint64_t ub_software_version;
/* Maybe missing in uberblocks we read, but always written */
uint64_t ub_mmp_magic; /* MMP_MAGIC */
/*
* If ub_mmp_delay == 0 and ub_mmp_magic is valid, MMP is off.
* Otherwise, nanosec since last MMP write.
*/
uint64_t ub_mmp_delay;-
L125
ub_txg: the txg this uberblock commits. On import, the highest valid one wins. -
L127
ub_timestamp: the tie-breaker between two uberblocks of the same txg. -
L128
ub_rootbp: the only pointer.dsl_pool_sync_mossets it once the MOS is written.
objset_phys_t
Excerpt of include/sys/dmu_objset.h, lines
80–95, OpenZFS zfs-2.4.4.
View on GitHub
typedef struct objset_phys {
dnode_phys_t os_meta_dnode;
zil_header_t os_zil_header;
uint64_t os_type;
uint64_t os_flags;
uint8_t os_portable_mac[ZIO_OBJSET_MAC_LEN];
uint8_t os_local_mac[ZIO_OBJSET_MAC_LEN];
char os_pad0[OBJSET_PHYS_SIZE_V2 - sizeof (dnode_phys_t)*3 -
sizeof (zil_header_t) - sizeof (uint64_t)*2 -
2*ZIO_OBJSET_MAC_LEN];
dnode_phys_t os_userused_dnode;
dnode_phys_t os_groupused_dnode;
dnode_phys_t os_projectused_dnode;
char os_pad1[OBJSET_PHYS_SIZE_V3 - OBJSET_PHYS_SIZE_V2 -
sizeof (dnode_phys_t)];
} objset_phys_t;-
L81
An objset begins with a whole dnode, the meta-dnode. Its blocks are the objset’s blocks of dnodes: object n is dnode n. A filesystem’s own objset hangs from the MOS the same way, by its dataset’s
ds_bp.
dnode_phys_t
Excerpt of include/sys/dnode.h, lines
213–229, OpenZFS zfs-2.4.4.
View on GitHub
typedef struct dnode_phys {
uint8_t dn_type; /* dmu_object_type_t */
uint8_t dn_indblkshift; /* ln2(indirect block size) */
uint8_t dn_nlevels; /* 1=dn_blkptr->data blocks */
uint8_t dn_nblkptr; /* length of dn_blkptr */
uint8_t dn_bonustype; /* type of data in bonus buffer */
uint8_t dn_checksum; /* ZIO_CHECKSUM type */
uint8_t dn_compress; /* ZIO_COMPRESS type */
uint8_t dn_flags; /* DNODE_FLAG_* */
uint16_t dn_datablkszsec; /* data block size in 512b sectors */
uint16_t dn_bonuslen; /* length of dn_bonus */
uint8_t dn_extra_slots; /* # of subsequent slots consumed */
uint8_t dn_pad2[3];
/* accounting is protected by dn_dirty_mtx */
uint64_t dn_maxblkid; /* largest allocated block ID */
uint64_t dn_used; /* bytes (or sectors) of disk space */- L215 The size of the object’s indirect blocks, at most 128 KiB: 1024 block pointers of 128 bytes.
- L216 The height of the object’s tree; 1 when the dnode points straight at data.
-
L217
How many of the root pointers,
dn_blkptr[], are used.
A write climbs to the root.
dbuf_dirty
Excerpt of module/zfs/dbuf.c, lines
2537–2554, OpenZFS zfs-2.4.4.
View on GitHub
if (db->db_level+1 < dn->dn_nlevels) {
dmu_buf_impl_t *parent = db->db_parent;
dbuf_dirty_record_t *di;
int parent_held = FALSE;
if (db->db_parent == NULL || db->db_parent == dn->dn_dbuf) {
int epbs = dn->dn_indblkshift - SPA_BLKPTRSHIFT;
parent = dbuf_hold_level(dn, db->db_level + 1,
db->db_blkid >> epbs, FTAG);
ASSERT(parent != NULL);
parent_held = TRUE;
}
if (drop_struct_rwlock)
rw_exit(&dn->dn_struct_rwlock);
ASSERT3U(db->db_level + 1, ==, parent->db_level);
di = dbuf_dirty(parent, tx);
if (parent_held)
dbuf_rele(parent, FTAG);- L2537 A block below the top level of its object has a parent indirect block.
-
L2542–L2545
The parent is at level + 1, block number
blkid >> epbs, with 2epbs pointers per indirect block (1024 for 128 KiB). The figure uses 2, to stay small. - L2552 Dirtying a block dirties its parent, and so on up; the top level is held by the dnode, marked dirty in turn. Every dirty block is written to a new place at sync.
The four beats. First the flush.
vdev_config_sync
Excerpt of module/zfs/vdev_label.c, lines
2085–2098, OpenZFS zfs-2.4.4.
View on GitHub
/*
* Flush the write cache of every disk that's been written to
* in this transaction group. This ensures that all blocks
* written in this txg will be committed to stable storage
* before any uberblock that references them.
*/
zio_t *zio = zio_root(spa, NULL, NULL, flags);
for (vdev_t *vd =
txg_list_head(&spa->spa_vdev_txg_list, TXG_CLEAN(txg)); vd != NULL;
vd = txg_list_next(&spa->spa_vdev_txg_list, vd, TXG_CLEAN(txg)))
zio_flush(zio, vd);
(void) zio_wait(zio);- L2085–L2090 The reason for the flush, in the code’s words: the blocks of this txg reach stable storage “before any uberblock that references them”.
- L2093–L2098 Every vdev written in this txg, flushed; the wait covers them all.
vdev_config_sync
Excerpt of module/zfs/vdev_label.c, lines
2100–2139, OpenZFS zfs-2.4.4.
View on GitHub
/*
* Sync out the even labels (L0, L2) for every dirty vdev. If the
* system dies in the middle of this process, that's OK: all of the
* even labels that made it to disk will be newer than any uberblock,
* and will therefore be considered invalid. The odd labels (L1, L3),
* which have not yet been touched, will still be valid. We flush
* the new labels to disk to ensure that all even-label updates
* are committed to stable storage before the uberblock update.
*/
if ((error = vdev_label_sync_list(spa, 0, txg, flags)) != 0) {
if ((flags & ZIO_FLAG_IO_RETRY) != 0) {
zfs_dbgmsg("vdev_label_sync_list() returned error %d "
"for pool '%s' when syncing out the even labels "
"of dirty vdevs", error, spa_name(spa));
}
goto retry;
}
/*
* Sync the uberblocks to all vdevs in svd[].
* If the system dies in the middle of this step, there are two cases
* to consider, and the on-disk state is consistent either way:
*
* (1) If none of the new uberblocks made it to disk, then the
* previous uberblock will be the newest, and the odd labels
* (which had not yet been touched) will be valid with respect
* to that uberblock.
*
* (2) If one or more new uberblocks made it to disk, then they
* will be the newest, and the even labels (which had all
* been successfully committed) will be valid with respect
* to the new uberblocks.
*/
if ((error = vdev_uberblock_sync_list(svd, svdcount, ub, flags)) != 0) {
if ((flags & ZIO_FLAG_IO_RETRY) != 0) {
zfs_dbgmsg("vdev_uberblock_sync_list() returned error "
"%d for pool '%s'", error, spa_name(spa));
}
goto retry;
}- L2100–L2108 Beat 2, and what a cut during it means: even labels newer than any uberblock are invalid, the odd ones still valid.
-
L2109
The even labels:
vdev_label_syncwrites labelslandl + 2, L0 and L2. - L2118–L2131 Beat 3, and the two cases of a cut during it: no new uberblock landed, or at least one did. The figure replays both.
-
L2133
The uberblocks;
vdev_uberblock_sync_listthen flushes them, and succeeds if at least one write did.
vdev_uberblock_sync
Excerpt of module/zfs/vdev_label.c, lines
1768–1797, OpenZFS zfs-2.4.4.
View on GitHub
/*
* We chose a slot based on the txg. If this uberblock has a special
* RAIDZ expansion state, then it is essentially an update of the
* current uberblock (it has the same txg). However, the current
* state is committed, so we want to write it to a different slot. If
* we overwrote the same slot, and we lose power during the uberblock
* write, and the disk does not do single-sector overwrites
* atomically (even though it is required to - i.e. we should see
* either the old or the new uberblock), then we could lose this
* txg's uberblock. Rewinding to the previous txg's uberblock may not
* be possible because RAIDZ expansion may have already overwritten
* some of the data, so we need the progress indicator in the
* uberblock.
*/
int m = spa_multihost(vd->vdev_spa) ? MMP_BLOCKS_PER_LABEL : 0;
int n = (ub->ub_txg - (RRSS_GET_STATE(ub) == RRSS_SCRATCH_VALID)) %
(VDEV_UBERBLOCK_COUNT(vd) - m);
/* Copy the uberblock_t into the ABD */
abd_t *ub_abd = abd_alloc_for_io(VDEV_UBERBLOCK_SIZE(vd), B_TRUE);
abd_copy_from_buf(ub_abd, ub, sizeof (uberblock_t));
abd_zero_off(ub_abd, sizeof (uberblock_t),
VDEV_UBERBLOCK_SIZE(vd) - sizeof (uberblock_t));
for (int l = 0; l < VDEV_LABELS; l++)
vdev_label_write(zio, vd, l, ub_abd,
VDEV_UBERBLOCK_OFFSET(vd, n), VDEV_UBERBLOCK_SIZE(vd),
vdev_uberblock_sync_done, good_writes,
flags | ZIO_FLAG_DONT_PROPAGATE);
- L1768–L1781 Why the slot must differ from the current one: a torn overwrite of the committed uberblock could lose it. The comment is about RAID-Z expansion, which reuses a txg; the slot rule below serves every txg.
-
L1782–L1784
The slot:
ub_txgmodulo the slot count (one fewer with multihost). Consecutive txgs take consecutive slots; the next txg never lands on the slot of the previous one. - L1792–L1796 The same slot in all four labels of the disk.
vdev_config_sync
Excerpt of module/zfs/vdev_label.c, lines
2144–2161, OpenZFS zfs-2.4.4.
View on GitHub
/*
* Sync out odd labels for every dirty vdev. If the system dies
* in the middle of this process, the even labels and the new
* uberblocks will suffice to open the pool. The next time
* the pool is opened, the first thing we'll do -- before any
* user data is modified -- is mark every vdev dirty so that
* all labels will be brought up to date. We flush the new labels
* to disk to ensure that all odd-label updates are committed to
* stable storage before the next transaction group begins.
*/
if ((error = vdev_label_sync_list(spa, 1, txg, flags)) != 0) {
if ((flags & ZIO_FLAG_IO_RETRY) != 0) {
zfs_dbgmsg("vdev_label_sync_list() returned error %d "
"for pool '%s' when syncing out the odd labels of "
"dirty vdevs", error, spa_name(spa));
}
goto retry;
}- L2145–L2152 Beat 4: the odd labels last. A cut here leaves the even labels and the new uberblocks, which suffice.
-
L2154
The same function as beat 2, with
l = 1: L1 and L3.
Which disks get labels and uberblocks: only the vdevs on the dirty list get labels; with that list empty, the uberblock goes to up to three top-level vdevs.
spa_sync_rewrite_vdev_config
Excerpt of module/zfs/spa.c, lines
10792–10819, OpenZFS zfs-2.4.4.
View on GitHub
if (list_is_empty(&spa->spa_config_dirty_list)) {
vdev_t *svd[SPA_SYNC_MIN_VDEVS] = { NULL };
int svdcount = 0;
int children = rvd->vdev_children;
int c0 = random_in_range(children);
for (int c = 0; c < children; c++) {
vdev_t *vd =
rvd->vdev_child[(c0 + c) % children];
/* Stop when revisiting the first vdev */
if (c > 0 && svd[0] == vd)
break;
if (vd->vdev_ms_array == 0 ||
vd->vdev_islog ||
!vdev_is_concrete(vd))
continue;
svd[svdcount++] = vd;
if (svdcount == SPA_SYNC_MIN_VDEVS)
break;
}
error = vdev_config_sync(svd, svdcount, txg);
} else {
error = vdev_config_sync(rvd->vdev_child,
rvd->vdev_children, txg);
}- L10792–L10815 No configuration change: up to three top-level vdevs, from a random one, skipping logs and vdevs without metaslabs. A pool of one disk gets every uberblock on that disk.
- L10816–L10818 A configuration change: every top-level vdev.
Where the labels are.
vdev_label_offset
Excerpt of module/zfs/vdev_label.c, lines
158–170, OpenZFS zfs-2.4.4.
View on GitHub
/*
* Basic routines to read and write from a vdev label.
* Used throughout the rest of this file.
*/
uint64_t
vdev_label_offset(uint64_t psize, int l, uint64_t offset)
{
ASSERT(offset < sizeof (vdev_label_t));
ASSERT0(P2PHASE_TYPED(psize, sizeof (vdev_label_t), uint64_t));
return (offset + l * sizeof (vdev_label_t) + (l < VDEV_LABELS / 2 ?
0 : psize - VDEV_LABELS * sizeof (vdev_label_t)));
}- L165–L166 The offset falls inside one label, and the device size is a multiple of a label.
- L168–L169 Labels 0 and 1 at the start, 2 and 3 in the last 512 KiB. On the figure’s 1 GiB disk: 0, 256 KiB, 1023.5 MiB and 1023.75 MiB. After the two front labels comes a 3.5 MiB boot area.
The import. Every slot of every label is read, and a valid one is kept if it beats the best so far.
vdev_uberblock_compare
Excerpt of module/zfs/vdev_label.c, lines
1495–1505, OpenZFS zfs-2.4.4.
View on GitHub
int
vdev_uberblock_compare(const uberblock_t *ub1, const uberblock_t *ub2)
{
int cmp = TREE_CMP(ub1->ub_txg, ub2->ub_txg);
if (likely(cmp))
return (cmp);
cmp = TREE_CMP(ub1->ub_timestamp, ub2->ub_timestamp);
if (likely(cmp))
return (cmp);- L1498 The higher txg wins.
- L1503 On the same txg, the later timestamp; then the multihost sequence, which the figure leaves out.
vdev_label_read_config
Excerpt of module/zfs/vdev_label.c, lines
832–863, OpenZFS zfs-2.4.4.
View on GitHub
for (int l = 0; l < VDEV_LABELS; l++) {
nvlist_t *label = NULL;
if (zio_wait(zio[l]) == 0 &&
nvlist_unpack(vp[l]->vp_nvlist, sizeof (vp[l]->vp_nvlist),
&label, 0) == 0) {
/*
* Auxiliary vdevs won't have txg values in their
* labels and newly added vdevs may not have been
* completely initialized so just return the
* configuration from the first valid label we
* encounter.
*/
error = nvlist_lookup_uint64(label,
ZPOOL_CONFIG_POOL_TXG, &label_txg);
if ((error || label_txg == 0) && !config) {
config = label;
for (l++; l < VDEV_LABELS; l++)
zio_wait(zio[l]);
break;
} else if (label_txg <= txg && label_txg > best_txg) {
best_txg = label_txg;
nvlist_free(config);
config = fnvlist_dup(label);
}
}
if (label != NULL) {
nvlist_free(label);
label = NULL;
}
}- L835–L837 A label counts only if its read succeeds, and the read checks the label’s own checksum: a torn label is skipped.
-
L852–L856
The label with the highest config txg not above
txg, the winning uberblock’s (the call). This is how even labels of a txg that did not commit are passed over.
After the import, the next txg is the winner’s + 1: when the old tree won, the txg that was lost is written again, and its uberblock goes to the same slot, over the torn one. The import also asks for a config update, which marks every vdev dirty for that txg: it rewrites the four labels, torn ones included, whatever else it changes.
What the figure leaves out
One dataset and very few blocks: two pointers per indirect block instead of up to 1024, the meta-dnode’s own
indirect levels and the step from the MOS to a filesystem’s objset folded into one block of dnodes. One disk, so
every uberblock and every label lands on it; its 128 slots are those of 512-byte sectors. The writes of one beat are
issued together in the code; the figure lands them in order (L0 then L2, L1 then L3, the uberblocks L0 to L3) and
tears the one in flight when the plug is pulled. The content of the config, the retry with
ZIO_FLAG_IO_RETRY, multihost, RAID-Z expansion, several sync passes in one txg and the freeing of the
old blocks (the snapshots room) are not shown. The 3D view draws the same
state as the 2D one.