译者注

该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单、高性能兼容Redis协议的数据库的经历。

首先这个"Redis"是非常简单的实现,但是他在优化这个简单"Redis"路程很有趣,也能给我们在从事性能优化工作时带来一些启示。

原作者:Ayende Rahien

原链接:

https://ayende.com/blog/197665-C/high-performance-net-building-a-redis-clone-analysis-ii

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

RavenDB地址:https://github.com/ravendb/ravendb

构建Redis克隆版-第二次分析

我要倒退几步,看看我接下来应该看哪里,看看我应该注意哪里。到目前为止,在本系列中,我主要关注的是如何读取和处理数据。但我认为我们应该退一两步,看看我们现在的总体情况。我在分析器中运行了使用Pipelines和字符串的版本,试图了解我们的进展情况。例如,在上一篇文章中,我使用的 ConcurrentDictionary 有很大的性能开销。现在还是这样吗?

以下是代码库中当前的热点数据:



更详细来看,如下所示:



可以看到处理网络请求占用了大部分的时间,我们再来看看HandleConnection代码:

public async Task HandleConnection()
{
while (true)
{
var result = await _netReader.ReadAsync();
var (consumed, examined) = ParseNetworkData(result);
_netReader.AdvanceTo(consumed, examined);
await _netWriter.FlushAsync();
}
}

查看代码和分析器的结果,我觉得我知道如何做的更好。下面的一个小修改给我带来了2%的性能提升。

public async Task HandleConnection()
{
// 复用了readTask 和 flushTask
// 降低了一些内存占用
ValueTask<ReadResult> readTask = _netReader.ReadAsync();
ValueTask<FlushResult> flushTask = ValueTask.FromResult(new FlushResult());
while (true)
{
var result = await readTask;
await flushTask;
var (consumed, examined) = ParseNetworkData(result);
_netReader.AdvanceTo(consumed, examined);
readTask = _netReader.ReadAsync();
flushTask = _netWriter.FlushAsync();
}
}

我们的想法是将网络的读写并行化。这是一个小小的提升,但是任何一点点帮助都是好的,特别是当各种优化会关联影响时。

看看这个,我们已经有将近20亿个ReadAsync调用,让我们看看它的成本是多少:

真是... 哇。

为什么InternalTokenSource如此昂贵?我敢打赌问题就在这里,它被锁定了。在我的用例中,我知道有一个单独的线程在运行这些命令,不会有并发问题,所以值得看看是否可以跳过它。不幸的是,没有一个简单的方法可以跳过检查。幸运的是,我可以从框架中复制代码并在本地对其进行修改,以了解这样做的影响。所以我就这样做了(在构造函数中初始化一次) :

这意味着我们在每次请求处理上有大约40%的改进。正如我前面提到的,这不是我们现在能够做到的,因为源码里面就有lock,但是这是一个关于使用 PipeReader 读取数据性能损耗有趣的点。

另一个非常有趣的方面是后端存储,它是一个ConcurrentDictionary。如果我们看看它的成本,我们会发现:

您会注意到,我正在使用NonBlocking的NuGet包,它提供了一个无锁的 ConcurrentDictionary实现。如果我们使用.NET框架中的默认实现,它确实使用了锁,我们将看到:

下面有它们的对比:



请注意,这两个选项之间存在非常大的成本差别(有利于非阻塞)。但是,当我们运行一个真实的基准测试时,它并没有特别大的差别。

那接下来呢?

看看分析器的结果,我们没有什么可以继续改进的。我们的大部分成本都在网络中,而不是在我们运行的代码中。

我们的大部分代码都在 ParseNetworkData 调用中,看起来像这样:

所以我们实际上花在执行服务器核心功能上的时间是可以忽略不计的。实际上,解析来自缓冲区的命令花费了大量时间。注意,在这里,我们实际上并不执行任何 I/O 操作,所有操作都在内存中的缓冲区上进行操作。

Redis协议对于机器解析来说并不友好,需要我们进行大量的查找才能找到分隔符(因此有很多的IndexOf()调用)。我不认为你能在这方面有显著的改进。这意味着我们必须考虑其他更好的性能选择。

我们花费了35% 的运行时来解析来自客户端的命令流,而我们执行的代码不到运行时的1% 。我不认为流解析还有重要的优化机会,因此我们只剩I/O的优化方向。我们能做得更好吗?

我们目前使用的是异步I/O和Pipelines。看看这个让我感兴趣的项目,它在Linux使用了IO_Uring(通过这个API)来满足他们的需要。它们的解析也很简单,请看这里,与我的代码运行的方式非常相似。

