Persistent memory based storage is able to perform IO at byte (or more accurately, cache line) granularity. However, we often want to expose such storage as traditional block devices. The block drivers for persistent memory will do exactly this. However, they do not provide any atomicity guarantees. Traditional SSDs typically provide protection against torn sectors in hardware, using stored energy in capacitors to complete in-flight block writes, or perhaps in firmware. We don’t have this luxury with persistent memory - if a write is in progress, and we experience a power failure, the block will contain a mix of old and new data. Applications may not be prepared to handle such a scenario.
The Block Translation Table (BTT) provides atomic sector update semantics for persistent memory devices, so that applications that rely on sector writes not being torn can continue to do so. The BTT manifests itself as a stacked block device, and reserves a portion of the underlying storage for its metadata. At the heart of it, is a indirection table that re-maps all the blocks on the volume. It can be thought of as an extremely simple file system that only provides atomic sector updates.
The underlying storage on which a BTT can be laid out is not limited in any way. The BTT, however, splits the available space into chunks of up to 512 GiB, called “Arenas”.
Each arena follows the same layout for its metadata, and all references in an arena are internal to it (with the exception of one field that points to the next arena). The following depicts the On-disk metadata layout (Can also be seen in detail in btt_layout.h):
The map is a simple lookup/indirection table that maps an LBA to an internal block. Each map entry is 32 bits. The two most significant bits are special flags, and the remaining form the internal block number.
Bit | Description |
---|---|
31 | ZERO flag - marks if a read to this block should return zeroes |
30 | ERROR flag - marks an error block. Cleared on write |
29 - 0 | Mappings to internal ‘postmap’ blocks |
Some of the terminology that will be subsequently used:
External LBA | LBA as made visible to upper layers. |
ABA | Arena Block Address - Block offset/number within an arena |
Premap ABA | The block offset into an arena, which was decided upon by range checking the External LBA |
Postmap ABA | The block number in the “Data Blocks” area obtained after indirection from the map |
nfree | The number of free blocks that are maintained at any given time. This is the number of concurrent writes that can happen to the arena. |
For example, after adding a BTT, we surface a disk of 1024G. We get a read for
the external LBA
at 768G. This falls into the second arena, and of the 512G
worth of blocks that this arena contributes, this block is at 256G. Thus, the
premap ABA
is 256G. We now refer to the map, and find out the mapping for
block ‘X’ (256G) points to block ‘Y’, say ‘64’. Thus the postmap ABA
is 64.
The BTT provides sector atomicity by making every write an “allocating write”, i.e. Every write goes to a “free” block. A running list of free blocks is maintained in the form of the BTT flog. ‘Flog’ is a combination of the words “free list” and “log”. The flog contains ‘nfree’ entries, and an entry contains:
Field | Description |
---|---|
lba | The premap ABA that is being written to |
old_map | The old postmap ABA - after ‘this’ write completes, this will be a free block. |
new_map | The new postmap ABA. The map will up updated to reflect this lba->postmap_aba mapping, but we log it here in case we have to recover. |
seq | Sequence number to mark which of the 2 sections of this flog entry is valid/newest. It cycles between 01->10->11->01 (binary) under normal operation, with 00 indicating an uninitialized state. |
lba’ | alternate lba entry |
old_map’ | alternate old postmap entry |
new_map’ | alternate new postmap entry |
seq’ | alternate sequence number. |
Each of the above fields is 32-bit, making one entry 16 bytes. Flog updates are done such that for any entry being written, it:
While ‘nfree’ describes the number of concurrent IOs an arena can process concurrently, ‘nlanes’ is the number of IOs the BTT device as a whole can process.
nlanes = min(nfree, num_cpus)
A lane number is obtained at the start of any IO, and is used for indexing into all the on-disk and in-memory data structures for the duration of the IO. It is protected by a spinlock.
Consider a case where we have two threads, one doing reads and the other, writes. We can hit a condition where the writer thread grabs a free block to do a new IO, but the (slow) reader thread is still reading from it. In other words, the reader consulted a map entry, and started reading the corresponding block. A writer started writing to the same external LBA, and finished the write updating the map for that external LBA to point to its new postmap ABA. At this point the internal, postmap block that the reader is (still) reading has been inserted into the list of free blocks. If another write comes in for the same LBA, it can grab this free block, and start writing to it, causing the reader to read incorrect data. To prevent this, we introduce the RTT.
The RTT is a simple, per arena table with ‘nfree’ entries. Every reader inserts into rtt[lane_number], the postmap ABA it is reading, and clears it after the read is complete. Every writer thread, after grabbing a free block, checks the RTT for its presence. If the postmap free block is in the RTT, it waits till the reader clears the RTT entry, and only then starts writing to it.
Consider a case where two writer threads are writing to the same LBA. There can be a race in the following sequence of steps:
free[lane] = map[premap_aba]
map[premap_aba] = postmap_aba
Both threads can update their respective free[lane] with the same old, freed postmap_aba. This has made the layout inconsistent by losing a free entry, and at the same time, duplicating another free entry for two lanes.
To solve this, we could have a single map lock (per arena) that has to be taken before performing the above sequence, but we feel that could be too contentious. Instead we use an array of (nfree) map_locks that is indexed by (premap_aba modulo nfree).
On startup, we analyze the BTT flog to create our list of free blocks. We walk through all the entries, and for each lane, of the set of two possible ‘sections’, we always look at the most recent one only (based on the sequence number). The reconstruction rules/steps are simple:
Read:
Write:
An arena would be in an error state if any of the metadata is corrupted irrecoverably, either due to a bug or a media error. The following conditions indicate an error:
If any of these error conditions are encountered, the arena is put into a read only state using a flag in the info block.
An implementation of the BTT is present in the PMDK, that one can experiment with today!