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

我的業務依賴一些數據,因為數據庫訪問慢,我把它放在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適用和不適用哪些場景了。

不同內存的服務器可以做虛擬化集羣麼_數據_02

如何使用它

筆者之前給EasyCaching提交了FasterKv的實現,但是由於有一些EasyCaching的高級功能在FasterKv上目前無法高性能的實現,所以單獨創建了這個庫,提供高性能和最基本的API實現;如果大家已經使用了EasyCaching,那麼可以直接使用EasyCaching.FasterKv這個NuGet包。

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

直接使用

我們可以直接通過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還存在,歡迎大家試用和反饋問題。