使用.NET简单实现一个Redis的高性能克隆版(二)
译者注
该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单、高性能兼容Redis协议的数据库的经历。
首先这个"Redis"是非常简单的实现,但是他在优化这个简单"Redis"路程很有趣,也能给我们在从事性能优化工作时带来一些启示。
原作者:Ayende Rahien
原链接:https://ayende.com/blog/197441-A/high-performance-net-building-a-redis-clone-analysis
另外Ayende大佬是.NET开源的高性能多范式数据库RavenDB所在公司的CTO,不排除这些文章是为了以后会在RavenDB上兼容Redis协议做的尝试。大家也可以多多支持,下方给出了链接
RavenDB地址:https://github.com/ravendb/ravendb
正文
在上一篇文章中,我用最简单的方式写了一个Redis克隆版本。它能够在我们的测试实例上每秒命中近100万个查询(c6g.4xlarge,使用16个内核和64 GB内存)。在我们更深入地进行优化之前,值得了解CPU时间实际花费在哪里。我在探查器下运行服务器,以查看各种代码所耗费的成本。
我喜欢使用dotTrace作为探查器,同时使用它的跟踪模式,因为它返回的数据中给了我各个模块、类和代码的执行时间以及调用次数。通常,我可以仅从这些细节中推断出很多关于系统性能的原因。
看看下面的统计数据,这是连接实际处理过程中的成本细分:

展开耗费CPU最多的System code,如下所示:

您可以看到FlushAsync()方法耗费的CPU做多。我们在这里做一个假设,当我们调用StreamWriter的FlushAsync()方法时,同样会刷新底层的流。深入研究下调用栈,似乎我们在TCP层面为每个命令都都进行了分包,这样效率是很低的。
如果我们将StreamWriter的AutoFlush属性改为true,这将导致它立即向网络流中写入数据,但不会在TCP流上调用flush,这会让TCP流更有效的利用缓冲空间。
涉及的代码更改是删除FlushAsync()调用并初始化StreamWiter,如下所示:
using var writer = new StreamWriter(stream)
{
NewLine = "\r\n",
AutoFlush = true,
};
让我们再次运行基准测试,这将给我们(在我的开发机器上):
- 138,979.57 QPS
[13.8w/s]– 使用 AutoFlush = true - 139,653.98 QPS
[13.9w/s]– 使用 FlushAsync
基本上,这两种选择都不怎么样。原因如下所示:
设置为True的AutoFlush不仅会刷新当前流,还会刷新基础流,从而使Stream他们处于相同的Position。
问题是我们需要刷新流,否则我们在内存中缓冲的结果数据不会发送给客户端。Redis基准测试在很大成都依赖管道(一次性发送多个命令),但是在实际过程中可能会收到一堆来自客户端的命令,这堆命令会写入(到输入缓冲区),然后不向客户端发送任何内容,因为输出的缓冲区并没有满。我们可以使用以下代码更改轻松地优化它:
var line = await reader.ReadLineAsync();
await writer.FlushAsync();
// 修改为以下代码
var lineTask = reader.ReadLineAsync();
if(lineTask.IsCompleted == false)
{
await writer.FlushAsync();
}
var line = await lineTask
我在这里所做的是直接写入StreamWriter,并且只有在没有更多的输入时才刷新缓冲区。这应该会大大减少包的发送次数,而且它确实做到了。再次运行基准测试可以得出以下结论:
- 229,783.30 QPS
[22.9w/s]– 使用延时刷新
我们只修改几行代码,却得到了几乎两倍的性能提升,这是令人影响深刻的。我们的想法是,缓冲更多的写入,并且不让它延时太久。如果写入足够的数据到StreamWriter缓冲区,它自己会自动的刷新。我们只会在没有其它需要读取的数据时手动刷新StreamWriter,这个操作是和读取并行进行的。
下图是新的耗时统计:

实际方法调用如下:

如果我们将其与第一次分析结果进行比较,我们可以发现一些非常有趣的数字。以前,我们为每个命令调用FlushAsync(请参阅ExecuteCommand&FlushAsync),现在我们更少调用它了。
您可以看到,现在大部分时间花费都在这个系统的“业务逻辑代码”中,从子系统的细分来看,现在很多时间都花费在处理集合中。
这里的GC花费也大幅下降(~5%)。我相当确定这是因为我们使用了新的方式刷新TCP流,但我没有仔细的去检查它。
请注意,虽然字符串处理和GC需要花费大量时间,但是集合/ExecuteCommand还是占用了更多的时间。
如果我们调查一下,我们会发现:

而且这非常有趣。
主要是因为主要成本在TryAddInternal中。我们知道在这种情况下存在很高的争用,但92%的时间直接花在了这个方法上吗?让我们看一下代码,它在做什么就会很明显:

ConcurrentDictionary对锁之间的调用进行分片。锁的数量由我们默认拥有的CPU内核数量定义。我们的的并发越多,我们就越能从增加分片数量中获益。我尝试将其设置为1024,并在分析器下运行它,这给我带来了几个百分点的改进,但并不是很多。很有价值,但不是我期望的水平。

现在,我们需要找出如何在让集合操作变得更快,但我们还必须考虑总体GC成本以及字符串处理细节。在下一篇文章中会有更多关于这一点的信息。
系列链接
使用.NET简单实现一个Redis的高性能克隆版(二)的更多相关文章
- 使用.NET简单实现一个Redis的高性能克隆版(三)
译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...
- 使用.NET简单实现一个Redis的高性能克隆版(四、五)
译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...
- 使用.NET简单实现一个Redis的高性能克隆版(六)
译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...
- 使用.NET简单实现一个Redis的高性能克隆版(七-完结)
译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...
- 使用.NET简单实现一个Redis的高性能克隆版(一)
译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...
- 简单创建一个SpringCloud2021.0.3项目(二)
目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上一篇教程 3. 创建公共模块Common 4. 网关Gateway 1. 创建Security 2. Security登陆配置 3 ...
- 简单创建一个SpringCloud2021.0.3项目(四)
目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上三篇教程 3. 日志处理 1. 创建日志公共模块 2. Eureka引入日志模块 4. 到此的功能代码 5. 注册中心换成naco ...
- 简单创建一个SpringCloud2021.0.3项目(三)
目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上俩篇教程 3. Gateway集成sentinel,网关层做熔断降级 1. 超时熔断降级 2. 异常熔断 3. 集成sentine ...
- 简单创建一个SpringCloud2021.0.3项目(一)
目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 新建父模块和注册中心 1. 新建父模块 2. 新建注册中心Eureka 3. 新建配置中心Config 4. 新建两个业务服务 1. ...
随机推荐
- 云开发中的战斗机 Laf,让你像写博客一样写代码
各位云原生搬砖师 and PPT 架构师,你们有没有想过像写文章一样方便地写代码呢? 怎样才能像写文章一样写代码? 理想的需求应该是可以在线编写.调试函数,不用重启服务,随时随地在 Web 上查看函数 ...
- PyTorch DataSet Normalization torchvision.transforms.Normalize()
特征缩放, 在这种情况下,我们不仅仅考虑是一个值的数据集,我们考虑的是具有多个特征和相关的值的样本或元素的数据集. 假如正在处理一个人的数据集, 归一化数据集有许多不同的 ...
- script标签crossorigin属性及同源策略和跨域方法
首先介绍(同源策略) 同源策略是浏览器最核心且基本的安全约定,要求协议.域名.端口都相同为同源,如果非同源时请求数据浏览器会在控制台抛出跨域异常错误,同源策略是浏览器的行为,即使客户端请求发送了,服务 ...
- 入坑KeePass(一)安全桌面输入管理密钥后,不能输入中文
坑一:设置了在安全桌面输入管理密钥后,重启电脑后今日keepass软件中不能输入中文: 解决方式:进入keepass软件中取消勾选,在重启软件就可以输入中文了.
- 根据数据中的key获取value值
一.测试数据准备 List<Map<String, String>> result = new ArrayList();Map<String, String> ma ...
- SAP 文件操作类 CL_GUI_FRONTEND_SERVICES
1 .文件下载. DATA: l_filename TYPE string, "file name l_path TYPE string, "file path l_fullpat ...
- 缤纷多彩的WPF样式框架,开源项目
下面介绍的四种主流样式框架(最近项目需要,所以了解了一些),在Nuget及Github均可以找到~ 首推样式框架MahApp.Metro 再推样式框架ModernUI 三推样式框架MaterialDe ...
- GitLab、Jenkins结合构建持续集成(CI)环境
1 持续集成 概述及运行流程 1.1 持续集成概述 持续集成概述:持续集成(Continuous integration)持续集成是指开发者在代码的开发过程中,可以频繁的将代码部署集成到主干,并迚程自 ...
- linux web漏洞扫描arachni
1. 下载arachni https://www.arachni-scanner.com/download/下载Linux x86 64bit 2. 上次解压直接使用 tar xzf arachni- ...
- Spring Data JPA系列5:让IDEA自动帮你写JPA实体定义代码
大家好,又见面了. 这是本系列的最后一篇文档啦,先来回顾下前面4篇: 在第1篇<Spring Data JPA系列1:JDBC.ORM.JPA.Spring Data JPA,傻傻分不清楚?给你 ...