BadgerDB

repository·main·Indexed 12 days ago

https://github.com/dgraph-io/badger

A high-performance, embeddable, and persistent key-value database written in pure Go. Optimized for SSDs using a WiscKey-inspired design that separates keys from values, BadgerDB provides ACID transactions with serializable snapshot isolation (SSI), TTL support, and 3D access (key-value-version). It serves as a performant alternative to RocksDB and BoltDB without requiring Cgo.

Tokens
31.9K
Snippets
115
Records
163
Agent score
96%

What's inside BadgerDB

  1. Overview of BadgerDB features and design

    main

    BadgerDB is an embeddable, persistent, and fast key-value (KV) database written in pure Go. It is designed to be a performant alternative to non-Go-based stores like RocksDB, specifically optimized for SSDs.

    Key Characteristics:

    • Pure Go: No Cgo dependency.
    • ACID Transactions: Supports concurrent ACID transactions with Serializable Snapshot Isolation (SSI) guarantees.
    • WiscKey Design: Based on the WiscKey paper, it separates keys from values to reduce write amplification and optimize for SSDs.
    • Advanced Access: Supports TTL (Time-To-Live) and 3D access (key-value-version) via its Iterator API.
    • Scalability: Capable of serving data sets spanning hundreds of terabytes.
  2. What is BadgerDB?

    main
    BadgerDB is an embeddable, persistent, and fast key-value (KV) database written in pure Go. It serves as the underlying storage engine for Dgraph and is designed as an efficient, Go-native alternative to non-Go key-value stores like RocksDB.
  3. Performance comparison of skl against other data structures

    main

    The skl (skiplist) implementation in Badger is designed for high performance. Benchmarks indicate that skl outperforms both the older skiplist and slist implementations, as well as a standard Go map protected by a read-write lock, particularly as the fraction of read/write operations shifts towards reads.

    # Example benchmark results showing skl performance vs Map
    BenchmarkReadWrite/frac_10-8           100000000       26.3 ns/op  # skl
    BenchmarkReadWriteMap/frac_10-8        10000000        212 ns/op   # map with RW lock
  4. Use Read-only and Read-write transactions

    main

    Badger uses transactions to ensure data consistency. The recommended way to use them is via closure wrappers which handle cleanup automatically.

    Read-only transactions (DB.View)

    Use db.View() for read operations. You cannot perform writes or deletes inside this closure.

    Read-write transactions (DB.Update)

    Use db.Update() for operations that modify data. If the closure returns an error, the transaction is discarded. If it returns nil, it is committed.

    Common Errors:

    • ErrConflict: Occurs during a conflict; you may want to retry the operation.
    • ErrTxnTooBig: Occurs when pending writes/deletes exceed limits. In this case, commit the current transaction and start a new one.
    // Read-only
    db.View(func(txn *badger.Txn) error {
      // read operations
      return nil
    })
    
    // Read-write
    db.Update(func(txn *badger.Txn) error {
      // write operations
      return nil
    })
  5. Use Merge operations

    main

    Badger supports ordered merge operations via MergeFunc. You define a function that takes an originalValue and a newValue and returns the merged result.

    1. Define a MergeFunc: func(original, new []byte) []byte.
    2. Get a merge operator: db.GetMergeOperator(key, mergeFunc, duration).
    3. Add values: m.Add(value).
    4. Retrieve the result: m.Get().

    The duration parameter specifies how often the merge function is run on values added via the operator.

    // Example: Append merge function
    func add(originalValue, newValue []byte) []byte {
      return append(originalValue, newValue...)
    }
    
    key := []byte("merge")
    m := db.GetMergeOperator(key, add, 200*time.Millisecond)
    defer m.Stop()
    
    m.Add([]byte("A"))
    m.Add([]byte("B"))
    
    res, _ := m.Get() // res is "AB"
  6. Understand BadgerDB's Serialization Versioning

    main

    BadgerDB uses a modified version of Semantic Versioning called Serialization Versioning to manage changes to the data format stored on disk. This is distinct from standard API SemVer because data format changes are more critical for databases than API changes.

    When choosing a version of BadgerDB, understand the implications of the MAJOR.MINOR.PATCH numbers:

    • MAJOR: A change in the MAJOR version indicates that the dataset requires a transformation (migration) before it can be used again. Upgrading between major versions (e.g., v1.x to v2.x) requires a planned migration strategy for your existing data.
    • MINOR: A change in the MINOR version means old datasets are still readable, but the API may have changed in either a backwards-compatible or incompatible way. Upgrading between minor versions (e.g., v1.5.x to v1.6.x) might break your build due to API changes, but once the code compiles, no data migration is required.
    • PATCH: A change in the PATCH version indicates backwards-compatible bug fixes. These changes should never break your build or your dataset.
  7. Implement pagination using prefix scans and cursors

    main

    Since Badger iterates in lexicographical order, you can implement pagination by using a 'cursor' (the last key from the previous page).

    To resume iteration:

    1. Use it.Seek(cursor) to locate the starting point.
    2. Use it.ValidForPrefix(prefix) to stay within the desired range.
    3. Crucial: After the first Seek, you must reset the search prefix back to the original base prefix (e.g., feed:user:) inside the loop. If you continue searching with the specific cursor-based prefix, the iteration will stop immediately after the first match.
    // startCursor may look like 'feed:tQpnEDVRoCxTFQDvyQEzdo:1733127486'.
    err = db.View(func(txn *badger.Txn) error {
            it := txn.NewIterator(opts)
            defer it.Close()
    
            p := prefix
            if startCursor != nil {
                 p = startCursor
            }
            iterNum := 0 
            for it.Seek(p); it.ValidForPrefix(p); it.Next() {
                // Revert prefix back to the base prefix to allow sequential iteration
                p = prefix
    
                item := it.Item()
                key := string(item.Key())
    
                if iterNum > limit {
                    nextCursor = key
                    return nil
                }
                iterNum++
    
                err := item.Value(func(v []byte) error {
                    fmt.Printf("key=%s, value=%s\n", k, v)
                    return nil
                })
                if err != nil {
                    return err
                }
            }
            if iterNum < limit {
                nextCursor = ""
            }
            return nil
        })
  8. How Badger's two-tier key management works

    main

    Badger uses a two-tier encryption system to balance security and performance:

    1. Master Key: A user-provided AES key that encrypts the Data Keys. The length of this key determines the AES variant:

      • 16 bytes: AES-128
      • 24 bytes: AES-192
      • 32 bytes: AES-256 Warning: Always use a cryptographically secure random key; never use predictable strings.
    2. Data Keys: Auto-generated keys that encrypt the actual data on disk. These are stored alongside the encrypted data.

    Benefits of this model:

    • Fast Master Key Rotation: Rotating the master key only requires re-encrypting the small Data Keys, not the entire dataset.
    • Automatic Data Key Rotation: Data keys rotate independently of the master key.
    • Minimal Performance Impact: The separation allows for efficient key management without massive re-encryption overhead.
  9. Access value versions in Badger

    main

    Badger supports '3D access' (key-value-version). You can interact with different versions of data in two ways:

    1. Iterator API: Provides direct access to specific value versions.
    2. Options Configuration: You can specify how many versions to keep per key using the Options struct during setup.
  10. Understand Badger's design principles

    main

    Badger is a pure Go key-value database designed for high performance on modern storage devices (SSDs) and large datasets spanning terabytes.

    Its core architecture is based on the WiscKey paper, which utilizes a design that separates keys from values. This separation significantly reduces write amplification compared to traditional LSM trees by storing values in a separate value log, making it highly optimized for SSD-conscious storage.

  11. Impact of Node Pooling on skl performance

    main
    Node pooling is used in the skl implementation to manage memory more efficiently. Profiling (pprof) shows that while node pooling changes the distribution of memory allocation (e.g., shifting load to skl.newNode or related functions), it is a key part of the optimization strategy for the skiplist within Badger.
  12. Benchmark Badger DB open performance

    main

    To benchmark the time it takes to open a Badger database, follow these steps to prepare a large dataset and clear system caches to ensure an accurate measurement.

    1. Generate test data

    Use the badger fill command to create a database with 2 billion key-value pairs (approximately 380GB of data) using the --sorted flag.

    2. Clear system caches

    To ensure the benchmark measures disk I/O rather than OS page cache, clear the buffers, swap memory, and flush disk buffers:

    • Clear kernel caches: echo 3 | sudo tee /proc/sys/vm/drop_caches
    • Reset swap: sudo swapoff -a && sudo swapon -a
    • Flush disk buffers: blockdev --flushbufs /dev/[DEVICE] (replace [DEVICE] with your actual partition, e.g., /dev/nvme0n1p4).

    3. Run the benchmark

    Execute the BenchmarkDBOpen test pointing to your data directory.

    Expected Result: For a database with 2 billion sorted entries, opening the DB typically takes approximately 23.851s.

    # 1. Create badger DB with 2 billion key-value pairs
    badger fill -m 2000 --dir="/tmp/data" --sorted
    
    # 2. Clear buffers and swap memory
    free -mh && sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && sudo swapoff -a && sudo swapon -a && free -mh
    
    # Flush disk buffers (example device)
    blockdev --flushbufs /dev/nvme0n1p4
    
    # 3. Run the benchmark
    go test -run=^$ github.com/dgraph-io/badger -bench ^BenchmarkDBOpen$ -benchdir="/tmp/data" -v