因此,为了进入性能的下一个阶段(提醒一下,我们现在的性能是180w/s) ,我们可能还需要使用基于IO_Uring的方法。有一个NuGet软件包来支持它,但是这使得我可以在一个晚上花几个小时来完成这个任务,而不是花几天或者一周的时间来完成。我不认为在不久的将来我会继续追求这个目标。

结尾

完结撒花!!!按照Ayende大佬的意思是后面会尝试在linux上使用IO_Uring来实现,目前来看大佬还没有其它的更新,已经发布的博文已经全部翻译。

我也在大佬博文底部提出了其它的一些性能优化的小建议,建议来自我之前发布的文章,同样高性能的网络服务开发。有兴趣的可以查看下方链接。

https://www.cnblogs.com/InCerry/p/highperformance-alternats.html

系列链接

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

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

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

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

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

后续大佬有其它更新的话,也欢迎艾特我催更

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

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

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

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

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

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

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

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

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

  5. SSDB是一个开源的高性能数据库服务器

    SSDB是一个开源的高性能数据库服务器, 使用Google LevelDB作为存储引擎, 支持T级别的数据, 同时支持类似Redis中的zset和hash等数据结构, 在同时需求高性能和大数据的条件下 ...

  6. 学习T-io框架,从写一个Redis客户端开始

    前言   了解T-io框架有些日子了,并且还将它应用于实战,例如 tio-websocket-server,tio-http-server等.但是由于上述两个server已经封装好,直接应用就可以.所 ...

  7. 简单动态字符串-redis设计与实现

    简单动态字符串 Sds (Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示, 几乎所有的 Redis 模块中都用了 sds. 本章将对 sds 的实现 ...

  8. 探索Redis设计与实现13:Redis集群机制及一个Redis架构演进实例

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  9. 最简单的一个Oracle定时任务

    最简单的一个Oracle定时任务一.在PLSQL中创建表:create table HWQY.TEST(CARNO     VARCHAR2(30),CARINFOID NUMBER) 二.在PLSQ ...

  10. 一个简单的CS系统打包过程图文版

    一个简单的CS系统打包过程图文版 1.     打包内容 1.1.  此次打包的要求和特点 主工程是一个CS系统: 此CS系统运行的先决条件是要有.Net Framework 3.5: 主工程安装完成 ...

随机推荐

  1. jQuery问题集锦

    [1]阻止提交表单 方法1: $(function () { $("input[type=submit]").click(function (event) { //如果不满足表单提 ...

  2. Couchbase 找回登录密码

    在项目里,我们使用Couchbase当做缓存层,效果非常好,支持集群与数据监测,可悲的有一次我新建了一个服务器,安装完成Couchbase之后,密码忘了! 怎么也想不起来,Couchbase又没有提供 ...

  3. 管理http服务的脚本

    因为老是须要又一次安装系统,重一次都要又一次设置http服务的启动脚本.麻烦,所以这一次就把脚本备份出来. httpd for Ubuntu system: nginx + php-fpm #! /b ...

  4. JavaScript表单验证年龄

    JavaScript表单验证年龄,判断一个输入量是否符合年龄,通过正则表达式实现. //检查年龄 function isAge(str){ var mydate=new Date; var now=m ...

  5. 醒醒吧!互联网的真正未来不是AI,更不是VR,AR,而是区块链

    这些力量并非命运,而是轨迹.他们提供的并不是我们将去向何方的预测,而是告诉我们,在不远的将来,我们会向那个方向前行,必然而然. ---凯文•凯利 文字与货币 人类在演化过程中,凭借智慧创造了无数事物, ...

  6. HTML+Css让网页自动适应电脑手机屏幕

    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scal ...

  7. 前言:JNI调用-简单使用

    JNI JNI是(Java Native Interface  本地接口)是一个协议,用来沟通Java 代码和C/C++代码,是 Java和 C.C++之间的桥梁. 通过JNI协议,Java可以调用外 ...

  8. Python生成一个不含回文字符串的字符串

    [本文出自天外归云的博客园] 回文字符串介绍 回文字符串就是对称的字符串,例如: “ABA” “ABBA” “ABCBA” 题目 给定一个字符串,请发明一种方法,让字符串中不包含回文字符串. 我的解法 ...

  9. VirtualBox 4.3.18 启动虚拟机时显示不能加载 R3模块并退出故障解决一例

    VirtualBox 升级到 4.3.1x后一直问题不断.搜了些资料,发现这货从最近的某个版本开始,为了安全,要校验进程完整性,那些在运行时要注入Virtualbox进程的模块都要进行校验.于是乎出现 ...

  10. Kattis - amsterdamdistance【数学】

    Kattis - amsterdamdistance[数学] 题意 给出两个点 算出从第一个点到第二个点的最短距离,只不过这里不是直角坐标系, 是雷达图 思路 因为内圈的圆的路径要比外圈的小,所以我们 ...