當一切正常時,沒有必要特別留意什么是事務日志,它是如何工作的。你只要確保每個數據庫都有正確的備份。當出現問題時,事務日志的理解對於采取修正操作是重要的,尤其在需要緊急恢復數據庫到指定點時。這系列文章會告訴你每個DBA應該知道的具體細節。
對於日志文件的最大日志吞吐量,我們從存儲架構思路的簡單回顧開始,然后進一步看下日志碎片如何影響需要日志讀取操作的性能,例如日志備份,或者故障恢復過程。
最后,我們會談下在日志大小和增長管理的最佳實踐,還有對過渡日志增長和碎片的正確處理。
物理架構
正確的物理硬件和架構會幫你保證日志吞吐量的最大可能,還有一些“黃金法則”。在這之前,已經有人談過了,尤其是Kimberly Tripp在她的《8步走向更好的事務日志吞吐》里,因此在這里不會更深入的探討這個話題。
要注意的是,對於日志文件,在設計內在物理結構時,我們的首要目標是最優日志寫吞吐量。對於每個添加、刪除或修改數據的事務,SQL Server寫入日志,這也包括數據庫維護操作,例如索引重建或重組,統計信息更新等等。
你只需要一個日志文件
從多個日志文件,在日志吞吐量方面,不會獲得性能。SQL Server不會並行寫入多個日志文件。只有一個情況SQL Server會寫入所有日志文件,那是當在每個日志文件里更新文件頭時,SQL Server寫入它來更新不同的LSN,例如最后檢查點,最早打開的事務,最后一次日志備份等等。當只更新文件頭時,很多人會誤認為SQL Server會寫入所有日志文件。
如果一個數據庫有4個日志文件,SQL Server會寫入日志到日志文件1,直到滿了,然后日志文件2,日志文件3和日志文件4,然后嘗試繞回重新寫入日志文件1。我們可以通過創建有多個日志文件(或通過對現存數據庫增加更多文件)的數據庫來驗證下。代碼8.1創建一個Person數據庫,有1個主數據文件和2個日志文件,每個在不同的硬盤上。
數據和備份文件位置
這篇文章里的例子都假設數據和日志文件位於D:\SQLData,所有備份位於:D:\SQLBackups,各自不同的位置。當運行這些例子時,直接修改這些位置到你系統合適的位置(並且注意,在實際的系統中,我們不會在同個硬盤上存儲數據和日志文件)。
注意對於這些數據和日志文件的大小和文件增長率設置,我們是基於AdventureWorks2008的:
1 USE master 2 GO 3 IF DB_ID('Persons') IS NOT NULL 4 DROP DATABASE Persons; 5 GO 6 7 CREATE DATABASE [Persons] ON PRIMARY 8 ( NAME = N'Persons' 9 , FILENAME = N'D:\SQLData\Persons.mdf' 10 , SIZE = 199680KB 11 , FILEGROWTH = 16384KB 12 ) 13 LOG ON 14 ( NAME = N'Persons_log' 15 , FILENAME = N'D:\SQLData\Persons_log.ldf' 16 , SIZE = 2048KB 17 , FILEGROWTH = 16384KB 18 ), 19 ( NAME = N'Persons_log2' 20 , FILENAME = N'C:\SQLData\Persons_log2.ldf' 21 , SIZE = 2048KB 22 , FILEGROWTH = 16384KB 23 ) 24 GO 25 26 ALTER DATABASE Persons SET RECOVERY FULL; 27 28 USE master 29 GO 30 BACKUP DATABASE Persons 31 TO DISK ='D:\SQLBackups\Persons_full.bak' 32 WITH INIT; 33 GO
代碼8.1:創建有2個日志文件的Persons數據庫。
接下來,代碼8.2創建一個范例Persons表。
1 USE Persons 2 GO 3 IF EXISTS ( SELECT * 4 FROM sys.objects 5 WHERE object_id = OBJECT_ID(N'dbo.Persons') 6 AND type = N'U' ) 7 DROP TABLE dbo.Persons; 8 GO 9 10 CREATE TABLE dbo.Persons 11 ( 12 PersonID INT NOT NULL 13 IDENTITY , 14 FName VARCHAR(20) NOT NULL , 15 LName VARCHAR(30) NOT NULL , 16 Email VARCHAR(7000) NOT NULL 17 ); 18 GO
代碼8.2:創建Persions表。
現在,我們會增加15000行到表,並運行DBCC LOGINFO。注意在我們的測試里,我們從AdventureWorks2005數據庫里的Person.Contact表里的數據來插入。你也可以使用AdventureWorks2008或AdventureWorks2012數據庫。
1 INSERT INTO dbo.Persons 2 ( FName , 3 LName , 4 Email 5 ) 6 SELECT TOP 15000 7 LEFT(aw1.FirstName, 20) , 8 LEFT(aw1.LastName, 30) , 9 aw1.EmailAddress 10 FROM AdventureWorks2005.Person.Contact aw1 11 CROSS JOIN AdventureWorks2005.Person.Contact aw2; 12 GO 13 14 USE Persons 15 GO 16 DBCC LOGINFO;
代碼8.3:插入數據並檢查VLF
SQL Server在主日志文件里(日志文件2)連續插入VLF,接下來插入第2個日志文件(日志文件3)。而且自動增長了主日志文件(SQL Server如何自動增長事務日志的,可以查看這個系列的第2篇文章)。
如果我們繼續增加記錄。SQL Server會繼續增長需要的2個文件,按順序填充VLF,一次一個VLF。圖8.1展示在重新運行代碼8.3,增加95000行后的情形(合計增加了110000行)。現在對於主日志文件,我們有12個VLF,對於第2個日志文件我們有8個VLF。
圖8.12個日志文件的連續使用。
在這個情況里,讀取日志的任何操作都會始於主日志里的4個VLF塊開始(FSeqNo 36-39),接下來在第2個日志文件里的4個塊(FSeqNo 40-43),再接下來是主日志里的4個塊,以此循環。這是為什么多個日志文件會降低I/O效率,我們會在下個環節進一步討論。
增加額外日志文件的唯一原因是一個例外場景,例如,磁盤上的日志文件滿了(看下第6篇),我們又臨時需要日志空間,增加額外日志文件是我們讓SQL Server退出只讀模式的最快方法。但是,一旦額外文件不需要后我們應該將它移除,我們稍后會討論,在 如果出錯了我們該怎么辦 部分。
對於日志文件使用專用硬盤/磁盤陣列
把數據文件與日志文件分別放在不同的硬盤,有很多理由來解釋它為什么是個很好的做法。首先,在硬盤故障時,這個架構提供更好的恢復機會。例如,如果存儲數據文件的陣列遭受災難性故障,但日志文件不會和它一起沉船。我們還是有機會進行尾日志備份,把數據庫恢復到非常接近災難發生的時間點(看下第5篇)。
其次,分離數據文件I/O和日志文件I/O可以用來優化I/O效率。SQL Server數據庫同時進行隨機和順序I/O操作。順序I/O是SQL Server可以讀寫塊,而不需要請求磁盤上磁頭的重新定位。SQL Server使用順序I/O進行預讀操作,還有所有的事務日志操作,使用傳統硬盤來說,它是最快的I/O類型。
隨機I/O是讀寫塊時,需要請求磁盤頭改變在磁盤上的位置。這會導致尋求延遲I/O,相對順序I/O,同時降低輸出(MB/s)和性能(IOPS)。一般來說讀操作,尤其在OLTP系統里,是隨機I/O,順序讀取相關的頁小塊是隨機I/O請求的一小部分。
從主順序I/O里分離主要的隨機I/O,我們避免了2者之間的沖突,提高全局的I/O效率。更進一步,對於日志文件的優化配置並不必和數據文件一樣。通過分離數據和日志文件,我們可以針對I/O活動類型為每個I/O子系統進行合適配置。例如,選擇優化的RAID配置作為磁盤陣列(下一部分會詳談)。
最后,要提的是在專門磁盤/陣列上的單個日志文件允許磁頭保持剛好穩定,因為SQL Server是順序寫入日志的。但是,在單個磁盤/陣列上的多個日志文件,磁頭會在每個日志間跳躍;我們沒有順序寫入,沒有磁盤搜索的話,因此我們降低了順序I/O的效率。
理想的情況,每個數據庫都應該在專門的磁盤陣列上有一個日志文件,但是很多系統這個並不現實,只是個理想。
由於同樣的原因,與順序I/O效率相關,在我們創建日志文件前,我們要對物理硬盤磁盤碎片整理下,這非常重要。
可能的話,對於日志硬盤,使用RAID 10
RAID,獨立磁盤冗余陣列(Redundant Array of Independent Disks)的縮寫,是用來實現下列目標的技術:
- 提高I/O性能級別,用每秒輸入/輸出操作衡量(Input/Output Operations Per Second(IOPS)),單位大致是(MB/秒/IO以KB為單位的大小)*1024
- 提高I/O吞吐量,用MB/秒來衡量,單位大致是(IOPS * IO以KB為單位的大小)/1024
- 提高在單個硬盤里的可用存儲量——你現在還不能購買5TB的單個硬盤,但你可以通過在RAID 5陣列里的6個1TB的硬盤,在操作系統里擁有5TB的硬盤。
- 獲得數據冗余,通過在多個硬盤分布存儲部分信息,或者在陣列里使用物理硬盤鏡像。
RAID級別的選擇很大程度上取決於磁盤必須支持的工作量,如剛才討論的,對於數據和日志文件的具體不同I/O工作量,意味着在每個情況下會有不同的RAID配置。
在I/O吞吐量和性能方面,我們應該努力優化日志文件陣列的順序寫。很多專家認為RAID 1 + 0就這一點而言是最佳選擇,盡管這個是以GB存儲空間最貴花費。
深入RAID
每個RAID級別優劣的完整參考不是這個系列文章的討論范圍。了解更多信息,我們請你參考《SQL Server故障排除(Troubleshooting SQL Server)》的第2章。
RAID 1+0是個內嵌的RAID,被稱為“鏡像條帶 ”。它通過每個硬盤的第一鏡像提供冗余,即RAID 1,然后使用RAID 0條帶化這些鏡像硬盤來提高性能。由於只有磁盤的一半空間可以使用,所以會大大增加成本。然后,這個配置提高冗余的最佳配置,因為即使多個硬盤損壞,系統還是正常運行的,也不會降低系統性能。
常見的更實惠的備用方法是RAID 5,"部分條帶",在多個硬盤間條帶數據,如RAID 0,只存儲部分數據,提供單個磁盤損壞保護。對於同樣的存儲,與RAID 1 + 0比,RAID 5需要更少的磁盤,且提供優異的讀性能。但是,維護部分化數據引發了寫性能上的損失。對於當下的存儲陣列,這只是個小問題,這是對於事務日志文件,很多DBA不推薦它的原因,因為它主要進行的是順序寫,要求最小可能的寫延遲。
假設,如我們剛才建議的,你能隔離每個數據庫日志文件在特定的磁盤陣列,至少對於那些有最大I/O工作量的數據庫,對於這些陣列是可以使用更昂貴的RAID 1 + 0,對於更小I/O工作量的數據庫可以使用RAID 5或RAID 1。
了解下不同RAID級別提供的I/O性能的情況,邪獵的3個可用配置是針對進行混合隨機讀寫操作性能平衡的400G的數據庫,對於SQL Server,連同理論上的I/O輸出率,基於64K的隨機I/O工作量。
- RAID 1使用1個15K RPM的600G硬盤=>11.5MB/秒,185 IOPS
- RAID 5使用5個15K RPM的146G硬盤=>22MB/秒,345 IOPS
- RAID 1 0使用14個15K RPM的73G硬盤=>101M/秒,1609 IOPS
請注意,這些值都是理論上的,在給出配置里盡基於硬盤的潛在I/O工作量。不考慮其他可能因素,對全局的工作量的影響,包括RAID控制器緩存大小和配置,RAID條帶大小,硬盤分區對齊,NTF格式分配單元大小。確保你選擇硬盤配置的唯一方法要處理好工作量位置,即對你的數據庫的I/O子系統進行合適的基准驗證,尤其是使用率。
對SQL Server進行存儲配置基准驗證
對給出的配置有很多現存的工具進行I/O輸出的衡量,最常用的工具是SQLIO和IOmeter。另外,還有SQLIOSim,用來測試磁盤配置的可靠性和完整性。
日志碎片和讀取日志操作
如第2篇所談的,在內部SQL Server把日志文件分割為多個子文件,即所謂的虛擬日志文件(VLF)。對於一個日志文件,在創建時,SQL Server決定分配給它的VLF的個數的大小,每次日志增加時,然后增加決定好的VLF個數,基於自動增長率的大小,如下所示(盡管對於很小的增長率,有時候增加的VLF會小於4個):
- 小於64MB——每次自動增加會創建4個新的VLF
- 64MB至1GB——8個VLF
- 大於1GB——16個VLF
例如,如果我們創建一個64MB的日志文件,設置增長率是16MB,那么日志文件初始會有8個VLF,每個8MB大小,每次日志增長時,SQL Server會增加4個VLF,每個4MB大小。如果數據庫吸引了比預期更多的用戶,但是文件設置還是保持不變,當日志增長到10GB大小,增長了640倍時,會有超過2500個VLF。
另一方面,如果日志16GB大小,那么每次增長會增加16個VLF,每個1GB大小。使用大的VLF,我們會占用日志的大部分,SQL Server不能截斷,如果一些因素進一步延遲截斷,意味這日志還要增長,增長得更快。
秘訣是保持正確的平衡。推薦的最大增長大小是8GB(Paul Randal在他的《日志文件內部和維護》視頻里建議的)。相反的,增長率必須足夠大來避免不合理太多的VLF個數。
有2個主要原因來避免頻繁小的日志增長。一個如在第7篇里談到的,日志文件不能獲得即時文件初始化的優勢,因此在資源來說,和數據文件增長比,日志增長會相對昂貴。另一個是碎片日志會妨礙讀取日志操作的性能。
很多操作會需要讀取事務日志,包括:
- 完整,差異和日志備份——盡管只有后來會讀取大量的日志部分。
- 故障恢復過程——為了保持數據和日志的一致性,撤銷任何沒有提交的事務,重做任何已經提交,寫入日志但沒有寫入數據文件的事務(參考第1篇)
- 事務復制——當從發布者到訂閱者移動修改時,事務復制日志閱讀器讀取日志
- 數據庫鏡像——在鏡像數據庫上,當從主到鏡像傳送最近的改變時,日志會被讀取
- 創建數據庫快照——在運行故障恢復過程時需要讀取日志
- DBCC CHECKDB——當它運行時會創建數據庫快照
- 修改數據抓取——使用事務復制日志閱讀起來跟蹤數據修改
最后,在一個日志文件里多少個VLF才合適的問題取決與日志的大小。通常,微軟認為超過200個VLF可能會有問題,但在一個非常大的日志文件(例如500GB)只有200個VLF也會是個問題,VLF太大,限制了空間重用。
為了了解在日志讀取時,碎片日志大小的影響,我們會運行一些測試,來看看在廣泛閱讀日志的兩個過程的影響,即日志備份和故障恢復過程。
免責申明
接下來的測試無法反應現實中在服務器級別硬件上運行的多用戶數據庫,上面有特定的RAID配置等等。我們在安裝在虛擬機上,獨立的SQL Server 2008實例上運行。你的數據會不同,在速度慢的硬盤上測試效果會更明顯。我們只想簡單的演示下日志碎片問題的影響,還有如何調查這些潛在影響的方法。
最后注意,Linchi Shea已經演示了一個只有16個VLF和2000個VLF之間,數據修改性能上的影響。
日志備份影響
為了了解在日志備份上,碎片日志影響的大小,我們會創建Persons
Lots
數據庫,故意創建一個小的2M日志文件,強制它在非常小的增長率來創建特別的碎片日志。我們會插入一些數據,運行大的更新來生成很多日志記錄,然后運行日志備份來看看會花多少時間。然后我們在預制好正確大小的日志文件進行同樣的測試。
首先,我們創建Persons
Lots
數據庫,日志文件只有2M大小,自動增長率是2MB。
1 /* 2 mdf: initial size 195 MB, 16 MB growth 3 ldf: initial size 2 MB, 2 MB growth 4 */ 5 6 USE master 7 GO 8 IF DB_ID('PersonsLots') IS NOT NULL 9 DROP DATABASE PersonsLots; 10 GO 11 12 -- Clear backup history 13 EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'PersonsLots' 14 GO 15 16 CREATE DATABASE [PersonsLots] ON PRIMARY 17 ( NAME = N'PersonsLots' 18 , FILENAME = N'C:\SQLData\PersonsLots.mdf' 19 , SIZE = 199680KB 20 , FILEGROWTH = 16384KB 21 ) 22 LOG ON 23 ( NAME = N'PersonsLots_log' 24 , FILENAME = N'D:\SQLData\PersonsLots_log.ldf' 25 , SIZE = 2048KB 26 , FILEGROWTH = 2048KB 27 ) 28 GO 29 30 ALTER DATABASE PersonsLots SET RECOVERY FULL; 31 32 USE master 33 GO 34 BACKUP DATABASE PersonsLots 35 TO DISK ='D:\SQLBackups\PersonsLots_full.bak' 36 WITH INIT; 37 GO 38 39 DBCC SQLPERF(LOGSPACE) ; 40 --2 MB, 15% used 41 USE Persons 42 GO 43 DBCC LOGINFO; 44 -- 4 VLFs
代碼8.4:創建Persons
Lots
數據庫
現在我們在很小的增長率里進行日志增長,如代碼8.5所示,為了創建特別的碎片日志文件。
1 DECLARE @LogGrowth INT = 0; 2 DECLARE @sSQL NVARCHAR(4000) 3 WHILE @LogGrowth < 4096 4 5 BEGIN 6 7 SET @sSQL = 'ALTER DATABASE PersonsLots MODIFY FILE (NAME = PersonsLots_log, SIZE = ' + CAST(4096+2048*@LogGrowth AS VARCHAR(10)) + 'KB );' 8 EXEC(@sSQL); 9 SET @LogGrowth = @LogGrowth + 1; 10 END 11 USE PersonsLots 12 GO 13 DBCC LOGINFO 14 --16388 VLFs 15 16 DBCC SQLPERF (LOGSPACE); 17 -- 8194 MB, 6.3% full
代碼8.5:對數據庫Persons
Lots
創建非常大的碎片日志。
這里我們增長日志在4096增長率,總大小是8GB(4096+2048*4096KB)。日志增加了4096倍,每次增加4個VLF,移動有了4+(4096*4)=16388個VLF。
現在重新運行代碼8.2來重建Persons表,但這次在PersonLots數據庫,然后調整代碼8.3來在表里插入100萬條記錄。現在我們將更新Person表來創建很多日志記錄。取決於你機器配置,當你運行代碼8.6時,你可以泡上一杯咖啡。
1 USE PersonsLots 2 GO 3 /* this took 6 mins*/ 4 DECLARE @cnt INT; 5 6 SET @cnt = 1; 7 8 WHILE @cnt < 6 9 BEGIN; 10 SET @cnt = @cnt + 1; 11 UPDATE dbo.Persons 12 SET Email = LEFT(Email + Email, 7000) 13 END; 14 15 DBCC SQLPERF(LOGSPACE) ; 16 --8194 MB, 67% used 17 DBCC LOGINFO; 18 -- 16388 VLFs
代碼8.6:在Persons表上的一個大更新。
最后,我們可以進行一次日志備份看看會花多少時間。我們在備份代碼后包含了注釋掉的備份統計信息。
1 USE master 2 GO 3 BACKUP LOG PersonsLots 4 TO DISK ='D:\SQLBackups\PersonsLots_log.trn' 5 WITH INIT; 6 7 /*Processed 666930 pages for database 'PersonsLots', file 'PersonsLots_log' on file 1. 8 BACKUP LOG successfully processed 666930 pages in 123.263 seconds (42.270 MB/sec).*/
代碼8.7:PersonsLots的日志備份(碎片日志)
作為比較,我們重復同樣的測試,但這次我們會仔細調整數據庫日志大小,讓它有合理的數目的大小合適的VLF。在代碼8.8,我們重建Persons數據庫,初始日志大小為2GB(16個VLF,每個128M大小)。然后我們人為增長日志,只有3步就到8GB大小,包含64個VLF(每個128M的大小)。
1 USE master 2 GO 3 IF DB_ID('Persons') IS NOT NULL 4 DROP DATABASE Persons; 5 GO 6 7 CREATE DATABASE [Persons] ON PRIMARY 8 ( NAME = N'Persons' 9 , FILENAME = N'C:\SQLData\Persons.mdf' 10 , SIZE = 2097152KB 11 , FILEGROWTH = 1048576KB 12 ) 13 LOG ON 14 ( NAME = N'Persons_log' 15 , FILENAME = N'D:\SQLData\Persons_log.ldf' 16 , SIZE = 2097152KB 17 , FILEGROWTH = 2097152KB 18 ) 19 GO 20 USE Persons 21 GO 22 DBCC LOGINFO; 23 -- 16 VLFs 24 25 USE master 26 GO 27 ALTER DATABASE Persons MODIFY FILE ( NAME = N'Persons_log', SIZE = 4194304KB ) 28 GO 29 -- 32 VLFs 30 31 ALTER DATABASE Persons MODIFY FILE ( NAME = N'Persons_log', SIZE = 6291456KB ) 32 GO 33 -- 48 VLFs 34 35 ALTER DATABASE Persons MODIFY FILE ( NAME = N'Persons_log', SIZE = 8388608KB ) 36 GO 37 -- 64 VLFs 38 39 ALTER DATABASE Persons SET RECOVERY FULL; 40 41 USE master 42 GO 43 BACKUP DATABASE Persons 44 TO DISK ='D:\SQLBackups\Persons_full.bak' 45 WITH INIT; 46 GO
代碼8.8:創建Persons數據庫並人為增長日志。
現在重新運行代碼8.2,8.3(有100萬條記錄)和8.6和我們剛才測試的一樣。你會發現,沒有發生日志增長。
代碼8.6運行得很快(在我們的測試里,只要一半的時間)。最后,重新運行日志備份。
1 USE master 2 GO 3 BACKUP LOG Persons 4 TO DISK ='D:\SQLBackups\Persons_log.trn' 5 WITH INIT; 6 7 /*Processed 666505 pages for database 'Persons', file 'Persons_log' on file 1. BACKUP LOG successfully processed 666505 pages in 105.706 seconds (49.259 MB/sec). 8 */
代碼8.9:Persons數據庫的日志備份(無碎片日志)
在日志備份的影響相對小,對這個大小的日志是可復寫的,與只有64個的,14292個VLF的日志,備份時間有近15-20%的增長,當然,這個是相對於小數據庫(固然有很嚴重的日志碎片)。
故障恢復影響
在這些測試里,我們調查在故障恢復上碎片的影響,因為這個過程需要SQL Server讀取活動日志,重做或撤銷需要的日志記錄來返回數據庫到一致的狀態。
大量重做
在第一個例子里,我們重用PersonsLots數據庫,刪除並重建,設置恢復模式為完整,進行完整備份然后插入100萬條記錄,如剛才所示代碼。
現在,在我們更新這些行前,我們將禁止自動化檢查點。
絕不禁止自動化檢查點!
這里我們這樣做純粹是測試為目的。在任何正常運行的SQL Server數據庫里我們絕不推薦禁止自動化檢查點。
當我們提交隨后的更新時,我們立即關閉數據庫,這樣的話,所有的更新已經寫入日志但沒有寫入數據文件。因此,在故障恢復期間,SQL Server會需要讀取所有相關的日志來重做所有的操作。
1 USE PersonsLots 2 Go 3 /*Disable Automatic checkpoints*/ 4 DBCC TRACEON( 3505 ) 5 6 /*Turn the flag off once the test is complete!*/ 7 --DBCC TRACEOFF (3505) 8 9 /* this took 5 mins*/ 10 BEGIN TRANSACTION 11 DECLARE @cnt INT; 12 13 SET @cnt = 1; 14 15 WHILE @cnt < 6 16 BEGIN; 17 SET @cnt = @cnt + 1; 18 UPDATE dbo.Persons 19 SET Email = LEFT(Email + Email, 7000) 20 END; 21 22 DBCC SQLPERF(LOGSPACE) ; 23 --11170 MB, 100% used 24 USE PersonsLots 25 GO 26 DBCC LOGINFO; 27 -- 22340 VLFs
代碼8.10:PersonsLots——禁止自動化檢查點,在顯性事務里運行更新。
現在我們提交事務,關閉數據庫。
1 /*Commit and immediately Shut down*/ 2 COMMIT TRANSACTION; 3 SHUTDOWN WITH NOWAIT
代碼8.11:提交事務,關閉SQL Server
在重啟SQL Server服務后,在恢復期間,嘗試訪問PersonsLots,你會看到如下信息。
1 USE PersonsLots 2 Go 3 /*Msg 922, Level 14, State 2, Line 1 4 Database 'PersonsLots' is being recovered. Waiting until recovery is finished.*/
代碼8.12:PersonsLots正在進行恢復操作。
在SQL Server開始恢復數據庫前,它需要打開日志,讀取每個VLF。因為多個VLF的影響會延伸到SQL Server重啟數據庫和開始恢復過程之間的時間。
因此,一旦數據庫是可訪問的,我們可以查看這2個事件之間的錯誤日志,即總的恢復時間。
1 EXEC sys.xp_readerrorlog 0, 1, 'PersonsLots' 2 3 /* 4 2012-10-03 11:28:14.240 Starting up database 'PersonsLots'. 5 2012-10-03 11:28:26.710 Recovery of database 'PersonsLots' (6) is 0% 6 complete (approximately 155 seconds remain). 7 2012-10-03 11:28:33.000 140 transactions rolled forward in database 8 'PersonsLots' (6). 9 2012-10-03 11:28:33.010 Recovery completed for database PersonsLots 10 (database ID 6) in 6 second(s) 11 (analysis 2238 ms, redo 4144 ms, undo 12 ms.) 12 */
代碼8.13:對PersonsLots信息進行錯誤日志查看。
在SQL Server啟動數據庫和開始恢復進程之間有近12.5秒。這是為什么會看到數據庫列為“in recovery(在恢復中)”,在錯誤日志里沒有看到任何初始恢復信息。恢復進程在7秒內完成。注意,在這三個恢復階段,SQL Server花費更多的時間在重做。
現在讓我們對Persons數據庫(預制日志文件大小)重做同樣的測試。
1 USE Persons 2 Go 3 /*Disable Automatic checkpoints*/ 4 DBCC TRACEON( 3505 ) 5 --DBCC TRACEOFF (3505) 6 7 USE Persons 8 Go 9 BEGIN TRANSACTION 10 DECLARE @cnt INT; 11 12 SET @cnt = 1; 13 14 WHILE @cnt < 6 15 BEGIN; 16 SET @cnt = @cnt + 1; 17 UPDATE dbo.Persons 18 SET Email = LEFT(Email + Email, 7000) 19 END; 20 21 DBCC SQLPERF(LOGSPACE) ; 22 -- 12288 MB, 87.2% used 23 USE Persons 24 GO 25 DBCC LOGINFO; 26 -- 96 VLFs 27 28 /*Commit and immediately Shut down*/ 29 COMMIT TRANSACTION; 30 SHUTDOWN WITH NOWAIT
代碼8.14:Persons:禁用自動化檢查點,運行並提交顯式事務,關閉SQL Server。
最后,我們再次查看錯誤日志,看下這2個之間的時間,即總的恢復時間。
1 EXEC sys.xp_readerrorlog 0, 1, 'Persons' 2 3 /* 4 2012-10-03 11:54:21.410 Starting up database 'Persons'. 5 2012-10-03 11:54:21.890 Recovery of database 'Persons' (6) is 0% 6 complete (approximately 108 seconds remain). 7 2012-10-03 11:54:30.690 1 transactions rolled forward in database 8 'Persons' (6). 9 2012-10-03 11:54:30.710 Recovery completed for database Persons 10 (database ID 6) in 3 second(s) 11 (analysis 2177 ms, redo 1058 ms, undo 10 ms.) 12 */
代碼8.15:對於Persons信息查看錯誤日志。
注意這次在SQL Server啟動數據庫和開始恢復的時間小於0.5秒。恢復過程只花了9秒。
注意在這些測試中,我們並沒有創建其他一樣的情形,除日志碎片外。對於開始,對於碎片日志數據庫,恢復過程前滾了140個事務,在第2個測試中,只前滾了1個。
不管怎樣,從測試里可以看出碎片日志會明顯延遲數據庫的實際恢復過程,在SQL Server讀取所有VLF時。
大量撤銷
作為另一個例子,我們可以執行我們長的更新事務,運行檢查點然后關閉SQL Server,讓事務未提交,來看看SQL Server會花多長時間來恢復數據庫,首先當日志是碎片時,然后當不是時。在每個情況里,這會強制SQL Server進行大量撤銷來進行恢復,我們來看下影響,內部是否有任何碎片日志。
對於這些測試我們不展示完整代碼了,因為和剛才的代碼基本一致。
1 /* (1) Recreate PersonsLots, with a fragmented log (Listing 8.4 and 8.5) 2 (2) Create Persons table, Insert 1 million rows (Listings 8.2 and 8.3) 3 */ 4 5 BEGIN TRANSACTION 6 7 /* run update from listing 8.6*/ 8 9 /*Force a checkpoint*/ 10 CHECKPOINT; 11 12 /*In an second session, immediately Shutdown without commiting*/ 13 SHUTDOWN WITH NOWAIT
代碼8.16:在PersonsLot上測試“大量撤銷”(碎片日志)
對Persons數據庫運行同個測試(代碼8.8,8.2,8.3,8.16),代碼8.17展示了每個數據庫結果的錯誤信息。
1 /* 2 PersonsLots (fragmented log) 3 4 2012-10-03 12:51:35.360 Starting up database 'PersonsLots'. 5 2012-10-03 12:51:46.920 Recovery of database 'PersonsLots' (17) is 0% 6 complete (approximately 10863 seconds remain). 7 2012-10-03 12:57:12.680 1 transactions rolled back in database 8 'PersonsLots' (17). 9 2012-10-03 12:57:14.680 Recovery completed for database PersonsLots 10 (database ID 17) in 326 second(s) 11 (analysis 30 ms, redo 78083 ms, undo 246689 ms.) 12 13 Persons (non-fragmented log) 14 15 2012-10-03 13:21:23.250 Starting up database 'Persons'. 16 2012-10-03 13:21:23.740 Recovery of database 'Persons' (6) is 0% 17 complete (approximately 10775 seconds remain). 18 2012-10-03 13:26:03.840 1 transactions rolled back in database 19 'Persons' (6). 20 2012-10-03 13:26:03.990 Recovery completed for database Persons 21 (database ID 6) in 279 second(s) 22 (analysis 24 ms, redo 57468 ms, undo 221671 ms.) 23 */
代碼8.17:對於PersonsLot和Person數據庫啟動和恢復信息的錯誤日志
對於PersongsLots數據庫啟動和開始恢復進程之間的延遲是11秒,Person只有0.5秒。
在這些撤銷例子里,和剛才重做例子比,整個恢復時間更長。對於PersonsLots,總恢復時間是326秒,沒有碎片日志的Person只有279秒。
修正日志大小
我們希望這篇文章里,剛才的例子已經清楚演示了事務日志文件太小是個非常壞的做法,那樣的話會允許在小增長率里增長。另外從model數據庫繼承下來的自動增長設置也是個非常壞的做法,這會允許當前事務日志大小以10%的步驟增長,
因為:
- 初始化時,當日志文件是小時,增長率增長會是小的,導致在日志里創建了大量的小的VLF,引起剛才談到的碎片問題。
- 當日志文件很大時,增長增長會相應的大,在初始化期間,事務日志需要歸零,大的增長會花費時間,如果日志不能增長的足夠快,這會導致9002錯誤(事務日志滿),即使在自動增長超時並回滾。
避免日志過分增長和日志碎片的方法是對日志(和數據)文件設置正確的初始大小,滿足當前的需求,並預計未來的增長情況。
理想上,做了這些,日志會從不增長,這並不說我們應該禁用自動增長功能。這肯定是個安全機制,我們應該正確設置日志的合適大小,這樣的話我們不會完全依賴控制日志增長的自動增長。我們可以配置自動增長為固定大小來允許日志文件快速增長,如果必要的話,對每個增長事件會最小化SQL Server增加到日志文件的VLF數。如剛才談到的,自動增長事件非常昂貴,因為有0初始化。為了在自動增長期間最小化超時發生的幾率,可以通過不同的大小設置衡量下事務日志增長所需要的時間,數據庫在正常工作量下運行,基於當前I/O子系統的配置,這是個很好的做法。
因此,我們如何正確調整大小?這個問題並不簡單。在例如“日志應該至少是數據庫大小的25%”這樣的建議后並沒有邏輯可言。我們必須直接基於下列考慮條件跟蹤日志增長來選擇合理的大小:
- 日志必須足夠大可以容下最大的單條事務。例如最大索引重建。這意味着日志必須大於數據庫里最大的索引,允許記錄在完整恢復下重建索引,並且足夠大容下可能同時大事務運行的所有活動。
- 日志大小必須對在日志備份間生成的日志負責(例如30分鍾,或者1個小時)。
- 日志大小必須對任何延誤事務的進程負責。例如復制,日志讀取器代理作業會一小時運行一次。
我們還要記住日志預留因素。當記錄事務時,日志子系統預留空間保證日志回滾時不會用完空間。這樣的話,需要的日志空間比操作的日志記錄的總大小要大。
簡單來說,回滾操作記錄補償日志記錄(compensation log record),如果回滾用完了日志空間,SQL Server會標記數據庫為可疑。這個日志注冊不是實際“使用的”日志空間,這是必須保留可用的空間量,但如果日志填充到點(已用空間+保留空間=日志大小)它會觸發自動增長事件,會被標記為已用空間,用於DBCC SQLPERF(LOGSPACE)。
因此,可用通過DBCC SQLPERF(LOGSPACE)來查看可用空間,在事務提交后,即使數據庫在完整恢復模式,沒有日志備份已運行。為了驗證這個,我們需要完整恢復模式的數據庫,表有50000條記錄。
1 BACKUP LOG Persons 2 TO DISK='D:\SQLBackups\Persons_log.trn' 3 WITH INIT; 4 5 -- start a transaction 6 BEGIN TRANSACTION 7 8 DBCC SQLPERF(LOGSPACE) 9 /*LogSize: 34 MB ; Log Space Used: 12%*/ 10 11 -- update the Persons table 12 UPDATE dbo.Persons 13 SET email = ' __ ' 14 15 DBCC SQLPERF(LOGSPACE) 16 /*LogSize: 34 MB ; Log Space Used: 87%*/ 17 18 COMMIT TRANSACTION 19 20 DBCC SQLPERF(LOGSPACE) 21 /*LogSize: 34 MB ; Log Space Used: 34%*/
代碼8.18:日志保留測試。
注意日志空間使用率從87%掉到34%,即使這是個完整恢復模式的數據庫,事務提交后沒有日志備份。SQL Server在這個情況下沒有截斷日志,僅僅在事務提交后,釋放了保留的日志空間。
已經設置了初始日志大小,基於這些需求,設置了合理的自動增長機制,監控下日志使用更加明智,對於日志自動增長事件設置警告,因為,如果我們已經正確做好我們的工作,日志增長會很少見。第9篇會詳細討論日志監控。
如果出問題要做什么
在這個最后的部分里,我們談下暴漲和碎片化日志文件的正確處理方法。或許數據庫最近才被我們關注;我們發現一些監控異常,並意識到日志已經近滿,磁盤上已經沒有空間進行緊急的索引維護操作。我們嘗試日志備份,但基於某些原因我們要進一步調查(查看第7篇),SQL Server不會截斷日志。為了贏得點時間,我們增加第2個日志文件,在獨立的硬盤上,操作如期繼續。
我們調查為什么日志大小會暴漲,原因是一個程序在數據庫里留下了“孤立的事務”。這個問題修正后,接下來日志備份會截斷日志,創造大量可重用空間。
下個問題是接下來做什么?現在我們的數據庫有多個日志文件,主日志文件已經滿了且有大量的碎片。
第一點我們想要的是盡快甩掉第2個日志文件。如剛才所說,有多個日志文件並沒有性能上的優勢,現在已經不需要了,它真的會降低任何還原操作,因為愛完整和差異還原操作期間,SQL Server需要0初始化掉日志。
運行代碼8.4重建PersonsLots數據庫,接下來運行代碼8.2和8.3來創建和插入數據到Persons表(文末有完整代碼)
我們假設,在這一點,DBA增加第2個3G的日志文件來容納數據庫維護操作。
1 USE master 2 GO 3 ALTER DATABASE PersonsLots 4 ADD LOG FILE ( NAME = N'PersonsLots_Log2', 5 FILENAME = N'D:\SQLData\Persons_lots2.ldf' , SIZE = 3146000KB , FILEGROWTH = 314600KB ) 6 GO
代碼8.19:增加3GB的日志文件到PersonsLots
等下,我們會解決延遲日志截斷的問題,現在在第一個日志文件里有足夠的可用空間,我們已經不再需要第2個日志文件,但它是存在的,我們來還原PersonsLots數據庫。
1 USE master 2 GO 3 RESTORE DATABASE PersonsLots 4 FROM DISK ='D:\SQLBackups\PersonsLots_full.bak' 5 WITH NORECOVERY; 6 7 RESTORE DATABASE PersonsLots 8 FROM DISK='D:\SQLBackups\PersonsLots.trn' 9 WITH Recovery; 10 11 /*<output truncated>… 12 Processed 18094 pages for database 'PersonsLots', file 'PersonsLots_log' on file 1. 13 Processed 0 pages for database 'PersonsLots', file 'PersonsLots_Log2' on file 1. 14 RESTORE LOG successfully processed 18094 pages in 62.141 seconds (2.274 MB/sec).*/
代碼8.20:還原PersonsLots(有第2個日志文件)
還原花費了60秒。如果我們重復同樣的步驟,但不增加第2個日志文件,相比而言,在我們的測試里,花了近8秒。
為了移除第2個日志文件,我們需要等待直到它已經不包含任何活動日志。因為我們的目標是移除它,我們可以收縮第2個日志文件文件為0(稍后會演示),對這個文件關閉自動增長,因為這會“鼓勵”活動日志完全移回到第1個日志文件。這點非常重要:這不會移動任何在第2個日志文件里的記錄到第1個日志文件。(有些人會這樣認為,因為當我們收縮數據文件時,如果我們指定EMPTYFILE參數,SQL Server會移動數據到同個文件組里的另一個數據文件)。
一旦第2個日志文件沒有包含任何活動日志,我們可以直接刪除它。
1 USE PersonsLots 2 GO 3 ALTER DATABASE PersonsLots REMOVE FILE PersonsLots_Log2 4 GO
代碼8.21:移除第2個日志文件。
這是解決的一個問題,但我們還是有暴漲和碎片話的主日志文件。在第7篇里我們就談到,收縮日志文件不應該是我們標准維護操作,在我們這個情況下是可以,在理論上我們已經調查並解決了日志過度增長的原因,因此收縮日志應該是一次性的事件。
推薦的方法是使用DBCC SHRINKFILE來重獲空間。如果我們不指定目標大小,或指定0作為目標大小,我們可以收縮日志到初始大小(在這個情況下是2MB)並最小化日志文件的碎片。如果初始日志初始大小很大,我們想收縮得更小,我們可以指定target_sise,例如1。
1 USE PersonsLots 2 GO 3 DBCC SHRINKFILE (N'PersonsLots_log' , target_size=0) 4 GO
代碼8.22:收縮主日志文件(部分成功)
從這個命令的輸出,我們看到當前數據庫大小(24128*8 KB個頁),收縮后的最小可能大小(256 * 8 KB個頁)。這表示我們的收縮不會完全。SQL Server收縮日志到包含活動日志部分的最后一個VLF的位置點,然后停止了。檢查下信息頁:
1 /*Cannot shrink log file 2 (PersonsLots_log) because the logical log file located at the end of the file is in use. 2 3 (1 row(s) affected) 4 DBCC execution completed. If DBCC printed error messages, contact your system administrator.*/
進行日志備份然后再次嘗試。
1 USE master 2 GO 3 BACKUP DATABASE PersonsLots 4 TO DISK ='D:\SQLBackups\PersonsLots_full.bak' 5 WITH INIT; 6 GO 7 8 BACKUP LOG PersonsLots 9 TO DISK = 'D:\SQLBackups\PersonsLots.trn' 10 WITH init 11 12 USE PersonsLots 13 GO 14 DBCC SHRINKFILE (N'PersonsLots_log' , 0) 15 GO
代碼8.23:日志備份后收縮主日志文件。
做完這個后,現在我們可以人為調整日志到需要的大小,如剛才代碼8.8所示。
小結
我們從會影響日志吞吐量的物理架構因素的簡述開始,例如需要分離日志文件I/O到各自的陣列,為這些陣列選擇最優的RAID級別。
這篇文章然后強調管理事務日志增長的必要性,而不是讓SQL Server的自動增長事件為我們管理。如果初始日志太小,然后讓SQL Server自動在小量的增長,我們會有大量的碎片日志。在這篇文章的日志里演示了它會如何影響需要讀取日志的任何SQL Server操作。
最后,我們討論了決定正確日志大小的因素,對一個給出的數據庫修正了自動增長率,我們提供了如何恢復有多個日志文件的數據庫到一個日志文件,並重新調整日志文件的大小和碎片。
下一篇文章,這個系列文章的最后一篇,我們會介紹監控日志活動、吞吐量和碎片的各個不同工具和技術。
擴展閱讀
致謝
非常感謝Jonathan Kehayias,為本文提供RAID部分內容。
(博主注:也非常感謝您這么耐心看完這篇文章,最近博主非常忙,原計划上個月就應該更新這篇文章,現在才完成,新的一年,歡迎大家和我繼續前行!)