在SQL Server 2014里,微軟引入了終極事務處理(Extreme Transaction Processing),即大家熟知的Hekaton。我在網上圍觀了一些文檔,寫這篇文章,希望可以讓大家更好的理解Hekaton,它的局限性,還有它驚艷的全新內存數據庫技術。這篇文章會通過下面幾個方面來講解Hekaton:
- 概況
- 可擴展性(Scalability)
- 局限性(Limitations)
1.概況
讓我們從XTP的簡潔概況開始。像XTP這樣的內存數據庫技術首要目標非常明確:盡可能高效的使用我們現有的服務器硬件。我們來看當下的現代服務器系統硬件,你會發現下列問題/局限性:
- 傳統存儲(機械硬盤)非常緩慢,企業若准備SSD存儲非常昂貴。另一方面主要內存(RAM)卻非常便宜,只要花100美元就可以配置到64GB內存,這可是標准版的SQL Server的最大支持內存數。
- CPU速度很難再提升。現在我們困在了3-4GHZ,再快點不太可能(當你嘗試超頻時你就會有這個體會)。
- 傳統的關系型數據庫管理系統(RDBMS)不能線性擴展,主要是因為內部的鎖,阻塞和封鎖(Locking, Blocking, and Latching)機制(數據結構的內存里鎖,當它們被讀寫訪問時)。
因此為了克服這些限制,你需要這樣的技術:
- 使用RAM將數據全部存在內存里來克服傳統旋轉硬盤(機械硬盤)的速度限制。
- 盡可能划算的使用當下有速度限制的CPU,使用盡可能少的CPU指令數,來實現近可能快的去執行關系數據庫管理系統(RDBMS)的查詢。
- 當對你的關系數據庫管理系統(RDBMS)執行讀/寫操作時,完全避免鎖/阻塞,和閂鎖(Locking/Blocking, and Latching)。
這3點就是SQL Server 2014里終極事務處理(Extreme Transaction Processing )的3大支柱:
使用XTP你可以在內存里緩存整個表(即所謂的內存優化表(Memory Optimized Tables)),存儲過程可以編譯為本機C代碼,而且對於內存優化表,鎖/阻塞和閂鎖(Locking/Blocking, and Latching)這些機制都是完全沒有的,因為XTP是基於樂觀的多版本並發控制(Multi Version Concurrency Control ,MVCC)的。我們來詳細看下這3大支柱。
內存中的存儲(In-Memory Storage)
對於服務器系統,內存越來越便宜了。你只要花幾百美元就可以裝備你的服務器為64G內存,64GB內存可是SQL Server標准版本支持的最大內存。因此XTP表(即內存優化表)是完全存在內存里的。從SQL Server角度來看,內存表的所有數據存在於FILESTREAM文件組,在SQL Server啟動時,它們從文件組讀取,然后在起飛時重建你的所有索引。
這也給你的目標恢復時間(Recovery Time Objective,RTO)帶來巨大壓力,因為一啟動你所有索引都被重建完成后,你的數據庫才是在線狀態。你的FILESTREAM文件組所存儲的存儲系統速度,因此也會直接影響目標恢復時間。因此你可以在FILESTREAM文件組里放置多個容器,這樣的話你可以在啟動期間分散I/O到多個存儲系統,從而讓你的數據庫盡快進入在線狀態。
在CTP1里,XTP只支持所謂的Hash-Indexes,它是在內存里完全存儲在哈希表里。SQL Server目前能查找和掃描Hash-Indexes。從CTP2起,微軟會引入所謂的Range Indexes,這樣可以是你的范圍查詢非常,非常快。Range Indexes是基於所謂的Bw-Tree。
每個內存優化表也是編譯為本地C代碼。對每個表你都會得到一個DLL,這個是通過cl.exe編譯的(微軟C語言編譯器,SQL Server 2014 自帶)。生成的DLL然后載入sqlservr.exe 的進程空間,這個可以在sys.dm_os_loaded_modules里看到。編譯本身在獨立的線程里完成,這就是說你眼疾手快的話,你可以在任務管理器里看到cl.exe。下面代碼給你展示了描述你的表的典型C語言代碼:
1 struct hkt_277576027 2 { 3 struct HkSixteenByteData hkc_1; 4 __int64 hkc_5; 5 long hkc_2; 6 long hkc_3; 7 long hkc_4; 8 }; 9 struct hkis_27757602700002 10 { 11 struct HkSixteenByteData hkc_1; 12 }; 13 struct hkif_27757602700002 14 { 15 struct HkSixteenByteData hkc_1; 16 }; 17 __int64 CompareSKeyToRow_27757602700002( 18 struct HkSearchKey const* hkArg0, 19 struct HkRow const* hkArg1) 20 { 21 struct hkis_27757602700002* arg0 = ((struct hkis_27757602700002*)hkArg0); 22 struct hkt_277576027* arg1 = ((struct hkt_277576027*)hkArg1); 23 __int64 ret; 24 ret = (CompareKeys_guid((arg0->hkc_1), (arg1->hkc_1))); 25 return ret; 26 } 27 __int64 CompareRowToRow_27757602700002( 28 struct HkRow const* hkArg0, 29 struct HkRow const* hkArg1) 30 { 31 struct hkt_277576027* arg0 = ((struct hkt_277576027*)hkArg0); 32 struct hkt_277576027* arg1 = ((struct hkt_277576027*)hkArg1); 33 __int64 ret; 34 ret = (CompareKeys_guid((arg0->hkc_1), (arg1->hkc_1))); 35 return ret; 36 }
可以看到,代碼本身並不直觀,但你可以通過C的結構使用來看出你的表結構是如何被描述的。XTP的好處是內存優化表是完全自然集成到SQL Server關系引擎的其余部分。因此你可以使用傳統的T-SQL代碼查詢這些表,可以進行備份/還原,還有集成HA/DR技術——微軟在集成領域做了大量的偉大工作。除了內存中存儲外,內存優化表也完全鎖/阻塞,和閂鎖,因為XTP是基於樂觀的多版本並發控制(MVCC)原則。在無鎖/閂鎖數據結構部分我們會繼續討論這個。
你需要注意的最重要事實是:你應該將性能最重要的表移入內存,不是你所有的數據庫。在接下來可擴展性部分里,我們會談到哪些情況使用XTP是有意義的。通常你會把你數據庫的95%使用基於傳統磁盤的表存儲,剩下的5%可以用內存優化表存儲。
本地編譯(Native Compilation)
微軟申明當前硬件系統的第一個問題是:傳統的,旋轉的存儲太慢。對此將表數據在內存中存儲。第2個問題需要申明的是:當下處理器的時鍾頻率卡在了3-4GHz。我們不能再快了,因為會引入散熱問題。因此當前時鍾周期必須盡可能有效的管理。這是當下T-SQL實現的巨大問題,因為T-SQL只是一個解釋性的語言。
在查詢優化期間,SQL Server的查詢優化器生成所謂的查詢樹,在執行期間,查詢樹從頭運算符到所有的樹節點被解讀。這會引入大量的額外CPU指令,會在SQL Server里的每個執行計划里執行。另外每個運算符(即所謂的迭代器(Iterator))是以C++類實現的,這就意味在執行各個運算符時,會用到所謂的虛擬函數調用(Virtual Function Calls )。虛擬方法調用根據需要執行的CPU指令又是很占資源的。總之,在運行期間,執行計划被解讀時,生成大量的CPU指令,即意味着當前的CPU沒有高效的使用。你在浪費寶貴的CPU周期,可以用另外更好的方式來使用,從而提速你的整個工作。
因為查詢引擎內的這些原因和限制,SQL Server引入XTP所謂的 本機編譯的存儲過程(Natively Compiled Stored Procedures)。背后的思路很簡單:存儲過程的整體編譯為本地C語言代碼,結果又是生成DLL,然后載入sqlservr.exe 的進程空間。因此在執行期間不需要解讀,虛擬函數調用完全消除。這樣的話做同樣數量的工作卻需要很少的CPU指令,這就意味着你的工作輸出量會更高,因為在可用的CPU周期里可以做更多的工作。
在2013年的北美TechEd上指出,對於一些特定的存儲過程,需要的CPU指令可以從1000000下降到近4000。想象下這個性能提升:25倍的性能提升!當我們談到當前XPT里的局限時,你會發現,這個提升並不是免費的……下面的代碼展示的是一個簡單存儲過程的典型C語言代碼:
1 HRESULT hkp_309576141( 2 struct HkProcContext* context, 3 union HkValue valueArray[], 4 unsigned char* nullArray) 5 { 6 unsigned long yc = 0; 7 long var_2 = (-2147483647 - 1); 8 unsigned char var_isnull_2 = 1; 9 HRESULT hr = 0; 10 { 11 var_2 = 0; 12 var_isnull_2 = 0; 13 } 14 yc = (yc + 1); 15 { 16 while (1) 17 { 18 unsigned char result_7; 19 unsigned char result_isnull_7; 20 result_7 = 0; 21 result_isnull_7 = 0; 22 if ((! var_isnull_2)) 23 { 24 result_7 = (var_2 < 10000); 25 } 26 else 27 { 28 result_isnull_7 = 1; 29 } 30 if ((result_isnull_7 || (! result_7))) 31 { 32 goto l_5; 33 } 34 hr = (YieldCheck(context, yc, 18)); 35 if ((FAILED(hr))) 36 { 37 goto l_1; 38 } 39 yc = 0; 40 { 41 long expr_9; 42 long expr_10; 43 long expr_11; 44 __int64 expr_12; 45 struct hkt_277576027* rec2_17 = 0; 46 unsigned char freeRow_17 = 0; 47 short rowLength; 48 static wchar_t const hkl_18[] = 49 { 50 73, 51 78, 52 83, 53 69, 54 82, 55 84, 56 }; 57 static wchar_t const hkl_19[] = 58 { 59 91, 60 79, 61 114, 62 100, 63 101, 64 114, 65 115, 66 93, 67 }; 68 static wchar_t const hkl_20[] = 69 { 70 91, 71 79, 72 114, 73 100, 74 101, 75 114, 76 73, 77 68, 78 93, 79 }; 80 static wchar_t const hkl_21[] = 81 { 82 73, 83 78, 84 83, 85 69, 86 82, 87 84, 88 }; 89 static wchar_t const hkl_22[] = 90 { 91 91, 92 79, 93 114, 94 100, 95 101, 96 114, 97 115, 98 93, 99 }; 100 static wchar_t const hkl_23[] = 101 { 102 91, 103 67, 104 117, 105 115, 106 116, 107 111, 108 109, 109 101, 110 114, 111 73, 112 68, 113 93, 114 }; 115 static wchar_t const hkl_24[] = 116 { 117 73, 118 78, 119 83, 120 69, 121 82, 122 84, 123 }; 124 static wchar_t const hkl_25[] = 125 { 126 91, 127 79, 128 114, 129 100, 130 101, 131 114, 132 115, 133 93, 134 }; 135 static wchar_t const hkl_26[] = 136 { 137 91, 138 80, 139 114, 140 111, 141 100, 142 117, 143 99, 144 116, 145 73, 146 68, 147 93, 148 }; 149 static wchar_t const hkl_27[] = 150 { 151 73, 152 78, 153 83, 154 69, 155 82, 156 84, 157 }; 158 static wchar_t const hkl_28[] = 159 { 160 91, 161 79, 162 114, 163 100, 164 101, 165 114, 166 115, 167 93, 168 }; 169 static wchar_t const hkl_29[] = 170 { 171 91, 172 81, 173 117, 174 97, 175 110, 176 116, 177 105, 178 116, 179 121, 180 93, 181 }; 182 static wchar_t const hkl_30[] = 183 { 184 73, 185 78, 186 83, 187 69, 188 82, 189 84, 190 }; 191 static wchar_t const hkl_31[] = 192 { 193 91, 194 79, 195 114, 196 100, 197 101, 198 114, 199 115, 200 93, 201 }; 202 static wchar_t const hkl_32[] = 203 { 204 91, 205 80, 206 114, 207 105, 208 99, 209 101, 210 93, 211 }; 212 goto l_16; 213 l_16:; 214 expr_9 = 1; 215 expr_10 = 1; 216 expr_11 = 1; 217 expr_12 = 2045; 218 goto l_15; 219 l_15:; 220 rowLength = sizeof(struct hkt_277576027); 221 hr = (HkRowAlloc((context->Transaction), (Tables[0]), rowLength, ((struct HkRow**)(&rec2_17)))); 222 if ((FAILED(hr))) 223 { 224 goto l_8; 225 } 226 freeRow_17 = 1; 227 if ((! (nullArray[1]))) 228 { 229 (rec2_17->hkc_1) = ((valueArray[1]).SixteenByteData); 230 } 231 else 232 { 233 hr = -2113929186; 234 if ((FAILED(hr))) 235 { 236 { 237 CreateError((context->ErrorObject), hr, 5, 18, hkl_20, 16, hkl_19, hkl_18); 238 } 239 if ((FAILED(hr))) 240 { 241 goto l_8; 242 } 243 } 244 } 245 (rec2_17->hkc_2) = expr_9; 246 (rec2_17->hkc_3) = expr_10; 247 (rec2_17->hkc_4) = expr_11; 248 (rec2_17->hkc_5) = expr_12; 249 freeRow_17 = 0; 250 hr = (HkTableInsert((Tables[0]), (context->Transaction), ((struct HkRow*)rec2_17))); 251 if ((FAILED(hr))) 252 { 253 goto l_8; 254 } 255 goto l_13; 256 l_13:; 257 goto l_14; 258 l_14:; 259 hr = (HkRefreshStatementId((context->Transaction))); 260 if ((FAILED(hr))) 261 { 262 goto l_8; 263 } 264 l_8:; 265 if ((FAILED(hr))) 266 { 267 if (freeRow_17) 268 { 269 HkTableReleaseUnusedRow(((struct HkRow*)rec2_17), (Tables[0]), (context->Transaction)); 270 } 271 SetLineNumberForError((context->ErrorObject), 18); 272 goto l_1; 273 } 274 } 275 yc = (yc + 1); 276 { 277 __int64 temp_34; 278 if ((! var_isnull_2)) 279 { 280 temp_34 = (((__int64)var_2) + ((__int64)1)); 281 if ((temp_34 < (-2147483647 - 1))) 282 { 283 hr = -2113929211; 284 { 285 hr = (CreateError((context->ErrorObject), hr, 2, 23, 0)); 286 } 287 if ((FAILED(hr))) 288 { 289 goto l_33; 290 } 291 } 292 if ((temp_34 > 2147483647)) 293 { 294 hr = -2113929212; 295 { 296 hr = (CreateError((context->ErrorObject), hr, 2, 23, 0)); 297 } 298 if ((FAILED(hr))) 299 { 300 goto l_33; 301 } 302 } 303 var_2 = ((long)temp_34); 304 var_isnull_2 = 0; 305 } 306 else 307 { 308 var_isnull_2 = 1; 309 } 310 l_33:; 311 if ((FAILED(hr))) 312 { 313 SetLineNumberForError((context->ErrorObject), 28); 314 goto l_1; 315 } 316 } 317 yc = (yc + 1); 318 } 319 l_5:; 320 } 321 yc = (yc + 1); 322 ((valueArray[0]).SignedIntData) = 0; 323 l_1:; 324 return hr; 325 }
你可以看到有很多的GOTO語句,很容易讓人想到是面條式代碼(spaghetti code)。但這個不是我們討論的范圍……
無鎖/閂鎖數據結構(Lock/Latch Free Data Structures)
在我們剛才討論XTP里的內存存儲數據時,我已經說過對內存優化表,SQL Server是以無鎖/閂鎖數據結構實現的。這意味着當想要讀寫你的數據時,沒有鎖/閂鎖涉及到的等待。在傳統的像SQL Server這樣關系數據庫管理系統(RDBMS)里,寫操作需要排它鎖(Exclusive Locks (X) ),讀操作需要共享鎖(Shared Locks (S) )。2個鎖是互斥的。
這就是說讀阻塞寫,寫阻塞讀。共享鎖能把持多久是通過不同的事務隔離級別控制的。這個方式稱為悲觀並發控制(Pessimistic Concurrency)。隨着SQL Server 2005的發布,微軟引入了新的並發模式:樂觀並發控制(Optimistic Concurrency)。使用樂觀並發控制,讀操作不再需要共享鎖。直接從TempDb永駐的版本存儲里讀取。
使用新的隔離級別Read Committed Snapshot Isolation (RCSI) ,你回退到語句開始后不再有效的記錄版本;使用隔離級別Snapshot Isolation,你回退到事務開始后不再有效的記錄版本,這意味這在Snapshot Isolation里你可以重復的讀。
設置這些新的隔離級別會大大促進你的整個工作量。但還有問題必須解決:
寫還是需要排它鎖,意味這並行寫操作還是會相互阻塞。
當訪問內存中的數據時(數據頁,索引頁),這些結構必須被閂鎖,意味着它們只能被單線程訪問。那是傳統多線程並發問題,需要以此方式(閂鎖)來解決。
因此XTP引入了基於多版本並發控制(Multi Version Concurrency Control ,MVCC)原則。使用MVCC就沒有鎖(甚至沒有排它鎖)和閂鎖。寫不會相互阻塞,因為哈希索引不是建立在頁上的,數據訪問是無閂鎖的(內部是用哈希桶的哈希表來存儲的)。在內存里不再有阻塞。當你使用XTP時,意味着卓越的吞吐量保證。但你的內存里訪問沒有閂鎖時,你的性能瓶頸將轉移,主要移向事務日志,在下個討論XTP擴展性時我們會談到。
MVCC的一個副作用是所謂的寫-寫沖突(Writer-Writer conflict)。你可以在XTP里對同個記錄進行多個寫操作,它們不會阻塞。第一個寫會勝出,其他所有並發的寫事務會失敗。這意味着你需要修改你的代碼來捕獲這個特定錯誤,然后重試你的事務。這和死鎖處理是一樣的。如果在你程序里已經有死鎖的實現方式,應該很容易對你的終端用戶處理寫-寫沖突。
2.可擴展性(Scalability)
現在你應該大致理解了XTP的主要概念和背后的原因,但最大的問題是,在哪些情況下才可以使用XTP呢。我認為XTP不是一個隨處可以部署的技術。你需要一個特殊情景來使用XTP才有意義。請相信我:我們現在所面臨的大多數SQL Server問題,基本是索引問題,或者是硬件的錯誤配置問題(尤其是SANS領域)。
當你面對這樣的問題時,我絕不推薦升級到XTP。一定要先分析下根源,在第一步就從根源解決問題。XTP應該是你最后才考慮的解決方法,因為當你的部分數據庫使用XTP時,你的問題分析方法就會完全不一樣了。對於XTP,你會遇到大量的各種限制。XTP是賊快(我可以說是TMD的快),但不是個歷史奇跡(all-time wonder),不是每個地方都適用的。
微軟推出XTP主要是為了克服加鎖競爭(Latch Contention)問題。剛才你已經看到,當你訪問數據頁進行讀寫活動時,加鎖競爭在內存里總會發生。當你的工作量越來越大,到了某個時間點,你就引入了加鎖競爭,因為單線程訪問內存里的這些頁。這里最常見的例子就是最后頁插入加鎖競爭(Last Page Insert Latch Contention)。
這個問題很容易重現:按照最佳實踐創建一個聚集鍵,這個鍵使用自增長來避免在聚集索引里的硬頁分裂。你的工作量不會延伸——相信我!這里的問題是:在INSERT語句期間,在你的聚集索引里只有一個熱塊——最后頁。下圖展示了這個現象:
從圖中可以看到,SQL Server需要橫穿聚集索引的右手,從索引根頁下至葉子層來在聚集索引的機翼后緣插入新的記錄。因此在葉子層你有單線程訪問葉子頁,這就意味着單線程插入(Single-Thread INSERT)操作。這會大大傷及你的性能。下圖展示了使用INT IDENTITY列的表里的簡單INSERT語句(自增長,導致最后頁插入加鎖競爭!),我使用ostress.exe程序(來自微軟RML工具的一部分)模擬不同用戶在16核的機器上的不同性能表現。
從圖中可以看到,隨着用戶的增加,工作量在逐步下降,你的閂鎖等待增加——這就是使用自增長值的最后頁插入加鎖競爭(Last Page Insert Latch Contention)。當訪問在內存里的索引頁和數據頁時,這個競爭就會發生,因為閂鎖。
有幾個方式可以克服最后頁插入加鎖競爭:
- 使用隨機聚集鍵,例如UNIQUEIDENTIFIER 在整個聚集索引葉子層分布式插入
- 實行哈希分區(Implement Hash Partitioning)
當你使用UNIQUEIDENTIFIER 作為你的聚集鍵時,首先你就會覺得自己做錯了,但你的工作吞吐量卻大幅度上升了。哈希分區是你另一個可以部署的選項。哈希分區意味着你為每個CPU內核創建不同的分區,使用取模運算符在不同的分區間,你的配分函數(Partition Function)分發記錄。下圖展示了這個方法:
你通過在不同分區里的不同B-Tree結構分發INSERT語句,因此你就可以並行在表里執行INSERT語句。但這個也是有缺點的,你需要SQL Server的企業版,用了這個分區,你的表就不能重新分區,你就不能有效使用分區消除(Partition Elimination)。下圖是對應的性能提升展示:
從圖中可以看到,當用戶達到64個前都是平穩延伸的,在128個用戶后,再次發生最后頁插入加鎖競爭,吞吐量再次下降。
現在假設我們啟用內存優化表,性能會發生如何的改變?表的生產力會賊快——XTP真是的TMD的快!因為沒有鎖和閂鎖!我們可以看下SQL Server批量請求時資源使用情況:
在這個情況下,我可以執行200個用戶的本地編譯存儲過程里的INSERT語句,可以接收25500批量請求/秒——0工作等待,0數據I/O!所有的一切都在內存里發生。但是:這次測試都是在虛擬機里執行。虛擬機有8個內核,分配了20G的內存,虛擬機和相關的SQL Server文件都存儲在PCI-E上的SSD上。
我現在碰到的XTP的生產力瓶頸在哪里呢?這個不容易馬上知道答案。首先你要考慮的是還是你的聚集鍵。使用XTP並不支持IDENTITY列。因此微軟建議使用序列對象(Sequence Object)。序列是完美的,你可以使用緩存,但是XTP是TMD的太快了,你馬上就碰上了SQL Server里的序列生成器(Sequence Generator)的競爭。SQL Server在你主數據文件的第132頁保存序列值。第132也是系統表sysobjvalues的一部分。當你讀寫那個頁時,SQL Server又會閂鎖那個頁,你的閂鎖競爭又回來了,但只在你的數據酷的不同領域上。你不能避免這個頁的閂鎖競爭,因為系統表還是存儲在傳統磁盤的表上。因此從這個角度來說序列並不是XTP的最佳解決方法,如果你想無限延伸你的工作量。
因此讓我們再次回到老朋友UNIQUEIDENTIFIER 這里。當生成UNIQUEIDENTIFIER 不會有任何競爭,因為生成是通過算法的。不好的是:函數NEWID()在SQL Server 2014的CTP1里並不支持。但這個也沒關系,你可以在存儲過程里寫入,在存儲過程里生成UNIQUEIDENTIFIER ,通過變量值傳給本地編譯的存儲過程。問題解決,這樣的話我可以把工作量提升至25500 批量請求/秒。從我的測試可以看出,UNIQUEIDENTIFIER 比序列更好,因為沒有需要協調和閂鎖的共享資源。這個是我的最大收獲。
因此現在的問題就是什么限制了25500 批量請求/秒的工作量?2個主要東西:事務日志和CPU使用率!我們首先來看看事務日志。在XTP里,微軟對此做了大量的優化工作,例如沒有UNDO記錄。微軟嘗試使事務日志量最小化來優化事務日志的寫入。寫得少,做得快。為了克服事務日志限制,XTP提供給你2類不同的內存優化表:
- SCHEMA_AND_DATA
- SCHEMA_ONLY
SCHEMA_AND_DATA意味着表架構和數據都永駐,因此只要你的事務一提交,XTP就要把事務日志記錄寫入事務日志。在事務執行期間,XTP從不把事務日志記錄寫入事務日志,因為你隨時可能回滾事務。SCHEMA_ONLY表數據的修改不進行日志記錄,並且表中的數據不保留在磁盤上:當你重啟你的SQL Server,只有空表,嗯???你要慎重考慮使用這個選項,什么時候是可以用的,什么時候是不行的。微軟建議下列2個場景可以使用這個選項:
- ASP.NET會話狀態數據庫
- 提取轉換加載(ETL)場景
ASP.NET會話狀態數據庫可以使用SCHEMA_ONLY選項,因為你這里不保存關鍵數據。會話狀態是關於你網站用戶的信息。對於ETL場景也同樣可以使用SCHEMA_ONLY選項,因為即使失敗也很容易重建數據。使用SCHEMA_ONLY選項,我可以獲得25500 批量請求/秒的工作量。使用SCHEMA_AND_DATA就成為了主要瓶頸,只能獲得15000 批量請求/秒。我說過,我的測試環境的虛擬機是運行在PCI-E上的SSD上(事務日志,數據文件,虛擬機),換做實體的純金屬服務器會更好。
當你使用SCHEMA_AND_DATA部署你的內存優化表,你還有一個東西:極快的事務日志——和往常一樣!當你使用SCHEMA_AND_DATA,CPU會稱為你的瓶頸,因為沒別的了。從剛才的圖中你可以看到虛擬機里CPU基本運行在85%。當我把所有都部署在實體服務器上是,我覺得會加倍它的工作量。因為我只分配16核中的8核給虛擬機。在主機有50%的平均CPU占用,因此加倍工作量應該不是問題。那應該是在很低成本機器上卻有50000批量請求/秒的工作量……
3.局限性(Limitations)
到目前位置,XTP的一切都看起來很棒,它應該是SQL Server里特定問題的最佳解決方案。但是XTP也有很大的代價——一大堆的局限性,尤其是現在的CTP1。下面列出部分,更多可以查看微軟在線幫助:
- 差異備份不支持
- 行大小限制為8kb
- 內存優化表不能truncate
- NEWID()尚未實現
- 用SCHEMA_AND_DATA部署的內存優化表必須要有主鍵
- ALTER TABLE/ALTER PROCEDURE不支持
- 外鍵(Foreign-Keys)不支持
- LOB數據類型不支持
- 本地編譯的存儲過程不會重編譯。由於統計信息改變,不重編譯會導致很糟的性能
- 在內存優化的存儲過程里不能訪問存放在傳統硬盤(機械硬盤)里的表
- 整個表的定義只能在一個CREATE TABLE里描述(包括索引和約束)
- 你不能從別的數據庫往內存優化表里直接插入數據,你需要一個中間表。這在剛才提到的ETL場景里會是個大問題
- ……
除了這些局限性外,目前的CTP1版本里的XTP還是有BUG的,數據庫居然崩潰了,也不能進行還原……不要問我如何重現這個BUG。
結論
XTP在SQL Server里的確是快速發展起來的技術。你需要考慮的唯一事情就是:當你基於XTP部署解決方案時,為你的事務日志准備盡可能快的存儲系統吧,這個會大幅度降低系統瓶頸!希望這篇文章可以幫你很好的了解XTP,也希望你在閱讀的時候,和我一樣享受這個撰寫過程。感謝您的閱讀!