UI
UltraInstinct AI TECH
Back to latest articles
Software DevelopmentAugust 29, 2026

B-Epsilon Tree Database Storage Engines and Lock-Free Flash Page Management on NVMe SSDs

A deep technical analysis of B-epsilon tree database engines, amortized I/O complexities, lock-free flash page indirection tables, and NVMe kernel-bypass storage co-design.

Technical visual for B-Epsilon Tree Database Storage Engines and Lock-Free Flash Page Management on NVMe SSDs
Advertisement
Google AdSense In-Article Contextual Slot

Introduction

Modern solid-state storage hardware has altered the performance landscape of database storage engines. Contemporary Non-Volatile Memory Express (NVMe) SSDs achieve millions of IOPS per device with deep parallel queues and sub-hundred-microsecond access latencies. However, conventional storage engine architectures remain tethered to paradigms designed for high-latency rotational media or earlier solid-state architectures.

Traditional $B^+$-tree storage engines incur random-write path penalties, producing high Write Amplification Factors (WAF) and thread contention on page latches. Conversely, Log-Structured Merge (LSM) trees eliminate in-place writes via append-only delta logs and multi-level merges. Yet, LSM-trees introduce unpredictable tail latencies from cascading compactions and sacrifice point-read performance due to multi-tier Bloom filter evaluation and index traversals.

The $\text{B}^\epsilon$-tree architecture addresses this structural trade-off. By parameterizing the balance between pivot indexing and internal node buffering, $\text{B}^\epsilon$-trees span the algorithmic continuum between $B^+$-trees and LSM-trees. When paired with a lock-free, page-indirection storage layer optimized for modern NVMe protocols—utilizing Linux io_uring or user-space kernel-bypass engines like SPDK (Storage Performance Development Kit)—the storage engine achieves optimal amortized write costs, deterministic point-read latencies, and latch-free concurrency across parallel execution rings.

Theoretical Foundations and I/O Cost Model of B^ε-Trees

The theoretical efficacy of the $\text{B}^\epsilon$-tree resides within the external memory model (Disk Access Model), parameterized by node size $B$ (measured in items or bytes), pivot fanout, and an adaptation exponent $\epsilon \in [0, 1]$.

       B^ε-Tree Node Topology: Node Size = B
+---------------------------------------------------+
|  Pivot Keys: [ K_1 | K_2 | ... | K_(B^ε - 1) ]   | (Pivots: B^ε)
+---------------------------------------------------+
|  Child Ptrs: [ P_0 | P_1 | ... | P_(B^ε - 1) ]   |
+---------------------------------------------------+
|  Message Buffer: [ Msg_1, Msg_2, ..., Msg_M ]    | (Capacity: B - B^ε)
+---------------------------------------------------+

In a standard $\text{B}^\epsilon$-tree:

  • An internal node occupies $B$ storage blocks.
  • The node allocates space for $B^\epsilon$ pivots (fanout $F = B^\epsilon$).
  • The remaining space, $B - B^\epsilon$, serves as an internal node message buffer.

Asymptotic I/O Complexity Bounds

Let $N$ represent the total number of records maintained within the tree. The tree height $H$ is dictated by the fanout:

$$H = \Theta\left(\log_{B^\epsilon} \left(\frac{N}{B}\right)\right) = \Theta\left(\frac{1}{\epsilon} \log_B N\right)$$

When an insertion, update, or deletion occurs, the operation is encoded as a discrete message and appended directly to the buffer of the root node. If the root buffer becomes saturated (exceeds $B - B^\epsilon$), a flush operation cascades a subset of buffered messages down to the appropriate child node.

Because a flush targets a specific child whose subtree spans a $1/B^\epsilon$ fraction of the current node's keyspace, the minimum number of messages routed to the selected child in a single flush is:

$$\text{Batch Size} = \frac{B - B^\epsilon}{B^\epsilon} = B^{1-\epsilon} - 1 = \Theta(B^{1-\epsilon})$$

Writing this batch of messages to the child requires $O(1)$ I/O operations. Consequently, the amortized I/O cost per message to traverse one level of the tree is:

$$\text{Cost}_{\text{level}} = O\left(\frac{1}{B^{1-\epsilon}}\right)$$

Multiplying this per-level cost across the full height of the tree yields the global amortized I/O complexity for insertions and deletions:

$$\text{Cost}_{\text{write}} = O\left(\frac{1}{\epsilon} \log_B N \cdot \frac{1}{B^{1-\epsilon}}\right) = O\left(\frac{\log_B N}{\epsilon B^{1-\epsilon}}\right)$$

