首页 > 数据库 > > KV 存储引擎 - Badger源码分析
kv引擎的应用场景
badger源码分析
优化与挑战
参考资料
kv引擎是提供对kv数据结构高性能读写的组件,通常嵌入其他DB server中提供从磁盘读写kv数据的一系列解决方案。
DB的数据模型通常分为,KV,表模型,文档模型,列存储,图模型。后四种从理论上都是可以由kv模型构建起来的。因此kv引擎成为现代很多nosql 数据库的底层存储引擎。旨在提供高性能的读写系统。kv引擎通常的数据结构是LSM的实现,天然对写入的高吞吐,是各种在线及分析型DB的理想选择。总结起来就是当今的各种分布式数据库都是以KV作为底层存储模型,在计算层映射为其目标存储模型的。可以说 一切皆kv。
工业上,levelDB,rocksDB,badger 是比较知名的kv存储引擎。TIDB(分布式关系型)底层封装rocksDB,influxDB(时序数据库)早期使用leveldb,MangoDB(文档数据库)底层也是kv存储,Dgraph(图数据库)底层使用badger作为存储,Hbase(列存储数据库)存储在分布式文件系统上的KV模型,以及在大型互联网公司会自研分布式kv数据库以支持大规模的在线读写请求(缓存,以及索引),等价实现Redis协议,将底层内存存储换成分布式的RocksDB等持久化存储引擎,例如picak数据库。
从文档中了解,项目所要解决的问题是什么,这个项目的意义是什么,了解足够的背景知识。
badger主要解决的是单机高性能的进行磁盘IO来读写kv数据并提供完善的api应对各种场景,同时badger将对于其他kv引擎来说其优势就是实现了whiskery论文,更加充分发挥ssd的优势,同时badger也是专门为了Dgraph这个图数据库开发的项目,为了解决go语言cgo调用rocksDB的复杂问题。
通常在文档或者wiki中会说明该项目参考了哪些经典资料,总能追溯到某个论文或者文章作为作者的灵感来源,找到一篇或多篇这样的文章仔细研读。
比如你就可以找到官网的whiskery论文。
找到官方使用的例子,查看这个项目是如何被使用的。
DB 初始化
读写事务
只读事务
范围查询
GC
LSM日志合并
go get github.com/dgraph-io/badger/v3
复制代码
package main
import (
"fmt"
badger "github.com/dgraph-io/badger/v3"
)
func main() {
// 打开DB
db, err := badger.Open(badger.DefaultOptions("/tmp/badger"))
defer db.Close()
// 读写事务
err = db.Update(func(txn *badger.Txn) error {
// Your code here…
txn.Set([]byte("answer"), []byte("42"))
txn.Get([]byte("answer"))
return nil
})
// 只读事务
err = db.View(func(txn *badger.Txn) error {
// Your code here…
txn.Get([]byte("answer_v1"))
return nil
})
// 遍历keys
err = db.View(func(txn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
opts.PrefetchSize = 10
it := txn.NewIterator(opts)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
k := item.Key()
err := item.Value(func(v []byte) error {
fmt.Printf("key=%s, value=%s\n", k, v)
return nil
})
if err != nil {
return err
}
}
return nil
})
// vlog 的GC
err = db.RunValueLogGC(0.7)
_ = err
}
复制代码
阅读源码有一个基本问题,那就是遇到一个函数究竟是先进去看实现自底向上的去阅读,还是先理解它的输入输出理解这个函数做了什么事情自顶向下的阅读。
delve 是一个go语言调试工具。
构造配置参数对象
打开或创建工作目录并上锁
打开或创建ManifestFile文件
创建DB对象
创建一个块缓存
创建一个索引缓存
开启key注册器
打开memtable 并全部追加到 imm 的列表中
创建一个激活的内存表
创建一个level管理器
vlog初始化
启动日志合并过程
获取DB的事务最大版本号
打开vlog文件, 关联mmap
开启负责处理写请求的工作协程
启动vlog文件的GC过程
检查事务是否关闭
创建一个事务对象
如果是一个读写事务
创建一个存储哪些key存在冲突的记录map
记录在当前事务上发生写入的key的列表
分配一个事务读取时间戳
从 nextReadTnx中获取一个时间戳的值
然后标记为开始,记录最后的事物时间戳后发送给一个markChan
等待与这个时间戳冲突的事物提交完成
Defer 丢弃最终的事务
执行闭包函数
Set kv
Get k
如果这是一个读写事务
提交事务
检查 pending数组是否为空,如果为空则直接返回
事务提交的前置检查
提交并发送日志返回一个回调函数进行等待
获取一把锁
创建一个提交时间戳
是否存在冲突
拿到当前事务的readTs标记其为完成
清理已经提交的事务的记录数组
标记当前的事务开始进行提交,分配了一个新的提交时间戳
将当前提交事务的时间戳和冲突的key组成对象记录在已提交事务的数组中
遍历每个在当前事务中写入的key,为其分配唯一的版本号
遍历pending数组处理每一个实体kv对象
追加一条标记事务结束的内部kv实体
创建一个写入请求
发送给写者channel中
返回一个回调函数
写入磁盘
如果值日志offset大于vlog最大文件大小,或者写入条数超购阈值
WaterMark 实现 - > badger 事务的设计
doWrite实现
读取writeCh中的req
Select writeCh 批量拼装 reqs
如果没有请求,则pending阻塞住,等待上一次批量写入的完成
开启一个协程处理此次写请求
整个写入vlog文件
检查写入的vlaue是否合法值是否足够写入4G大小的文件,因为值指针是uin32
获取vlog的文件锁,获取最大的文件id,最大的也是活跃的
拿到句柄后遍历reqlist
分req写入LSM中
检查当前的值是否超过了值的阈值
没有超过则 直接将值存储到mt中
先写入wal中,用于日志恢复
编码kv数据
直接copy数据到mmap内存上
真正存储到跳表中
超过则 将值的指针存储到mt中 指针指向vlog中的位置
当该doWrite关闭后,先读取所有writeCh中的请求后最后处理一次写入请求
与读写事务的区别 就是只获取了读时间戳,而不需要提交事务
检查key是否合法
创建一个itme对象
如果是一个读写事务 则从pending数组中找是否有刚刚被修改的key
以readTs拼接时间戳版本号
获取内存表列表
遍历内存表从跳表中get key信息,并且其最大版本号小于等于查询版本时直接返回
如果没有则从level中查询
逐层遍历
从某一层获取数据
获取本层中sstable句柄
用不带有时间戳的key计算hash值
遍历tables
判断此table是否存在此key,使用boolFilter
创建一个迭代器对象
Seek 这个key
判断是向前迭代还是向后迭代
置位指针是否从头迭代
二分查找确定key在哪一个block上
从索引块中获得此key所在的bolckIdx的位置
然后先从缓存中加载块缓存的地址缓存的key是fid+offset组成
没有则从mmap映射的文件句柄中获取并加载到内存后返
解析当前block的元数据
查找block中的key
比较value的版本,返回最大版本号的value对象
拼装数据返回结果item
创建一个默认的迭代器对象
配置预读kv条数为10
创建一个事务层迭代器对象
获取内存表列表
给当前vlog对象一个迭代器引用计数表示当前迭代器
创建一个迭代器数组
创建一个对PendingWrites数组的迭代器,并指定是正向还是反向
为每个内存表创建一个迭代器并append到迭代器数组中
为每个level创建一个迭代器
遍历每一个level中的table创建迭代器
如果是L0层则所有的table都要创建迭代器
如果是非0层则先判断是否需要前缀迭代需要的话则按前缀过滤
使用boolfilter过滤掉一定不存在前缀key的table
将table按遍历方向进行反转后返回
根据迭代器数组创建一个合并迭代器
seek(nil) 将游标移动到最初始位置,遍历的,迭代性的调研底层迭代器的seek
预先读取几个kv到迭代器的链表中
解析value的值,并从中判断是否过期与删除
创建一个item对象,并填充
放到预先读取的list上,直到读取了足够的预取内容
检查itme是否合法,如果合法就是说item不为空,则调用next方法再次执行上述步骤
获取一个vlog文件并执行GC
如果有其他GC任务执行则返回错误
选择一个vlog文件
选择包含最多可丢弃数据的vlog文件
当这个文件进行GC
重写当前的vlog文件
更新GC统计结果
启动合并协程
启动4个协程进行压缩
随机延迟1s执行
每10s 2号协程序 执行一次
执行合并逻辑
创建一个压缩任务
如果是0压缩,那就执行l0的特殊压缩策略
先尝试L0到Lbase的压缩
失败了会尝试l0到l0的压缩过程
运行压缩任务
对合并的table 合理的拆分为多个可并行的子压缩
真正的合并表返回新
将其写入清淡文件
对新表与旧表进行替换
删除旧表
打印日志
启动flush 内存表协程
接受flushChan的消息,并且做一个聚合操作
对一段时间内的多个内存表进行一个合并
创建一个迭代器然后处理合并过程
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69999831/viewspace-3148257/ ,如需转载,请注明出处,否则将追究法律责任。