使用.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个核心上。

以下是更改后的结果:7b0b8e37bee1310f124a62b7a2636f3b.png请注意,我们现在跑分的数据是125w/s,比上一次几乎增长了25%。下面是这一次新代码的分析器结果:1e0138ed3113f1cafa4a8fca763ade76.png因此在本例中,花费了大量的时间来处理各种各样的字符串,等待GC(大约占30%)。集合的成本下降了很多。还有一些其它的开销出现在我眼前,看看这里:f0a94ef86919cb79e9d38f65e0d470a4.png对于“简单”属性查找来说,这个开销非常惊人。另外SubString函数的调用开销也很大,超过整个系统开销的6%。在研究系统其它部分时,看到了这个:9d2cf7df420523bc816f47ce54c0dac5.png这真的很有趣,因为我们花了很多的时间在等待队列中是否有新的元素,其实我们可以做更多的事情,而不是就在那干等着。

我还尝试了其它的线程数量,如果只运行一个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);
    }

}

公众号

之前一直有朋友让开通公众号,由于一直比较忙没有弄。现在终于抽空弄好了,译者公众号如下,欢迎大家关注。1b385c490eef061744f9fe6d79e022f7.png

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
.NET Redis队列是一种在.NET应用程序中使用Redis作为队列存储的技术。Redis是一个开源的内存数据结构存储系统,它支持多种数据结构,包括字符串、列表、集合、哈希等。而.NET是一种广泛使用的开发框架,可以用于构建各种类型的应用程序。 .NET Redis队列的主要功能是实现消息的发布和订阅。发布者可以将消息发布到Redis队列中,而订阅者可以从队列中获取并处理这些消息。这种发布-订阅模式使得应用程序可以很方便地进行异步消息传递,提高了系统的响应性和可伸缩性。 .NET Redis队列具有以下几个特点: 1. 高性能Redis作为内存存储系统,具有非常高的读写速度。它使用了类似于单线程的事件循环模型,可以处理大量并发请求。 2. 持久化:Redis支持将数据写入磁盘进行持久化,以防止系统故障导致数据丢失。这使得即使在应用程序重启后,之前发布的消息也能够被订阅者接收到。 3. 可靠性:Redis内置了一些容错机制,如主从复制和故障转移等。这些机制可以保证即使在某个Redis节点发生故障时,仍然能够提供连续的服务。 4. 多语言支持:Redis提供了多种编程语言的客户端库,包括.NET。这意味着使用.NET开发的应用程序可以方便地与Redis进行通信。 总之,.NET Redis队列是一种高性能、可靠且易于使用的消息传递机制。通过将Redis.NET应用程序结合使用,可以实现高效的异步消息传递,并提升应用程序的性能和可扩展性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值