Latch是什麼
Latch是SQL Server引擎保證內存中的結構的一致性的輕量同步機制。比如索引,數據頁和內部結構(比如非葉級索引頁)。SQL Server使用Buffer Latch保護緩衝池中的頁,用I/O Latch保護還未加載到緩衝池的頁,用Non-Buffer Latch保護內存中的內部結構。
Buffer Latch:當工作線程訪問緩衝池中的某個頁之前,必須要先獲得此頁的Latch。主要用於保護用戶對象和系統對象的頁。等待類型表現爲PAGELATCH_*
I/O Latch:當工作線程請求訪問的頁未在緩衝池中時,就會發一個異步I/O從存儲系統將對應的頁加載到緩衝池中。這個過程會獲取相應頁上 I/O Latch,避免其它線程使用不兼容的Latch加載同一頁到緩衝池中。等待類型表現爲PAGEIOLATCH_*
Non-Buffer Latch:保護緩衝池頁之外的內部內存結構時使用。等待類型表現爲LATCH_XX。
Latch只在相應頁或者內部結構被操作期間持有,而不像鎖那樣在整個事務中持有。比如,使用WITH NOLOCK查詢某表。查詢過程中不會在表的任何層級上獲取共享鎖,但是在數據頁可以讀取前,需要獲取這些的頁的Latch。
Latch的模式
Latch跟鎖一樣,是SQL Server引擎併發控制的一部分。在高併發環境下的Latch爭用的情況無可避免。SQL Server使用模式兼容性強制互不兼容的兩個Latch請求線程中的一個等待另一個完成操作並釋放目標資源上的Latch後,才能訪問此目標資源。
Latch有5種模式:
KP – Keep Latch 保證引用的結構不能被破壞
SH – Shared Latch, 讀數據頁的時候需要
UP – Update Latch 更改數據頁的時候需要
EX – Exclusive Latch 獨佔模式,主要用於寫數據頁的時候需要
DT – Destroy Latch 在破壞引用的數據結構時所需要
Latch模式兼容性,Y表示兼容,N表示不兼容:
KP | SH | UP | EX | DT |
Y | Y | Y | Y | N |
Y | Y | Y | N | N |
Y | Y | N | N | N |
Y | N | N | N | N |
N | N | N | N | N |
影響Latch爭用的因素
因素 | 明說 |
使用過多的邏輯CPU | 任何多核的系統都會出現Latch爭用。Latch爭用超過可接受程度的系統,多數使用16個或者以上的核心數。 |
架構設計和訪問模式 | B樹深度,索引的大小、頁密度和設計,數據操作的訪問模式都可能會導致過多的Latch爭用 |
應用層的併發度過高 | 多數Latch爭用都會伴有應用層的高併發請求。 |
數據庫邏輯文件的佈局 | 邏輯文件佈局影響着分配單元結構(如PFS,GAM,SGAM,IAM等)的佈局,從而影響Latch爭用程度。最著名的例子就是:頻繁創建和刪除時臨表,導致tempdb的PFS頁爭用 |
I/O子系統性能 | 大量的PAGEIOLATCH等待就說明SQL Server在等待I/O子系統。 |
診斷Latch爭用
診斷的主要方法和工具包括:
觀察性能監視器的CPU利用率和SQL Server等待時間,判斷兩者是否具有關聯性。
通過DMV獲取引起Latch爭用的具體類型和資源。
診斷某些Non-Buffer Latch爭用,可能還需要獲取SQL Server進程的內存轉儲文件並結合Window調試工具一起分析。
Latch爭用是一種正常的活動,只有當爲獲取目標資源的Latch而產生的爭用和等待時間影響了系統吞吐量時,才認爲是有害的。爲了確定一個合理的爭用程度,需要結合性能、系統吞吐理、IO和CPU資源一起分析。
通過等待時間衡量Latch爭用對應用性能的影響程度
1.頁平均Latch等待時間增長與系統吞吐理增長一致。
如果頁平均Latch等待時間增長與系統吞吐理增長一致,特別是Buffer Latch等待時間增長的超過了存儲系統的響應時間,就應該使用sys.dm_os_waiting_tasks檢查當前的等待任務。還需要結合系統活動和負載特徵觀察。診斷的一般過程:
使用“Query sys.dm_os_waiting_tasks Ordered by Session ID”腳本或者“Calculate Waits Over a Time Period”腳本觀察當前的等待任務和平均Latch等待時間情況。
使用“QueryBufferDescriptorsToDetermineObjectsCausingLatch”腳本確定爭用發生的位置(索引和表)。
使用性能計數器觀察MSSQL%InstanceName%\Wait Statistics\Page Latch Waits\Average Wait Time或者查詢sys.dm_os_wait_stats觀察頁平均Latch等待時間。
2.業務高峯期Latch等待時間佔總等待時間百分比。
如果Latch等待時間比率隨着應用負載增長而直線增長,可能Latch爭用會影響性能,需要優化。通過等待統計性能計數觀察Page和Non-Page Latch等待情況。然後將之與CPU\RAM\IO\Network吞吐量等相關的計數器比較。例如使用Transactions/sect和Batch Requests/sec衡量資源利用情況。
sys.dm_os_wait_stats沒有包含所有等待類型的等待時間,這是因爲它記錄是的自上次實例啓動(或清空)以後的等待數據。也可以通過dbcc SQLPERF ('sys.dm_os_wait_stats', 'CLEAR')手動清空它。在業務高峯前取一次sys.dm_os_wait_stats數據,在業務高峯時取一次,然後計算差異。
3. 系統吞吐理沒有增長(甚至下降),同時應用負載加重,SQL Server可用的CPU增加。
在高併發和多CPU系統中,對類似自增聚集索引的併發插入,會導致一種像現:CPU數量增加,而系統吞吐量下降,同時Latch頁等待會增加。
4. 應用負載增長時CPU利用率卻沒有增長。
當CPU利用率沒有隨着應用併發負載增長而增長,說明SQL Server在等待某種資源(Latch爭用的表現)。
查詢當前的Latch
下面的查詢可以查看當前實時的等待信息。wait_type爲PAGELATCH_* 和PAGEIOLATCH_*的即爲Buffer Latch的等待。
SELECT wt.session_id, wt.wait_type
, er.last_wait_type AS last_wait_type
, wt.wait_duration_ms
, wt.blocking_session_id, wt.blocking_exec_context_id, resource_description
FROM sys.dm_os_waiting_tasks wt
JOIN sys.dm_exec_sessions es ON wt.session_id = es.session_id
JOIN sys.dm_exec_requests er ON wt.session_id = er.session_id
WHERE es.is_user_process = 1
AND wt.wait_type <> 'SLEEP_TASK'
ORDER BY wt.wait_duration_ms desc
查詢返回列的說明:
列 | 說明 |
Session_id | task所屬的session id |
Wait_type | 當前的等待類型 |
Last_wait_type | 上次發生等待時的等待類型 |
Wait_duration_ms | 此等待類型的等待時間總和(毫秒) |
Blocking_session_id | 當前被阻塞的session id |
Blocking_exec_context_id | 當前task的ID |
Resource_description | 具體等待的資源 |
下面的查詢返回Non-Buffer Latch的信息
select * from sys.dm_os_latch_stats where latch_class <> 'BUFFER' order by wait_time_ms desc
返回列的說明:
列 | 說明 |
Latch_class | Latch類型 |
Waiting_requests_count | 當前Latch類型發生的等待次數 |
Wait_time_ms | 當前Latch類型發生等待的時間總和 |
Max_wait_time_ms | 當前Latch類型發生等待最長時間 |
Latch爭用的常見場景
尾頁的數據插入爭用
在聚集索引表上,如果聚集索引的首列(leading key)是一個有序增長的列(如自增列和Datetime),則可能導致Latch爭用。
這種場景中表除了在歸檔時很少執行刪除和更新操作;通常表很大,行較窄。
插入一條數據到索引中的步驟:
1. 檢索B樹,定位到新行將要被存儲的頁
2. 在此頁上加上排他Latch(PAGELATCH_EX),避免其它操作同時修改此頁。然後在所有非葉級頁中加上共享Latch(PAGELATCH_SH).
有時也會在非葉頁獲取排他Latch,如頁拆分時直接受到影響的非葉頁。
3. 向日志文件寫一條記錄,表示此行已經被修改
4. 向頁中寫中新行並標記爲髒頁
5. 釋放所有Latch.
如果表的索引是基於有序增長的鍵,則新行都會被插入到B樹的最後一頁,直到這頁存滿。高併發負載下,就會導致聚集和非聚集索引B樹中最右頁被爭用。通常這種併發爭用發生在以插入操作爲主且頁密度較大的索引上。通過sys.dm_db_index_operational_stats的可以觀察到頁Lath爭用和B樹中非葉級頁Latch爭用的情況。
舉個例子:
線程A和線程B同時向尾頁(比如1999頁)插入新行。邏輯上,兩者都可以同時獲取尾頁中對應行的行級排他鎖。但是,爲維護內存完整性一次只可以有一個線程獲取到頁上排他Latch。假設A獲取EX Latch,則B就需要等待。則B就會在 sys.dm_os_waiting_tasks表現出等待類型爲PAGELATCH_EX的等待。
具有非聚集索引的小表上進行隨機插入導致的Latch爭用
將表當做臨時隊列結構使用,通常會出現這種場景。在滿足下面的條件時,就可能出現Latch爭用(包括EX和SH):
高併發的INSERT,DELETE,UPDATE和SELECT操作
頁密度較大,行較窄
表的行數較少,所以B樹也較淺,索引深度在2~3級。
較淺的B樹上執行大量隨機的INSERT極可能導致頁拆分。執行頁拆分時,需要在B樹所有層級上獲取SH Latch和在數據發生修改的所有頁上獲取EX Latch。在非常高併發的INSERT和DELETE情況,還極可能導致B樹ROOT頁發生拆分。ROOT頁拆分會導致Non-Buffer Latch:ACCESS_METHODS_HBOT_VIRTUAL_ROOT。
可以通過下面的腳本觀察表的索引深度:
select o.name as [table], i.name as [index],
indexProperty(object_id(o.name), i.name, 'indexDepth')
+ indexProperty(object_id(o.name), i.name, 'isClustered') as depth, --clustered index depth reported doesn't count leaf level
i.[rows] as [rows], i.origFillFactor as [fillFactor],
case (indexProperty(object_id(o.name), i.name, 'isClustered'))
when 1 then 'clustered'
when 0 then 'nonclustered'
else 'statistic'
end as type
from sysIndexes i
join sysObjects o on o.id = i.id
where o.type = 'u'
and indexProperty(object_id(o.name), i.name, 'isHypothetical') = 0 --filter out hypothetical indexes
and indexProperty(object_id(o.name), i.name, 'isStatistics') = 0 --filter out statistics
order by o.name
PFS頁的Latch爭用
這屬於分配瓶頸的一種情況。PFS記錄着數據頁的空間使用情況。PFS頁上使用1個字節(Byte)表示一個頁的使用情況。一個PFS頁可以表示8088個數據頁,所以每8088個數據頁就會有一個PFS頁。一個數據文件的第二個頁就是PFS頁(PageID=2)。
當需要分配空間給新對象或者數據操作,SQL Server會在PFS頁上獲取SH Latch查看目標區區是否有可用的頁。如果有,則會在PFS上獲取 UP Latch,並更新對應頁的空間使用信息。類似的過程也會發生在SAM,GSAM頁上。在多CPU的系統上,文件組中只有很少的數據文件,過多的PFS頁請求,則可能導致Latch爭用。邊種場景在Tempdb中會相對較常見些。
當在Tempdb的PFS或者SGAM頁上出現較多PATHLATCH_UP等待時,可以採取下面的方法消除Latch爭用:
增加Tempdb的數據文件個數,個數=CPU的核心數
啓用跟蹤標記TF 1118
Tempdb中的表值函數導致的Latch爭用
這個發生的原因跟PFS的Latch爭用一樣。每次調用多語句的表值函數總要創建和刪除表變量,當在一個查詢的多次引用多語句的表值函數時就可能會生很多的表變量創建和刪除操作,從而導致Latch爭用。
解決不同模式下的Latch爭用
使用已有的列將數據分佈到所有的索引鍵區間
在銀行ATM系統場景中考慮使用ATM_ID將對交易表的INSERT操作分佈到所有鍵值區間。因爲一個用戶同一時間只能在使用一臺ATM。這樣就將銷售系統的場景中,可以考慮使用Checkout_ID列或者Store_ID列。這種方式需要使用唯一複合索引。這個複合索引的首鍵通常使用被選定的標識列或者某些列經過Hash的計算列,然後再結合其它列共同組成。使用Hash計算列會較優,因爲標識列的唯一值會太多,造成鍵值區間過多,也會讓表數據的物理結構變差。雖然Hash計算列的索引鍵值區間較少,但對於分散INSERT負載減少Latch爭用而言,已經足夠了。比如銷售系統中,可以使用Store_ID對CPU核數取模做爲Hash值。
這種方式會加大索引碎片,降低範圍掃描的性能。可能還需要修改應用架構,例如語句的WHERE需要根據新的索引結構進行調整。
示例:在一個32核心的系統中的交易表
原表結構:
create table table1
(
TransactionID bigint not null,
UserID int not null,
SomeInt int not null
)
go
alter table table1
add constraint pk_table1
primary key clustered (TransactionID, UserID)
go
方式1.:使用UserID做爲索引首鍵,將INSERT操作分佈到所有數據頁上。需要注意的是:索引修改後,所有SELECT的WHERE等式中需要同時指定UserID和TransactionID。
create table table1
(
TransactionID bigint not null,
UserID int not null,
SomeInt int not null
)
go
alter table table1
add constraint pk_table1
primary key clustered (UserID, TransactionID)
go
方式2:使用TransactionID對CPU核數取模做爲索引首鍵,將INSERT操作較均勻分佈到表上。
create table table1
(
TransactionID bigint not null,
UserID int not null,
SomeInt int not null
)
go
-- Consider using bulk loading techniques to speed it up
ALTER TABLE table1
ADD [HashValue] AS (CONVERT([tinyint], abs([TransactionID])%(32))) PERSISTED NOT NULL
alter table table1
add constraint pk_table1
primary key clustered (HashValue, TransactionID, UserID)
go
使用GUID列做爲索引首鍵
這個我本人非常不認同,所以也不想分析了,只是爲了維護完整性加在這裏。併發負載和Latch爭用嚴重到需要GUID這種極端的方式來分散負載時,分表、分區或者使用Redis類產品纔是更好的方法。
使用計算列對錶進行Hash分區
表分區能減少Latch爭用。使用計算列對錶進行Hash分區,一般的步驟:
新建或者使用現有的文件組承載各分區
如果使用新文件組,需要考慮IO子系統的優化和文件組中數據文件的合理佈局。如果INSERT負載佔比較高,則文件組的數據文件個數建議爲物理CPU核心數的1/4(或者1/2,或者相等,視情況而定)。
使用CREATE PARTITION FUNCTION將表分成N個分區。N值等於上一步的數據文件的個數。
使用CREATE PARTITION SCHEME綁定分區函數到文件組,然後再添加一個smallint或者tinyint類型的Hash列,再計算出合適的Hash分佈值(例如HashBytes值取模 或者取Binary_Checksum值)。
示例代碼:
--Create the partition scheme and function, align this to the number of CPU cores 1:1 up
to 32 core computer
-- so for below this is aligned to 16 core system
CREATE PARTITION FUNCTION [pf_hash16] (tinyint) AS RANGE LEFT FOR VALUES
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
CREATE PARTITION SCHEME [ps_hash16] AS PARTITION [pf_hash16] ALL TO ( [ALL_DATA] )
-- Add the computed column to the existing table (this is an OFFLINE operation)
-- Consider using bulk loading techniques to speed it up
ALTER TABLE [dbo].[latch_contention_table]
ADD [HashValue] AS (CONVERT([tinyint], abs(binary_checksum([hash_col])%(16)),(0)))
PERSISTED NOT NULL
--Create the index on the new partitioning scheme
CREATE UNIQUE CLUSTERED INDEX [IX_Transaction_ID]
ON [dbo].[latch_contention_table]([T_ID] ASC, [HashValue])
ON ps_hash16(HashValue)
分區後,邏輯上INSERT仍然集中到表尾部,但是Hash分區將INSERT分散到各分區的B樹的尾部。所以能減少Latch爭用。
使用Hash分區消除INSERT的Latch爭用,需要權衡的事項:
通常SELECT語句的查詢謂詞部分需要修改,使其包含Hash分區列。這會導致查詢計劃的分區消除不可用。
某些其它查詢(如基於範圍查詢的報表)也不能使用分區消除。
當分區表與另一個表JOIN時,如果使用分區消除,則另一個表需要在同樣的鍵上實現Hash分區並且Hash鍵需要包括在JOIN條件裏。
Hash分區會使滑動窗口歸檔和分區歸檔功能不可用。
總結
1. 本文以SQLCAT的Latch Contention白皮書爲基礎,結合Paul Randal關於Latch的博文,以及本人的經驗而成。
2. Buffer Latch Contention較容易定位和處理。Non-Buffer Latch 是較爲棘手的,因爲太少關於此類型Latch的說明資料,造成有時定位到類型,也不知道它是什麼意思,無從下手。