译者注

该原文是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的高性能克隆版(三)的更多相关文章

  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. 简单创建一个SpringCloud2021.0.3项目(三)

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上俩篇教程 3. Gateway集成sentinel,网关层做熔断降级 1. 超时熔断降级 2. 异常熔断 3. 集成sentine ...

  7. 简单创建一个SpringCloud2021.0.3项目(四)

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上三篇教程 3. 日志处理 1. 创建日志公共模块 2. Eureka引入日志模块 4. 到此的功能代码 5. 注册中心换成naco ...

  8. 简单创建一个SpringCloud2021.0.3项目(二)

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上一篇教程 3. 创建公共模块Common 4. 网关Gateway 1. 创建Security 2. Security登陆配置 3 ...

  9. 简单创建一个SpringCloud2021.0.3项目(一)

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 新建父模块和注册中心 1. 新建父模块 2. 新建注册中心Eureka 3. 新建配置中心Config 4. 新建两个业务服务 1. ...

随机推荐

  1. 121_Power Query之R.Execute的read.xlsx&ODBC

    博客:www.jiaopengzi.com 焦棚子的文章目录 请点击下载附件 一.问题 pq在用 Excel.Workbook 读取一些Excel早期版本(.xls后缀)的文件时候,报错:DataFo ...

  2. iTextSharp 提取签名图像

    原文 本文使用 iTextSharp 5.5.13.2,记录使用 iTextSharp 提取图片时,获得的知识点. pdf 中的签名并不是单纯的一张图片,它是由一张基础的底色图和一张蒙版图片组成.需要 ...

  3. 成本节省 50%,10 人团队使用函数计算开发 wolai 在线文档应用

    作者: 马锐拉 我们的日常工作场景几乎离不开"云文档".目前,人们对于文档的需求再不仅仅是简单的记录,而扩展到办公协同.信息组织.知识分享等.在国内众多在线文档中,wolai 因为 ...

  4. SSE图像算法优化系列三十二:Zhang\Guo图像细化算法的C语言以及SIMD指令优化

    二值图像的细化算法也有很多种,比较有名的比如Hilditch细化.Rosenfeld细化.基于索引表的细化.还有Opencv自带的THINNING_ZHANGSUEN.THINNING_GUOHALL ...

  5. Eoapi — 一个可拓展的开源 API 工具

    ​ 在社区中时常会出现"抱怨某商业产品越来越臃肿"的声音,API 工具也是如此.从最早期只做 API 调试的工具,到经过多年的演进后集成全面功能的"庞然大物", ...

  6. android系统常见问题类型

    android系统中常见的异常问题,包括上层应用.框架.内核.驱动等,一般来说有如下一些异常问题类型: ANR,Answer No Response,应用无响应. FC,Force Close,强制退 ...

  7. 3D编程模式:依赖隔离模式

    大家好~本文提出了"依赖隔离"模式 系列文章详见: 3D编程模式:开篇 本文相关代码在这里: 相关代码 目录 编辑器需要替换引擎 设计意图 定义 应用 扩展 最佳实践 更多资料推荐 ...

  8. 【C++ 字符串题目】 输入三个人名,按字母顺序排序输出

    题目来源:https://acm.ujn.edu.cn Problem A: [C++ 字符串] 输入三个人名,按字母顺序排序输出 Time Limit: 1 Sec  Memory Limit: 1 ...

  9. Typora配置阿里云图床

    一.Typora安装PicGo 更新typora到最新版,打开文件-->偏好设置-->图像-->上传服务选择PicGo-Core-->下载或更新   二.注册并配置阿里云 1. ...

  10. alertmanager集群莫名发送resolve消息的问题探究

    alertmanager集群莫名发送resolve消息的问题探究 术语 告警消息:指一条告警 告警恢复消息:指一条告警恢复 告警信息:指告警相关的内容,包括告警消息和告警恢复消息 问题描述 最近遇到了 ...