谈谈在Bitcask中用读写锁实现并发控制的性能表现

news2024/11/25 15:50:44

背景

最近被问了几次nutsdb事务是怎么实现的,也就是并发控制是怎么做的。我说,用一把大的读写锁,写事务拿到写锁,读事务拿读锁,这样子做的。提问者先是震惊,接着说是有一点鄙夷,我感觉大概心里是在想,怎么这么low啊。我感觉用读写锁也还好,应该也不至于那么不堪吧。为什么呢?这篇文章记录了我在这个过程中的一些思考和探索,里面有一些属于个人观点。如若有说的不对的地方,还请多多指正。

为什么会觉得读写锁不太行呢?

我们先来一波理论分析,为什么提问者会感觉读写锁不是很可以呢?读写并发有四种情况,读读,写写,读写,写读。读写锁在处理写事务的时候是会阻塞住读事务,写写就没什么好说的,必定互斥,读读可以并发。

在这个逻辑下,可以保证数据的安全和一致性,因为对数据有修改的操作都是在互斥的环境下执行。不过互斥会影响一些性能啦,因为你在写东西的时候别人都不可以做事了。所以如果能解放一部分互斥环境下并发操作,这样就会带来一些性能提升。

如果我们用MVCC做多版本控制,毕竟bitcask天生就是会存多版本的数据的,每个读写操作在刚开始的时候拿到一个属于自己的版本,而不是读锁或者写锁,那么读写就存在一定的并发空间,从而释放一定的读写并发能力。至于MVCC怎么做这里就不多赘述了,这是个很大的话题,容我后面补一篇文章说明。

但是事实真的是这样吗?是的,至少在理论上。但是我认为,在bitcask模型中,乃至在存储引擎中,思考问题应该要将磁盘IO操作对性能的影响纳入思考范围内。如果都是内存操作,并发能力的提升当然给程序带来巨大的提升,但是如果要把磁盘带上,就不一定了。为什么?

为什么要考虑磁盘IO操作对性能的影响?

在过去几十年的时间里,计算机各个部分的硬件取得了飞跃性的发展,但是每个组件之间的性能差距同时也越拉越大。一个一分钟7200转的硬盘,完成一次磁盘读取大概要9ms,但是一台台500 -MIPS的机器这时候可以完成几十万条指令。

在这个背景下,让我们把思绪回到bitcask模型中。数据写入的时候会先写入到wal中,防止数据丢失,不过现在有一些存储引擎实现了一些策略,可以做个buffer,buffer满了之后再批量写入,或者做个批量数据写入,提升性能嘛,毕竟每一条数据都直接写磁盘也蛮奢侈的。这时候就是看用户对数据完整性的要求了,因为这里可能会丢失一些数据。这些都不是什么大事,反正最后都是要写磁盘的。

至于读取,bitcask不像基于LSM实现的存储引擎那样,可以在memtable或者block cache中读取数据。bitcask读取数据是会去磁盘中读取的。

综上,无论读写,bitcask都需要都需要和磁盘打交道。那么这个时候读写锁和版本控制这两种并发方式的性能差异真的有那么重要吗?锁资源的并发增强对比起IO的开销,我认为是萤火和皓月的区别,所以我感觉读写锁也没有那么不堪,甚至好像还可以。

怀着这样的想法,我在电脑上简单的做了个实验,验证一下这个想法。

做一下实验

怎么实验呢。我的想法是简单模拟读写锁和版本控制的逻辑,读写操作就做简单的读取和写入一些数据。大致探究在不同的读写操作比例上的性能差异,比如读操作占总操作的百分之90,性能对比如何,读操作占10%,性能对比如何。好,让我们马上进入实验环节。

模拟版本控制的并发读写方式:

type VersionControlWriter struct {
	version atomic.Value
	fd      *os.File
}

func (vw *VersionControlWriter) Write() {
	vw.increase()
	vw.fd.Write([]byte(writeStr))
}

func (vw *VersionControlWriter) Read() {
	vw.increase()
	vw.fd.ReadAt(readBytes, 0)
}

