.Net 下高效能分表分庫元件-連線模式原理

薛家明發表於2021-12-08

ShardingCore

ShardingCore 一款ef-core下高效能、輕量級針對分表分庫讀寫分離的解決方案,具有零依賴、零學習成本、零業務程式碼入侵。



介紹

在分表分庫領域java有著很多的解決方案,尤其是客戶端解決方案(ShardingSphere),因為客戶端解決方案有著極高的效能,但是缺點也很明顯資料庫連結的消耗相對較高,使用語言的限制讓我們.Net望而卻步,但是哪怕是有著這些缺點其實也不足以掩蓋客戶端分表分庫帶來的便捷與高效。
目前本人所開發的ShardingCore 是.Net下基於efcore2+的所有版本的分表分庫很多都是借鑑了ShardingSphere,並且對其很多缺點進行了彌補。這邊可能有人就要說了,你為什麼做個efcore的不做個ado.net的呢,說實話我這邊確實有一個ado.net版本的分表分庫,你可以理解為ShardingSphere的.Net復刻版本sharding-conector 最最最初版本的分表聚合已經實現底層原理和ShardingSphere一致使用的Antlr4的分詞。為什麼不對這個版本進行推進轉而對efcoresharding-core版本進行升級維護呢,這邊主要有兩點,第一點如果我是在ado.net上進行的推進那麼勢必可以支援更多的orm框架,但是orm框架下的很多特性將可能無法使用,並且需要維護各個資料庫版本之間的差異。比如efcore下的批量操作等一些列優化語法是很難被支援的。第二點針對某個orm的擴充套件效能和使用體驗上遠遠可以大於通用性元件。這就是我為什麼針對ShardingCore進行推進、優化和升級的原因。

效能

其實效能一直是大家關注的一個點,我用了ShardingCore那麼針對特定的查詢他的損耗是多少是一個比較令人關注的話題。接下來我放出之前做的兩次效能比較,當然這兩次比較並不是特意準備的,是我邊開發邊跑的一個是sqlserver 一個是mysql

效能測試

以下所有資料均在開啟了表示式編譯快取的情況下測試,並且電腦處於長時間未關機並且開著很多vs和idea的情況下僅供參考,所有測試都是基於ShardingCore x.3.1.63+ version

以下所有資料均在原始碼中有案例

efcore版本均為6.0 表結構為string型id的訂單取模分成5張表

N代表執行次數

sql server 2012,data rows 7734363 =773w

