RocksDB:使用 Flash 和 RAM 存储的持久键值 Key-Value

发布时间: 更新时间: 总字数:3722 阅读时间:8m 作者: IP上海 分享 网址

RocksDB 是由 Meta (原 Facebook) 开发的一款高性能、嵌入式键值(Key-Value)存储引擎。它基于 Google 的 LevelDB 开发,专为多核 CPU 和快速存储设备(如 SSD/NVMe)进行了深度优化。

介绍

RocksDB 不是像 MySQL 或 Redis 那样的独立服务器,而是一个C++ 库,直接链接到应用程序中运行。许多知名数据库和流计算系统(如 CockroachDB, TiKV, Flink, Kafka Streams, MyRocks)底层都使用 RocksDB 作为存储引擎。

核心特性

  • 架构:基于 LSM-Tree (Log-Structured Merge Tree),擅长高写入吞吐量。
  • 可调优:提供极丰富的配置参数(BlockSize, Memtable 大小, 压缩算法等)以适应不同负载。
  • 特性:支持事务、快照(Snapshot)、列族(Column Families)、布隆过滤器(Bloom Filter)。

架构

RocksDB 的架构基于 LSM-Tree (Log-Structured Merge Tree),其术语体系主要围绕数据写入流程存储文件结构以及逻辑隔离展开。

以下是 RocksDB 中最核心的术语介绍,分为 逻辑结构内存组件磁盘组件后台操作 四个部分。

逻辑结构

Column Family (列族)

  • 概念:类似于关系型数据库(如 MySQL)中的 (Table)
  • 作用
    • 逻辑隔离:可以把不同类型的数据放在不同的 Column Family (CF) 中(例如:一个存用户元数据,一个存用户日志)。
    • 独立配置:每个 CF 可以有独立的配置参数(如压缩算法、缓存大小、MemTable 大小)。这允许根据数据的读写特性进行精细化调优。
  • 底层机制
    • 所有的 CF 共享同一个 WAL(Write Ahead Log)文件(保证原子写入)。
    • 每个 CF 拥有独立的 MemTable 和 SST 文件集合。
    • 默认情况下,RocksDB 只有一个名为 "default" 的列族。

内存组件 (In-Memory)

MemTable (内存表)

  • 概念:数据的写入缓冲区
  • 流程:所有写入请求(Put/Delete)首先都会写入 MemTable。它通常是一个跳表(SkipList),数据在内存中是有序的。
  • 读写:提供极快的读写速度。读取时会先查 MemTable,查不到再查磁盘。

Immutable MemTable (不可变内存表)

  • 概念:已经写满的 MemTable。
  • 流程:当活跃的 MemTable 写满(达到 write_buffer_size)后,它会变成不可变状态(只读),并等待后台线程将其 Flush(刷盘)到磁盘生成 SST 文件。

磁盘组件 (On-Disk)

WAL (Write Ahead Log, 预写日志)

  • 概念:用于故障恢复的日志文件。
  • 作用:数据写入 MemTable 之前,会先顺序追加写入 WAL。如果系统崩溃(Crash),RocksDB 重启时可以通过重放 WAL 来恢复内存中尚未刷盘的数据。
  • 生命周期:当 MemTable 成功刷入磁盘生成 SST 后,对应的 WAL 会被删除。

SST / SSTable (Static Sorted Table)

  • 概念:RocksDB 在磁盘上存储数据的核心文件格式。
  • 特点
    • 静态 (Static):文件一旦生成,就不会再被修改(Immutable)。
    • 有序 (Sorted):文件内部的 Key 是有序排列的,便于二分查找。
    • 分层 (Leveled):SST 文件分布在不同的层级(Level 0 到 Level N)中。
  • 内部结构:通常包含 Data Block(数据块)、Index Block(索引块)、Bloom Filter(布隆过滤器)等。

MANIFEST (清单文件)

  • 概念:数据库的元数据文件
  • 作用:记录了数据库当前的状态。比如:当前有哪些 SST 文件、每个 SST 文件属于哪一层 (Level)、每个 SST 的 Key 范围(最小 Key 和最大 Key)等。
  • 重要性:如果 MANIFEST 丢失,数据库将无法打开。

