.NET性能优化-使用内存+磁盘混合缓存

内存,性能,磁盘,NET · 浏览次数 : 2984

小编点评

**FasterKvCache配置** ```services.AddFasterKvCache<string>(options =>{ options.IndexCount = 1024; options.MemorySizeBit = 20; options.PageSizeBit = 20; options.ReadCacheMemorySizeBit = 20; options.ReadCachePageSizeBit = 20; // use MessagePack serializer options.UseMessagePackSerializer(); }, "MyKvCache"); ``` **性能测试** * 100%读 * 100%写 * 50%读写随机比较 **结果** * 1600w/s TPS **其他** * 可以使用 SQLite 进行内存存储。 * 缓存类型线程数为 `Mean(us)Error(us)StdDev(us)Gen0Gen1AllocatedfasterKvCacheRead859.953.8542.5491.52597.02NULLfasterKvCacheWrite863.671.0320.6830.79353.63NULLfasterKvCacheRandom464.421.3920.9211.7098.38NULLfasterKvCacheRead464.670.6280.3742.563511.77NULLfasterKvCacheRandom864.803.6392.1661.09865.33NULLfasterKvCacheWrite1211.261.5571.033.41816.13NULLfasterKvWrite1378.6017.75511.74442.4805-195.8 KB **总结** FasterKvCache 是一个性能不俗的缓存。它可以使用多种技术进行内存存储,并具有很高的性能。

正文

我们回顾一下上一篇文章中的内容,有一个朋友问我这样一个问题:

我的业务依赖一些数据,因为数据库访问慢,我把它放在Redis里面,不过还是太慢了,有什么其它的方案吗?

其实这个问题比较简单的是吧?Redis其实属于网络存储,我对照下面的这个表格,可以很容易的得出结论,既然网络存储的速度慢,那我们就可以使用内存RAM存储,把放Redis里面的数据给放内存里面就好了。

操作 速度
执行指令 1/1,000,000,000 秒 = 1 纳秒
从一级缓存读取数据 0.5 纳秒
分支预测失败 5 纳秒
从二级缓存读取数据 7 纳秒
使用Mutex加锁和解锁 25 纳秒
从主存(RAM内存)中读取数据 100 纳秒
在1Gbps速率的网络上发送2Kbyte的数据 20,000 纳秒
从内存中读取1MB的数据 250,000 纳秒
磁头移动到新的位置(代指机械硬盘) 8,000,000 纳秒
从磁盘中读取1MB的数据 20,000,000 纳秒
发送一个数据包从美国到欧洲然后回来 150 毫秒 = 150,000,000 纳秒

提出这个方案以后,接下来就遇到了另外一个问题:

但是数据比我应用的内存大,这怎么办呢?

在上篇文章中,我们提到了使用FASTER作为内存+磁盘混合缓存的方案,但是由于FASTER的API比较难使用,另外在纯内存场景中表现不如ConcurrentDictionary,所以最后得出的结论也是仅供参考。

经过一段时间的研究,笔者实现了一个基于微软FasterKv封装的进程内混合缓存库(内存+磁盘),它有着更加易用的API,接下来就和大家讨论讨论它。

FasterKvCache架构

这里需要简单的说一说FasterKvCache的架构,它核心使用的FasterKv,所以架构实际上和FasterKv一致,其原理比较复杂,所以笔者简化了原理图,大概就如下所示:

FasterKv的热数据会在内存中,而全量的数据会持久化在磁盘中。这中间有一些缓存淘汰算法,所以大家看到这张图就能明白FasterKvCache适用和不适用哪些场景了。

如何使用它

笔者之前给EasyCaching提交了FasterKv的实现,但是由于有一些EasyCaching的高级功能在FasterKv上目前无法高性能的实现,所以单独创建了这个库,提供高性能和最基本的API实现;如果大家已经使用了EasyCaching,那么可以直接使用EasyCaching.FasterKv这个NuGet包。

如果使用需要FasterKvCache的话,只需要安装Nuget包,Nuget包不同的功能如下所示,其中序列化包可以只安装自己需要的即可。

软件包名 版本 备注
FasterKv.Cache.Core 1.0.0-rc1 缓存核心包,包含FasterKvCache主要的API
FasterKv.Cache.MessagePack 1.0.0-rc1 基于MessagePack的磁盘序列化包,它具有着非常好的性能,但是需要注意它稍微有一点使用门槛,大家可以看它的文档。
FasterKv.Cache.SystemTextJson 1.0.0-rc1 基于System.Text.Json的磁盘序列化包,它是.NET平台上性能最好JSON序列化封装,但是比MessagePack差。不过它易用性非常好,无需对缓存实体进行单独配置。

使用

直接使用

我们可以直接通过new FasterKvCache(...)的方式使用它,目前它只支持基本的三种操作GetSetDelete。为了方便使用和性能的考虑,我们将FasterKvCache分为两种API风格,一种是通用对象风格,一种是泛型风格。

  • 通用对象:直接使用new FasterKvCache(...)创建,可以存放任意类型的Value。它底层使用object类型存储,所以内存缓冲内访问值类型对象会有装箱和拆箱的开销。
  • 泛型:需要使用new FasterKvCache<T>(...)创建,只能存放T类型的Value。它底层使用T类型存储,所以内存缓冲内不会有任何开销。

当然如果内存缓冲不够,对应的Value被淘汰到磁盘上,那么同样都会有读写磁盘、序列化和反序列化开销。

通用对象版本

代码如下所示,同一个cache实例可以添加任意类型:

using FasterKv.Cache.Core;
using FasterKv.Cache.Core.Configurations;
using FasterKv.Cache.MessagePack;

// create a FasterKvCache
var cache = new FasterKv.Cache.Core.FasterKvCache("MyCache",
    new DefaultSystemClock(),
    new FasterKvCacheOptions(),
    new IFasterKvCacheSerializer[]
    {
        new MessagePackFasterKvCacheSerializer
        {
            Name = "MyCache"
        }
    },
    null);

var key = Guid.NewGuid().ToString("N");

// sync 
// set key and value with expiry time
cache.Set(key, "my cache sync", TimeSpan.FromMinutes(5));

// get
var result = cache.Get<string>(key);
Console.WriteLine(result);

// delete
cache.Delete(key);

// async
// set
await cache.SetAsync(key, "my cache async");

// get
result = await cache.GetAsync<string>(key);
Console.WriteLine(result);

// delete
await cache.DeleteAsync(key);

// set other type object
cache.Set(key, new DateTime(2022,2,22));
Console.WriteLine(cache.Get<DateTime>(key));

输出结果如下所示:

my cache sync
my cache async
2022/2/22 0:00:00
泛型版本

泛型版本的话性能最好,但是它只允许添加一个类型,否则代码将编译不通过:

// create a FasterKvCache<T> 
// only set T type value
var cache = new FasterKvCache<string>("MyTCache",
    new DefaultSystemClock(),
    new FasterKvCacheOptions(),
    new IFasterKvCacheSerializer[]
    {
        new MessagePackFasterKvCacheSerializer
        {
            Name = "MyTCache"
        }
    },
    null);

Microsoft.Extensions.DependencyInjection

当然,我们也可以直接使用依赖注入的方式使用它,用起来也非常简单。按照通用和泛型版本的区别,我们使用不同的扩展方法即可:

var services = new ServiceCollection();
// use AddFasterKvCache
services.AddFasterKvCache(options =>
{
    // use MessagePack serializer
    options.UseMessagePackSerializer();
}, "MyKvCache");

var provider = services.BuildServiceProvider();

// get instance do something
var cache = provider.GetService<FasterKvCache>();

泛型版本需要调用相应的AddFasterKvCache<T>方法:

var services = new ServiceCollection();
// use AddFasterKvCache<string>
services.AddFasterKvCache<string>(options =>
{
    // use MessagePack serializer
    options.UseMessagePackSerializer();
}, "MyKvCache");

var provider = services.BuildServiceProvider();

// get instance do something
var cache = provider.GetService<FasterKvCache<string>>();

配置

FasterKvCache构造函数

public FasterKvCache(
    string name,	// 如果存在多个Cache实例,定义一个名称可以隔离序列化等配置和磁盘文件
    ISystemClock systemClock,	// 当前系统时钟,new DefaultSystemClock()即可
    FasterKvCacheOptions? options,	// FasterKvCache的详细配置,详情见下文
    IEnumerable<IFasterKvCacheSerializer>? serializers,	// 序列化器,可以直接使用MessagePack或SystemTextJson序列化器
    ILoggerFactory? loggerFactory)	// 日志工厂 用于记录FasterKv内部的一些日志信息

FasterKvCacheOptions 配置项

对于FasterKvCache,有着和FasterKv差不多的配置项,更详细的信息大家可以看FasterKv-Settings,下方是FasterKvCache的配置:

  • IndexCount:FasterKv会维护一个hash索引池,IndexCount就是这个索引池的hash槽数量,一个槽为64bit。需要配置为2的次方。如1024(2的10次方)、 2048(2的11次方)、65536(2的16次方) 、131072(2的17次方)。默认槽数量为131072,占用1024kb的内存。
  • MemorySizeBit: FasterKv用来保存Log的内存字节数,配置为2的次方数。默认为24,也就是2的24次方,使用16MB内存。
  • PageSizeBit:FasterKv内存页的大小,配置为2的次方数。默认为20,也就是2的20次方,每页大小为1MB内存。
  • ReadCacheMemorySizeBit:FasterKv读缓存内存字节数,配置为2的次方数,缓存内的都是热点数据,最好设置为热点数据所占用的内存数量。默认为20,也就是2的20次方,使用16MB内存。
  • ReadCachePageSizeBit:FasterKv读缓存内存页的大小,配置为2的次方数。默认为20,也就是2的20次方,每页大小为1MB内存。
  • LogPath:FasterKv日志文件的目录,默认会创建两个日志文件,一个以.log结尾,一个以obj.log结尾,分别存放日志信息和Value序列化信息,注意,不要让不同的FasterKvCache使用相同的日志文件,会出现不可预料异常默认为{当前目录}/FasterKvCache/{进程Id}-HLog/{实例名称}.log
  • SerializerName:Value序列化器名称,需要安装序列化Nuget包,如果没有单独指定Name的情况下,可以使用MessagePackSystemTextJson默认无需指定
  • ExpiryKeyScanInterval:由于FasterKv不支持过期删除功能,所以目前的实现是会定期扫描所有的key,将过期的key删除。这里配置的就是扫描间隔。默认为5分钟
  • CustomStore:如果您不想使用自动生成的实例,那么可以自定义的FasterKv实例。默认为null

所以FasterKvCache所占用的内存数量基本就是(IndexCount*64)+(MemorySize)+ReadCacheMemorySize,当然如果Key的数量过多,那么还有加上OverflowBucketCount * 64

容量规划

从上面提到的内容大家可以知道,FasterKvCache所占用的内存字节基本就是(IndexCount * 64)+(MemorySize) + ReadCacheMemorySize + (OverflowBucketCount * 64)。磁盘的话就是保存了所有的数据+对象序列化的数据,由于不同的序列化协议有不同的大小,大家可以先进行测试。

内存数据存储到FasterKv存储引擎,每个key都会额外元数据信息,存储空间占用会有一定的放大,建议在磁盘空间选择上,留有适当余量,按实际存储需求的 1.2 - 1.5倍预估。

如果使用内存存储 100GB 的数据,总的访问QPS不到2W,其中80%的数据都很少访问到。那么可以使用 【32GB内存 + 128GB磁盘】 存储,节省了近 70GB 的内存存储,内存成本可以下降50%+。

性能

目前作者还没有时间将FasterKvCache和其它主流的缓存库进行比对,现在只对FasterKvCache、EasyCaching.FasterKv和EasyCaching.Sqlite做的比较。下面是FasterKVCache的配置,总占用约为2MB。

services.AddFasterKvCache<string>(options =>
{
    options.IndexCount = 1024;
    options.MemorySizeBit = 20;
    options.PageSizeBit = 20;
    options.ReadCacheMemorySizeBit = 20;
    options.ReadCachePageSizeBit = 20;
    // use MessagePack serializer
    options.UseMessagePackSerializer();
}, "MyKvCache");

由于作者笔记本性能不够,使用Sqlite无法在短期内完成100W、1W个Key的性能测试,所以我们在默认设置下将数据集大小设置为1000个Key,设置50%的热点Key。进行100%读、100%写和50%读写随机比较。

可以看到无论是读、写还是混合操作FasterKvCache都有着不俗的性能,在8个线程情况下,TPS达到了惊人的1600w/s

缓存 类型 线程数 Mean(us) Error(us) StdDev(us) Gen0 Gen1 Allocated
fasterKvCache Read 8 59.95 3.854 2.549 1.5259 7.02 NULL
fasterKvCache Write 8 63.67 1.032 0.683 0.7935 3.63 NULL
fasterKvCache Random 4 64.42 1.392 0.921 1.709 8.38 NULL
fasterKvCache Read 4 64.67 0.628 0.374 2.5635 11.77 NULL
fasterKvCache Random 8 64.80 3.639 2.166 1.0986 5.33 NULL
fasterKvCache Write 4 65.57 3.45 2.053 0.9766 4.93 NULL
fasterKv Read 8 92.15 10.678 7.063 5.7373 - 26.42 KB
fasterKv Write 4 99.49 2 1.046 10.7422 - 49.84 KB
fasterKv Write 8 108.50 5.228 3.111 5.6152 - 25.93 KB
fasterKv Read 4 109.37 1.476 0.772 10.9863 - 50.82 KB
fasterKv Random 8 119.94 14.175 9.376 5.7373 - 26.18 KB
fasterKv Random 4 124.31 6.191 4.095 10.7422 - 50.34 KB
fasterKvCache Read 1 207.77 3.307 1.73 9.2773 43.48 NULL
fasterKvCache Random 1 208.71 1.832 0.958 6.3477 29.8 NULL
fasterKvCache Write 1 211.26 1.557 1.03 3.418 16.13 NULL
fasterKv Write 1 378.60 17.755 11.744 42.4805 - 195.8 KB
fasterKv Read 1 404.57 17.477 11.56 43.457 - 199.7 KB
fasterKv Random 1 441.22 14.107 9.331 42.9688 - 197.75 KB
sqlite Read 8 7450.11 260.279 172.158 54.6875 7.8125 357.78 KB
sqlite Read 4 14309.94 289.113 172.047 109.375 15.625 718.9 KB
sqlite Read 1 56973.53 1,774.35 1,173.62 400 100 2872.18 KB
sqlite Random 8 475535.01 214,015.71 141,558.14 - - 395.15 KB
sqlite Random 4 1023524.87 97,993.19 64,816.43 - - 762.46 KB
sqlite Write 8 1153950.84 48,271.47 28,725.58 - - 433.7 KB
sqlite Write 4 2250382.93 110,262.72 72,931.96 - - 867.7 KB
sqlite Write 1 4200783.08 43,941.69 29,064.71 - - 3462.89 KB
sqlite Random 1 5383716.10 195,085.96 129,037.28 - - 2692.09 KB

总结

可以看到FasterKvCache有着不俗的性能,目前也在笔者朋友的项目使用上了,反馈不错,解决了他的缓存问题。由于现在还只是1.0.0-rc1版本,还有很多特性没有实现。可能有一些BUG还存在,欢迎大家试用和反馈问题。

Github开源地址:
https://github.com/InCerryGit/FasterKvCache

参考链接

https://developer.aliyun.com/article/740811

与.NET性能优化-使用内存+磁盘混合缓存相似的内容:

.NET性能优化-使用内存+磁盘混合缓存

我们回顾一下上一篇文章中的内容,有一个朋友问我这样一个问题: > 我的业务依赖一些数据,因为数据库访问慢,我把它放在Redis里面,不过还是太慢了,有什么其它的方案吗? 其实这个问题比较简单的是吧?Redis其实属于网络存储,我对照下面的这个表格,可以很容易的得出结论,既然网络存储的速度慢,那我们就

.NET性能优化-ArrayPool同时复用数组和对象

前两天在微信后台收到了读者的私信,问了一个这样的问题,由于私信回复有字数和篇幅限制,我在这里统一回复一下。读者的问题是这样的: 大佬您好,之前读了您的文章受益匪浅,我们有一个项目经常占用 7-8GB 的内存,使用了您推荐的ArrayPool以后降低到 4GB 左右,我还想着能不能继续优化,于是 du

[转帖]腾讯服务注册中心演进及性能优化实践

https://my.oschina.net/u/4587289/blog/5577840 导语 注册中心作为微服务架构的核心,承担服务调用过程中的服务注册与寻址的职责。注册中心的演进是随着业务架构和需求的发展而进行演进的。腾讯当前内部服务数超百万级,日调用量超过万亿次,使用着统一的注册中心 ——

[转帖]【mmap】深度分析mmap:是什么 为什么 怎么用 性能总结

`https://blog.csdn.net/bandaoyu/article/details/106750990` 目录 有什么用? 1、文件映射 2、分配内存(匿名文件映射) mmap基础概念 mmap内存映射原理 mmap和常规文件操作的区别 mmap优点总结 mmap相关函数 mmap使用细

