LevelDB

repository·main·Indexed 12 days ago

https://github.com/google/leveldb

A high-performance, ordered key-value storage library developed by Google. It provides an efficient mapping from string keys to string values with support for atomic batches via write_batch.h, snapshots, and custom sorting through leveldb::Comparator. LevelDB is a library for single-process access and does not include a built-in client-server implementation or a relational SQL model.

Tokens
6.7K
Snippets
21
Records
32
Agent score
97%

What's inside LevelDB

  1. Concurrency in LevelDB

    main

    LevelDB has specific concurrency constraints:

    • Process Level: Only one process may open a database at a time. LevelDB uses OS locks to enforce this.
    • Thread Level: A single leveldb::DB object is thread-safe and can be shared across multiple threads without external synchronization.
    • Non-thread-safe objects: Objects like leveldb::Iterator and leveldb::WriteBatch are not thread-safe. If multiple threads must access the same instance of these objects, you must implement your own locking protocol.
  2. Understand the LevelDB file format structure

    main

    LevelDB stores data in files consisting of a sequence of data blocks, followed by meta blocks, a metaindex, an index, and a fixed-size footer.

    Key components include:

    • Data Blocks: Contain sorted key/value pairs, optionally compressed.
    • Meta Blocks: Optional blocks containing metadata (e.g., filters or statistics).
    • Metaindex Block: Maps meta block names to their BlockHandle.
    • Index Block: Maps keys to the BlockHandle of the data block containing them.
    • Footer: A fixed-size block at the end of the file containing the BlockHandle for the metaindex and index blocks, plus a magic number.

    Internal pointers are represented as a BlockHandle, which consists of an offset (varint64) and a size (varint64).

    File Layout:
    <beginning_of_file>
    [data block 1]
    [data block 2]
    ...
    [data block N]
    [meta block 1]
    ...
    [meta block K]
    [metaindex block]
    [index block]
    [Footer] (fixed size; starts at file_size - sizeof(Footer))
    <end_of_file>
  3. Core LevelDB Features and API Concepts

    main

    LevelDB is an ordered key-value store that maps string keys to string values. Key features include:

    • Arbitrary Byte Arrays: Keys and values can be any byte array.
    • Ordered Storage: Data is stored sorted by key. You can provide a custom comparison function via comparator.h to override the default sort order.
    • Basic Operations: The primary API consists of Put(key, value), Get(key), and Delete(key).
    • Atomic Batches: Use write_batch.h to apply multiple changes in a single atomic operation.
    • Snapshots: Create transient snapshots to obtain a consistent view of the data.
    • Iteration: Supports both forward and backward iteration over the data via iterator.h.
    • Compression: Automatically uses Snappy compression, with Zstd support available.
    • Extensibility: External activity (like file system operations) is handled through a virtual env.h interface, allowing for OS interaction customization.
  4. How compaction works in LevelDB

    main

    Compaction is a background process that merges files to maintain the leveled structure and reclaim space.

    Process

    1. Selection: For level $L$, the system picks one file from level $L$ and all overlapping files from level $L+1$.
      • Note: Level-0 compactions are special and may pick multiple level-0 files if they overlap.
    2. Merging: The contents are merged into a sequence of new level-$(L+1)$ files.
    3. File Sizing: A new output file is started once the current one reaches the target size (2MB) or if the key range overlaps more than ten level-$(L+2)$ files.
    4. Cleanup: Old files are discarded, and new files are added to the serving state.

    Key Behaviors

    • Rotation: Compactions for a level rotate through the key space. The next compaction picks the first file starting after the ending key of the last compaction at that level.
    • Garbage Collection: Compactions drop overwritten values and deletion markers (if no higher levels contain overlapping ranges).
  5. Understand the LevelDB Log Format

    main

    LevelDB log files are composed of a sequence of 32KB blocks. The final block in a file may be a partial block. Each block contains a sequence of records and an optional trailer.

    Block Structure

    A block consists of zero or more records followed by an optional trailer. A record cannot start within the last six bytes of a block. Any remaining bytes (up to 6) form the trailer, which must consist entirely of zero bytes and should be skipped by readers.

    Record Structure

    Each record is defined by the following fields (all little-endian):

    • checksum: uint32 (CRC32C of the type and data[])
    • length: uint16 (The length of the data field)
    • type: uint8 (The record type)
    • data: uint8[length] (The actual payload)

    Record Types

    LevelDB uses four record types to handle both single-block records and records that span multiple blocks:

    • FULL (1): The record contains the entire user data in a single fragment.
    • FIRST (2): The first fragment of a user record that has been split across blocks.
    • MIDDLE (3): An interior fragment of a split user record.
    • LAST (4): The final fragment of a split user record.

    Fragmented Record Example

    When a user record is larger than a block or spans a block boundary, it is split:

    1. A FIRST record contains the initial fragment.
    2. One or more MIDDLE records contain the intermediate fragments.
    3. A LAST record contains the final fragment.

    If exactly seven bytes remain in a block and a new non-zero length record is added, the writer must emit a FIRST record with zero bytes of user data to fill the remaining seven bytes, then continue the user data in subsequent blocks.

    block := record* trailer?
    record :=
      checksum: uint32     // crc32c of type and data[] ; little-endian
      length: uint16       // little-endian
      type: uint8          // One of FULL, FIRST, MIDDLE, LAST
      data: uint8[length]
  6. Use WriteBatch for atomic updates

    main

    To ensure multiple updates are applied atomically (all or nothing), use the leveldb::WriteBatch class. This prevents partial updates if a process crashes. WriteBatch also improves performance for bulk updates by grouping multiple mutations into a single write operation.

    Note: When performing operations like moving a value from key1 to key2, call Delete(key1) before Put(key2, value) within the batch to avoid issues if key1 and key2 are identical.

    #include "leveldb/write_batch.h"
    
    std::string value;
    leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
    if (s.ok()) {
      leveldb::WriteBatch batch;
      batch.Delete(key1);
      batch.Put(key2, value);
      s = db->Write(leveldb::WriteOptions(), &batch);
    }
  7. LevelDB Limitations

    main

    Before using LevelDB, be aware of the following constraints:

    • No Relational Model: It is not a SQL database. It does not support SQL queries or indexes.
    • Single Process Access: Only one process (which may be multi-threaded) can access a specific database instance at a time.
    • No Built-in Client-Server: LevelDB is a library, not a server. To provide client-server access, you must wrap the library in your own server implementation.
  8. Understand leveldb::Slice

    main

    LevelDB uses leveldb::Slice for it->key() and it->value() to avoid expensive memory copies of large byte arrays. A Slice is a lightweight structure containing a pointer to an external byte array and a length.

    Critical Safety Warning: A Slice does not own the data it points to. You must ensure the underlying memory (e.g., a std::string) remains valid for the entire lifetime of the Slice. If the source string goes out of scope, the Slice becomes a dangling pointer.

    Conversions:

    • std::string $\rightarrow$ Slice: Implicitly supported.
    • Slice $\rightarrow$ std::string: Use .ToString().
    // Conversion to Slice
    leveldb::Slice s1 = "hello";
    std::string str("world");
    leveldb::Slice s2 = str;
    
    // Conversion from Slice
    std::string str_back = s1.ToString();
  9. How the "filter" Meta Block works

    main

    If a FilterPolicy is used when opening the database, a filter block is stored in the table. The metaindex maps the key filter.<N> (where <N> is the string from FilterPolicy::Name()) to the filter block's BlockHandle.

    The filter block stores a sequence of filters. Each filter $i$ covers keys from data blocks whose file offsets fall within the range [ i*base ... (i+1)*base-1 ]. Currently, base is 2KB.

    Filter Block Format:

    1. A sequence of filters: [filter 0], [filter 1], ..., [filter N-1]
    2. An offset array: [offset of filter 0], [offset of filter 1], ..., [offset of filter N-1] (each 4 bytes)
    3. Metadata: [offset of beginning of offset array] (4 bytes) and lg(base) (1 byte).
  10. Use Snapshots for consistent reads

    main

    Snapshots provide a consistent, read-only view of the entire database at a specific point in time. You can use a snapshot with leveldb::ReadOptions::snapshot to ensure that an iterator or a Get operation sees the state of the database as it existed when the snapshot was taken, even if other threads are performing writes.

    Lifecycle: You must manually release snapshots using db->ReleaseSnapshot(snapshot_handle) when they are no longer needed to allow the engine to reclaim resources.

    leveldb::ReadOptions options;
    options.snapshot = db->GetSnapshot();
    
    // ... apply updates to db ...
    
    leveldb::Iterator* iter = db->NewIterator(options);
    // ... read using iter to view the state when the snapshot was created ...
    
    delete iter;
    db->ReleaseSnapshot(options.snapshot);
  11. How sorted tables and levels work

    main

    LevelDB organizes sorted tables (*.ldb) into a hierarchy of levels to manage data migration and read performance:

    • Level 0 (Young Level): Contains sorted tables generated directly from log files. Files in Level 0 may have overlapping key ranges.
    • Levels 1 and above: Files in these levels have distinct, non-overlapping key ranges.
    • Compaction Trigger: When the combined size of files in level $L$ exceeds $10^L$ MB (e.g., 10MB for level-1, 100MB for level-2), a compaction is triggered to merge files into level $L+1$.
    • Data Migration: Compactions gradually migrate updates from the young level to the largest levels using bulk reads and writes to minimize expensive seeks.
  12. Understand the LevelDB file structure

    main

    A LevelDB database is represented as a set of files stored in a single directory. The key components are:

    • Log files (*.log): Store a sequence of recent updates. When a log reaches its size threshold (default ~4MB), it is converted into a sorted table and a new log is created.
    • Sorted tables (*.ldb): Store entries sorted by key (values or deletion markers). These are organized into levels.
    • MANIFEST: A log file that tracks the set of sorted tables in each level, their key ranges, and other metadata. A new MANIFEST is created whenever the database is reopened.
    • CURRENT: A text file containing the name of the latest MANIFEST file.
    • Info logs (LOG, LOG.old): Files containing informational messages.
    • Miscellaneous: LOCK and *.dbtmp files may also be present.