Point queries must search the message buffers along a single root-to-leaf path and inspect the leaf, traversing $H$ nodes:

$$\text{Cost}_{\text{point_query}} = O\left(\frac{1}{\epsilon} \log_B N\right)$$

Range queries over $k$ contiguous records require routing to the initial leaf followed by reading sequential leaf nodes:

$$\text{Cost}_{\text{range_query}} = O\left(\frac{1}{\epsilon} \log_B N + \frac{k}{B}\right)$$

Parameter Spectrum Analysis

The tuning variable $\epsilon$ governs the system's operational characteristics across the storage performance frontier:

Metric / Variant $B^+$-Tree ($\epsilon = 1$) $\text{B}^\epsilon$-Tree ($\epsilon = 0.5$) LSM-Tree ($\epsilon = 0$)
Pivot Fanout $B$ $\sqrt{B}$ $O(1)$
Buffer Capacity $0$ $B - \sqrt{B}$ $B$
Insert I/O Cost $O(\log_B N)$ $O\left(\frac{\log_B N}{\sqrt{B}}\right)$ $O\left(\frac{\log_T N}{B}\right)$
Point Query I/O $O(\log_B N)$ $O(2 \log_B N)$ $O(L \cdot \log_B N)$ (No Filter)
Write Amplification Very High ($>50\times$) Extremely Low ($<3\times$) Moderate ($10\times - 30\times$)

By fixing $\epsilon \approx 0.5$, write costs decrease by a factor of $\sqrt{B}$ (for a 4 KB page with 16-byte keys, $\sqrt{B} \approx 16$), while point query costs merely double relative to a fully optimized $B^+$-tree.

       B^+ Tree (ε = 1)                 B^ε Tree (ε = 0.5)                 LSM Tree (ε = 0)
+-----------------------------+   +-----------------------------+   +-----------------------------+
| Nodes contain only pivots;  |   | Nodes balance pivots with   |   | Memory-to-disk append log;  |
| 0 buffer space. Low-latency |   | sizable message buffers.    |   | full run merges across      |
| reads, unbuffered writes.   |   | Balanced read/write Pareto. |   | tiers. High write throughput|
+-----------------------------+   +-----------------------------+   +-----------------------------+

Node Structural Layout, Message Routing, and Propagation Mechanics

Implementing a $\text{B}^\epsilon$-tree demands a memory layout that supports fast in-node binary searches over variable-length pivots while allowing lock-free appends and batch extractions within the message buffer.

         Internal Node Physical Memory Layout
+-----------------------------------------------------------+
| Lock-Free Slot Array (Offsets, Pivot Hashes, Child PIDs)  |
+-----------------------------------------------------------+
| Routing Filter (Fingerprint array / SIMD search vector)   |
+-----------------------------------------------------------+
| Append-Only Staged Message Buffer (Payload Arena)         |
| [ Message Header | Key Length | Key | Value Length | Val ]|
+-----------------------------------------------------------+

Message Encoding and Type Semantics

Messages within an internal buffer represent deferred mutations. Each message contains a 1-byte opcode, an 8-byte monotonic Logical Sequence Number (LSN), a variable-length key, and a variable-length payload:

enum class OpCode : uint8_t {
    INSERT      = 0x01,
    DELETE      = 0x02,
    UPDATE      = 0x03, // Upsert delta
    MERGE_DELTA = 0x04
};

struct __attribute__((packed)) MessageHeader {
    OpCode   op;
    uint64_t lsn;
    uint32_t key_len;
    uint32_t val_len;
};

When a query traverses downward, it applies an accumulator fold over all matching key messages encountered across internal buffers before applying the resulting mutations to the leaf page.

Downward Batch Propagation Algorithm

A node buffer flushes when its memory consumption crosses a predefined capacity threshold $\tau \cdot (B - B^\epsilon)$, where $\tau \approx 0.85$.

