网站权重数据包,西安定制网站建设,个人网站域名选择,wordpress超级排版器插件译者注该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单、高性能兼容Redis协议的数据库的经历。首先这个Redis是非常简单的实现#xff0c;但是他在优化这个简单Redis路程很有趣#xff0c;也能给我们在从事性能优化工作时带来一些启…译者注该原文是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 Spanbyte Span new Spanbyte(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(){ArrayPoolbyte.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%左右。基于可重用缓冲区https://gist.github.com/ayende/f6263d5ddd331a7f8263ef892b45f526基于字符串https://gist.github.com/ayende/bc52b3cbdb6d5ebd8fa00ac5d014a876我猜测是因为我们这个场景的分配模式非常适合GC所做的那种启发式处理。我们要么有长期对象(在缓存中)么有非常短期的对象。值得指出的是网络中命令的实际解析并不使用字符串。只有实际的键和值实际上被转换为字符串。其余部分使用原始字节数据。下面是对字符串版本的代码进行分析的结果:使用可重用缓冲区也如下所示:这里有一些有趣的事情值得注意。ExecCommand 的成本几乎是基于字符串版本尝试的两倍。深入挖掘我相信错误就在这里:var buffer ArrayPoolbyte.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 buffernewItem new Item(item.Key, val);key item.Key;
}
else
{var keyBuffer ArrayPoolbyte.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的高性能克隆版四、五