译者注

该原文是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目前代码中高成本的地方就是字符串的处理,下面的分析器图表实际上有一些误导:



字符串占用了运行时的12.57%的时间,另外就是GC Wait, 我们需要清理掉这些开销。这意味着我们之前写的代码是非常低效的。

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

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

考虑以下一组事件流程:

在上面的例子中,线程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%左右。

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

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

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

使用可重用缓冲区也如下所示:

这里有一些有趣的事情值得注意。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 buffer
newItem = 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%的版本。这看起来是这样的:

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

  • 1.57 ms - 基于字符串
  • 1.79 ms - 基于可重用缓冲区(减少内存使用量)
  • 1.04 ms - 基于可重用缓冲区(优化查找)

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

  • 基于字符串 – 16.0w次/秒
  • 可重用缓冲区(减少内存代码)– 18.6w次/秒
  • 可重用缓冲区(优化查找)– 17.5w次/秒

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

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

系列链接

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

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

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

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

使用.NET简单实现一个Redis的高性能克隆版(六)的更多相关文章

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

    译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...

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

    译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...

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

    译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...

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

    译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...

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

    译者注 该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单.高性能兼容Redis协议的数据库的经历. 首先这个"Redis"是非常简单的实现,但是他 ...

  6. 发布一个参考ssdb,用go实现的类似redis的高性能nosql:ledisdb

    起因 ledisdb是一个参考ssdb,采用go实现,底层基于leveldb,类似redis的高性能nosql数据库,提供了kv,list,hash以及zset数据结构的支持. 我们现在的应用极大的依 ...

  7. Nginx+Lua+MySQL/Redis实现高性能动态网页展现

    Nginx结合Lua脚本,直接绕过Tomcat应用服务器,连接MySQL/Redis直接获取数据,再结合Lua中Template组件,直接写入动态数据,渲染成页面,响应前端,一次请求响应过程结束.最终 ...

  8. java架构之路-(Redis专题)Redis的高性能和持久化

    上次我们简单的说了一下我们的redis的安装和使用,这次我们来说说redis为什么那么快和持久化数据 在我们现有的redis中(5.0.*之前的版本),Redis都是单线程的,那么单线程的Redis为 ...

  9. [开源] gnet: 一个轻量级且高性能的 Golang 网络库

    Github 主页 https://github.com/panjf2000/gnet 欢迎大家围观~~,目前还在持续更新,感兴趣的话可以 star 一下暗中观察哦. 简介 gnet 是一个基于 Ev ...

随机推荐

  1. Python数据分析--Numpy常用函数介绍(4)--Numpy中的线性关系和数据修剪压缩

    摘要:总结股票均线计算原理--线性关系,也是以后大数据处理的基础之一,NumPy的 linalg 包是专门用于线性代数计算的.作一个假设,就是一个价格可以根据N个之前的价格利用线性模型计算得出. 前一 ...

  2. Vue2-组件通讯传值

    Vue2组件通讯传值 方法 Slot插槽--父向子内容分发,子组件只读 mixin混入--定义公共变量或方法,mixin数据不共享,组件中mixin实例互不影响 provide+inject--依赖注 ...

  3. Navicat破解激活流程

    ​​ ​ Navicat Navicat Premium 是一套数据库开发工具,让你从单一应用程序中同时连接 MySQL.MariaDB.MongoDB.SQL Server.Oracle.Postg ...

  4. springcloud-- Alibaba-nacos--支持的几种服务消费方式

    通过<Spring Cloud Alibaba基础教程:使用Nacos实现服务注册与发现>一文的学习,我们已经学会如何使用Nacos来实现服务的注册与发现,同时也介绍如何通过LoadBal ...

  5. Redis集群搭建 三主三从 docker版 急速搭建

    最近学习了docker 发现使用docker搭建一个redis非常的简单接下来就是搭建步骤 1.首先清空一下容器  #清空所有容器docker rm -f $(docker ps -aq) 2.然后创 ...

  6. MVC - forward 和 redirect 的区别

    MVC - forward 和 redirect 的区别 forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服 ...

  7. 浏览器代理user-agent

    两种方法: 法1:浏览器地址栏输入:about://version,然后复制用户代理: 如果法1不行,法2肯定可以. 法2:打开任意浏览器,输入任意网址,下面以火狐和百度网址为例来进行说明: 打开火狐 ...

  8. 最简单的离散概率分布,伯努利分布 《考研概率论学习之我见》 -by zobol

    上文讲了离散型随机变量的分布,我们从最简单的离散型分布伯努利分布讲起,伯努利分布很简单,但是在现实生活中使用的很频繁.很多从事体力工作的人,在生活中也是经常自觉地"发现"伯努利分布 ...

  9. React技巧之检查元素是否可见

    原文链接:https://bobbyhadz.com/blog/react-check-if-element-in-viewport 作者:Borislav Hadzhiev 正文从这开始~ 总览 在 ...

  10. 《HelloGitHub》第 75 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...