今天我想談下一個特別的等待類型,這個等待可以說是有時很難進行故障排除:線程池(THREADPOOL)等待。這個等待類型是SQL Server內部線程調度機制特有的。
或許你已經知道SQL Server內部使用自有的“操作系統”來實現線程調度和內存管理——SQLOS。SQLOS提供一系列的工作者線程,用來執行提交給SQL Server的查詢。這問題就是這些工作者線程有時被耗盡——可能因為鎖/阻塞(Locking/Blocking)等情況。在這個情況下,SQL Server在引擎內部不能執行更多的請求,因為沒有空閑的工作線程可用。
你可以通過max worker threads選項(通過sp_configure)來配置SQLOS可用工作線程數。默認情況這個選項的值為0,這就是說SQL Server自身可以決定可以使用的工作者線程數。可用工作者線程數取決於CPU架構(x32,x64)和你有的可用CPU數。MSDN在線幫助提供下列不同組合情況的可用工作線程數:
CPU數 32位 64位
小於等於4個 256 512
8個 288 576
16個 352 704
32個 480 960
你也可以通過sys.dm_os_sys_info里的max_workers_count列來檢查你的SQL Server實例使用的工作線程數。
1 SELECT max_workers_count FROM sys.dm_os_sys_info
用下面的例子,我想演示SQL Server里如何獲得線程耗盡(thread starvation),還有你怎么解決它。
警告:請不要在生產系統里執行下列操作!!!
首先我們創建一個數據庫並建立一個簡單表作為我們樣例情景。我想盡可能的特例,因此表名和列名都是特別的。
1 USE master 2 GO 3 4 CREATE DATABASE ThreadPoolWaits 5 GO 6 7 USE ThreadPoolWaits 8 GO 9 10 -- Create a new test table (this one will be unique on earth – hopefully…) 11 CREATE TABLE [SomeCrazyUniqueTableName_6EFF088F-443B-4EBC-A4C7-9FC146D2EE49] 12 ( 13 [MyUniqueColumnName1_F67DAC4A-C202-49BB-829A-071130BF1160] INT IDENTITY(1, 1) NOT NULL PRIMARY KEY, 14 [MyUniqueColumnName2_438B7184-B476-48A4-B5FA-DC34B99FA0A4] INT 15 ) 16 GO 17 18 -- Insert a record 19 INSERT INTO [SomeCrazyUniqueTableName_6EFF088F-443B-4EBC-A4C7-9FC146D2EE49] 20 VALUES (1) 21 GO
從剛才的代碼里我們可以看到,我們的表定義非常簡單。下一步我會創建一個新的存儲過程,里面封裝了一些讀操作。
1 -- Create a stored procedure that encapsulates a read workload 2 CREATE 3 PROCEDURE MyCustomUniqueStoredProcedureName_ReadWorkload 4 AS 5 BEGIN 6 SELECT * FROM [SomeCrazyUniqueTableName_6EFF088F-443B-4EBC-A4C7-9FC146D2EE49] 7 END 8 GO
最后我們開始一個新的事務,對剛才創建的表進行一個更新,但從不提交那個事務:
1 -- Begin a transaction that never commits… 2 BEGIN TRANSACTION 3 UPDATE [SomeCrazyUniqueTableName_6EFF088F-443B-4EBC-A4C7-9FC146D2EE49] WITH (TABLOCKX) 4 SET [MyUniqueColumnName2_438B7184-B476-48A4-B5FA-DC34B99FA0A4] = 2 5 GO
至此,在你的SQL Server實例里有了個掛起的事務,因為TABLOCKX查詢提示,它有排它表鎖(exclusive table lock)。當你從不同的會話(新建查詢)執行剛才創建的存儲過程時,這個存儲過程會等待,因為它對讀取的記錄需要獲取共享鎖(Shared lock)。
1 EXEC MyCustomUniqueStoredProcedureName_ReadWorkload 2 GO
你也可以通過DMV sys.dm_db_tran_locks來檢查這個鎖/阻塞(Locking/Blocking)情況,它會展示你一個等待請求:
1 SELECT 2 resource_associated_entity_id, 3 request_mode, 4 request_status, 5 request_session_id 6 FROM sys.dm_tran_locks 7 WHERE resource_database_id = DB_ID('ThreadPoolWaits') AND resource_type = 'OBJECT' 8 GO
在這個簡單情況下,在SQL Server里只有一個等待查詢,沒啥特別發生。當你使用大量查詢,大於可用最大工作線程數時,SQL Server會作如何反應呢?我們來試下。我用ostress.exe來完成這個任務,它是微軟免費提供的RML工具的一部分。在我的配置(32位系統,4個CPU)SQL Server內部使用256個工作者線程。因為我通過下列的命令行,用ostress.exe模擬300個同時發生連接給SQL Server(RML Utilities for SQL Server->RML Cmd Prompt,請確認當前windows登錄帳號可以正常訪問當前新建的ThreadPoolWaits數據庫):
ostress.exe -Q"EXEC ThreadPoolWaits.dbo.MyCustomUniqueStoredProcedureName_ReadWorkload" -n300
當你執行這個命令提示后,需要花費幾秒時間,ostress.exe才會創建300個工作者線程,並沒有什么特別發生,目前看起來都一切正常。
我們現在來分析下這個情況,通過SSMS新建一個查詢連接。我滴神呀,連接不上了:
SQL Server沒響應了!!!或者說已經掛掉!!!這就對了,因為現在我們已經耗盡了最大可用工作者線程。所有提交給SQL Server的請求當前都在等待一個共享鎖(LCK_M_S等待),而且所有其他的工作線程不能加入SQL Server內部隊列,因為已經沒有可用工作者線程了(THREADPOOL等待)。但是我們現在如何解決這個問題?重啟SQL Server一直不是個好選擇…….
幸運的是,自SQL Server 2005開始提供了數據庫管理員專用連接(Dedicated Admin Connection) (DAC)。你可以使用這個連接登錄到SQL Server,即使當工作者線程耗盡或內存壓力太大,因為在SQLOS內部DAC有它自己特有的
- 調度(Scheduler)
- 內存節點(Memory Node)
- TCP端口
因此SQL Server能接受並提供服務給DAC連接——即使像現在這個高度復雜的故障排除情況。但必須注意,對於整個SQL Server實例,只有1個可用的DAC連接。當你想通過DAC連接時,你必須使用下列語法:admin:<servername> ,這里<servername>就是你SQL Server實例名稱。因為讓我們啟動新的SSMS實例,通過DAC登錄到SQL Server。請注意,你不是通過DAC連接到對象瀏覽器(Object Explorer)。通過DAC連接只能使用簡單的查詢窗口:
當你通過DAC成功連接后,你就能運行你的診斷查詢。你必須知道:DAC不支持自動完成(智能提示),因為自動完成在背后使用它特有的連接——因此你要知道想用來故障排除的DMV。第1步我們可以檢查下sys.dm_exec_requests,看看在SQL Server里當前等待的請求:
1 SELECT 2 r.command, 3 r.sql_handle, 4 r.plan_handle, 5 r.wait_type, 6 r.wait_resource, 7 r.wait_time, 8 r.session_id, 9 r.blocking_session_id 10 FROM sys.dm_exec_requests r INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id 11 WHERE s.is_user_process = 1 12 GO
在我的配置里,這個查詢返回我237行,這就是說237個用戶的請求(WHERE s.is_user_process = 1)在SQL Server里,當前在等待。但我們同時用300個連接的用戶啟動我們的存儲過程。sys.dm_exec_requests向你展示的只是那些在SQL Server內部潛在的工作者線程,因為這些請求當前正在SQL Server內部執行。但其他的呢?那些其他的掛起的請求通過sys.dm_os_waiting_tasks可以看到——它們都有THREADPOOL的等待:
1 SELECT * FROM sys.dm_os_waiting_tasks 2 WHERE wait_type = 'THREADPOOL' 3 GO
它們正在等待一個新的工作線程從工作池里釋放。但在我們的情況里,每個線程當前都暫停並束縛於用戶請求,因此這些請求會永遠等下去!你會看到THREADPOOL只在sys.dm_os_waiting_tasks里,從不在sys.dm_exec_requests里,因為在sys.dm_exec_requests里的請求已經束縛於SQL Server里的工作者線程。當你看sys.dm_exec_requests的輸出結果,在記錄集的最后你會看到 session_id 列和blocking_session_id列。這2列向你展示了SQL Server內部的阻塞鏈(blocking chain):
1 SELECT 2 r.command, 3 r.sql_handle, 4 r.plan_handle, 5 r.wait_type, 6 r.wait_resource, 7 r.wait_time, 8 r.session_id, 9 r.blocking_session_id 10 FROM sys.dm_exec_requests r INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id 11 WHERE s.is_user_process = 1
可以看到,幾乎每個會話都有一個59的blocking_session_id,而且session_id59有一個53的blocking_session_id。session_id53就是我們阻塞頭(head blocker)!讓我們進一步分析下阻塞頭(head blocker)會話53:
1 -- Analyze the head blocker session 2 SELECT 3 login_time, 4 [host_name], 5 [program_name], 6 login_name 7 FROM sys.dm_exec_sessions 8 WHERE session_id = 53 9 GO 10 11 -- Analyze the head blocker connection 12 SELECT 13 connect_time, 14 client_tcp_port, 15 most_recent_sql_handle 16 FROM sys.dm_exec_connections 17 WHERE session_id = 53 18 GO
這里最有意思的列,是來自sys.dm_exec_connections的most_recent_sql_handle列,這里我們可以獲得執行的SQL語句。
但你使用DMF sys.dm_exec_sql_text傳入most_recent_sql_handle列值時,你就可以拿到執行的SQL語句:
1 SELECT text FROM sys.dm_exec_sql_text(0x01001400B883BC05A8831D06000000000000000000000000) 2 GO
我們把語句復制出來看看:
BEGIN TRANSACTION UPDATE [SomeCrazyUniqueTableName_6EFF088F-443B-4EBC-A4C7-9FC146D2EE49] WITH (TABLOCKX) SET [MyUniqueColumnName2_438B7184-B476-48A4-B5FA-DC34B99FA0A4] = 2
這不就是我們最先的查詢么?我們把事務打開,但一直沒提交。現在我們跟蹤到了有問題的查詢,它導致了THREADPOOL和LCK_M_S等待,最后可以通過DAC的把這個會話殺掉:
1 KILL 53 2 GO
這個會話殺掉的話要花幾秒鍾,最后你的阻塞情況也消失了。現在你的SQL Server又能接受新的連接,可以正常工作了。當你通過DAC連接解決完問題后,不要忘記關閉這個連接,因為在SQL Server實例里只有1個可用DAC連接。然后當你查看SQL Server錯誤日志時,你可以看到類似下列的信息:
(管理->SQL Server 日志->存檔編號1)
看不懂,沒關系,俺們再到Windows日志里看看:
這也象征着在你SQL Server實例內部工作者線程耗盡。這個情況的副作用也導致所謂的調度死鎖(Deadlocked Schedulers) 。當SQL Server遇到調度死鎖,SQL Server會在你的日志目錄寫入堆棧轉儲(Deadlocked Schedulers)
有時我會看見在SQL Server內部盲目重新配置max worker threads設置,因為他們認為服務器的工作量需要更多的工作者線程。但是SQL Server里的每個問題,都有導致問題的根本原因,就如你剛才看到的一樣。剛才情況的根源是一個未提交的事務,導致阻塞情況,引起線程耗盡,最后導致SQL Server無響應。從這個解釋你可以發現,在你找到根源前都有很長的問題鏈——在你下次處理故障排除時,記住這個。
對你的操作,造成服務器幾乎當機,別放在心上,因為俺們只是演習,不是在生產系統。重要的是我們學會了如何處理這個問題。請繼續關注!
參考文章:
https://www.sqlpassion.at/archive/2011/10/25/troubleshooting-threadpool-waits/