2018年8月30日 星期四

交易紀錄檔 VLF的數量對效能影響


在SQLSERVER中,不論DB在哪種復原模式下,所有的異動都會寫入交易紀錄檔。差別在於紀錄的方式以及清除的方式不同。因此,如果交易紀錄檔的效能不好,其影響是全面性的。

SQLSERVER會將交易紀錄檔(transaction log file / ldf)劃分成多個「虚擬紀錄檔(Virtual Log File - VLF)來管理。
SQL2014以前的版本(不含2014),當建立、手動或自動成長LDF檔時,系統會將其劃分成數個VLF,如下表所示:

增量的大小
VLF數
< 64MB
4
>=64MB and < 1GB
8
>=1GB
16

在SQL2014,則有不同的演算法:
如果成長的量小於現行log file size的1/8,那麼就只增加一個VLF。
如果大於現行log file size的1/8,那麼則依照舊有的規則。

我們可以使用dbcc loginfo來查看VLF的資訊,這也是目前唯一的方法。
以下圖為例,該資料庫的交易紀錄檔(*.ldf),共被劃分為10個VLF來管理。


所以,假設你建立資料庫時,LDF檔初始大小設為10MB,那麼資料庫建立後,LDF檔會被切割為4個VLF。如果有設定自動成長,每次成長10MB,那麼每成長一次,就會再多出4個VLF。
在一個繁忙的系統中,交易紀錄檔的初始大小如果不夠大,在系統運作的過程中,則會經常觸發自動成長。每次成長,系統就會將該空間依前述的規則切成數個VLF,因此VLF的數量也會增加。
自動成長所分配的空間,加入到原本的ldf檔的尾部。在邏輯上它會是連續的。但從實體上看,則整個ldf檔會是由許多不連續的空間所組成。因此,如果你的VLF的數量越多,表示你的交易紀錄檔(ldf)破碎的越嚴重。通常這被稱為 Transaction Log Fragmentation - 交易紀錄碎片,或者 VLF Fragmentation 。

我們知道,一般資料檔的碎片,會影響效能,所以我們通常會定期的檢查並重整。

但~~~對於交易紀錄檔的碎片呢?

或許大家也跟我初次聽到時一樣~~~「啥….交易紀錄也有碎片…!!!」

交易紀錄碎片的定義是比較粗糙的,一般是使用交易紀錄檔中VLF的數量來評估。也就是VLF的數量越多,表示交易紀錄的碎片越嚴重。
至於VLF的數量超過多少個才應該進行一些處理,這並沒有一個確切的標準可供遵循。一般的建議是VLF不要超過1000個。

我們可以使用下列指令,來取得所有DB的VLF的計數:

DECLARE @query varchar(1000),
  @dbname varchar(1000),
  @count int
SET NOCOUNT ON
DECLARE csr CURSOR FAST_FORWARD READ_ONLY
FOR
SELECT name
FROM sys.databases

CREATE TABLE ##loginfo
(
  dbname varchar(100),
  num_of_rows int)
OPEN csr
FETCH NEXT FROM csr INTO @dbname
WHILE (@@fetch_status <> -1)
BEGIN

CREATE TABLE #log_info
(
  RecoveryUnitId tinyint,
  fileid tinyint,
  file_size bigint,
  start_offset bigint,
  FSeqNo int,
[status] tinyint,
  parity tinyint,
  create_lsn numeric(25,0)
)

SET @query = 'DBCC loginfo (' + '''' + @dbname + ''') '
INSERT INTO #log_info
EXEC (@query)
SET @count = @@rowcount
DROP TABLE #log_info
INSERT ##loginfo
VALUES(@dbname, @count)
FETCH NEXT FROM csr INTO @dbname

END

CLOSE csr
DEALLOCATE csr

SELECT dbname,
  num_of_rows
FROM ##loginfo
--WHERE num_of_rows >= 50
ORDER BY dbname

DROP TABLE ##loginfo

接下來,我們實際測試一下,交易紀錄的碎片真的對效能有影響嗎?

首先我建立兩個DB:
VLF_LARGE:模擬有許多的VLF,它的LOG的初始值10MB,然後用alter database的指令不斷的加LOG空間,以模擬自動成長,目地是增加VLF的數量。
VLF_SMALL:模擬有較少的VLF,它的LOG的初始值直接給4GB。

 
VLF_SMALL
VLF_LARGE
資料檔大小
4GB
4GB
資料檔路徑
H:\SQLData
H:\SQLData
LOG檔大小
4GB
4GB
LOG檔路徑
G:\SQLLog
G:\SQLLog
復原模式
Full
Full
VLF數量
16
4,008