关键流程与机制

LSM-Tree 层级 (Level 0, Level 1…)

RocksDB 的磁盘文件是分层管理的:

  • Level 0 (L0):MemTable 刷盘后直接生成的 SST 都在 L0。这是唯一允许 Key 范围重叠的一层。读取时如果需要在 L0 查找,必须遍历 L0 的所有文件(性能最差)。
  • Level 1 ~ Level N:从 L1 开始,每一层内的所有 SST 文件,其 Key 范围互不重叠。Level 越高,数据越冷(旧),总容量越大。

Flush (刷盘)

  • 动作:将 Immutable MemTable 的内容写到磁盘,生成一个 L0 SST 文件。

Compaction (压缩/合并)

  • 概念:RocksDB 最核心的后台维护操作。
  • 为什么需要
    1. L0 层文件允许重叠,读性能差,需要合并到 L1。
    2. 数据主要以追加方式写入,旧数据(被覆盖的 Key)和删除标记(Tombstone)占据空间。
  • 动作
    • 读取高层(如 L0)的 SST 和低层(如 L1)有重叠的 SST。
    • 进行多路归并排序。
    • 丢弃旧版本数据和已删除数据
    • 生成新的 SST 文件放到下一层。
  • 代价:Compaction 会消耗大量的 CPU 和磁盘 I/O(写放大),是调优的重点。

Bloom Filter (布隆过滤器)

  • 概念:一种概率型数据结构,存储在 SST 文件中。
  • 作用快速判断 Key 是否不存在
  • 流程:在读取 SST 文件前,先查 Bloom Filter。如果它说Key 不存在,那就绝对不存在,直接跳过该文件(节省磁盘 I/O)。如果它说可能存在,才去真正读取数据块。

Block Cache (块缓存)

  • 概念:内存中的缓存区。
  • 作用:缓存 SST 文件中的解压后的数据块(Data Block)。热点数据的读取通常直接命中 Block Cache,不需要读盘。

生命周期

一个 Key-Value 的生命周期通常是:

  1. 写入 -> 追加 WAL -> 写入 MemTable
  2. MemTable 满了 -> 变 Immutable -> Flush 到磁盘 -> 生成 L0 SST (WAL 删除)。
  3. 后台 Compaction -> L0 SST 与 L1 SST 合并 -> 生成新的 L1 SST … 逐渐下沉到 Lmax。
  4. 读取 -> 查 MemTable -> 查 Block Cache -> 查 L0 SST (所有文件) -> 查 L1~Ln SST (二分查找 + Bloom Filter)。

常用工具与维护命令

RocksDB 官方提供了两个核心命令行工具用于运维和调试:

  1. ldb: 用于读写数据、查询元数据、修复数据库。
  2. sst_dump: 用于直接分析底层的 SST 物理文件。

注意:

  • 以下命令假设你已经编译了 RocksDB,工具通常位于源码目录的 tools/ 下,或者已安装到系统路径。
  • 停机维护:大部分 ldb 写操作命令建议在数据库停止服务(关闭应用)时运行,避免锁冲突。
  • 请将 --db=/tmp/test_db 替换为你实际的数据库路径。

安装:

apt install rocksdb-tools

数据查询与操作

这是最常用的工具,类似于一个简单的命令行客户端。

  • 写入/更新数据

    # 写入 key="a", value="b" (如果 DB 不存在则创建)
    ldb --db=/tmp/my_db --create_if_missing put a b
    
  • 文件结构

