線程池等待故障排除


今天我想談下一個特別的等待類型,這個等待可以說是有時很難進行故障排除:線程池(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_idsession_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_connectionsmost_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 

這不就是我們最先的查詢么?我們把事務打開,但一直沒提交。現在我們跟蹤到了有問題的查詢,它導致了THREADPOOLLCK_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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM