【存储】blotdb的原理及实现(2)

【存储】etcd的存储是如何实现的(3)-blotdb

在etcd系列中,我们对作为etcd底层kv存储的boltdb进行了比较全面的介绍。但是还有两个点没有涉及。

第一点是boltdb如何和磁盘文件交互。
持久化存储和我们一般业务应用程序的最大区别就是其强依赖磁盘文件。一方面文件数据结构和内存数据结构的差异很大,需要设计合适的文件数据结构(文件布局)来保证足够的读写效率;另一个方面,在与磁盘文件的读写交互上也需要做各种优化以提升db的整体性能。boltdb的文件布局在上一篇中已经介绍过了,本篇中会介绍mmap,boltdb用来做文件交互的技术。

第二点是boltdb如何管理free page。
在第一篇中我们讲过,boltdb采用了shadow paging(影子分页)的实现,这种类似copy

文章目录

  • mmap
  • freelist
    • 数组freelist
    • map freelist
    • 页的释放
      • 事务的记录
      • 页的释放

mmap

mmap是linux提供的一个系统调用。

mmap的作用是将进程的一块虚拟内存和文件相映射,通过对该虚拟内存的读写就可以直接对文件进行读写。相对于一般的使用read、write系统调用来读写文件的情况,使用mmap可以减少用户空间和内核空间之间的数据拷贝,提高效率。

mmap的原理是将一块虚拟内存和一块内核物理内存相映射,以此来减少用户空间和内核空间的数据拷贝。更具体的细节这里就不展开,网上有不少文章都介绍的非常详细,感兴趣可自行了解。

mmap是一种高效的操作文件的方式,但是应用在数据库也会有一些问题。这些问题产生的根本原因就是在mmap中,内存回写文件完全由内核管理而无法由存储应用层控制。可能导致的后果有:

  • 事务安全

在mmap中,内存刷盘的过程完全由操作系统控制,其无法感知上层应用的情况,会存在事务尚未提交,就将脏页刷盘的情况。这种情况可能会对事务造成影响。

博主认为在存在mvcc的db中其实是没有影响的,如果没有mvcc,则确实会产生中间状态。因为mvcc是存储应用层针对事务设计,如果没有mvcc,则事务完全依赖持久化能力。当然实际情况会更复杂,应该根据不同的实现具体情况具体分析。

解决mmap事务安全可以采用wal+copy-on-write,或者采用shadow paging(影子分页)。boltdb采用的是shadow paging。其能解决问题的根本原因是shadow paging中,每次写入都会将更新写入新的页而不是在原有页上update。在页表刷盘前,即使新的页被回写磁盘,该页也只会被当作空白页对待。

  • i/o停顿

操作系统无法感知存储应用层对数据的使用情况,其页面的置换无法保证db的热点数据在内存中,会导致i/o的性能波动。

除以上问题外,mmap还存在一些其他的问题,数据库大神andy专门写了一篇论文来论证为什么不要在数据库中使用mmap,详情见论文。但实际上,在工业界的很多知名项目中都使用了mmap,其中的考量博主目前也没有深入研究,留待下回分解。

freelist

freelist在内存中维护了所有的空闲页,以便快速的进行页的分配。boltdb支持两种形式的freelist,分别以数组和map的形式维护空闲页面,可以在创建DB对象时通过option中的FreelistType参数控制。

const (// FreelistArrayType indicates backend freelist type is arrayFreelistArrayType = FreelistType("array")// FreelistMapType indicates backend freelist type is hashmapFreelistMapType = FreelistType("hashmap")
)

官方解释中,数组形式的freelist实现简单,但是性能较差,尤其当db比较大时,并且容易产生文件碎片。map形式的freelist性能很快,在文件碎片问题上表现更好,但不能保证分配的页是offset最小的页。默认使用数组形式的freelist。

数组freelist

数组形式的freelist使用数组按照升序来维护所有的page id。当需要分配一块n页的连续空间时,会从前向后找到第一块大于n页的连续空间,从中截取n页分配。这种形式保证了我们一定能拿到offset最小的符合要求的page。但是遍历的方式是O(n)的复杂度,当db很大时,可能会产生严重的性能问题。同时这种方式也很容易产生文件碎片。

// 省略其他字段
type freelist struct {freelistType   FreelistType                // freelist typeids            []pgid                      // all free and available free page ids.
}

map freelist

