使用.NET简单实现一个Redis的高性能克隆版(六)

译者注

该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单、高性能兼容Redis协议的数据库的经历。首先这个"Redis"是非常简单的实现,但是他在优化这个简单"Redis"路程很有趣,也能给我们在从事性能优化工作时带来一些启示。原作者:Ayende Rahien 原链接:https://ayende.com/blog/197569-B/high-performance-net-building-a-redis-clone-skipping-strings

另外Ayende大佬是.NET开源的高性能多范式数据库RavenDB所在公司的CTO,不排除这些文章是为了以后会在RavenDB上兼容Redis协议做的尝试。大家也可以多多支持,下方给出了链接RavenDB地址:https://github.com/ravendb/ravendb

构建Redis克隆版-字符串处理

我克隆版Redis目前代码中高成本的地方就是字符串的处理,下面的分析器图表实际上有一些误导:9b0d5a3b5556b191562857a85c470500.png字符串占用了运行时的12.57%的时间,另外就是GC Wait, 我们需要清理掉这些开销。这意味着我们之前写的代码是非常低效的。

我们的测试场景现在也只涉及 GET 和 SET 请求,没有删除、过期等。我提到这一点是因为我们正在考虑用什么来替换字符串。

最简单的选择是用字节数组替换它,但它仍然是托管内存,并且会产生与 GC 相关的成本。我们可以池化这些字节数组,但是我们还有一个重要的问题要回答,我们如何知道什么时候不再使用池化的数组,也就是说,什么什么把它归还到池中?

考虑以下一组事件流程:1a766635332276ed068a81d9822aed3c.png

在上面的例子中,线程2访问了值缓冲区,但是在Time-3中我们使用SET abc命令替换了原来的数据,导致线程2访问的不再是原来的数据。

我们需要找一个方法,将值缓冲区保留到没有任何对象引用它的时候,另外在销毁它时我们要将它归还到池中。

我们可以通过手动管理内存的方式来实现这个,这是很可怕的。实际上我们可以使用一些不同的方式,比如利用GC来达到我们的目的。

public class ReusableBuffer
{public byte[] Buffer;public int Length;public Span<byte> Span => new Span<byte>(Buffer, 0, Length);public ReusableBuffer(byte[] buffer, int length){Buffer = buffer;Length = length;}public override bool Equals(object? obj){if (obj is not ReusableBuffer o)return false;return o.Span.SequenceEqual(Span);}public override int GetHashCode(){var hc = new HashCode();hc.AddBytes(Span);return hc.ToHashCode();}// 关键是这里,声明一个析构函数// 当GC需要释放它的时候会调用~ReusableBuffer(){ArrayPool<byte>.Shared.Return(Buffer);}
}

想法很简单。我们有一个持有缓冲区的类,当 GC 注意到它不再被使用时,它将把它的缓冲区归还到池中。这个想法是我们依靠 GC 来为我们解决这个(真正困难的)问题。虽然这会将一些成本转移到终结器,但是目前来说我们不必担心这个问题。不然,你就得经历很多困难来编写手动管理内存的代码。

ReusableBuffer类还实现了GetHashCode()/Equals(),它允许我们将其用作字典中的Key。

现在我们有了键和值的后台存储,让我们看看如何从网络读写。现在我将回到 ConcurrentDictionary 实现,一次只处理一个事情。

以前,我们使用 StreamReader/StreamWriter 来完成工作,现在我们将使用 System.IO.Pipelines 中的 PipeReader/PipeWriter。这将使我们能够轻松地直接处理原始字节数据,并且这是为高性能场景设计的。

我编写了两次代码,一次使用可重用的缓冲区模型,一次使用 PipeReader/PipeWriter 并分配字符串。我惊讶地发现,我的可重用缓冲区的性能差距只有字符串实现的1% (简单得多)。顺便说一句,那是1%的错误方向。

在我的机器上,基于可重用的缓冲区是16.5w/s,而基于字符串的系统是每秒16.6w/s。

下面是基于可重用缓冲区的完整方法源代码。比较一下,这是基于字符串的。基于字符串的代码行比基于字符串的代码行短50%左右。

  • 基于可重用缓冲区:https://gist.github.com/ayende/f6263d5ddd331a7f8263ef892b45f526

  • 基于字符串:https://gist.github.com/ayende/bc52b3cbdb6d5ebd8fa00ac5d014a876

我猜测是因为我们这个场景的分配模式非常适合GC所做的那种启发式处理。我们要么有长期对象(在缓存中),么有非常短期的对象。

值得指出的是,网络中命令的实际解析并不使用字符串。只有实际的键和值实际上被转换为字符串。其余部分使用原始字节数据。

下面是对字符串版本的代码进行分析的结果:d1e2fdcdd32e709ef3ecceba3bccfda1.png

使用可重用缓冲区也如下所示:96c7a8f828b0e80a1c58f83de5c14428.png

这里有一些有趣的事情值得注意。ExecCommand 的成本几乎是基于字符串版本尝试的两倍。深入挖掘,我相信错误就在这里:

var buffer = ArrayPool<byte>.Shared.Rent((int)cmds[2].Length);
cmds[2].CopyTo(buffer);
var val = new ReusableBuffer(buffer, (int)cmds[2].Length);
Item newItem;
ReusableBuffer key;
if (_state.TryGetValue(_reusable, out var item))
{// can reuse key buffernewItem = new Item(item.Key, val);key = item.Key;
}
else
{var keyBuffer = ArrayPool<byte>.Shared.Rent((int)cmds[1].Length);cmds[1].CopyTo(keyBuffer);key = new ReusableBuffer(keyBuffer, (int)cmds[1].Length);newItem = new Item(key, val);
}
_state[key] = newItem;
WriteMissing();

这段代码负责在字典中设置项。但是,请注意,我们正在对每个写操作执行读操作?这里的想法是,如果我们现在_state中已经存在了这个值,那么我们就避免再次为它分配缓冲区,而是重用它。

但是,这段代码处于这个基准测试的关键路径中,代价相当高昂。我修改了这段代码,不再重用,总是new对象进行分配,我们得到了一个比字符串版本快1~3%的版本。这看起来是这样的:553f02493e5aa01ffa98c2e9794d2a99.png

换句话说,这是当前每次操作对应的性能表(在探查器下):

  • 1.57 ms - 基于字符串

  • 1.79 ms - 基于可重用缓冲区(减少内存使用量)

  • 1.04 ms - 基于可重用缓冲区(优化查找)

得出的那些结果都在我计算机使用分析器运行的。让我们看看当我在生产实例上运行它们时,最终的结果是怎么样的?

  • 基于字符串 – 16.0w次/秒

  • 可重用缓冲区(减少内存代码)– 18.6w次/秒

  • 可重用缓冲区(优化查找)– 17.5w次/秒

这些结果与我们在开发机器中看到的结果并不匹配。可能的原因是并发和请求数量足够高,负载足够大,以至于我们看到大规模内存优化的效果要好很多。

这是我能得出的唯一结论,减少分配内存,能够在这样的高负载场景下处理更多的请求。

系列链接

使用.NET简单实现一个Redis的高性能克隆版(一)

使用.NET简单实现一个Redis的高性能克隆版(二)

使用.NET简单实现一个Redis的高性能克隆版(三)

使用.NET简单实现一个Redis的高性能克隆版(四、五)

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

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

相关文章

Netty 4.x – ByteBuf

2019独角兽企业重金招聘Python工程师标准>>> 1、ByteBuf与Java NIO Buffer ByteBuf则是Java NIO Buffer的新轮子&#xff0c;官方列出了一些ByteBuf的特性&#xff1a; 需要的话&#xff0c;可以自定义buffer类型&#xff1b;通过组合buffer类型&#xff0c;可实现透…

Vue3关于响应式数据类型(ref、reactive、toRef、以及toRefs)

ref 接受一个内部值&#xff0c;返回一个响应式的、可更改的 ref 对象&#xff0c;此对象只有一个指向其内部值的 property .value。类型function ref<T>(value: T): Ref<UnwrapRef<T>>interface Ref<T> {value: T }详细信息ref 对象是可更改的&#x…

智能路由器-OpenWRT 系列四 (挂载移动设备)

每次ssh登陆OpenWRT安装新软件时&#xff0c;都必须更新opkg opkg update安装驱动 为了可以识别U盘/移动硬盘&#xff0c;必须安装相关的驱动。 一类是usb相关的驱动 opkg install kmod-usb-core #usb驱动 opkg install kmod-scsi-core #SCSI总线驱动 opkg install kmod-scs…

jstl 处理Date 时间

1、引入 <% taglib prefix"fmt" uri"http://java.sun.com/jsp/jstl/fmt" %> <fmt:formatDate value"${Date}" pattern"yyyy-MM" /><!-- 这里的value只能是date类型--> pattern 随意发挥了&#xff01;转载于:https:…

