許多有經驗的資料庫開發或者DBA都曾經頭痛於並行查詢計劃,尤其在較老版本的資料庫中(sqlserver2000、oracle 7、mysql等)。但是隨著硬體的提升,尤其是多核處理器的提升,並行處理成為了一個提高大資料處理的高效方案尤其針對OLAP的資料處理起到了很好的作用。
充分高效地利用並行查詢需要對排程、查詢優化和引擎工作等有一個比較好的瞭解,但是針對一般場景的應用我們只需要如何常規使用即可,這裡也就不深入描述了,感興趣可以一起討論。
那麼這裡我就簡單介紹下SQLServer中並行的應用?
什麼是並行?
我們從小就聽說過“人多力量大”、“人多好辦事”等,其思想核心就是把一個任務分給許多人,這樣每個人只需要做很少的事情就能完成整個任務。更重要的是,如果額外的人專門負責分配工作,那麼任務的完成時間就可以大幅減少了。
數糖豆
設想你正面對一個裝滿各式各樣糖豆的罐子,並且要求書有多少個。假設你能平均每秒數出五個,需要大於十分鐘才能數完這個盒子裡的3027個糖豆。
如果你有四個朋友幫助你去做這個任務。你就有了多種策略來安排這個數糖豆任務,那讓我們模仿SQLServer 將會採取的策略來完成這個任務。你和4個朋友圍坐在一個桌子四周,糖果盒在中心,用勺子從盒子中拿出糖豆分給大家去計數。每個朋友還有一個筆和紙去記錄數完的糖豆的而數量。
一旦一個人輸完了並且盒子空了,他們就把自己的紙給你。當你收集完每個人的計數,然後把所有的數字加在一起就是糖豆的數量。這個任務也就完成了。大概1-2分鐘,完成的效率提高了四倍多。當然四個人累加也是十分鐘左右甚至還要多(因為多出來了分配和累加的過程)。這個任務很好的展示了並行的優點,也沒有其他額外的工作需要處理。
使用SQLServer 完成“數糖豆”
當然SQLServer 不會去數罐子裡的糖豆,那我就讓它去計算表裡的行數。如果表很小那麼執行計劃如圖1:
圖1 序列執行計劃:
這個查詢計劃使用了單一程式,就好像自己一個人數糖豆一樣。計劃本身很簡單:流聚合操作符負責統計接收來自索引掃描操作符的行數,然後統計出總行數。相似的情況下,如果盒子裡面糖豆非常少,雖然分配糖豆的時間會減少很多,但是統計步驟就顯得效率不是那麼高了,因為相對於大數量的糖豆這部分的所佔時間就高很多了。
所以當表足夠大,SQLServer 優化器可以選擇增加更多的執行緒,執行計劃如圖2:
圖2 並行計數計劃
右側三個操作符中的黃色箭頭圖示表示引入了多執行緒。每個執行緒被分配了一部分工作,然後完成分分部工作被聚集在一起成為最終結果。如同前面人工數糖豆的例子一樣,並行計劃有很大可能提高完成速度,因為多執行緒在計數上更優。
並行如何工作?
設想一下,如果SQLServer沒有內建對於並行的支援。或許我們只能手動去平均劃分並行查詢來實現效能優化,然後分別執行分配的流,獨立地訪問伺服器。
圖3 手動分配並行
每次查詢都必須手寫分隔錶行數的獨立查詢,確保全表資料都被查詢到。幸運的是SQLServer 能在一個處理單元內完成每一個分隔的獨立執行緒,然後接收三個部分結果集只需要三分之一的時間左右。自然地我們還需要額外的時間來合併三個結果集。
並行執行多個序列計劃
回想一下圖2中顯示的並行查詢計劃,然後假設SQLServer 分配了三個額外的執行緒在執行時去查詢。概括的講,重新生成並行計劃來展示SQLServer 執行三個獨立序列的計劃流(這個表示是我自己起的不是很精確。)
圖4: 多序列計劃
每個執行緒被分配三個branch 中的一個,最後匯聚到Gather Streams(流聚合) 操作符。注意這個圖中只有流聚合操作符帶有黃色並行箭頭;所以這個操作符是這個計劃中僅有的與多執行緒互動的操作符。這種通用策略有兩個原因始適合SQLServer的。
首先,所有必要地執行序列計劃SQL程式碼已經存在並且已經被優化多年和線上釋出。其次,方法的方位很合適:如果更多執行緒被呼叫,SQLServer 能輕易新增額外計劃分之來分配更多執行緒。
額外的執行緒數量分配給每一個並行計劃,這被稱為並行度(縮寫為DOP)。SQLServer 在查詢開始之前就選擇了DOP,然後不需要計劃重新編譯就能改變並行度。最大DOP對於每一個並行區域都是由SQLServer的邏輯處理單元的可利用數量決定的(物理核)
並行掃描和並行頁支援
圖4中的問題是每個索引掃描操作符都會去數整個輸入集的每一行。不及時糾正,計劃就會產生錯誤的結果集並且和可能花費更多時間。手工並行的例子通過使用where子句來避免這個問題。
SQLServer 沒有用相同的方法,因為分配工作假定平均地使每個查詢接收相等的可利用資源,並且每個資料行需要相同的處理。在一個簡單例子中,例如統計一個表中的行數,這種假定可能會效果很好(同一個伺服器沒有其他活動的時候),並且三個查詢可能返回的查詢也是完全等時的。
與分配固定數量行數給每個執行緒不同,SQLServer使用儲存引擎的功能叫做“Parallel Page Supplier ”來按需分配行數給執行緒。在查詢計劃中是看不到“Parallel Page Supplier ”的,因為它不是查詢處理器的一部分,但是我們能擴充圖4來形象的展示他的連線方式:
圖5: Parallel Page Supplier
這裡的關鍵點就是demand-based (基於需求)架構;通過響應現成的請求提供一個行數的批處理給需要更多工作的執行緒去做。對比數糖豆的案例,Parallel Page Supplier 就像是專門用勺子從罐子裡面拿出糖豆的過程。只有一個勺子防止兩個人都去數相同的豆子。並且其他執行緒將會數更多豆子來補償。
注意Parallel Page Supplier 的使用並不阻止現有的優化像預讀掃描(在硬碟上提前讀取資料)。事實上,這種預讀在這種情況下效率要比單執行緒還要好,這個單執行緒是底層的物理掃描而不是之前我們看到的三個獨立的手動並行的例子。
Parallel Page Supplier 也不會限制索引掃描;SQLServer利用它當多執行緒協同讀取一個資料架構。資料架構可能是堆、聚集索引表、或者一個索引,並且操作可以是掃描或者查詢。如果後者(查詢)更高效,考慮索引查詢操作就像一個部分掃描,例如它能查詢到第一個符合條件的行然後掃面範圍的結尾。
執行上下文
與手動並行例子的機制相似,但是又與建立獨立連線的序列查詢,SQLServer 使用了一個輕量級的構造稱之為“執行上下文”來實現並行。
一個執行上下文來自查詢計劃的一部分,該內容通過填寫在計劃重新編譯和優化後的細節來產生。這些細節包括了直到執行才有的引用物件(如批處理中的臨時表)和執行時的引數以及區域性變數。這裡就不展開講了,微軟的白皮書中由於詳細的介紹。
SQLServer 執行一個並行計劃,通過為每一個查詢計劃的並行區域派生一個DOP執行上下文,利用獨立的執行緒在上下文中執行序列計劃包含的部分。為了幫助概念的理解,圖6中展示了三個執行上下文,每個顏色區分執行上下文的範圍。雖然並不是明顯地展示出來,但是一個Parallel Page Supplier 還是被用來協調索引掃描,避免重複讀取。
圖6: 並行計劃執行上下文
為了更具體的觀察抽象概念,圖7展示了並行行計數查詢包含的資訊,在SSMS的選項中,“Actual Execution Plan”(實際執行計劃),開啟左側擴充套件+。
圖7: 並行計劃行計數
兩個圖片對比,行處理的數字一個是3一個是113443。資訊來自於屬性視窗,通過點選操作符(或者連結線)然後按下F4,或者右鍵屬性。右鍵操作符或者線,並且選擇彈出選單的屬性。
右邊的插圖中我們能看到每個執行緒讀取的行數和總行數;注意兩個執行緒處理了相似的行數(40000左右),但是第三個執行緒值處理了32000行。如上所述,基於需求的架構取決於每個執行緒時間因素和處理器負載等等,及時是輕負載的機器也會有不平衡的現象。
左側的這個圖展示了三個結果結被收集在一起的過程,彙總了每個程式的結果集。它的元素是並行執行執行緒的數量。
Schedulers, Workers, 以及Tasks
這篇文章到目前為止‘thread’ 和‘worker’理解上是一致的。現在我們需要定義更加精確,如下。
Schedulers
一個scheduler 在SQLserver 中代表一個邏輯處理器,或者是一個物理CPU,或許是一個處理核心,或許是在一個核(超執行緒)上執行的多個硬體執行緒之一。排程器的主要目的就是允許SQLServer精確控制執行緒排程,而不是依賴Windows作業系統的泛型演算法。
每個排程器確保僅有一個協調執行執行緒在執行(就作業系統而言)在指定時間內。這樣做的重要好處就是減少了上下文切換,並且減少了呼叫windows核心的次數。序列的三個部分覆蓋了任務排程和執行的內部詳細資訊。
關於任務排程在可以在DMV(sys.dm_os_schedulers)中檢視。
Workers 和Threads
一個SQLServer 工作執行緒是一個抽象表示一個單一的作業系統執行緒或者一個光纖。很少系統執行光纖模式任務排程,因此大部分文件都是使用了工作執行緒來強調對於大多數實際目的而言,一個worker就是一個執行緒。一個工作執行緒繫結一個具體的排程。關於工作執行緒的資訊可以通過DMVsys.dm_os_workers來檢視。
Tasks
可以這樣定義Tasks:
一個任務表示一個被SQLServer 排程的執行緒的單位。一個批處理能對映一個或者多個任務。例如,一個並行查詢將被多個任務執行。
擴充套件這個簡單的定義,一個任務就被SQLServer 工作執行緒執行的一件工作。一個批處理僅包含一個序列執行計劃就是單任務,並且將被單一連線提供的執行緒執行(從開始到結束)。這種情況下,執行必須等待另一個事件(例如從硬碟讀取)完成。單執行緒被分配一個任務,然後直到被完全完成否則不能執行其他任務單元。
執行上下文
如果一個任務描述被完成的工作,一個執行上下文就是工作發生的地方。每個任務在一個執行上下文內執行,標識在DMVsys.dm_os_tasks中的exec_context_id列中(你也可以看到執行上下文使用ecid 列在sys.sysprocesses檢視中)
交換操作符
簡要回顧,我們已經看到SQLServer通過併發執行一個序列計劃的多個例項來執行一個並行計劃。每個序列計劃都是一個單獨的任務,在各自的執行上下文內獨立執行各自的執行緒。最終這些執行緒的結果成為交換操作符的組成部門,就是將並行計劃的執行上下文連線在一起。一般來說,一個複雜的查詢計劃可以包含多個序列或者並行區域,這些區域由交換操作符來連線。
到目前為止,我們已經看到只有一種形式的連線操作符,叫做流聚合,但是它能以另外兩種進化的形式繼續出現如下:
圖8: 交換邏輯操作符
這些形式的交換操作符就是在一個或者多個執行緒內移動行,分配獨立的行給多個執行緒。不同的邏輯形式的操作符要麼是引入新的序列或者並行區域,要麼是分配重定向行給在兩個並行區域的介面。
不僅可以分割、合併、重定向行在多執行緒上,還可以做到如下事情:
- 使用五中不同的策略來確定輸出輸入行的路線。
- 如果需要,可以保留輸入行的順序。
- Much of this flexibility stems from its internal design, so we will look at that first. 靈活源自其內部設計,因此我們要先觀察
交換操作符內部
交換操作符有兩個完全不同的子元件:
- 生產者, 連線輸入端的執行緒
- 消費者, 連線輸出端的執行緒
圖9 展示了一個流聚合操作符的放大檢視(圖6)
圖9: 流聚合內部構造
每個生產者 收集它的輸入行並且將輸入包裝成一個或者多個記憶體中的快取。一旦快取滿了,生產者將會將其推入到消費者端。每個生產者和消費者都執行在相同的執行緒作為其連線執行上下文(如同連線的顏色暗示)。消費者端的交換操作符當它被上級操作符要求就從快取中讀取一行資料(如同本例中的紅色的陰影資料流聚合)。
主要好處之一就是複雜度通常與分享資料的多個執行的執行緒有關,而這些執行緒由SQLServer一個內部操作符處理。另外,在計劃中的非交換操作符是完全序列執行的,並且不需要去關心這些問題。
交換操作符使用快取來減少開銷,並且為了實現控制基本種類的流(例如為了阻止快速生產者比慢速消費者快太多)。精確分配緩衝區,隨著交換的不同快取區也變化,不論是否需要保留順序,並且決定如何匹配生產者和消費者的資料行,
路由行
如上所述,一個交換操作符能決定一個生產者應該匹配哪一個特定的行資料。這個決定依賴於被交換操作符指定的分塊型別。並且有五個可選型別,
型別 | 描述 |
Hash | 最常見,通過計算當前行的一個或者多個列上的雜湊函式來選擇消費者。 |
輪循 | 每個新的行按照固定的序列被髮送給下一個消費者 |
廣播 | 每一行被髮送給所有消費者。 |
請求 | 每一行被髮送給第一個請求的消費者。這是僅有的通過消費者內部的交換符拉出行的分割型別。 |
範圍 | 每一個消費者被分配一個不重疊的範圍值。特定的輸入列分成範圍決定消費者獲得的行。 |
請求和範圍分割型別是比前面三種更少見的,並且一般只在操作分割槽表的查詢計劃中能看到。請求型別是用來收集分割槽的連線來分配分割槽ID給下一個工作執行緒。例如,當建立分割槽索引的時候使用範圍分割型別,那麼如果要想查到屬於哪種型別需要在查詢計劃中查詢:
圖10: 交換操作分割型別
保留輸入順序
一個交換操作符可以選擇配置來保留排序順序。在計劃中輸入的行已經排序的時候對後面的操作符是很有用的(沿用開始的排序,或者作為一個從索引中讀取的已經排序的序列)。如果交換操作符沒有保留上順序,在交換器需要重新建立排序後優化器將必須引入額外的排序操作符。普通的請求排序輸入的操作符包括流聚合、分段和合並連線。圖11展示一個需要重新分配流的排序操作:
圖11: 保留順序的重新分配流
注意合併交換自身不會排序,它要求輸入行必須進行排序嗎。合併交換是效率更低比非保留順序的,並且是有一定的效能問題的。
最大並行度
微軟給出的官方指導:
請遵循以下準則:
1. 伺服器的有8個或更少的處理器,使用下列配置其中N等於處理器數:MAXDOP=0到N。
2. 對於具有NUMA配置的伺服器,MAXDOP不應超過分配給每個NUMA節點的cpu數。
3. 超執行緒已啟用的伺服器的MAXDOP值不應超過物理處理器的數量。預設為0表示資料庫引擎自行分配。
總結
通過一個簡單的查詢引入並行,並且對照了一個真實的數糖豆的案例,為了研究SQLServer中並行的使用的優點,暫時沒有考慮與多執行緒設計相關的複雜情況。我們發現了並行查詢計劃可以包含多個並行和序列區域,通過交換操作符繫結在一起。
並行區域擴充套件出多個序列查詢,每個序列都使用了獨立執行緒來處理執行上下文的任務。交換操作符被用來匹配執行緒之間的行並且在並行計劃中實現與不止一個執行緒互動。最後,我們看到了SQLServer 提供了一個Parallel Page Supplier,當保證是正確的結果集時,允許多個執行緒可以協同掃描表和索引。
除此之外還介紹了交換操作符以及操作符內部詳細構造以及最佳實踐中的並行度配置。這裡都這是從概念上做了介紹,如果線下有問題可以一起研究選擇出最好的實現方式。