func (vw *VersionControlWriter) increase() {
	v := vw.version.Load().(int)
	v++
	vw.version.Store(v)
}

可以看到就是简单的用一个原子变量来代表读写事务的版本。每次读写事务开始操作之前,先通过原子变量自增的方式来获取事务版本,保证每个事务都获得自增的,独一无二的id。

至于写入的话是把hello world以追尾的方式写入磁盘。读取的话是读取文件开头的第一条Hello world。

读写锁并发读写方式模拟:

type RWLockWriter struct {
	lock sync.RWMutex
	fd   *os.File
}

func (rw *RWLockWriter) Write() {
	rw.lock.Lock()
	defer rw.lock.Unlock()
	rw.fd.Write([]byte(writeStr))
}

func (rw *RWLockWriter) Read() {
	rw.lock.RLock()
	defer rw.lock.RUnlock()
	rw.fd.ReadAt(readBytes, 0)
}

可以看到,读写锁的方式读取写入的数据都是一样的。最大程度保持两边实验环境的相似程度。我把这两个结构体抽象成一个接口ReadWrite,在流量控制的时候抽象成一个函数。代码如下所示。

type ReadWrite interface {
	Read()
	Write()
}

func flowControl(threshold int, rw ReadWrite) {
	seed := rand.Intn(100)
	if seed < threshold {
		rw.Write()
	} else {
		rw.Read()
	}
}

我用一个随机数来分配读写流量。随机数的算法都有等概率性,所以可以放心用它来做流量切分。另外在这段代码中我们可以看到的是,对于版本控制来说,不管读写有没有冲突,也不管数据一致性的情况,只要有读操作我就立马读,这样子对于这个实验来说,其实他的实现效果会偏好。

然后我们在go中写下一个benchmark.

func BenchmarkWriterPerformance(b *testing.B) {
	for _, config := range []TestConfig{
		{
			flowControlThreshold: 20,
		},
		{
			flowControlThreshold: 50,
		},
		{
			flowControlThreshold: 80,
		},
	} {
		rw, vw, err := initTestResources()
		if err != nil {
			b.Error(err)
		}
		b.Run(fmt.Sprintf("test the performance for RWLock for flowcontorl %d", config.flowControlThreshold), func(b *testing.B) {
			b.ResetTimer()
			b.RunParallel(func(pb *testing.PB) {
				for pb.Next() {
					flowControl(config.flowControlThreshold, rw)
				}
			})

		})
		b.Run(fmt.Sprintf("test the performance for version control flowcontorl %d", config.flowControlThreshold), func(b *testing.B) {
			b.ResetTimer()
			b.RunParallel(func(pb *testing.PB) {
				for pb.Next() {
					flowControl(config.flowControlThreshold, vw)
				}
			})
		})
		err = closeTestResources(rw, vw)
		if err != nil {
			b.Error(err)
		}
	}

}

让我们运行这个benchmark看看结果:

image-20230626223849907

可以看到,在读操作占比比较多的时候,读写锁和版本控制性能上还是有一些差别的。大概20%这样。但是写操作占比越来越大的时候性能上的差距愈发接近。其实这也符合我们之前做的理论分析和假设。

总结

可以看到,对于存储引擎,IO才是最大的瓶颈。那么为什么MySQL要实现MVCC呢。MySQL一般是读多写少,所以做MVCC收益比较大。另外MySQL是有数据缓存在内存的,可能对于大多数读取操作来说,根本不需要磁盘IO去读取。这样一来,又是一大波优化。但是当我们把目光拉会到bitcask,读写操作都需要经过磁盘,批次写只是延迟写入,我认为这样只是降低了写操作的比例,因为几个写操作合并成一个写入磁盘了。本质上没多大差别。

所以这也符合了我一开始心中的想法,读写锁,也没那么不堪。对于基于bitcask模型实现的存储引擎来说。