Step 1: Identify saturated parent node buffer.
Step 2: Find child subtree with largest pending message payload.
Step 3: Extract, sort, and batch messages matching child's key range.
Step 4: Atomic append batch to child node buffer via CAS or append-stage.
Step 5: Reclaim processed message bytes from parent node buffer via GC.
  1. Child Selection: Scan the node's pivot slot directory to calculate the pending message byte count per child pointer: $$\text{Child}^* = \arg\max_{c} \sum_{m \in \text{Buffer}} |m| \cdot \mathbb{I}(\text{Pivot}_{c-1} \le m.\text{key} < \text{Pivot}_c)$$
  2. Extraction and Sorting: Extract the continuous subarray or linked chain of messages designated for $\text{Child}^*$. Sort them by Key ASC, LSN ASC.
  3. Compaction on Injection: If duplicate mutations for identical keys exist in the batch, collapse them in-memory to preserve buffer bandwidth (e.g., INSERT(k, v1) + UPDATE(k, v2) -&gt; INSERT(k, v2)).
  4. Append to Child: Atomically write the compacted batch into $\text{Child}^$'s message buffer. If $\text{Child}^$ is a leaf, apply the updates directly into the sorted leaf array.

Lock-Free NVMe Page Translation and Atomic Indirection

Physical SSDs cannot perform in-place updates at the flash memory cell level. Modifying an existing flash page requires writing to an erased physical block via out-of-place writes.

To prevent physical lock contention, mapping bottlenecks, and thread synchronization overhead, the storage engine decouples logical node identifiers from physical flash page offsets via a lock-free page indirection table.

                  Lock-Free Mapping Architecture
Logical
Page ID (PID)      Mapping Table Array                Physical NVMe Page
+-------+         +--------------------+             +--------------------+
| PID 4 | ------&gt; | [Ptr] Atomic CAS   | ----------&gt; | Physical Node Data |
+-------+         +--------------------+             +--------------------+
                             |
                      (Swaps pointer)
                             v
                  +--------------------+
                  | Delta Record Chain |
                  +--------------------+

Atomic Mapping Table Architecture

The Mapping Table is a flat, contiguous array of 64-bit atomic pointers indexed directly by a physical Logical Page ID (PID):

struct MappingEntry {
    // Top 16 bits: State flags / Epoch tag
    // Bottom 48 bits: Virtual memory pointer or direct NVMe Block Address (LBA)
    std::atomic target_address;
};

class LockFreePageTable {
    static constexpr uint64_t DISK_FLAG_MASK = 1ULL &lt;&lt; 63;
    static constexpr uint64_t PTR_MASK       = (1ULL &lt;&lt; 48) - 1;
    MappingEntry* entries;

public:
    bool CasMapping(uint32_t pid, uint64_t expected_addr, uint64_t new_addr) {
        return entries[pid].target_address.compare_exchange_strong(
            expected_addr,
            new_addr,
            std::memory_order_acq_rel,
            std::memory_order_acquire
        );
    }

    uint64_t LoadAddress(uint32_t pid) {
        return entries[pid].target_address.load(std::memory_order_acquire);
    }
};

Delta Page Chaining and Consolidation

Modifications to internal nodes and leaves avoid rewriting whole $B$-sized pages immediately. Instead, they prepend delta records directly to the physical memory descriptor.

       Memory Representation of Delta-Chained Page Node
+-------------------+      +-------------------+      +-------------------+
| Base Page (Node)  | &lt;--- | Delta Node Record | &lt;--- | Delta Flush Batch |
| LBA: 0x000F4200   |      | LSN: 1042         |      | LSN: 1089         |
+-------------------+      +-------------------+      +-------------------+
                                                        ^
                                                        | (Mapping Table Pointer)
                                               [PID 4 Entry in Indirection Table]

When thread $T_1$ flushes a message batch to node $\text{PID}_k$:

  1. $T_1$ allocates a memory-backed DeltaFlushNode pointing to the current value of MappingTable[PID_k].
  2. $T_1$ attempts an atomic compare_exchange_strong on MappingTable[PID_k].
  3. If CAS succeeds, the update is instantly visible without acquiring a node write-lock.
  4. If the delta chain length exceeds a threshold (e.g., length $> 8$), the next traversing thread triggers a lock-free page consolidation. The thread allocates a new contiguous base node, merges the base and delta chain entries, writes the merged page out to an unallocated NVMe block address, and updates the MappingTable entry via CAS to point to the newly persisted NVMe physical address.

Epoch-Based Memory Reclamation (EBR)

Memory safety across concurrent lock-free pointer updates is maintained using Epoch-Based Reclamation:

class EpochManager {
public:
    std::atomic global_epoch{0};
    
    struct ThreadState {
        std::atomic local_epoch{0};
        std::atomic active{false};
        std::vector retired_list[3];
    };

    void EnterCriticalRegion(ThreadState&amp; ts) {
        ts.active.store(true, std::memory_order_relaxed);
        ts.local_epoch.store(global_epoch.load(std::memory_order_relaxed), 
                             std::memory_order_seq_cst);
    }

