目录
背景
介绍
Redis协议
软件设计
本文是关于为Redis服务器创建异步客户端的系列文章中的第一篇,该客户端低分配,因此GC压力小,数据复制最少。这是使用技术完成的,这些技术使Kestrel成为TechEmpower 纯文本性能测试第13轮中记录的每秒原始请求数排名前十的Web服务器之一。
前段时间,我开始编写一个异步的.NET Core Redis客户端。当时没有一个Redis客户端支持.NET Core,我想写一篇关于如何为一个简单的协议实现一个客户端的文章。
不幸的是,VS2015 RC1和RC2的变化表明该平台将在一段时间内不稳定,虽然我有一个相当完整的实现,但我把它放在架子上,直到.NET和Visual Studio世界变得更加稳定。
随着即将发布的VS2019、.NET 3.0以及CLI、NetStandard和工具的稳定,我认为是时候重新审视这个项目了。最让我对.NET Core产生兴趣的一件事是性能提升了多少,尤其是在Kestrel Web服务器性能方面。
.NET Core团队,尤其是David Fowler,利用他们学到的知识改进Kestrel,并创建了一组库,允许以很少或没有内存分配和最少数据复制的方式处理数据流。这是通过反转现有的 Stream范式来完成的,这样数据缓冲区就不是将数据缓冲区推入和拉出流,而是由低级 API管理并推送到应用程序。这些使用高效的内存缓冲池和结构来实现性能,使Kestrel成为可用的最快的Web服务器之一。
话虽如此,似乎Kestrel现在使用System.IO.Pipelines NuGet包,并且它也在SignalR 中使用。作为Kestrel项目的一部分,创建了许多基于低级流水线的库,以实现低分配、高性能的网络IO,以取代基于流的IO。有一个基于Socket的IO的实现。这可以在Nuget.org 的Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets上找到。
几年前,我们查看了我们的网页响应时间的性能,发现它严重不足。对于每个请求,我们都会对常见请求的数据进行数据库请求,并对内容进行复杂且昂贵的清理和格式化。
我们开始了一个项目,使用缓存各种信息和视图模型来提高站点的性能。这种缓存需要分布式,以便我们网络场中的所有服务器都与最新数据保持一致和最新。在评估了几个选项后,我们决定使用Redis,因为它的速度、成本、广泛采用、好评,以及其数据结构和API的强大功能。
由此产生的性能提升超出了我们的预期,需要几秒甚至几十秒的页面在不到一秒的时间内返回,通常不到500毫秒,大大降低了我们SQL Server的CPU负载。通过添加后台事件处理和优化一些昂贵且大量使用的算法,已经实现了进一步的性能改进,但我怀疑我们能做的任何事情都不能产生我们使用Redis获得的改进。
我们当前的实现使用ServiceStack Redis Client V3。我们也有一个使用StackExchange.Redis客户端的实现,但我也遇到了一些问题。我不得不研究代码以解决许多问题,并且像任何程序员一样,决定我可以做得更好,或者至少是不同的。这主要是由于C#语言的改进,例如扩展方法。这允许我创建一个小客户端,它只是向Redis服务器发送和从Redis服务器接收内容。实际的命令是使用扩展方法实现的。这消除了服务堆栈实现中的巨大类和StackExchange实现中的一些代码重复,允许每个类具有更大的单一职责。
这两个库中有许多很棒的想法,例如StackExchange客户端中的ConnectionMultiplexer 允许共享单个套接字,而不必在每次需要访问Redis服务器时创建新的套接字连接。将在本系列文章的后面部分实现这一点。
此实现的目标是
客户端使用Redis序列化协议 (RESP) 与Redis服务器通信,详见Redis 协议规范。正如规范所述:
Redis客户端使用名为RESP的协议与Redis服务器通信。(Redis序列化协议)。虽然该协议是专为Redis设计的,但它也可用于其他客户端-服务器软件项目。
RESP是以下各项之间的折衷:
RESP可以序列化不同的数据类型,如整数、字符串、数组。还有一种特定的错误类型。请求以字符串数组的形式从客户端发送到Redis服务器,这些字符串表示要执行的命令的参数。Redis使用特定于命令的数据类型进行回复。
RESP是二进制安全的,不需要处理从一个进程传输到另一个进程的批量数据,因为它使用前缀长度来传输批量数据。
如果您需要澄清有关我正在做什么的任何事情,我将把它留给读者参考规范,而不是详细介绍协议。它小巧、简单且易于理解。当我解释使用它们的代码时,我会解释具体的协议细节。
基于管道的套接字传输的神奇之处在于它为一对管道公开了PipeReaders和PipeWriters。一个管道(OutputPipe)将数据从应用程序传输到传输程序,而另一个管道(InputPipe)将数据从传输程序传输到应用程序。
Connection公开了一个IDuplexPipe Application,它有一个Input PipeReader和一个Output PipeWriter。Input设置为InputPipe.Reader,而Output设置为OutputPipe.Writer。该连接有两个任务,一个从Socket读取数据并将其写入InputPipe,第二个任务从OutputPipe读取数据并将其写入Socket。
管道使用一组内存块来提供和重用缓冲区来存储数据。这与Streams范式不同,在这种范式中,用户负责分配和管理用于读取和写入Stream的数据缓冲区。结果是管道传输需要很少或不需要缓冲区分配和垃圾收集来从Socket读取和写入。事实上,在大多数情况下,很少需要将数据从一个缓冲区复制到另一个缓冲区,直到需要进行这种复制才能从接收到的数据中反序列化某个对象。
这意味着我们的Redis协议处理程序需要做两件事:
因此,从两个管道创建测试传输层很简单。被测代码连接到应用程序端,测试读取和写入传输端,允许测试预期的功能,而实际上不需要为单元测试设置Redis实例。
当然,在某些时候需要与真实的Redis服务器进行实际通信以验证单元测试的假设。为此,我将使用Redis Docker容器。
https://www.codeproject.com/Articles/5274503/Creating-a-Redis-Client-using-the-NET-System-IO-Pi