map形式的freelist采用三个map维护空闲页,或者说连续的页面组成的不同大小的连续空间。

  • freemaps的key是空间大小(页数表示),value是以对应大小连续空间的起始page id表示的集合;
  • forwardMap的key是连续空间起始page id,value为连续空间大小;
  • backwardMap的key是连续空间结束page id,value为连续空间大小;
// 省略其他字段
type freelist struct {freelistType   FreelistType                // freelist typefreemaps       map[uint64]pidSet           // key is the size of continuous pages(span), value is a set which contains the starting pgids of same sizeforwardMap     map[pgid]uint64             // key is start pgid, value is its span sizebackwardMap    map[pgid]uint64             // key is end pgid, value is its span size
}type pidSet map[pgid]struct{}

当需要分配一块n页的连续空间时,会在freemaps中查找是否存在n页的连续空间,如有,则选择一块分配,否则随机选择大于n页的连续空间分配(这样看在碎片问题上也没好到哪去)。forwardMap和backwardMap的作用则是在释放内存时进行连续空间的合并。

页的释放

上面提到了freelist有数组和map两种实现,其差别主要在空闲页的分配上。个人觉得freelist的最有意思的点在页的释放上,这也是freelist跨事务管理page的体现。

在介绍具体的实现前,我们先回顾下boltdb事务的特点。

  • boltdb支持同时进行多个读事务和最多一个写事务。
  • 读事务是快照读,快照的内容是读事务开始前最近一次已提交写事务。
  • 读事务不会对db的数据造成影响,但是会导致过期的页不能被释放。(长读事务会导致db空间暴涨)

回顾过以上内容,我们再来看具体的实现。

事务的记录

boltdb会将读写事务和只读事务分别记录。其中读写事务同时最多存在一个,只读事务同时存在多个,采用数组记录,并且boltdb会按照txnid升序维护只读事务。

// 省略其他字段
type DB struct {rwtx     *Tx         // 读写事务txs      []*Tx       // 只读事务
}

当事务结束(提交或者回滚)时,会调用事务的close方法消除事务。