.NET性能优化-使用RecyclableMemoryStream替代MemoryStream

提到MemoryStream大家可能都不陌生,在编写代码中或多或少有使用过;比如Json序列化反序列化、导出PDF/Excel/Word、进行图片或者文字处理等场景。但是如果使用它高频、大数据量处理这些数据,就存在一些性能陷阱。 今天给大家带来的这个优化技巧其实就是池化MemoryStream的版本

.NET性能优化-复用StringBuilder

在之前的文章中,我们介绍了dotnet在字符串拼接时可以使用的一些性能优化技巧。比如: 为StringBuilder设置Buffer初始大小 使用ValueStringBuilder等等 不过这些都多多少少有一些局限性,比如StringBuilder还是会存在new StringBuilder()这

使用.NET7和C#11打造最快的序列化程序-以MemoryPack为例

## 译者注 本文是一篇不可多得的好文,MemoryPack 的作者 neuecc 大佬通过本文解释了他是如何将序列化程序性能提升到极致的;其中从很多方面(可变长度、字符串、集合等)解释了一些性能优化的技巧,值得每一个开发人员学习,特别是框架的开发人员的学习,一定能让大家获益匪浅。 ## 简介 我发

.NET周报【12月第1期 2022-12-08】

国内文章 CAP 7.0 版本发布通告 - 支持延迟消息,性能炸了? https://www.cnblogs.com/savorboard/p/cap-7-0.html) 今天,我们很高兴宣布 CAP 发布 7.0 版本正式版,我们在这个版本中带来了大批新特性以及对性能的优化和改进。 使用.NET7

.NET性能优化-是时候换个序列化协议了

计算机单机性能一直受到摩尔定律的约束,随着移动互联网的兴趣,单机性能不足的瓶颈越来越明显,制约着整个行业的发展。不过我们虽然不能无止境的纵向扩容系统,但是我们可以分布式、横向的扩容系统,这听起来非常的美好,不过也带来了今天要说明的问题,分布式的节点越多,通信产生的成本就越大。 网络传输带宽变得越来越

.Net核心级的性能优化(GC篇)

1.前言 大部分人对于.Net性能优化,都停留在业务层面。或者简单的.Net框架配置层面。本篇来看下.Net核心部分GC垃圾回收配置:保留VM,大对象,独立GC,节省内存等.Net8里面有很多的各种GC配置,用以帮助你的程序进行最大程度性能提升和优化。 文章分为两部分,第一个是GC有哪些动作可以性能