$ ls /tmp/my_db/
000013.sst  IDENTITY  LOG                       lost             MANIFEST-000014  OPTIONS-000012
CURRENT     LOCK      LOG.old.1766288971448365  MANIFEST-000001  OPTIONS-000007
  • 读取单个 Key

    # 获取 key 为 "a" 的值
    ldb --db=/tmp/my_db get a
    
    # 如果 key 是十六进制 (hex) 格式
    ldb --db=/tmp/my_db get --hex 0x7573657231
    
  • 扫描数据 (Scan)

    # 扫描并打印所有键值对
    ldb --db=/tmp/my_db scan
    
    # 限制扫描数量,只看前 10 条
    ldb --db=/tmp/my_db scan --max_keys=10
    
    # 指定扫描的起始 Key
    ldb --db=/tmp/my_db scan --from="user_1000"
    
  • 指定列族 (Column Family) 操作 RocksDB 支持多列族,默认都在 “default” 列族下。

    # 扫描指定列族 "cf1" 的数据
    ldb --db=/tmp/my_db --column_family=cf1 scan
    

数据库状态检查与维护

  • 查看数据库元数据 (Manifest) 查看 LSM-Tree 的结构,包括每一层(Level)有哪些 SST 文件。

    ldb --db=/tmp/my_db manifest_dump
    
  • 强制进行 Compact (压缩整理) 如果磁盘空间占用过高,或删除大量数据后空间未释放,可以手动触发。

    ldb --db=/tmp/my_db compact
    
  • 修复数据库 如果数据库因异常崩溃导致无法打开(如 MANIFEST 文件损坏),可以尝试修复(警告:可能导致部分数据丢失)。

    ldb --db=/tmp/my_db repair
    

底层文件分析

当需要排查具体的物理文件(.sst 文件)是否损坏或查看其属性时使用。

  • 查看 SST 文件属性 检查文件包含的 Key 范围、压缩算法、数据量等。

    # 替换具体的 .sst 文件路径
    $ sst_dump --file=/tmp/my_db/000013.sst --command=show_properties
    options.env is 0x59f0ccfeb050
    Process /tmp/my_db/000013.sst
    Sst file format: block-based
    
  • 扫描 SST 文件内容 直接读取物理文件中的内容,不经过数据库逻辑。

    $ sst_dump --file=/tmp/my_db/000013.sst --command=scan --read_num=10
    options.env is 0x602309e21050
    Process /tmp/my_db/000013.sst
    Sst file format: block-based
    from [] to []
    'a' seq:0, type:1 => b
    
  • 检测文件一致性 验证 SST 文件的 Checksum,检查文件是否损坏。

    sst_dump --file=/tmp/my_db/000013.sst --command=check
    
  • 导出数据为 Hex (Raw Dump) 将 SST 文件内容转储为可读的十六进制文本,用于深度 Debug。

    sst_dump --file=/tmp/my_db/000013.sst --command=raw
    

使用

RocksDB 是 C++ 编写的库,因此在 Python 和 Golang 中使用时,都需要先在操作系统层面安装 RocksDB 的 C++ 动态链接库(Shared Library),否则无法编译或运行。

前置条件:安装系统库

在运行代码前,请确保操作系统已安装 RocksDB C++ 核心库。

  • MacOS (Homebrew):
    brew install rocksdb
    
  • Ubuntu / Debian:
    sudo apt-get update
    sudo apt-get install -y librocksdb-dev
    
  • CentOS / RHEL:
    sudo yum install rocksdb-devel
    

Python 示例

pip install rocksdict,这是一个基于 Rust 封装的 RocksDB 库,安装更简单

  • main.py
from rocksdict import Rdict

# 打开/创建数据库
db = Rdict("./test_rocksdict")

# 写
db["key1"] = "value1"
db[b"key2"] = b"value2"

# 读
print(db["key1"])

# 删
del db["key1"]

# 自动关闭
db.close()
  • 其他包: python-rocksdb
# 需要先确保系统已经安装了 rocksdb C++ 动态库 (如 apt install librocksdb-dev)
pip install git+https://github.com/twmht/python-rocksdb.git

pip install python-rocksdb -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com

Golang 示例

Go 语言需要使用 CGO 调用 C++ 库。目前社区较活跃的封装库是 linxGnu/grocksdb (它是 gorocksdb 的增强版)。

  • 初始化模块并下载库
mkdir go-rocksdb-demo
cd go-rocksdb-demo
go mod init go-rocksdb-demo
# 下载依赖 (确保 CGO_ENABLED=1)
go get github.com/linxGnu/grocksdb
  • Go 代码 (main.go)
