.Net 下高性能分表分庫組件-連接模式原理
- 2021 年 12 月 8 日
- 筆記
- dotNETCore, efcore, sharding-core, shardingcore, 分庫, 分表
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
將變成並行數目.
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生態下最最最完美的了真正做到了三零
的框架:零依賴
,零學習成本
,零業務程式碼入侵
分表分庫組件求贊求star
您的支援是開源作者能堅持下去的最大動力
- Github ShardingCore
- Gitee ShardingCore
QQ群:771630778
個人QQ:326308290(歡迎技術支援提供您寶貴的意見)
個人郵箱:[email protected]