接著我在兩個DB各建立一個空的TABLE,然後測試從另一個DB的TABLE(300萬筆),將資料一次性新增進來。並且再測試update及delete。

經過四輪的測試,結果如下表
DML
VLF_SMALL
VLF_LARGE
1
2
3
4
平均
1
2
3
4
平均
INSERT
24
18
18
17
19.25
16
24
23
25
22
UPDATE
5
4
7
5
5.25
9
8
10
14
10.25
DELETE
7
5
11
7
7.5
17
10
21
15
15.75



從結果來看,VLF Fragmentation確實對這種大資料量的操作效能有所影響,我這個案例是測試300萬筆,是一個較大IO的操作。

由於交易紀錄是循序性的寫入,本文所測試的這種大資料量的操作,可能一次的操作所產生的交易紀錄量,就會跨許多不同的VLF才能儲存,因此若VLF碎片過高,會影響寫入的效能。

如果是小資料量的操作(如OLTP),由於交易紀錄量小(一次交易量不超過1個VLF),則不受VLF碎片的影響。

上述的測試,其DB後端storage是傳統的旋轉式硬碟。

有趣的是,後來我改用本機的SSD硬碟進行上述測試,則不論VLF_LARGE或VLF_SMALL,兩者對於DML的效能幾乎完全一致,也就是說如果DB的儲存媒體是SSD,則DML的效能不受VLF Fragementation的影響。這跟索引碎片在SSD的結果類似,碎片之所以在傳統旋轉式硬碟上會影響效能,主要是磁頭的物理移動頻繁所致,而SSD由於其存取的方式不同,所以沒有這個問題。




VLF數量對recovery效能的影響
VLF數量的多寡,對於recovery的影響則是巨大的,如果你的資料庫在啟動時需要很久才會online,且狀態處於recovery pending,那麼這篇文章會對你有所幫助。

我們做一個簡單的測試,有興趣的朋友也可以跟著試試看。

CREATE DATABASE [VLF_SMALL]
 ON  PRIMARY
( NAME = N'VLF_SMALL', FILENAME = N'H:\SQLData\VLF_SMALL.mdf' , SIZE = 2GB , FILEGROWTH = 1024KB )
 LOG ON
( NAME = N'VLF_SMALL_log', FILENAME = N'G:\SQLLog\VLF_SMALL_log.ldf' , SIZE = 2GB , FILEGROWTH = 10%)
GO
CREATE DATABASE [VLF_LARGE]
 ON  PRIMARY
( NAME = N'VLF_LARGE', FILENAME = N'H:\SQLData\VLF_LARGE.mdf' , SIZE = 2GB , FILEGROWTH = 1024KB )
 LOG ON
( NAME = N'VLF_LARGE_log', FILENAME = N'G:\SQLLog\VLF_LARGE_log.ldf' , SIZE = 1MB , FILEGROWTH = 10%)
GO

use master
go
declare @x int
declare @ssql varchar(1024)
set @x=10
while (@x<=2048)
   begin
      set @x=@x+1
      set @ssql='alter database VLF_LARGE modify file ( name = ''VLF_LARGE_log'', size = '+convert(varchar,@x)+'MB )'
      exec (@ssql)
   end
go

dbcc loginfo('VLF_LARGE')   --查看VLF數量有8160個
go
dbcc loginfo('VLF_SMALL')   --查看VLF數量有16個
go

資料庫相關指標如下
 
VLF_SMALL
VLF_LARGE
資料檔大小
2GB
2GB
資料檔路徑
H:\SQLData
H:\SQLData
LOG檔大小
2GB
2GB
LOG檔路徑
G:\SQLLog
G:\SQLLog
復原模式
Full
Full
VLF數量
16
8,160


這兩個 DB都是空的,剛剛建立,其中沒有任何的user tables。接著我們將它offline後再online,並計算online所需的時間,指令如下:
declare @x datetime
alter database VLF_SMALL set offline

set @x=getdate()
alter database VLF_SMALL set online

select datediff(ss,@x,getdate())





由上圖可見,VLF_SAMLL的recovery僅需1秒鐘。而VLF_LARGE的recovery需49秒,兩者recovery的速度差異非常的大。

