.Net 下高性能分表分庫組件-連接模式原理

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(歡迎技術支持提供您寶貴的意見)

個人郵箱:[email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章