使用.NET简单实现一个Redis的高性能克隆版(三)
译者注
该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单、高性能兼容Redis协议的数据库的经历。
首先这个"Redis"是非常简单的实现,但是他在优化这个简单"Redis"路程很有趣,也能给我们在从事性能优化工作时带来一些启示。
原作者:Ayende Rahien
原链接:https://ayende.com/blog/197473-C/high-performance-net-building-a-redis-clone-architecture
构建Redis克隆版-架构
在之前的文章中,我们尝试用最简单的方式来完成一个Redis克隆版。打开一个套接字来监听,为每个客户端单独分配一个Task来从网络读取数据,解析命名并执行它。虽然在流水线上有一些小的改进,但也只仅此而已。
让我们退一步来构建一个与Redis架构更为接近的Redis克隆版。为此,我们需要在一个线程中完成所有工作。这在C#中是比较难实现的,没有用于执行Redis那样工作类型的API。更确切的来说是有Socket.Select()方法,但是需要我们自己在此基础上构建一切(比如我们必须写代码处理缓冲、字符串等等)。
考虑到这是通往最终建议的架构的一个中途站,我决定完全跳过这个。相反,我将首先专注于消除系统中的主要瓶颈,即ConcurrentDictionary。
分析器的结果表明,我们这最大的开销就是ConcurrentDictionary的可伸缩性。即使我使用了1024个分片的锁,它仍然占用50%的时间开销。问题是,我们能做得更好吗?我们可以尝试一个更好的选择,就是我们不再使用ConcurrentDictionary,而是直接使用单独的Dictionary来分片,这样的话每个Dictionary都不需要并发就可以访问。
我的想法是这样的,我们将为客户端提供常规的读写操作。但是,我们不会直接在I/O上处理这些命令,而是将其路由到一个专用的线程(使用它自己的Dictionary)来完成这项工作。因为我是16核的机器,我将创建10个这样的线程(假设它们每个都能分配到1个核心),并且我能够将I/O处理放到其余的6个核心上。
以下是更改后的结果:

请注意,我们现在跑分的数据是125w/s,比上一次几乎增长了25%。
下面是这一次新代码的分析器结果:

因此在本例中,花费了大量的时间来处理各种各样的字符串,等待GC(大约占30%)。集合的成本下降了很多。
还有一些其它的开销出现在我眼前,看看这里:

对于“简单”属性查找来说,这个开销非常惊人。另外SubString函数的调用开销也很大,超过整个系统开销的6%。
在研究系统其它部分时,看到了这个:

这真的很有趣,因为我们花了很多的时间在等待队列中是否有新的元素,其实我们可以做更多的事情,而不是就在那干等着。
我还尝试了其它的线程数量,如果只运行一个ExecWorker,我们的运行速度是40w/s,两个线程,我们的运行速度是70w/s。当使用4个专用于处理请求的线程时,我们的运行速度是106w/s。
因此,很明显,我们需要重新考虑这种方案,我们不能够正确地扩展到合适的数值。
注意,这种方法也不利用流水线。我们分别处理每个命令和其他命令。我的下一步是添加对使用这种方法的流水线的支持,并测量这种影响。
从另一方面来说,我们现在的性能还是100w/s,考虑到我只花了很少的时间来实现方案,从这个方案可以获得25w/s的性能提升,这是令人激动人心的。从侧面说,我们还有更多的事情可以做,但我想把重点放在修复我们第一个方案上。
下面是当前的状态,因此您可以与原始代码比较。
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Threading.Channels;
var listener = new TcpListener(System.Net.IPAddress.Any, 6379);
listener.Start();
var redisClone = new RedisClone();
while (true)
{
var client = listener.AcceptTcpClient();
var _ = redisClone.HandleConnection(client); // run async
}
public class RedisClone
{
ShardedDictionary _state = new(Environment.ProcessorCount / 2);
public async Task HandleConnection(TcpClient tcp)
{
var _ = tcp;
var stream = tcp.GetStream();
var client = new Client
{
Tcp = tcp,
Dic = _state,
Reader = new StreamReader(stream),
Writer = new StreamWriter(stream)
{
NewLine = "\r\n"
}
};
await client.ReadAsync();
}
}
class Client
{
public TcpClient Tcp;
public StreamReader Reader;
public StreamWriter Writer;
public string Key;
public string? Value;
public ShardedDictionary Dic;
List<string> Args = new();
public async Task ReadAsync()
{
try
{
Args.Clear();
var lineTask = Reader.ReadLineAsync();
if (lineTask.IsCompleted == false)
{
await Writer.FlushAsync();
}
var line = await lineTask;
if (line == null)
{
using (Tcp)
{
return;
}
}
if (line[0] != '*')
throw new InvalidDataException("Cannot understand arg batch: " + line);
var argsv = int.Parse(line.Substring(1));
for (int i = 0; i < argsv; i++)
{
line = await Reader.ReadLineAsync();
if (line == null || line[0] != '$')
throw new InvalidDataException("Cannot understand arg length: " + line);
var argLen = int.Parse(line.Substring(1));
line = await Reader.ReadLineAsync();
if (line == null || line.Length != argLen)
throw new InvalidDataException("Wrong arg length expected " + argLen + " got: " + line);
Args.Add(line);
}
switch (Args[0])
{
case "GET":
Key = Args[1];
Value = null;
break;
case "SET":
Key = Args[1];
Value = Args[2];
break;
default:
throw new ArgumentOutOfRangeException("Unknown command: " + Args[0]);
}
Dic.Run(this);
}
catch (Exception e)
{
await HandleError(e);
}
}
public async Task NextAsync()
{
try
{
if (Value == null)
{
await Writer.WriteLineAsync("$-1");
}
else
{
await Writer.WriteLineAsync($"${Value.Length}\r\n{Value}");
}
await ReadAsync();
}
catch (Exception e)
{
await HandleError(e);
}
}
public async Task HandleError(Exception e)
{
using (Tcp)
{
try
{
string? line;
var errReader = new StringReader(e.ToString());
while ((line = errReader.ReadLine()) != null)
{
await Writer.WriteAsync("-");
await Writer.WriteLineAsync(line);
}
await Writer.FlushAsync();
}
catch (Exception)
{
// nothing we can do
}
}
}
}
class ShardedDictionary
{
Dictionary<string, string>[] _dics;
BlockingCollection<Client>[] _workers;
public ShardedDictionary(int shardingFactor)
{
_dics = new Dictionary<string, string>[shardingFactor];
_workers = new BlockingCollection<Client>[shardingFactor];
for (int i = 0; i < shardingFactor; i++)
{
var dic = new Dictionary<string, string>();
var worker = new BlockingCollection<Client>();
_dics[i] = dic;
_workers[i] = worker;
// readers
new Thread(() =>
{
ExecWorker(dic, worker);
})
{
IsBackground = true,
}.Start();
}
}
private static void ExecWorker(Dictionary<string, string> dic, BlockingCollection<Client> worker)
{
while (true)
{
var client = worker.Take();
if (client.Value != null)
{
dic[client.Key] = client.Value;
client.Value = null;
}
else
{
dic.TryGetValue(client.Key, out client.Value);
}
var _ = client.NextAsync();
}
}
public void Run(Client c)
{
var reader = _workers[c.GetHashCode() % _workers.Length];
reader.Add(c);
}
}
公众号
之前一直有朋友让开通公众号,由于一直比较忙没有弄。
现在终于抽空弄好了,译者公众号如下,欢迎大家关注。