// * Summary *

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.18363.1500 (1909/November2019Update/19H2)
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET SDK=6.0.100
[Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT

Method N Mean Error StdDev Median
NoShardingIndexFirstOrDefaultAsync 10 2.154 ms 0.1532 ms 0.4443 ms 1.978 ms
ShardingIndexFirstOrDefaultAsync 10 4.293 ms 0.1521 ms 0.4485 ms 4.077 ms
NoShardingNoIndexFirstOrDefaultAsync 10 823.382 ms 16.0849 ms 18.5233 ms 821.221 ms
ShardingNoIndexFirstOrDefaultAsync 10 892.276 ms 17.8131 ms 16.6623 ms 894.880 ms
NoShardingNoIndexCountAsync 10 830.754 ms 16.5309 ms 38.6405 ms 821.736 ms
ShardingNoIndexCountAsync 10 915.630 ms 8.8511 ms 7.3911 ms 914.107 ms
NoShardingNoIndexLikeToListAsync 10 7,008.918 ms 139.4664 ms 166.0248 ms 6,955.674 ms
ShardingNoIndexLikeToListAsync 10 7,044.168 ms 135.3814 ms 132.9626 ms 7,008.057 ms
NoShardingNoIndexToListAsync 10 787.129 ms 10.5812 ms 8.8357 ms 785.798 ms
ShardingNoIndexToListAsync 10 935.880 ms 16.3354 ms 15.2801 ms 940.369 ms

mysql 5.7,data rows 7553790=755w innerdb_buffer_size=3G

// * Summary *

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.18363.1500 (1909/November2019Update/19H2)
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET SDK=6.0.100
[Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT

Method N Mean Error StdDev Median
NoShardingIndexFirstOrDefaultAsync 10 5.020 ms 0.1245 ms 0.3672 ms 4.855 ms
ShardingIndexFirstOrDefaultAsync 10 7.960 ms 0.1585 ms 0.2514 ms 7.974 ms
NoShardingNoIndexFirstOrDefaultAsync 10 11,336.083 ms 623.8044 ms 1,829.5103 ms 11,185.590 ms
ShardingNoIndexFirstOrDefaultAsync 10 5,422.259 ms 77.5386 ms 72.5296 ms 5,390.019 ms
NoShardingNoIndexCountAsync 10 14,229.819 ms 82.8929 ms 77.5381 ms 14,219.773 ms
ShardingNoIndexCountAsync 10 3,085.268 ms 55.5942 ms 49.2828 ms 3,087.704 ms
NoShardingNoIndexLikeToListAsync 10 27,046.390 ms 71.2034 ms 59.4580 ms 27,052.316 ms
ShardingNoIndexLikeToListAsync 10 5,707.009 ms 106.8713 ms 99.9675 ms 5,672.453 ms
NoShardingNoIndexToListAsync 10 26,001.850 ms 89.2787 ms 69.7030 ms 25,998.407 ms
ShardingNoIndexToListAsync 10 5,490.659 ms 71.8199 ms 67.1804 ms 5,477.891 ms

具體可以通過first前兩次結果來計算得出結論單次查詢的的損耗為0.2-0.3毫秒之間,通過資料聚合和資料路由的損耗單次在0.3ms-0.4ms,其中建立dbcontext為0.1毫秒目前沒有好的優化方案,0.013毫秒左右是路由表示式解析和編譯,複雜表示式可能更加耗時,剩下的0.2毫秒為資料來源和表字尾的解析等操作包括例項的反射建立和資料的聚合,
sqlserver的各項資料在分表和未分表的情況下都幾乎差不多可以得出在770w資料集情況下資料庫還並未是資料瓶頸的關鍵,但是mysql可以看到在分表和未分表的情況下如果涉及到沒有索引的全表掃描那麼效能的差距將是分表後的表數目之多,測試中為5-6倍,也就是分表數目

如果你可以接受單次查詢的損耗在0.2ms-0.3ms的那相信這款框架將會是efcore下非常完美的一款分表分庫元件


連結模式

說了這麼多這邊需要針對ShardingCore在查詢下面涉及到N表查詢後帶來的連結消耗是一個不容小覷的客觀因素。所以這邊參考ShardingSphere進行了類似原理的實現。就是如果查詢涉及不同庫那麼直接併發,如果是同庫的將根據使用者配置的單次最大連結進行序列查詢,並且動態選擇使用流式聚合和記憶體聚合。

首先我們看下ShardingSphere的連結模式在限制連結數的情況下是如何進行處理的

針對不同的資料庫採用並行執行,針對同一個資料庫根據使用者配置的最大連線數進行分庫序列執行,並且因為需要控制連結數所以會將結果集儲存在記憶體中,最後通過合併返回給客戶端資料。
之後我們會講這個模式的缺點並且ShardingCore是如何進行優化的

你可能已經蒙了這麼多名稱完全沒有一個概念。接下來我將一一進行講解,首先我們來看下連結模式下有哪些引數

MaxQueryConnectionsLimit

最大併發連結數,就是表示單次查詢sharding-core允許使用的dbconnection,預設會加上1就是說如果你配置了MaxQueryConnectionsLimit=10那麼實際sharding-core會在同一次查詢中開啟11條連結最多,為什麼是11不是10因為sharding-core會預設開啟一個連結用來進行空dbconnection的使用。如果不設定本引數那麼預設是cpu執行緒數Environment.ProcessorCount

ConnectionMode

連結模式,可以由使用者自行指定,使用記憶體限制,和連線數限制或者系統自行選擇最優

連結模式,有三個可選項,分別是:

MEMORY_STRICTLY

記憶體限制模式最小化記憶體聚合 流式聚合 同時會有多個連結

MEMORY_STRICTLY的意思是最小化記憶體使用率,就是非一次性獲取所有資料然後採用流式聚合

CONNECTION_STRICTLY

連線數限制模式最小化併發連線數 記憶體聚合 連線數會有限制

CONNECTION_STRICTLY的意思是最小化連線併發數,就是單次查詢併發連線數為設定的連線數MaxQueryConnectionsLimit。因為有限制,所以無法一直掛起多個連線,資料的合併為記憶體聚合採用最小化記憶體方式進行優化,而不是無腦使用記憶體聚合

SYSTEM_AUTO

系統自動選擇記憶體還是流式聚合

系統自行選擇會根據使用者的配置採取最小化連線數,但是如果遇到分頁則會根據分頁策略採取記憶體限制,因為skip過大會導致記憶體爆炸

解釋

MEMORY_STRICTLY

MEMORY_STRICTLY記憶體嚴格模式,使用者使用本屬性後將會嚴格控制查詢的聚合方式,將會採用流式聚合的迭代器模式,而不是一次性全部去除相關資料在記憶體中排序獲取,通過使用者配置的MaxQueryConnectionsLimit連線數來進行限制,比如MaxQueryConnectionsLimit=2,並且本次查詢涉及到一個庫3張表,因為程式只允許單次查詢能併發2個連結,所以本次查詢會被分成2組每組兩個,其中第二組只有一個,在這種情況下第一次併發查詢2條語句因為採用記憶體嚴格所以不會將資料獲取到記憶體,第二次在進行一次查詢並將迭代器返回一共組合成3個迭代器後續通過流式聚合+優先順序佇列進行返回所要的資料,在這種情況下程式的記憶體是最少的但是消耗的連結也是最大的。當使用者手動選擇MEMORY_STRICTLYMaxQueryConnectionsLimit將變成並行數目. 該模式下ShardingCoreShardingSphere的處理方式類似基本一致

CONNECTION_STRICTLY

CONNECTION_STRICTLY連線數嚴格模式,使用者使用本屬性後將會嚴格控制查詢後的同一個資料庫下的同時查詢的連結數,不會因為使用流式記憶體而導致迭代器一致開著,因為一個迭代器查詢開著就意味著需要一個連結,如果查詢需要聚合3張表那麼就需要同時開著三個連結來迭代保證流式聚合。通過使用者配置的MaxQueryConnectionsLimit連線數來進行限制,比如MaxQueryConnectionsLimit=2,並且本次查詢涉及到一個庫3張表,因為程式只允許單次查詢能併發2個連結,所以本次查詢會被分成2組每組兩個,其中第二組只有一個,在這種情況下第一次併發查詢2條語句因為採用連線數嚴格所以不會一直持有連結,會將連結結果進行每組進行合併然後將連線放回,合併時還是採用的流式聚合,會首先將第一組的兩個連結進行查詢之後將需要的結果通過流式聚合取到記憶體,然後第二組會自行獨立查詢並且從第二次開始後會將上一次迭代的記憶體聚合資料進行和本次查詢的流式聚合分別一起聚合,保證在分頁情況下記憶體資料量最少。因為如果每組都是用獨立的記憶體聚合那麼你有n組就會有n*(skip+take)的數目,而ShardingSphere採用的是更加簡單的做法,就是將每組下面的各自節點都自行進行記憶體聚合,那麼如果在skip(10).take(10)的情況下sql會被改寫成各組的各個節點分別進行skip(0).take(20)的操作那麼2組執行器的第一組將會有40條資料第二組將會有20條資料一共會有60條資料遠遠操作了我們所需要的20條。所以在這個情況下ShardingCore第一組記憶體流式聚合會返回20條資料,第二組會將第一組的20條資料和第二組的進行流式聚合記憶體中還是隻有20條資料,雖然是連線數嚴格但是也做到了最小化記憶體單元。當使用者手動選擇CONNECTION_STRICTLYMaxQueryConnectionsLimit將是正則的最小化連結數限制

SYSTEM_AUTO

SYSTEM_AUTO系統自行選擇,這是一個非常幫的選擇,因為在這個選擇下系統會自動根據使用者配置的MaxQueryConnectionsLimit來自行控制是採用流式聚合還是記憶體聚合,並且因為我們採用的是同資料庫下面最小化記憶體相比其他的解決方案可以更加有效和高效能的來應對各種查詢。僅僅只需要配置一個最大連線數限制既可以適配好連線模式。

這邊極力推薦大家在不清楚應該用什麼模式的時候使用SYSTEM_AUTO並且手動配置MaxQueryConnectionsLimit來確定各個環境下的配置一直而不是採用預設的cpu執行緒數。

首先我們通過每個資料庫被路由到了多少張表進行計算期望使用者在配置了xx後應該的並行數來進行分組,sqlCount :表示這個資料庫被路由到的表數目,exceptCount :表示計算出來的應該的單次查詢並行數

//程式碼本質就是向上取整
    int exceptCount =
                Math.Max(
                    0 == sqlCount % maxQueryConnectionsLimit
                        ? sqlCount / maxQueryConnectionsLimit
                        : sqlCount / maxQueryConnectionsLimit + 1, 1);

第二次我們通過判斷sqlCountmaxQueryConnectionsLimit的大小來確定連結模式的選擇


        private ConnectionModeEnum CalcConnectionMode(int sqlCount)
        {
            switch (_shardingConfigOption.ConnectionMode)
            {
                case ConnectionModeEnum.MEMORY_STRICTLY:
                case ConnectionModeEnum.CONNECTION_STRICTLY: return _shardingConfigOption.ConnectionMode;
                default:
                {
                    return _shardingConfigOption.MaxQueryConnectionsLimit < sqlCount
                        ? ConnectionModeEnum.CONNECTION_STRICTLY
                        : ConnectionModeEnum.MEMORY_STRICTLY; ;
                }
            }
        }

比較

針對ShardingSphere的流程圖我們可以看到在獲取普通資料的時候是沒有什麼問題的,但是如果遇到分頁也就是

select * from order limit 10,10

這種情況下會被改寫成

select * from order limit 0,20

我們可以看到如果是ShardingSphere的流程模式那麼在各個節點處雖然已經將連線數控制好了但是對於每個節點而言都有著20條資料,這種情況下其實是一種非常危險的,因為一旦節點過多並且limit的跳過頁數過多每個節點儲存的資料將會非常恐怖。

所以針對這種情況ShardingCore將同庫下的各個節點組的查詢使用StreamMerge而不是MemoryMerge,並且會對各個節點間建立聯絡進行聚合保證在同一個資料庫下只會有20條資料被載入到記憶體中,大大降低了記憶體的使用,提高了記憶體使用率。

當然具體情況應該還需要再次進行優化並不是簡單的一次優化就搞定的比如當跳過的頁數過多之後其實在記憶體中的一部分資料也會再次進行迭代和新的迭代器比較,這個中間的效能差距可能需要不斷地嘗試才可以獲取一個比較可靠的值

總結

目前已經有很多小夥伴已經在使用SharidingCore了並且在使用的時候也是相對比較簡單的配置既可以“完美”目前她在使用的各種框架譬如:AbpVNext....基本上在繼承和使用方面可以說是目前efcore生態下最最最完美的了真正做到了三零的框架:零依賴,零學習成本,零業務程式碼入侵

最後放一張圖

是我這邊給ShardingSphere提的建議,也證實了我對該聚合模型的優化是可以有效解決在分頁下面聚合各資料庫節點下的記憶體使用情況

分表分庫元件求贊求star

您的支援是開源作者能堅持下去的最大動力


部落格

QQ群:771630778

個人QQ:326308290(歡迎技術支援提供您寶貴的意見)

個人郵箱:326308290@qq.com

相關文章