windows系统vscode检查更新菜单消失且不能自动更新(1.70.0不能自动更新到1.70.2

翻了半天vscode的issues中终于找到了https://github.com/microsoft/vscode/issues/157624 解决方案 不能用管理员身份运行vscode&#xff0c;不然就没有检查更新菜单&#xff0c;关闭后重启vscode就有更新菜单了 相关回答 翻译一下&#xff0c;就是vscode分用户类型和系统类型两…

LinkedHashMap的实现原理

1. LinkedHashMap概述&#xff1a; LinkedHashMap是Map接口的哈希表和链接列表实现&#xff0c;具有可预知的迭代顺序。此实现提供所有可选的映射操作&#xff0c;并允许使用null值和null键。此类不保证映射的顺序&#xff0c;特别是它不保证该顺序恒久不变。 LinkedHashMap…

验证-表单令牌

表单令牌验证规则支持对表单的令牌验证&#xff0c;首先需要在你的表单里面增加下面隐藏域&#xff1a;或者然后在你的验证规则中&#xff0c;添加token验证规则即可&#xff0c;例如&#xff0c;如果使用的是验证器的话&#xff0c;可以改为&#xff1a;如果你的令牌名称不是_…

WPF 用户控件分享之边上带输入框的圆圈

WPF 用户控件分享之边上带输入框的圆圈独立观察员 2022 年 8 月 20 日最近有这样一个需求&#xff0c;有一圈圆形&#xff0c;每个圆形边上有个输入框&#xff0c;以下是完成后的效果图&#xff1a;拿到这个需求后&#xff0c;分析界面上每个圆形和输入框应该视为一个用户控件&…

总统与乞丐

一位总统带着孙子散步&#xff0c;有个乞丐向他鞠躬敬礼&#xff0c;总统马上驻足还礼&#xff0c;而且弯腰更深。孙子不解:“他只是个乞丐啊&#xff01;” 总统回答&#xff1a;“我绝不允许一个乞丐比总统更有礼貌&#xff01;不要以为别人尊敬你&#xff0c;是因为你很优秀…

VUE3 v-show 引起的表单验证问题

<el-form ref"formRef" :rules"rules" :model"form" label-width"80px"><el-form-item label"任务类型" prop"TaskType"><el-radio-group v-model"form.TaskType" change"handleCh…

ThreadPoolExecutor

简述&#xff1a; 有的时候&#xff0c;系统处理很多任务&#xff0c;如何这些任务要是都是通过new Thread来做的话&#xff0c;系统就不得不常常的创建之后还要销毁Thread&#xff0c;这个是非常消耗时间而且还占用资源&#xff0c;所以我们通过创建线程池来管理我们的线程。 …

你总说时间很少

你总说时间很少 没时间看哭泣的骆驼 少了心情 你总说时间很少 没时间撩拨那爱的罗曼史 少了喜欢 你总说时间很少 没时间吮吸印度洋的水清沙白 少了勇敢 你总说时间很少 没时间寻找相伴的野马 少了感觉 可到最后 时间真的很少 有了喜欢 就少了心情 有了感觉 就少了勇敢…

Unity 将是驱动 C# 增长的引擎吗 ?

C# 在中国的采用需要一个杀手级应用的带动&#xff0c; 那么这样的一个杀手级应用是 Unity吗&#xff0c;我这里大胆推测采用CoreCLR 的新一代完全采用C#构建的Unity 将是这样的一个杀手级应用。Unity已被广泛应用于数字孪生、数字城市、数字工厂等场景&#xff0c;成为各产业加…

HTML5video 标签

属性 值 说明 autoplay autoplay 如果出现该属性&#xff0c;则视频在就绪后马上播放。 controls controls 如果出现该属性&#xff0c;则向用户显示控件&#xff0c;比如播放按钮。 preload preload 如果出现该属性&#xff0c;则视频…

Windows 超级开源实用工具:Microsoft PowerToys

实用工具集&#xff1a; Always on Top ​通过 Always on Top&#xff0c;可使用快捷键方式 (⊞ WinCtrlT) 将窗口固定在其他窗口的顶部。 ​ PowerToys Awake PowerToys Awake旨在使计算机保持唤醒状态&#xff0c;且无需管理其电源和睡眠设置。 运行耗时较长的任务时&#…

Python入门之数据类型

字符串 列表 元组 字典 转载于:https://www.cnblogs.com/py17/p/8919495.html

Kubeadm 快速搭建 k8s v1.24.1 集群(openEuler 22.03 LTS)

kubeadm 简介kubeadm 是 Kubernetes&#xff08;以下简称 k8s&#xff09;官方提供的用于快速安装部署 k8s 集群的工具&#xff0c;伴随 k8s 每个版本的发布都会同步更新&#xff0c;kubeadm 会对集群配置方面的一些实践做调整&#xff0c;通过实验 kubeadm 可以学习到 k8s 官方…

汇编试验五:编写、调试具有多个段的程序

ds 数据段放入数据正确&#xff1b; 两次push 操作后&#xff0c;ss栈段正确&#xff1b; 由于pop 操作顺序&#xff0c;ds数据段并没有发生改变&#xff1b; Source Code: assume cs:code, ds:data, ss:stackdata segmentdw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H …

SVN四部曲之SVN设置详解深入

想知道不同的设置是干什么用的&#xff0c;你只需将鼠标指针在编辑框/选项框上停留一秒钟...一个帮助提示气泡就会弹出来。 常规设置 图 4.68. 设置对话框&#xff0c;常规设置页面 这个对话框允许你指定自己喜欢的语言&#xff0c;同时也可做那些与Subversion相关的特殊设置。…

Vue3.2单文件组件setup的语法糖总结

目录 前言 setup语法糖 一、基本用法 二、data和methods 三、计算属性computed 四、监听器watch、watchEffect 五、自定义指令directive 六、import导入的内容可直接使用 七、声明props和emits 八、父组件获取子组件的数据 九、provide和inject传值 十、路由useRou…