PS。本人水平有限,这个实验也有一些取巧的地方,另外实验环境实在我的电脑上,实验结果有不具备普遍性结论的可能,大家也可以在自己电脑上运行一下这个代码。代码地址我贴在下面。另外这篇文章个人观点比较多。大家有什么疑问可以联系我,大家一起交流探讨。感谢。

延伸阅读

  1. 文章中实验代码地址:https://github.com/elliotchenzichang/go-experiment/blob/master/go/writor_test.go

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

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

相关文章

【踩坑指南】Django+channels WebSocket配置

目前我搜到网上所有配置Djangochannels的教程/博客中&#xff0c;都没有提及这一点。希望能帮助你 踩的坑必须写在最前面&#xff1a; 根据文档的步骤去配置&#xff0c;每次到执行python manage.py 的时,使用的是默认的development server&#xff0c;而不是我们想要的Star…

解决:.prettierrc 配置完后,自动保存并没有格式化代码

如果你也碰到了同样的问题&#xff0c;请先确保&#xff1a; .prettierrc 文件已正确配置&#xff0c;例如我的&#xff1a; {"semi": false,"singleQuote": true,"arrowParens": "always","trailingComma": "all&qu…

卷积神经网络参数量和计算量的计算方法

本文总结了标准卷积、分组卷积和全连接层参数量和计算量的计算方法&#xff0c;如有错误&#xff0c;麻烦大家指正 一、标准卷积 假设输入特征的shape为[, , ]&#xff0c;卷积核的shape为[, , , ]&#xff0c;输出特征的shape为[, , ]&#xff0c;则&#xff0c; 标准卷积运…

C++特殊类设计及类型转换

目录 一、特殊类的设计 1.不能被拷贝的类 2.只能在堆区构建对象的类 3.只能在栈区构建对象的类 4.不能被继承的类 二、单例模式 1.饿汉模式 2.懒汉模式 3.线程安全 4.单例的释放 三、C类型转换 1.C语言的类型转换 2.static_cast 3.reinterpret_cast 4.const_cas…

Python补充笔记1-字符串

目录 1.字符串的驻留机制​编辑 2.字符串查找 2.1字符串查询操作方法 3.字符串大小写转换 3.1字符串的大小写转换方法 4.字符串内容对齐 4.1字符串内容对齐操作方法 5.字符串的劈分 5.1字符串劈分操作的方法​编辑 6.字符串判断 6.1判断字符串操作的方法​编辑 6.2字符串替换和…

虚拟化技术及实时虚拟化概述

版权声明&#xff1a;本文为本文为博主原创文章&#xff0c;未经本人同意&#xff0c;禁止转载。如有问题&#xff0c;欢迎指正。博客地址&#xff1a;https://www.cnblogs.com/wsg1100/ 实时虚拟化技术是一种针对实时应用场景的虚拟化技术&#xff0c;它要求在保证虚拟化优势…

STM32 ws2812b 最快点灯cubemx

文章目录 前言一、cubemx配置二、代码1.ws2812b.c/ws2812b.h2.主函数 前言 吐槽 想用stm32控制一下ws2812b的灯珠&#xff0c;结果发下没有一个好用的。 emmm&#xff01;&#xff01;&#xff01; 自己来吧&#xff01;&#xff01;&#xff01;&#xff01; 本篇基本不讲原理…

6、传输层TCP28

TCP协议&#xff1a;传输控制协议 1、协议实现 16位源端端口&16位对端端口&#xff1a;描述通信俩端进程32位序号&#xff1a;告诉接收端&#xff0c;这条数据在整体数据中的排序&#xff0c;接收端根据序号进行排序32位确认序号&#xff1a;向发送端进行回复确定&#xff…

pytest-html报告修改与汉化

目录 前言 生成报告 测试代码 原始报告 修改Environment 修改后的效果 修改Summary 修改后的效果 修改Results 优化Test 解决中文乱码 删除多余部分 修改后的效果 删除Links 修改后的效果 增加失败截图与用例描述 完整的conftest.py代码 汉化报告 修改plugin…

ClickHouse进阶

