LevelDb
LevelDb 是 Google 开源的持久化 KV 单机存储引擎。
针对存储面对的普遍随机 IO 问题,leveldb 采用了 merge-dump 的方式,将逻辑场景的写请求转换成顺序写log 和写 memtable 操作,由后台进程将 memtable 持久化成 sstable。
对于读请求,随机 IO 还是无法避免,但它设计了一系列策略来保证读的效率。
1. 特点
- 键和值都是任意的字节数组
- 数据根据键排序,排序规则可重载
- 有三种基本的操作:Put(key,value), Get(key), Delete(key)
- 支持多个操作组合的原子操作
- 用户可以创建临时快照,得到一个一致的数据视图
- 支持向前和向后的数据迭代
- 数据用Snappy压缩库自动压缩
- 外部操作(如文件系统操作等)通过一个虚拟接口使用,用户可以对操作系统进行定制相应操作
2. 局限性
- 这不是一个SQL数据库。它不具有关系数据模型,它不支持SQL查询,也不支持索引
- 在同一时间只有一个进程(可以是多线程)可以访问一个数据库
- 有没有客户端-服务器模型的支持
3. 性能
以下是谷歌官方提供的性能测试报告:
3.1 测试环境
LevelDB: version 1.1
日期: Sun May 1 12:11:26 2011
CPU: 4 x Intel(R) Core(TM)2 Quad CPU Q6600@2.40GHz
CPUCache: 4096 KB
键: 每个16 bytes
值: 每个100 bytes(压缩后50 bytes) 元组数目: 1000000
内存大小: 110.6 MB (估计)
文件大小: 62.9 MB (估计)
3.2 写的性能
顺序添加 : 1.765 micros/op; 62.7 MB/s
同步添加 : 268.409 micros/op; 0.4 MB/s (10000 ops)
随机添加 : 2.460 micros/op; 45.0 MB/s
数据重写 : 2.380 micros/op; 46.5 MB/s
随机写入的速度可以达到每秒40万条
3.3 读的性能
随机读取 : 16.677 micros/op; (大约每秒6万条)
顺序读取 : 0.476 micros/op; 232.3 MB/s
逆序读取 : 0.724 micros/op; 152.9 MB/s
如果增加数据压缩后,性能有所提高
随机读取 : 11.602 micros/op; (大约每秒8.5万条)
顺序读取 : 0.423 micros/op; 232.3 MB/s
逆序读取 : 0.663 micros/op; 152.9 MB/s
如果提供足够的缓存性能还会有所提升
随机读取 : 9.775 micros/op; (不使用压缩大约每秒10万条)
随机读取 : 5.215 micros/op; (使用压缩大约每秒19万条)
顺序读的性能尤为突出,在每秒230万条以上
4. 使用方法
4.1 打开一个数据库
一个leveldb数据库有一个名字对应的文件系统目录,所有数据库的内容存储在此目录中:
1 #include <assert> 2 #include "leveldb/db.h" 3 4 leveldb::DB* db; 5 leveldb::Options options; 6 options.create_if_missing = true; 7 leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db); 8 assert(status.ok());
如果你想在数据库已经存在的情况下报错,那需要设置options如下:
1 options.error_if_exists = true;
4.2 状态
也许您已经到leveldb::Status,这类型可以得到leveldb的大部分功能结果,您可以检查结果是否是ok的,还可以打印相关的错误消息:
1 leveldb::Status s = ...; 2 if (!s.ok()) cerr << s.ToString() << endl;
4.3 关闭数据库
当您对一个数据库的操作完成了,只需删除数据库对象就可以了关闭数据库:
1 ... open the db as described above ... 2 ... do something with db ... 3 delete db;
4.4 读和写
leveldb提供了Put,delete,Get方法来修改和查询数据库:
1 std::string value; 2 leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value); 3 if (s.ok()) s = db->Put(leveldb::WriteOptions(), key2, value); 4 if (s.ok()) s = db->Delete(leveldb::WriteOptions(), key1);
4.5 原子化更新
leveldb提供一个操作序列的原子化:
1 #include "leveldb/write_batch.h" 2 ... 3 std::string value; 4 leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value); 5 if (s.ok()) { 6 leveldb::WriteBatch batch; 7 batch.Delete(key1); 8 batch.Put(key2, value); 9 s = db->Write(leveldb::WriteOptions(), &batch); 10 }
除了用于原子化,WriteBatch也可以通过把很多操作放到一起用来加快批更新。
4.6 同步写入
默认情况下,每次写入leveldb是异步的:写入函数只要把写操作交付给操作系统就返回,从操作系统的内存传输到底层的持久性存储是异步的。
可以打开sync标志来指定某次写操作直到被写入的数据已经被一路推到永久存储时才返回:
1 leveldb::WriteOptions write_options; 2 write_options.sync = true; 3 db->Put(write_options, ...);
异步写入的速度往往超过同步写入的一千倍,但异步写的缺点是机器崩溃可能会导致最后的少量更新丢失。同步写可以更新标记,说明崩溃时在何处重新启动。
4.7 并发
一个数据库只能由一个进程打开,而在一个单一的过程中,同一个leveldb:: DB对象可以被多个并发的线程安全地共享。
4.8 迭代
下面的例子展示了如何打印在一个数据库中的所有键值对:
1 leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions()); 2 for (it->SeekToFirst(); it->Valid(); it->Next()) { 3 cout << it->key().ToString() << ": " << it->value().ToString() << endl; 4 } 5 assert(it->status().ok()); // Check for any errors found during the scan 6 delete it;
下面例子显示了如何只处理范围[start,limit)内的键:
1 for (it->Seek(start); it->Valid() && it->key().ToString() < limit; it->Next()) { 2 ... 3 }
您也可以以相反的顺序处理条目。 (警告:反向迭代可能会略慢于正向迭代。):
1 for (it->SeekToLast(); it->Valid(); it->Prev()) { 2 ... 3 }
4.9 快照
快照提供key-value整个存储状态的只读视图,ReadOptions::snapshot可能非NULL来表明读操作于DB的特定版本状态。
如果ReadOptions::snapshot是NULL,会对当前状态产生快照。
快照通过DB::GetSnapshot()方法创建:
1 leveldb::ReadOptions options; 2 options.snapshot = db->GetSnapshot(); 3 ... apply some updates to db ... 4 leveldb::Iterator* iter = db->NewIterator(options); 5 ... read using iter to view the state when the snapshot was created ... 6 delete iter; 7 db->ReleaseSnapshot(options.snapshot);
需要注意的是当一个快照不再需要,要用DB::ReleaseSnapshot方法来释放。
5. 参数设定
参数可以通过改变include/leveldb/options.h中定义的默认值来设定。
leveldb 中启动时的一些配置,通过 Option 传入。
get/put/delete 时,也有相应的ReadOption/WriteOption。
5.1 参数项
1 // include/leveldb/option.h 2 Options { 3 // 传入的 comparator 4 const Comparator* comparator; 5 // open 时,如果 db 目录不存在就创建 6 bool create_if_missing; 7 // open 时,如果 db 目录存在就报错 8 bool error_if_exists; 9 // 是否保存中间的错误状态(RecoverLog/compact),compact 时是否读到的 block 做检验。 10 bool paranoid_checks; 11 // 传入的 Env。 12 Env* env; 13 // 传入的打印日志的 Logger 14 Logger* info_log; 15 // memtable 的最大 size 16 size_t write_buffer_size; 17 // db 中打开的文件最大个数 18 // db 中需要打开的文件包括基本的 CURRENT/LOG/MANIFEST/LOCK, 以及打开的 sstable 文件。 19 // sstable 一旦打开,就会将 index 信息加入 TableCache,所以把 20 // (max_open_files - 10)作为 table cache 的最大数量. 21 int max_open_files; 22 // 传入的 block 数据的 cache 管理 23 Cache* block_cache; 24 // sstable 中 block 的 size 25 size_t block_size; 26 // block 中对 key 做前缀压缩的区间长度 27 int block_restart_interval; 28 // 压缩数据使用的压缩类型(默认支持 snappy,其他类型需要使用者实现) 29 CompressionType compression; 30 } 31 32 // include/leveldb/option.h 33 struct ReadOptions { 34 // 是否对读到的 block 做校验 35 bool verify_checksums; 36 // 读到的 block 是否加入 block cache 37 bool fill_cache; 38 // 指定读取的 SnapShot 39 const Snapshot* snapshot; 40 } 41 42 // include/leveldb/option.h 43 struct WriteOptions { 44 // write 时,记 binlog 之后,是否对 binlog 做 sync。 45 bool sync; 46 // 如果传入不为 NULL,write 完成之后同时做 SnapShot. 47 const Snapshot** post_write_snapshot; 48 }
另外还有一些编译时的常量,与 Option 一起控制:
1 // db/dbformat.h 2 namespace config { 3 // level 的最大值 4 static const int kNumLevels = 7; 5 // level-0 中 sstable 的数量超过这个阈值,触发 compact 6 static const int kL0_CompactionTrigger = 4; 7 // level-0 中 sstable 的数量超过这个阈值, 慢处理此次写(sleep1ms) 8 static const int kL0_SlowdownWritesTrigger = 8; 9 // level-0 中 sstable 的数量超过这个阈值, 阻塞至 compact memtable 完成。 10 static const int kL0_StopWritesTrigger = 12; 11 // memtable dump 成的 sstable,允许推向的最高 level 12 // (参见 Compact 流程的 VersionSet::PickLevelForMemTableOutput()) 13 static const int kMaxMemCompactLevel = 2; 14 } 15 // db/version_set.cc 16 namespace leveldb { 17 // compact 过程中,level-0 中的 sstable 由 memtable 直接 dump 生成,不做大小限制 18 // 非 level-0 中的 sstable 的大小设定为 kTargetFileSize 19 static const int kTargetFileSize = 2 * 1048576; 20 // compact level-n 时,与 level-n+2 产生 overlap 的数据 size (参见 Compaction) 21 static const int64_t kMaxGrandParentOverlapBytes = 10 * kTargetFileSize; 22 }
5.2 块大小
leveldb把相邻键码放入相同的块,以这样的块为单元来转移到持久存储中,缺省的块大小约为4096未压缩字节。
主要是做批量扫描的应用可以通过增加块大小来提高性能。
使用小于1KB或比高于MB数量级的块大小没有什么好处,而压缩会在块大小较大的条件下更有效。
5.3 压缩
每个块在写入持久存储之前被单独压缩。压缩是缺省的,因为默认的压缩方法是非常快的,不可压缩的数据会自动禁用压缩。
在少见情况,应用程序可能要完全禁用压缩来改善性能:
1 leveldb::Options options; 2 options.compression = leveldb::kNoCompression; 3 ... leveldb::DB::Open(options, name, ...) ....
6. 校验
ReadOptions::verify_checksums可以设置为true来强制读操作对所有数据校验,默认为false
如果数据库损坏了,leveldb::RepairDB函数可以恢复尽可能多的数据。