簡介
SQL Server OS是在Windows之上,用於服務SQL Server的一個使用者層級的作業系統層次。它將作業系統部分的功能從整個SQL Server引擎中抽象出來,單獨形成一層,以便為儲存引擎提供服務。SQL Server OS主要提供了任務調度、記憶體配置、死結檢測、資源檢測、鎖管理、Buffer Pool管理等多種功能。本篇文章主要是談一談SQL OS中所提供的任務調度機制。
搶佔式(Preemptive)調度與非搶佔式(non-Preemptive)調度
資料庫層面的任務調度的起源是ACM上的一篇名為“Operating System Support for Database Management”。但是對於Windows來說,在作業系統層面專門加入支援資料庫的任務調度,還不如在SQL Server中專門抽象出來一層進行調度,既然可以抽象出來一層進行資料庫層面的任務調度,那麼何不在這個抽象層進行記憶體和IO等的管理呢?這個想法,就是SQL Server OS的起源。
在Windows NT4之後,Windows任務調度是搶佔式的,也就是說Windows任務是根據任務的優先順序和時間片來決定。如果一個任務的時間片用完,或是有更高優先順序的任務正在等待,那麼作業系統可以強制剝奪正在啟動並執行線程(線程是任務調度的基本單位)所佔用的CPU,將CPU資源讓給其它線程。
但是對於SQL Server來說,這種非合作式的、基於時間片的任務調度機制就不那麼合適了。如果SQL Server使用Windows內的任務調度機制來進行任務調度的話,Windows不會根據SQL Server的調度機制進行最佳化,只是根據時間片和優先順序來中斷線程,這會導致如下兩個缺陷:
- Windows不會知道SQL Server中任務(也就是SQL OS中的Task,會在文章後面講到)的最佳中斷點,這勢必會造成更多的Context Switch(Context Switch代價非常非常高昂,需要線程字使用者態和核心態之間轉換),因為Windows調度不是線程本身決定是否該出讓CPU,而是由Windows決定。Windows並不會知道當前資料庫中對應的線程是否正在做關鍵任務,只會不分青紅皂白的奪取線程的CPU。
- 連入SQL Server的串連不可能一直在執行,每一個Batch之間會有大量空閑時間。如果每個串連都需要單獨佔用一個線程,那麼SQL Server維護這些線程就需要消耗額外的資源,這是很不明智的。
而對於SQL Server OS來說,線程調度採用的合作模式而不是搶佔模式。這是因為這些資料庫內的任務都在SQL Server這個SandBox之內,SQL Server充分相信其內線程,所以除非線程主動放棄CPU,SQL Server OS不會強制剝奪線程的CPU。這樣一來,雖然Worker之間的切換依然是通過Windows的Context Switch進行,但這種合作模式會大大減少所需Context Switch的次數。
SQL Server決定哪一個時間點哪一個線程運行,是通過一個叫Scheduler的東西進行的,下面讓我們來看Scheduler。
Scheduler
SQL Server中每一個邏輯CPU都有一個與之對應的Scheduler,只有拿到Scheduler所有權的任務才允許被執行,Scheduler可以看做一個隊SQLOS來說的邏輯CPU。您可以通過sys.dm_os_schedulers這個DMV來看系統中所有的Scheduler,1所示。
圖1.查看sys.dm_os_schedulers
我的筆記本是一個i7四核8線程的CPU,對應的,可以看到除了DAC和運行系統任務的HIDDEN Scheduler,剩下的Scheduler一共8個,每個對應一個邏輯CPU,用於處理內部Task。當然,您也可以通過設定Affinity來將某些Scheduler Offline,2所示。注意,這個過程是線上的,無需重啟SQL Server就能實現。
圖2.設定Affinity
此時,無需重啟執行個體就能看到4個Scheduler被Offline,3所示:
圖3.線上Offline 4個Scheduler
一般來說,除非您的伺服器上運行其他執行個體或程式,否則不需要控制Affinity。
在圖1中,我們還注意到,除了Visible的Scheduler之外,還有一些特殊的Scheduler,這些Scheduler的ID都大於255,這類Scheduler都用於系統內部使用,比如說資源管理、DAC、備份還原作業等。另外,雖然Scheduler和邏輯CPU的個數一致,但這並不意味著Scheduler和固定的邏輯CPU相綁定,而是Scheduler可以在任何CPU上運行,只有您設定了Affinity Mask之後,Scheduler才會被固定在某個CPU上。這樣的一個好處是,當一個Scheduler非常繁忙時,可能不會導致只有一個物理CPU繁忙,因為Scheduler會在多個CPU之間移動,從而使得CPU的使用傾向於平均。
這意味著對於一個比較長的查詢,可以前半部分在CPU0上執行,而後半部分在CPU1上執行。
另外,在每一個Scheduler上,同一時間只能有一個Worker運行,所有的資源都就緒但沒有拿到Scheduler,那麼這個Worker就處於Runnable狀態。下面讓我們來看一看Worker。
Worker
每一個Worker可以看做是對應一個線程(或纖程),Scheduler不會直接調度線程,而是調度Worker。Worker會隨著負載的增加而增加,換句話說,Worker是按需增加,直到增加到最大數字。在SQL Server中,預設的Worker最大數是由SQL Server進行管理的。根據32位還是64位,以及CPU的數量來設定最大Worker,具體的計算公式,您可以參閱BOL:http://msdn.microsoft.com/zh-cn/library/ms187024(v=sql.105).aspx。當然您也可以設定最大Worker數量,4所示。
圖4.設定最大Worker數量
如果是自動設定,那麼SQL Server的最大背景工作執行緒數量可以在sys.dm_os_sys_info中看到,5所示。
圖5.查看自動設定的最大Worker數量
一般來說,這個值您都無需進行設定,但也有一些情況,需要設定這個值。那就是Worker線程用盡,此時除了DAC之外,您甚至無法連入SQL Server。
Worker實際上會對應Windows上的一個線程,並與某個特定Scheduler綁定,每一個Worker只要開始執行Task,除非Task完成,否則Worker永遠不會放棄這個Task,如果一個Task在運行過程由於鎖、IO等陷入等待,那麼實際上Worker就會陷入等待。
此外,同一個串連內的多個Batch之間傾向於使用同一個Worker,比如第一個Batch使用了Worker 100,那麼第二個Batch也同樣傾向於是用Worker 100,但這並不絕對。
正在啟動並執行任務所是用的Worker,我們可以通過DMV sys.dm_exec_requests查看正在啟動並執行任務,其中的Task_Address列可以看到正在啟動並執行Task,再通過sys.dm_os_tasks的Worker_Address來查看對應的Worker。
SQL Server會為每一個Worker保留大約2M左右的記憶體,對於每一個Scheduler上所能有的Worker數量是伺服器的最大Worker數量/線上的Scheduler,每一個Scheduler所綁定的Worker會形成Worker池,這意味著每一個Scheduler需要Worker時,首先在Worker池中中尋找閒置Worker,如果沒有閒置Worker時,才會建立新的Worker。這個行為會和串連池類似。
那麼當一個Scheduler空閑超過15分鐘,或是Windows面臨記憶體壓力時。SQL Server就會嘗試Trim這個Worker池來釋放被Worker所佔用的記憶體。
Task
Task是Worker上啟動並執行最小任務單元。只能拿到Worker的Task才能夠運行。我們可以看下面一個簡單的例子,如代碼1所示。
SELECT @@VERSION goSELECT @@SPID go
代碼1.一個串連上的兩個Batch
代碼1中的兩個Batch屬於一個串連,每一個Batch中都是一個簡單的Task,如我們前面所說,這兩個Task更傾向於複用同一個Worker,因為他們屬於同一個串連。但也有可能,這兩個Task使用了不同的Worker,甚至是不同的Scheduler。
除了使用者所用的Task之外,還有一些永久的系統Task,這類Task會永遠佔據Worker,這些Task包括死結檢測、Lazy Writer等。
Task在Scheduler上的平均分配
新的Task還會嘗試在Scheduler之間平均分配,可以通過sys.dm_os_schedulers來看到一個load_factor列,這列的值就是用於供Task向Scheduler進行分配時,用來參考。
每次一個新的Task進入Node時,會選擇負載最少的的Scheduler。但是,如果每次都來做一次選擇,那麼就會在Task入隊時造成瓶頸(這個瓶頸類似於TempDB SGAM頁爭搶)。因此SQL OS對於每一個串連,都會記住上次啟動並執行Scheduler ID,在新的Task進入時作為提示(Hint)。但如果一個Scheduler的負載大於所有Scheduler平均值的20%,則會忽略這個提示。負載可以通過上面提到的load_factor列來看,對於某個Task啟動並執行時間比較長,則很有可能造成Scheduler上Task分配的不均勻。
Worker的Yield
由於SQL Server是非搶佔式調度,那麼就不能為了完成某個Task,讓Worker佔據Scheduler一直運行。如果是這樣,那麼處於Runnable的Worker將會饑餓,這不利於大量並發,也違背了SQL OS調度的初衷。
因此,在合適的時間點讓出Scheduler就是關鍵。Worker讓出CPU使得其它Worker可以啟動並執行過程稱之為yield。yield大體可分為兩種,一種是所謂的“natural yield”,這種方式是Worker在運行過程中被鎖或是某些資源阻塞,此時,該Worker就會讓出Scheduler來讓其它Worker運行。另外一種情況是Worker沒有遇到阻塞,但在時間片到了之後,主動讓出Scheduler,這就是所謂的“voluntarily yield”,這也就是SOS_SCHEDULER_YIELD等待類型的由來,一個Worker由RUNNING狀態轉到WAITING狀態的過程被稱之為switching。SQL OS的一個基本思想就是,要多進行switching,來保證高並發。下面我們來看幾種常見的yield情境:
- 基於時間片的voluntarily yield大概使得Worker每4秒yield一次。這個值可以通過sys.dm_os_schedulers的quantum_length_us列看到。
- 每64K結果集排序,就做一次yield。
- 語句complie,會做yield。
- 讀取資料頁時
- batch中每一句話做完,就會做一次yield。
- 如果用戶端不能及時取走資料,worker也會做yield。
SQL Server OS中的搶佔式任務調度
對於一些代碼來說,SQL Server會存在一些搶佔式代碼。如果您在等待類型中看到“PREEMPTIVE_*”類型的等待,說明這裡面的代碼正在運行在搶佔式任務調度模式。這類任務包括擴充預存程序、調用Windows API、日誌增長(日誌填0)。我們知道,合作式的任務調度需要任務本身Yield,但這類代碼在SQL Server 之外,如果讓他們運行在合作式任務調度這個SandBox之內,這類代碼如果不yield,則會永遠佔用Scheduler。這是非常危險的。
因此,在進入搶佔式模式之前,首先需要將Scheduler的控制權交給在Runable隊列中的下一個Worker。此時,搶佔式模式啟動並執行代碼不再由SQL OS控制,轉而由Windows任務調度系統控制。因此一個Task的生命週期如果再加上轉到搶佔式任務調度模式,則會6所示。
圖6.一個Task完整的生命週期
每一個Scheduler的任務調度
對於每一個Scheduler的調度,一個簡單的模型7所示。
圖7.一個Scheduler的調度周期模型
小結
SQL Server OS在Windows之上抽象出一套非搶佔式的任務調度機制,從而減少了Context Switch。同時,又有一套線程自己的yield機制,相比Windows隨機搶佔資料庫之內的線程而言,讓線程自己來yield則會大量減少Context Switch,從而提升了並發性。