package main

import (
	"fmt"
	"log"

	"github.com/linxGnu/grocksdb"
)

func main() {
	dbPath := "./test_rocksdb_go"

	// 1. 设置 Options
	bbto := grocksdb.NewDefaultBlockBasedTableOptions()
	bbto.SetBlockCache(grocksdb.NewLRUCache(32 << 20)) // 32MB Block Cache

	opts := grocksdb.NewDefaultOptions()
	opts.SetBlockBasedTableFactory(bbto)
	opts.SetCreateIfMissing(true)

	// 2. 打开数据库
	db, err := grocksdb.OpenDb(opts, dbPath)
	if err != nil {
		log.Fatal("无法打开数据库: ", err)
	}
	// 确保程序结束时关闭数据库
	defer db.Close()

	fmt.Println("数据库已打开:", dbPath)

	// 准备读写选项 (默认即可)
	wo := grocksdb.NewDefaultWriteOptions()
	ro := grocksdb.NewDefaultReadOptions()

	// 3. 写入数据 (Put)
	key := []byte("user_2002")
	value := []byte("Bob")
	err = db.Put(wo, key, value)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("写入数据: key=%s, value=%s\n", key, value)

	// 4. 读取数据 (Get)
	slice, err := db.Get(ro, key)
	if err != nil {
		log.Fatal(err)
	}
	// slice 需要手动释放内存
	defer slice.Free()

	if slice.Exists() {
		fmt.Printf("读取数据: key=%s, value=%s\n", key, slice.Data())
	} else {
		fmt.Println("数据不存在")
	}

	// 5. 批量操作 (WriteBatch)
	batch := grocksdb.NewWriteBatch()
	defer batch.Destroy() // 释放 batch 内存

	batch.Put([]byte("conf_a"), []byte("1"))
	batch.Put([]byte("conf_b"), []byte("2"))
	batch.Delete([]byte("user_2002")) // 删除上面的 Bob

	err = db.Write(wo, batch)
	if err != nil {
		log.Fatal("批量写入失败:", err)
	}
	fmt.Println("批量操作完成")

	// 6. 迭代器 (Iterator)
	iter := db.NewIterator(ro)
	defer iter.Close()

	fmt.Println("开始扫描数据:")
	// SeekToFirst: 从头开始; Seek(key): 从指定 key 开始
	for iter.SeekToFirst(); iter.Valid(); iter.Next() {
		// 获取 Key 和 Value (注意: Data() 返回的 slice 是引用,如需长期持有需拷贝)
		k := iter.Key()
		v := iter.Value()
		fmt.Printf("Scan -> Key: %s, Value: %s\n", k.Data(), v.Data())
		k.Free()
		v.Free()
	}

	if err := iter.Err(); err != nil {
		log.Fatal(err)
	}
}

运行 Go 代码

# 必须开启 CGO
CGO_ENABLED=1 go run main.go

常见问题排查

  1. 编译报错 fatal error: rocksdb/c.h: No such file

    • 原因: 找不到 C++ 头文件。
    • 解决: 确认已安装 librocksdb-dev (Linux) 或 rocksdb (Mac)。如果安装了但仍报错,可能需要设置 CGO_CFLAGSCGO_LDFLAGS 环境变量指向头文件位置。
  2. 运行时报错 ImportError (Python) 或 library not found (Go)

    • 原因: 找不到 .so.dylib 动态库。
    • 解决: 确认库在 /usr/local/lib/usr/lib 下。Linux 下可能需要运行 sudo ldconfig 刷新库缓存。
  3. 锁文件错误 (LOCK: Resource temporarily unavailable)

    • 原因: 一个 RocksDB 数据库目录同一时间只能被一个进程打开。
    • 解决: 确保没有其他终端(或上次运行崩溃的进程)正在占用该目录。

参考

  1. https://github.com/facebook/rocksdb
  2. https://rocksdb.org/
本文总阅读量 次 本站总访问量 次 本站总访客数
Home Archives Categories Tags Statistics