系列链接
使用.NET简单实现一个Redis的高性能克隆版(一)
使用.NET简单实现一个Redis的高性能克隆版(二)
使用.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. Gateway集成sentinel,网关层做熔断降级 1. 超时熔断降级 2. 异常熔断 3. 集成sentine ...
- 简单创建一个SpringCloud2021.0.3项目(四)
目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上三篇教程 3. 日志处理 1. 创建日志公共模块 2. Eureka引入日志模块 4. 到此的功能代码 5. 注册中心换成naco ...
- 简单创建一个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. 新建父模块和注册中心 1. 新建父模块 2. 新建注册中心Eureka 3. 新建配置中心Config 4. 新建两个业务服务 1. ...
随机推荐
- 双webview模式,子窗口打不开或者无法切换
iOS 真机调试时,发现window.open 无效.可以结合plusReady里面不执行一起参考,博主在当时遇到这个问题只查询了资料,而后并没有来得及自己亲自验证以下方法的可行性.来日再遇上mui的 ...
- 即时通讯IM,是时代进步的逆流?看看JNPF怎么说
JNPF快速开发平台所包含的第四个重要的开发框架是即时通讯沟通工具.即时沟通工具的目的是让各大企事业单位在各种业务工作流程环境下实现实时无缝协同办公,打破信息数据孤岛,形成高效的层级流转审批和各流程环 ...
- Cent OS8.0 及以上版本安装禅道教程
Cent OS8系统下安装禅道需要搭建环境如下:httpd ,mariadb , php7.2 再运行禅道 一,环境说明: 运行环境推荐使用 Apache + PHP(7.0/7.1/7.2版本) + ...
- SSH和SCP的使用方法
1.SSH使用方法 ssh 用户名@IP 例: ssh ubuntu@192.168.1.190 最近因为项目需求,需要通过ssh来登录Windows,但是一开始一直无法登录,参考下面这个帖子解决了, ...
- flink 流的合并
flink 流的合并操作 union union只能合并类型相同的数据,合并的结果仍然是DataStream,结果操作与未合并之前一致. public static void main(String[ ...
- [二进制漏洞]栈(Stack)溢出漏洞 Linux篇
目录 [二进制漏洞]栈(Stack)溢出漏洞 Linux篇 前言 堆栈 堆栈(Stack)概念 堆栈数据存储方式 函数调用 函数调用C语言代码 函数调用过程GDB调试 函数Call返回原理 函数栈帧 ...
- 『忘了再学』Shell流程控制 — 38、while循环和until循环介绍
目录 1.while循环 2.until循环 1.while循环 对while循环来讲,只要条件判断式成立,循环就会一直继续,直到条件判断式不成立,循环才会停止.和for循环的第二种格式for((初始 ...
- Vue.js CLI4 Vue.config.js标准配置 (最全注释)
前言: Vue.js CLI工具 不知不觉发展到了4.0时代,CLI给人最直白的感受是没有了build文件夹跟config文件夹,所有的配置都在Vue.config.js完成.那么该文件的配置至关重要 ...
- 多台云服务器的 Kubernetes 集群搭建
环境 两台或多台腾讯云服务器(本人搭建用了两台),都是 CentOs 7.6, master 节点:服务器为 4C8G,公网 IP:124.222.61.xxx node1节点:服务器为 4C4G,公 ...
- 【python基础】第10回 周总结
路径 可以简单的理解为路径就是某个事物所在的具体位置(坐标) 1.相对路径:必须有一个参考系,就是相对于自己的目标文件的位置. 2.绝对路劲:不需要有参考系,是指文件在硬盘上真正存在的路径. 计算机五 ...