ShardingCore
ShardingCore 一款ef-core下高性能、輕量級針對分表分庫讀寫分離的解決方案,具有零依賴、零學習成本、零業務代碼入侵。
Github Source Code 助力dotnet 生態 Gitee Source Code
介紹
在分表分庫領域java有着很多的解決方案,尤其是客戶端解決方案(ShardingSphere),因為客戶端解決方案有着極高的性能,但是缺點也很明顯數據庫鏈接的消耗相對較高,使用語言的限制讓我們.Net望而卻步,但是哪怕是有着這些缺點其實也不足以掩蓋客戶端分表分庫帶來的便捷與高效。
目前本人所開發的ShardingCore 是.Net下基於efcore2+的所有版本的分表分庫很多都是借鑒了ShardingSphere,並且對其很多缺點進行了彌補。這邊可能有人就要說了,你為什么做個efcore的不做個ado.net的呢,說實話我這邊確實有一個ado.net版本的分表分庫,你可以理解為ShardingSphere的.Net復刻版本sharding-conector 最最最初版本的分表聚合已經實現底層原理和ShardingSphere一致使用的Antlr4的分詞。為什么不對這個版本進行推進轉而對efcore的sharding-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_STRICTLY后MaxQueryConnectionsLimit將變成並行數目. 該模式下ShardingCore和ShardingSphere的處理方式類似基本一致

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_STRICTLY后MaxQueryConnectionsLimit將是正則的最小化鏈接數限制

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);
第二次我們通過判斷sqlCount和maxQueryConnectionsLimit的大小來確定鏈接模式的選擇
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
您的支持是開源作者能堅持下去的最大動力
- Github ShardingCore
- Gitee ShardingCore
QQ群:771630778
個人QQ:326308290(歡迎技術支持提供您寶貴的意見)
個人郵箱:326308290@qq.com
