.NET效能優化-使用記憶體+磁碟混合快取

2022-11-22 12:02:30

我們回顧一下上一篇文章中的內容,有一個朋友問我這樣一個問題:

我的業務依賴一些資料,因為資料庫存取慢,我把它放在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