使用.NET简单实现一个Redis的高性能克隆版
使用.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的高性能克隆版(一)
作者:InCerry
作者Github:https://github.com/incerrygit
出处:https://www.cnblogs.com/InCerry/p/Use-Dotnet-Make-A-Simple-High-Performance-Redis-2.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:本博客版权归「InCerry」所有。
分类: C#, .NET, 高性能, Redis
标签: .NET, Redis, 实现原理
编辑推荐: · ASP.NET Core 6框架揭秘实例演示[31]:路由高阶用法 · 使用前端技术实现静态图片局部流动效果 · 前端构建效率优化之路 · .NET性能优化-使用SourceGenerator-Logger记录日志 · 记一次 Linux server 偶发 CPU 飙升问题的跟进与解决
Copyright © 2022 InCerry Powered by .NET 6 on Kubernetes & Theme Silence v3.0.0
相关文章
- Redis: 单线程架构带来的优势(redis是单线程的)
- 分布式数据库实现Redis单实例分布式数据库管理(redis单实例)
- Redis实现高性能数据持久化(redis 做持久化)
- 腾讯云Redis以高性能存储数据(腾讯redis存储)
- 算法加速Redis性能提升(算法redis)
- 榜店,把Redis带到你身边(榜店redis)
- 探索Redis如何登陆rediscli(登陆redis-cli)
- 探索Redis备份的最佳实践(查看redis的备份方式)
- 的数据库打破常规新一代高性能数据库 Redis之外的选择(有比redis更高性能)
- 构建高性能Redis集群,优化推荐系统(推荐系统 redis集群)
- 京东云Redis使用更高性能的非凡之选(京东云redis)
- 一键部署Redis快速获取高性能服务器(一键部署redis)
- 在Web服务器上使用Redis进行高性能处理(web服务器 redis)
- TP5使用Redis实现更高性能的数据存储(tp5 连接redis)
- 慢Redis故投向SSDB牺牲一切只为更高性能(ssdb慢redis好多)
- 库如何看待Redis数据库的性能表现(如何评价redis数据)
- 集群搭建多站点Redis集群,实现高性能储存(多站点redis)
- Redis存储哪些数据(哪些数据放redis)
- 高性能Redis秒杀技术之测试(redis高并发秒杀测试)
- 面对面分布式Redis应聘者的面试(分布式redis面试)
- 使用Redis增强页游服务端性能(redis页游服务端)
- Redis集群实现负载均衡重分配(redis集群重分配)
- 采用Redis集群方案搭建高性能多线程系统(redis集群方案多线程)
- 解决Redis链接池满载的新方案(redis 链接池满了)
- Redis实现高性能的评论缓存方案(redis评论缓存方案)
- 空前绝后Redis集群深度整合JWT(redis集群jwt)
- Redis实现不同前缀的高性能缓存(redis设置不同前缀)
- Redis在数据持久化中扮演重要角色(redis 表名)
- Redis配置最佳实践实现高性能优化(redis配置性能优化)
- Redis编译失败报错挑战解决(redis编译一直报错)
- Redis从2009年起开花结果(redis诞生日期)