SQL SERVER在執行recovery時,一次掃瞄一個VLF,當VLF數目多,會有較頻繁的VLF切換,故導致recovery所需時間的增加。
當你的SQL SERVER啟動時DB StartUp需要太長的時間,請檢查DB的VLF數量是否太多。

VLF的數量及大小多少才適合,有幾個重要的參考指標:
一、每個VLF size不超過500MB。
二、在SQL2012當VLF數量超過1000個時,當SQL start、restore、attach時會有警告。
三、VLF的數量上限不要超過1000個。
四、VLF數量的甜蜜點(sweet spot)為100個。

當超過1萬個VLF時,如果DB有recovery的情況時,則系統會發出警告,但警告的內容會告訴你說DB超過1仟個VLF時可能導致long startup and backup效能問題。但其實只要有recovery的動作,如offline/online、restore都會跳訊息。訊息如下:
Database dbName has more than n virtual log files which is excessive. Too many virtual log files can cause long startup and backup times. Consider shrinking the log and using a different growth increment to reduce the number of virtual log

VLF的數量及大小,也是影響SQL SERVER效能的一個重要環節,就像是我們日常重整索引的碎片,VLF碎片也需要定期的關注。

下一篇文章,我想接著談談VLF碎片的解決方法以及交易紀錄檔的壓縮(Shrink)。

2017年5月17日 星期三

SQLSERVER wait type之SOS_SCHEDULER_YIELD

如同在「SQL Server的等待分析」中所說,SQLSERVER會給一個處於RUNNING的thread一個限量的時間(4ms),如果這個thread在這個限量的時間內沒有執行完畢。若這個thread不需等待任何資源,則會將這個thread退回到Queue中(RUNNABLE),即使目前沒有其它的thread在等侯CPU也是一樣。

SOS_SCHEDULER_YIELD嚴格的來說,它並不是一種等待,它只是線程在4ms時間內,無法完成工作而將CPU讓出來,以避免其它線程「餓死」的一種調度做法,所以它不會顯示在sys.dm_os_waiting_tasks中。它與一般的resource wait不同,它被稱為signal wait。如果系統中沒有其它的線程在runnable中,通常它的signal wait time的時間會很短,也就是很快又會回到running的狀態。

當一個thread超過限量時間,退回到runnable時,將被標示「SOS_SCHEDULER_YIELD」,直到它回到running狀態。這段時間將被紀錄為「訊號等待時間signal wait time」。

如果一個命令執行,耗用的CPU時間不超過4ms,那麼就不會有SOS_SCHEDULER_YIELD的出現,如果你看到許多次的SOS_SCHEDULER_YIELD,表示你的系統有許多命令耗用超過4ms的CPU。

當SOS_SCHEDULER_YIELD的等待次數很高,但總的等待時間卻不高,整體而言CPU並不忙碌。這通常是無害的,僅代表有較多的命令不能在4ms的限量時間完成。例如DBA關閉了平行處理(MAXDOP=1),也有可能造成此現象。

當SOS_SCHEDULER_YIELD的等待次數很高,而且總的等待時間也很高。這非常可能代表了CPU有壓力。表示你的系統有許多CPU密集型的查詢,例如許多複雜的SQL statement,如table scan、複雜或不符合SARG的查詢..等,你可能需要針對特定的語法或物件進行優化。如果伴隨著高CXPACKET的出現,則需要去調整平行處理的配置。(詳見:SQLSERVER wait type之CXPACKET)

當SOS_SCHEDULER_YIELD的等待次數不高,但總的等待時間卻很高。這種情況表示你SQLSERVER所在的主機,作業系統的其它程式在搶佔CPU的資源。如果你在同一台主機上同時安裝了SQLSERVER和AP伺服器(或其它非DB相關的軟體),就有可能發生這種現象。或者在作業系統開啟了省電功能,也可能導致此種現象。

2017年5月15日 星期一

SQLSERVER wait type之RESOURCE_SEMAPHORE

RESOURCE_SEMAPHORE
當SQL Server收到用戶查詢時,它首先建立一個已編譯(complied plan)的計劃,然後根據已編譯的計劃建立一個執行計劃(execution plan)。