    void ExitCriticalRegion(ThreadState&amp; ts) {
        ts.active.store(false, std::memory_order_release);
    }

    void Retire(ThreadState&amp; ts, void* ptr) {
        uint64_t e = global_epoch.load(std::memory_order_relaxed);
        ts.retired_list[e % 3].push_back(ptr);
    }

    void ReclaimSafePointers(ThreadState&amp; ts);
};

Memory blocks displaced by CAS operations are queued into the current epoch index bucket. The global epoch counter advances only after all registered worker threads have advanced past the target epoch, guaranteeing that no hardware worker or execution thread holds dangling references to dereferenced delta fragments.

Asynchronous Hardware Interfacing via Kernel-Bypass and io_uring

Sub-millisecond flash write paths can experience significant performance overhead when using synchronous kernel system calls like pwritev() or standard POSIX AIO (libaio). Context switches, kernel page cache locks, and hardware interrupt-handling overhead create CPU bottlenecks.

Modern $\text{B}^\epsilon$-tree implementations bypass these limitations using Linux io_uring operating in kernel polling mode (IORING_SETUP_SQPOLL) or user-space SPDK drivers.

       Linux io_uring SQPOLL Architecture for Storage Nodes
+-------------------------------------------------------------------+
| User-Space Application Ring (Lock-Free B^ε-Tree Engine)           |
|                                                                   |
|  +---------------------------+     +---------------------------+  |
|  | Submission Queue (SQ)     |     | Completion Queue (CQ)     |  |
|  +---------------------------+     +---------------------------+  |
+---------------|----------------------------------^----------------+
                | Memory-Mapped Ring (No Syscall)  | Polled Completion
                v                                  |
+--------------------------------------------------|----------------+
| Kernel / Hardware Boundary                       |                |
|                                                  |                |
|  +-----------------------------+                 |                |
|  | SQ Polling Thread (Kernel)  | ----------------+                |
|  +-----------------------------+                                  |
|                 | (Direct DMA Dispatch)                           |
|                 v                                                 |
|  +-------------------------------------------------------------+  |
|  | NVMe Controller Physical Command Queues (Submission/Compl.) |  |
|  +-------------------------------------------------------------+  |
+-------------------------------------------------------------------+

Submission Queue Entry (SQE) Batch Dispatch

When consolidated nodes or leaf write operations reach the persistence threshold, the engine constructs contiguous buffer descriptors and registers them with the submission ring:

struct AsyncIOContext {
    struct io_uring ring;
    
    void InitializeRing(uint32_t queue_depth) {
        struct io_uring_params params;
        memset(¶ms, 0, sizeof(params));
        params.flags = IORING_SETUP_SQPOLL | IORING_SETUP_IOPOLL;
        params.sq_thread_idle = 2000; // ms
        
        io_uring_queue_init_params(queue_depth, &amp;ring, ¶ms);
    }

    void SubmitNodeFlush(int fd, uint32_t pid, void* buffer, 
                         size_t size, uint64_t lba_offset) {
        struct io_uring_sqe* sqe = io_uring_get_sqe(&amp;ring);
        
        io_uring_prep_write(sqe, fd, buffer, size, lba_offset);
        sqe-&gt;user_data = static_cast(pid);
        sqe-&gt;flags |= IOSQE_ASYNC;
        
        io_uring_submit(&amp;ring);
    }
};

By decoupling persistence requests from the tree traversal path, worker threads execute uninterrupted:

  1. Messages are appended to in-memory node buffers.
  2. Flush tasks register vectorized write operations within the io_uring ring buffer.
  3. The kernel submission thread dispatches I/O commands to the NVMe device controller via direct DMA without switching CPU rings.
  4. Completion status triggers an atomic state change in the mapping table, transitioning the node address from temporary memory pointers to direct NVMe physical LBAs.

Storage Reclamation, Write Amplification, and Crash Consistency

Operating an append-only, lock-free flash page engine requires addressing garbage collection, physical write amplification, and crash consistency guarantees.

       Physical NVMe Space Segments and Reclamation
+-------------------------------------------------------------------+
| Active Segment (Append Allocations)                               |
| [ Page 1 ][ Page 2 ][ Page 3 ][ Free Space...                   ] |
+-------------------------------------------------------------------+
| Inactive Segment (Candidate for Garbage Collection)               |
| [ Dead Page ][ Alive Page ][ Dead Page ][ Dead Page ][ Alive Page]|
+-------------------------------------------------------------------+
                               |
                   Segment Cleaning Process
                               v
+-------------------------------------------------------------------+
| Relocated Survivors (New Segment) | Erased Segment Block          |
| [ Alive Page ][ Alive Page ]      | [ Available for Allocation  ] |
+-------------------------------------------------------------------+

Segment-Level Garbage Collection and Wear Leveling

The flash space is managed as a logical ring of fixed-size extents or segments (e.g., 64 MB blocks).

  • Dead Space Identification: When a consolidated page is committed to a new LBA, the obsolete LBA is marked as invalid in a persistent segment allocation bitmap.
  • Segment Compaction: When free segment space falls below a low-water threshold, the GC background worker selects segments with the lowest live-to-dead byte ratio: $$\text{Victim Segment} = \arg\min_{S} \left(\frac{\text{LiveBytes}(S)}{\text{TotalBytes}(S)}\right)$$
  • Atomic Relocation: For each surviving page in the victim segment:
    1. The page is read into a temporary DMA buffer.
    2. The page is appended to the currently active write segment.
    3. The indirection table pointer is conditionally swapped via CAS: $$\text{CAS}(\text{MappingTable}[\text{PID}], \text{OldLBA}, \text{NewLBA})$$
    4. Once all live pages migrate, the victim segment is released and queued for an asynchronous NVMe Dataset Management (TRIM/DEALLOCATE) command.

Crash Consistency: Epoch Checkpoints and Intent Logging

To provide full ACID recovery without paying the cost of immediate write-ahead logging (WAL) for every message buffer append:

  1. In-Flight Intent Logging: High-velocity mutation messages enter a small, parallel, circular direct-IO WAL striping across fast Non-Volatile Dual In-line Memory Modules (NVDIMM) or raw NVMe LBAs.
  2. Epoch Checkpointing: Periodically, the global epoch advances, pinning the Mapping Table state. A single root manifest block is written to disk via atomic write commands containing the snapshot pointer: $$\text{Manifest Block} \leftarrow {\text{GlobalEpoch}, \text{MappingTableSnapshotRoot}, \text{TreeRootPID}, \text{CRC32}}$$
  3. Recovery Sequence: Upon restart after a crash:
    • Read the manifest block with the highest validated epoch and matching CRC32 checksum.
    • Reconstruct the LockFreePageTable directly from the immutable snapshot pages.
    • Replay the small intent log entries whose $\text{LSN} > \text{EpochManifest}.\text{LSN}$ directly into the memory buffers of root and top-level internal nodes.

Conclusion

The combination of $\text{B}^\epsilon$-tree storage structures and lock-free page management offers a performant architectural foundation for database engines running on high-capacity NVMe storage. By mathematically balancing fanout and node buffering via the $\epsilon$ parameter, $\text{B}^\epsilon$-trees reduce write amplification from $O(\log_B N)$ down to $O(\frac{1}{\epsilon B^{1-\epsilon}} \log_B N)$ without sacrificing point lookup latency or scan efficiency.

Simultaneously, pairing this design with lock-free page indirection tables, epoch-based memory reclamation, and polling-driven asynchronous hardware interfaces (io_uring and SPDK) eliminates CPU-level lock contention and POSIX system call overheads. The resulting database architecture matches the capabilities of high-throughput NVMe flash devices—providing sustained ingestion rates, bounded read latencies, and deterministic operational characteristics under heavy concurrent loads.

References

  1. Bender, M. A., Farach-Colton, M., Fineman, J. T., Fogel, Y. R., Kuszmaul, B. C., & Nelson, J. (2015). An Introduction to $\text{B}^\epsilon$-trees and Write-Optimization. USENIX ;login:, 40(5). URL: https://www3.cs.stonybrook.edu/~bender/pub/2015-login-betree.pdf
  2. Levandoski, J., Lomet, D., & Sengupta, S. (2013). The Bw-Tree: A B-tree for New Hardware Platforms. IEEE 29th International Conference on Data Engineering (ICDE). URL: https://www.microsoft.com/en-us/research/publication/the-bw-tree-a-b-tree-for-new-hardware-platforms/
  3. Axboe, J. (2019). Efficient IO with io_uring. Linux Kernel Documentation. URL: https://kernel.dk/io_uring.pdf

Privacy & Cookies

We use minimal cookies and privacy-respecting analytics to improve technical content and optimize reader experience. Review our Privacy Policy.