// 省略多余代码
func (tx *Tx) close() {if tx.db == nil {return}if tx.writable {// Remove transaction ref & writer lock.tx.db.rwtx = nil} else {tx.db.removeTx(tx)}
}

页的释放

页的释放分为两部分:

  • 读写事务中有delete或者update(影子分页的特点)时将page标记为待释放;
  • 持有待释放page的只读事务结束,读写事务开始前将不被只读事务持有的待释放page释放;

在读写事务中,当发生update或者delete操作时,对应的页需要被释放。但是这些页不能在读写事务提交时立刻被释放,因为可能会被只读事务的快照持有。

所以freelist对外提供free方法,记录当前事务需要释放的页。读写事务中在对应的位置调用free方法。

待释放的page以txPending的方式组织,这种组织方式是为了在事务回滚时快速进行回滚操作。

type txPending struct {ids              []pgidalloctx          []txid // txids allocating the idslastReleaseBegin txid   // beginning txid of last matching releaseRange
}

free方式的实现如下。其会记录释放的页以及对应分配该页的txnid。在boltdb中只有开启读写事务才会对txnid进行递增,所以txnid可以认为是版本的概念。当一个版本不被只读事务持有,那么该版本分配的待释放页就可以释放。在boltdb中,只读事务永远只能拿到最新版本的快照而无法获取旧版本的快照,所以页总是可以被释放。但同时长的只读事务也会导致页迟迟不能释放,从而可能会导致db空间快速增长。

// free releases a page and its overflow for a given transaction id.
// If the page is already free then a panic will occur.
func (f *freelist) free(txid txid, p *page) {if p.id <= 1 {panic(fmt.Sprintf("cannot free page 0 or 1: %d", p.id))}// Free page and all its overflow pages.txp := f.pending[txid]if txp == nil {txp = &txPending{}f.pending[txid] = txp}allocTxid, ok := f.allocs[p.id]if ok {delete(f.allocs, p.id)} else if (p.flags & freelistPageFlag) != 0 {// Freelist is always allocated by prior tx.allocTxid = txid - 1}for id := p.id; id <= p.id+pgid(p.overflow); id++ {// Verify that page is not already free.if _, ok := f.cache[id]; ok {panic(fmt.Sprintf("page %d already freed", id))}// Add to the freelist and cache.txp.ids = append(txp.ids, id)txp.alloctx = append(txp.alloctx, allocTxid)f.cache[id] = struct{}{}}
}

在新读写事务开启时,会根据txPending和当前只读事务的状态来释放待释放的page,可以认为是一种lazy的策略,但是在这个场景下效果很好。

boltdb会遍历进行中的所有只读事务(db.Txs),调用release和releaseRange方法来释放已结束版本的待释放页。

func (db *DB) freePages() {// Free all pending pages prior to earliest open transaction.sort.Sort(txsById(db.txs))minid := txid(0xFFFFFFFFFFFFFFFF)if len(db.txs) > 0 {minid = db.txs[0].meta.txid}if minid > 0 {db.freelist.release(minid - 1)}// Release unused txid extents.for _, t := range db.txs {db.freelist.releaseRange(minid, t.meta.txid-1)minid = t.meta.txid + 1}db.freelist.releaseRange(minid, txid(0xFFFFFFFFFFFFFFFF))// Any page both allocated and freed in an extent is safe to release.
}

至此,我们对boltdb的各个方面进行了比较完善的讲解。


如果觉得本文对您有帮助,可以请博主喝杯咖啡~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/182035.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux系统之一次性计划任务at命令的基本使用

Linux系统之一次性计划任务at命令的基本使用 一、at命令介绍二、at命令的使用帮助2.1 at命令的help帮助信息2.2 at命令的语法解释 三、at命令的日常使用3.1 立即执行一次性任务3.2 指定时间执行一次性任务3.3 查询计划任务3.4 其他指定时间用法3.5 删除已经设置的计划任务3.6 显…

深度学习毕设项目 基于生成对抗网络的照片上色动态算法设计与实现 - 深度学习 opencv python

文章目录 1 前言1 课题背景2 GAN(生成对抗网络)2.1 简介2.2 基本原理 3 DeOldify 框架4 First Order Motion Model 1 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点&#xff0c;往往达不到毕业答辩的要求&am…

echarts案例网站

一、ppchart 网站&#xff1a;https://ppchart.com/#/ 二、echarts官网示例 网站&#xff1a;https://echarts.apache.org/examples/zh/index.html

1992-2021年区县经过矫正的夜间灯光数据(GNLD、VIIRS)

1992-2021年区县经过矫正的夜间灯光数据&#xff08;GNLD、VIIRS&#xff09; 1、时间&#xff1a;1992-2021年3月&#xff0c;其中1992-2013年为年度数据&#xff0c;2013-2021年3月为月度数据 2、来源&#xff1a;DMSP、VIIRS 3、范围&#xff1a;区县数据 4、指标解释&a…

NeurIPS 2023|AI Agents先行者CAMEL:第一个基于大模型的多智能体框架

AI Agents是当下大模型领域备受关注的话题&#xff0c;用户可以引入多个扮演不同角色的LLM Agents参与到实际的任务中&#xff0c;Agents之间会进行竞争和协作等多种形式的动态交互&#xff0c;进而产生惊人的群体智能效果。本文介绍了来自KAUST研究团队的大模型心智交互CAMEL框…

双指针算法(题目与答案讲解)

文章目录 题目移动零复写零两数之和N数之和(>2个数) 答案讲解移动零复写零两数之和N数之和 题目 力扣 移动零 1、移动零:题目链接 复写零 2、复写零:题目链接 两数之和 3、两数之和题目链接 N数之和(>2个数) 4、N数之和(三个数、四个数) 三个数:题目链接 四个数题目链接…

Docker、Kubernetes、OCI、CRI-O、containerd、runc 之间的关系以及它们是如何一起工作的?

最近网上看到一张图片&#xff0c;能够很清晰地展现出 Docker、Kubernetes、OCI、CRI-O、containerd、runc 之间的关系以及它们是如何在一起工作的&#xff0c;如下&#xff1a; 本文可以作为之前一篇文章&#xff08;《K8s、Docker、CRI、OCI 之间的爱恨情仇》&#xff09;的…

依靠堡塔面板,飞速部署Java项目

依靠堡塔面板&#xff0c;飞速部署Java项目 环境介绍 环境介绍&#xff1a; 面板版本&#xff1a;8.0.26 操作系统版本&#xff1a;CentOS7.9.2009 Nginx版本&#xff1a;1.22 Java环境&#xff1a;Tomcat8&#xff0c;JDK&#xff1a;OpenJDK-1.8.0-internal MySQL版本&#…

CodeMeter软件保护及授权管理解决方案(二)

客户端管理工具 CodeMeter Runtime是CodeMeter解决方案中的重要组成部分&#xff0c;其为独立软件包&#xff0c;开发者需要把CodeMeter Runtime和加密后的软件一起发布。CodeMeter Runtim包括以下组件用于实现授权的使用&#xff1a; CodeMeter License Server授权服务器 Co…

7 种 JVM 垃圾收集器详解

一、概述 如果说收集算法是内存回收的方法论&#xff0c;那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定&#xff0c;因此不同的厂商、版本的虚拟机所提供的垃圾收集器都可能会有很大差别&#xff0c;并且一般都会提供参数供用…

11月29日作业

作业&#xff1a; 自己封装一个矩形类(Rect)&#xff0c;拥有私有属性:宽度(width)、高度(height)&#xff0c; 定义公有成员函数: 初始化函数:void init(int w, int h) 更改宽度的函数:set_w(int w) 更改高度的函数:set_h(int h) 输出该矩形的周长和面积函数:void show(…

HarmonyOS4.0开发应用(一)【工具安装】

工具安装 地址:https://developer.harmonyos.com/cn/develop/deveco-studio#download 我是windows&#xff0c;所以安装的windows 解压后双击该文件进行安装 安装完成后可选择是否导入开发工具的设置 这里选择不导入进入到工具内 点击Next进行安装 都没问题最终就可以开…

linux 脚本之条件语句 if与case

1. 测试 条件测试&#xff1a;判断某需求是否满足&#xff0c;需要由测试机制来实现&#xff0c;专用的测试表达式需要由测试命令辅助完成。 测试过程&#xff0c;实现评估布尔声明&#xff0c;以便用在条件性环境下进行执行 若真&#xff0c;则状态码变量 $? 返回0若假&am…

接口性能测试 —— Jmeter并发与持续性压测

接口压测的方式&#xff1a; 1、同时并发&#xff1a;设置线程组、执行时间、循环次数&#xff0c;这种方式可以控制接口请求的次数 2、持续压测&#xff1a;设置线程组、循环次数&#xff0c;勾选“永远”&#xff0c;调度器&#xff08;持续时间&#xff09;&#xff0c;这种…

轻松整合Knife4j:快速搭建Swagger文档界面与接口调试

Knife4j 是一个为 Java 开发者提供的 Swagger 文档聚合工具&#xff0c;它是 Swagger-Bootstrap-UI 的升级版。它的主要功能是生成和展示 API 文档&#xff0c;让开发者能够更轻松地查看和测试接口。 整合 Knife4j&#xff08;Swagger-Bootstrap-UI 的升级版&#xff09;到 Spr…

从 Elasticsearch 到 SelectDB,观测云实现日志存储与分析的 10 倍性价比提升

作者&#xff1a;观测云 CEO 蒋烁淼 & 飞轮科技技术团队 在云计算逐渐成熟的当下&#xff0c;越来越多的企业开始将业务迁移到云端&#xff0c;传统的监控和故障排查方法已经无法满足企业的需求。在可观测理念逐渐深入人心的当下&#xff0c;人们越来越意识到通过多层次、…

第三方发起备份的ORA-00245问题

文章目录 前言一、信息确认共享目录位置控制文件快照位置节点1节点2 二、RAC修改snapshot controlfile 参数三、字典表确认以及测试 前言 在使用 AnyBackup 管理控制台发起 Oracle RAC 数据库备份后&#xff0c;在任务历史记录 > 执行输出中显示如下错误信息&#xff1a; c…

Unity版本使用情况统计(更新至2023年10月)

本期UWA发布的内容是第十三期Unity版本使用统计&#xff0c;统计周期为2023年5月至2023年10月&#xff0c;数据来源于UWA网站&#xff08;www.uwa4d.com&#xff09;性能诊断提测的项目。希望给Unity开发者提供相关的行业趋势&#xff0c;了解近半年来哪些Unity版本的使用概率更…

智慧环保:视频监控平台EasyCVR与AI智能分析在环保领域的应用

人工智能&#xff08;AI&#xff09;视频分析技术在环保领域有着广泛的应用&#xff0c;通过智能识别和跟踪技术&#xff0c;AI视频分析可以实时监测空气质量、水质和噪音等环境指标&#xff0c;帮助环保部门及时发现污染源并进行有效治理&#xff0c;提高监测、管理和保护环境…

zookeeper集群(很少用)+kafka集群(常用)

一、zookeeper zookeeperkafka&#xff08;2.7.0版本&#xff09; kafka&#xff08;3.4.1版本&#xff09;不依赖于zookeeper 1、定义&#xff1a;zookeeper开源&#xff0c;分布式架构&#xff0c;提供协调服务&#xff08;Apache项目&#xff09;&#xff0c;基于观察者模…