當SQL Server建立一個已編譯的計劃時,它會計算兩個記憶體授予參數(memory grant parameters),稱為「所需記憶體required memory」和「額外記憶體additional memory」。所需記憶體是運行排序和雜湊連接(sort and hash join)所需的最小記憶體。這是必需的,因為如果沒有這個記憶體可用,查詢將不會啟動。
額外的記憶體是將臨時的資料行(temporary rows)存儲在記憶體中所需的記憶體量。這被稱為額外的,如果沒有足夠的可用記憶體,則可以將查詢存儲在磁碟上。

首先,SQLSERVER計算這個查詢執行需要多少記憶體。這通常是所需記憶體和額外記憶體的總和,但如果您的查詢正在使用平行處理,則所需的記憶體將為(所需記憶體* DOP)。再加上「額外記憶體」。這被稱為請求的記憶體(requested memory)。

SQL Server中有一個稱為RESOURCE SEMAPHORE的內部設備,它被用來授予請求的記憶體給每個查詢。如果記憶體不足以授予時,那麼該查詢將處於等待記憶體的狀態,其等待類型即為「RESOURCE_SEMAPHORE」。

我們可以通過SELECT * FROM sys.dm_exec_query_resource_semaphores

來查詢每個「資源集區resource pool」已授予或未授予的記憶體大小資訊。

resource_semaphore_id為 0,主要用於「一般查詢」。resource_semaphore_id為1,則用於小型查詢(所謂小型查詢是指請求的記憶體 ﹤5MB,且查詢成本 ﹤3)
,上圖中可以看到有100個查詢正在等待記憶體的授予。
通過SELECT * FROM sys.dm_exec_query_memory_grants可以查詢每個查詢的記憶體授予的詳細資料
grant_time及granted_memory_kb為NULL,表示正在等待授予記憶體。
我們也可以通過這些資訊,找到最耗用記憶體的查詢,並且透過上面指令所得到的sql_handle欄位值,去取得SQL語法。

RESOURCE_SEMAPHORE_QUERY_COMPILE
另外有個等待類型「RESOURCE_SEMAPHORE_QUERY_COMPILE」主要是在編譯執行計畫時,編譯這個動作也需要記憶體。
SQLSERVER將編譯可用的記憶體分為三級(3個gateway)「small、medium、big」,剛開始都是給予small gateway級別的記憶體,但如果一個複雜的語法,small記憶體無法滿足編譯所需,則會要求進入「更上一級」記憶體授予,直至big gateway。
每一級gateway可容許同時編譯的數量是有限制「Configured Unit」的,如果更上一級的gateway,因為限額已滿無法授予記憶體給此編譯動作,那麼將進入「等待」,此時的wait type即為「RESOURCE_SEMAPHORE_QUERY_COMPILE」。

SQLSERVER內建了3個編譯所需記憶體的關卡(gateway)「small、Medium、Big」,可以透過dbcc memorystatus得知。
1.small gateway:
configure unit 通常是cpu core*4。
2.Medium gateway:
configure unit 通常是cpu core*1。
3.Big gateway:
configure unit 通常是1。

Summary
RESOURCE_SEMAPHORE
表示目前記憶體可用空間不足以執行SQL語法。
解決方案:
一、調整SQL語法,特別是(order by或hash join)。
二、擴充記憶體大小。
三、調降MAXDOP。
四、清除或搬移舊資料至歷史區,降低ONLINE資料量。

RESOURCE_SEMAPHORE_QUERY_COMPILE
表示SQL語法過於複雜,或者複雜的SQL查詢太密集,系統Memory Gateway不足以編譯這些SQL。
解決方案:
一、調降SQL語法的複雜度。
二、增加CPU的core數。
三、避免過度使用WITH RECOMPILE。


SQLSERVER wait type之CXPACKET

CXPACKET這種等待類型(wait type),是SQLSERVER最常見的等待類型之一。
CXPACKET這個詞來自於「Class Exchange Packet」,可以把它解釋為平行線程(thread)間的資料交換。
當SQLSERVER決定使用平行處理來執行一句SQL statement時,會啟動一個協調控制線程(coordinator/control thread),線程編號為0,即thread 0,接著視所能使用的CPU核心數,分別啟動一個線程。
協調線程負責各線程間的協調,不參與SQL的執行。其餘的thread則按分配的工作同時執行。各個線程執行的時間有快有慢,先執行完的線程,需等待其它線程執行完成,這時的「等待」類型,即為CXPACKET。如下圖所示:
上圖各threads暗紅色部份的時間加總,即為執行此SQL statement的CXPACKET等待時間。