一、Explain查看执行计划 在 clickhouse 20.6 版本之前要查看 SQL 语句的执行计划需要设置日志级别为 trace 才能可以看到&#xff0c;并且只能真正执行 sql&#xff0c;在执行日志里面查看。 在 20.6 版本引入了原生的执行计划的语法。在 20.6.3 版本成为正式版本的功能。 …

常见的JS内置对象——字符串、数学、日期

二、字符串&#xff08;string&#xff09; 创建 一般使用第一种方式 2&#xff09;字符串的遍历 注意&#xff1a;没有foreach方法 3&#xff09;字符串的常见方法 substr()和substring()&#xff1a; substr()参数是从哪个位置开始&#xff0c;截多长 substring()参数是从…

完美匹配:一种简单的神经网络反事实推理学习表示方法

英文题目&#xff1a;Perfect Match: A Simple Method for Learning Representations For Counterfactual Inference With Neural Networks 翻译&#xff1a;完美匹配&#xff1a;一种简单的神经网络反事实推理学习表示方法 单位&#xff1a; 论文链接&#xff1a;https://a…

【状态估计】基于FOMIAUKF、分数阶模块、模型估计、多新息系数的电池SOC估计研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

C++ 创建共享内存

共享内存用于实现进程间大量的数据传输&#xff0c;共享内存是在内存中单独开辟一段内存空间&#xff0c;这段内存空间有自己特有的数据结构&#xff0c;包括访问权限、大小和最近访问时间等。 1、shmget函数 #include <sys/ipc.h> #include <sys/shm.h> int shm…

c++——多态(补充)

优先查看&#xff1a;c——多态_Hiland.的博客-CSDN博客 目录 菱形虚拟继承子类的重写问题 菱形虚拟继承中的偏移量补充 逆向思维——汇编查看多态中被重写的虚函数 菱形虚拟继承子类的重写问题 继承环节时&#xff0c;菱形虚拟继承解决了菱形继承的数据冗余和二义性问题。…

C# Modbus通信从入门到精通(11)——Modbus RTU(调试软件Modbus Slave和Modbus Poll的使用)

前言 我们在开发Modbus程序的时候,会需要测试以下我们写的Modbus程序有没有问题,这时候就需要使用到Modbus Slave和Modbus Poll这两个软件,Modbus Slave是模拟Modbus从站,Modbus Poll是模拟Modbus从站主站的, 1、Modbus Slave 一般情况下我们开发的嗾使Modbus主站程序,…

性能测试(Jemeter)

1.性能指标 响应时间&#xff1a;一次请求的往返时间tps&#xff1a;每秒系统能够处理的事务数&#xff0c;比如订单中的下单操作&#xff0c;下单后续有很多操作&#xff0c;比如创建订单&#xff0c;扣除库存&#xff0c;清算库存等&#xff0c;这个完整操作就是一个完整的事…

【数据分享】1929-2022年全球站点的逐日最大持续风速数据(Shp\Excel\12000个站点)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、能见度等指标&#xff0c;说到气象数据&#xff0c;最详细的气象数据是具体到气象监测站点的数据&#xff01; 对于具体到监测站点的气象数据&#xff0c;之前我们分享过1929-2022年全球气象…

Qt添加第三方字体

最近开发项目时&#xff0c;据说不能用系统自带的微软雅黑字体&#xff0c;于是找一个开源的字体&#xff0c;思源黑体&#xff0c;这个是google和Adobe公司合力开发的可以免费使用。本篇记录一下Qt使用第三方字体的方式。字体从下载之家下载http://www.downza.cn/soft/266042.…

Pytest参数化——那些你不知道的使用技巧

目录 前言 装饰测试类 输出 说明 装饰测试函数 单个数据 输出 说明 一组数据 输出 说明 图解对应关系 组合数据 输出 说明 标记用例 输出 说明 嵌套字典 输出 增加可读性 使用ids参数 输出 说明 自定义id做标识 输出 说明 总结 总结&#xff1a; 前…