thread 0的CXPACKET的等待時間,其實就代表了整個SQL statement執行的時間。因此在平行處理的計畫中,CXPACKET是必然會發生的,即使各線程都同時完成。
通常的建議是「只要CXPACKET總時間不超過總等待時間的50%」,則不應把它視為一個問題而是一個指標。

我們可以使用下列語法,查詢各等待事件所佔的時間、佔比、累進佔比
WITH Waits AS
 (
 SELECT 
   wait_type, 
   wait_time_ms / 1000. AS wait_time_s,
   100. * wait_time_ms / SUM(wait_time_ms) OVER() AS pct,
   ROW_NUMBER() OVER(ORDER BY wait_time_ms DESC) AS rn
 FROM sys.dm_os_wait_stats
 WHERE wait_type 
   NOT IN
     ('CLR_SEMAPHORE', 'LAZYWRITER_SLEEP', 'RESOURCE_QUEUE',
   'SLEEP_TASK', 'SLEEP_SYSTEMTASK', 'SQLTRACE_BUFFER_FLUSH', 'WAITFOR',
   'CLR_AUTO_EVENT', 'CLR_MANUAL_EVENT')
   ) -- filter out additional irrelevant waits
   
SELECT W1.wait_type,
 CAST(W1.wait_time_s AS DECIMAL(12, 2)) AS wait_time_s,
 CAST(W1.pct AS DECIMAL(12, 2)) AS pct,
 CAST(SUM(W2.pct) AS DECIMAL(12, 2)) AS running_pct
FROM Waits AS W1
 INNER JOIN Waits AS W2 ON W2.rn <= W1.rn
GROUP BY W1.rn, 
 W1.wait_type, 
 W1.wait_time_s, 
 W1.pct

HAVING SUM(W2.pct) - W1.pct < 95; -- percentage threshold;
CXPACKET雖然是因平行處理而產生,但產生高的CXPACKET並不一定是「平行處理的問題」,常常它代表了其它問題的症狀。

例如CXPACKET如果伴隨著高的PAGEIOLATCH_*(可能還會見到IO_COMPLETION, ASYNC_IO_COMPLETION),這很大的背後原因是表示I/O子系統無法滿足平行處理的需求。這種情況下,你如果去調整最大並行度,或許會看到CXPACKET降低了,但其實你的系統效能(AP執行的反應時間)並不會得到提昇,問題沒有得到根本的解決。
這種情況,應首先考慮I/O的優化。
當然,直接去昇級I/O的硬體(例如換成SSD)也是一種方法,只是這有點強人所難了;我們可以針對相關的SQL語法/Table/index等進行優化(例如索引未匹配、碎片整理..等),以儘量減少I/O,然後再做觀察。

另外,是否會採用平行處理,以及平行處理的各個工作線程(如上圖的thread 1~4)的工作量分配,均是由系統依據統計值計算而來。如果統計值是正確的,那麼平行處理各工作線程的工作分配通常會較為平均,其CXPACKET的等待總時間應該不會太高。如果統計值過期,那就有可能不當的啟用平行處理,或者造成各線程工作分配不均,形成較大的CXPACKET等待。面對CXPACKET,也可以先嘗試由統計值更新著手。

當高CXPACKET伴隨著高SOS_SCHEDULER_YIELD及LACTH_*的時候,這種情況表示「平行處理」確實是問題了,這時候你可能需要去調整系統的平行處理組態。

SQL SERVER的平行處理程度,主要受下列兩個選項的控制
cost threshold for parallelism (平行處理原則的成本臨界值)
SQL SERVER執行SQL前,先估計SQL所需的時間成本(單位為秒),如果大於上述臨界值(預設值為5),那麼就會採用平行處理來執行SQL語法。這個值該不該調整,該調成多少?讀者們可以參考下列兩篇文章(感謝PASS的好友Rock提供):

max degree of parallelism (平行處理原則的最大程度)
如果SQL SERVER決定採用平行處理,將依照max degree of parallelism選項所設定的值(threads)同時執行。0:代表無限制(最大限度)。

max degree of parallelism要設定為多少才是適當的,這個有許多種說法。主要還是要看實際的系統狀況而定。底下的表格僅為個人經驗的建議值。
上面的表格只是我個人的初始性建議,之後最好視系統實際的情況,觀察相關的效能參數,做出符合實際環境的調整。