SQL Server中的事務日志管理(6/9):大容量日志恢復模式里的日志管理


當一切正常時,沒有必要特別留意什么是事務日志,它是如何工作的。你只要確保每個數據庫都有正確的備份。當出現問題時,事務日志的理解對於采取修正操作是重要的,尤其在需要緊急恢復數據庫到指定點時。這系列文章會告訴你每個DBA應該知道的具體細節。


這個標題有點用詞不當,因為運行在大容量日志恢復模式里的數據庫,我們通常是長期不管理日志。但是,DBA會考慮在大容量加載時,短期切換到大容量恢復模式。當數據庫在大容量模式里運行時,一些其他例如索引重建的操作會最小化日志(minimally logged),因此在日志里會使用很小的空間。對非常大的表,當重建聚集索引時,或當大容量加載百萬行的數據時,在大容量日志恢復模式里運行會減少日志空間的使用,與完整恢復模式比,可以有非常大的區別。

但是,我們應該只在完全理解它對數據庫備份和恢復的影響后才使用大容量日志恢復模式。例如,在包含最小化日志操作的日志記錄的日志備份,是不能恢復數據庫到特定時間點的。另外,如果運行的是最小化日志模式,有尾日志會備份失敗的特殊情形,當數據庫運行在大容量日志模式里,在事務日志里活動部分存在的記錄和數據文件會因災難而不可用(例如磁盤故障)。

如果你不幸碰到這樣的災難,這些限制會導致數據丟失。檢查下對於問題數據庫服務級別協議(Service Level Agreement(SLA)),對於數據丟失的可接受級別;如果是零容忍,那是不能使用大容量日志模式,即使用於很短時間,是可接受的。相反,肯定的,如果這樣的數據庫需要常規的索引維護或大容量加載,那數據庫所有者必須理解在完整恢復模式里,進行這些操作對數據庫日志空間分配的影響。

對大多數數據庫已經提過這個,切換到大容量日志恢復模式能讓SQL Server對特定操作最小化日志,在與日志飛速增長的斗爭中是非常有用的武器。在大多數情況下,SLA會允許足夠余地讓使用可接受,使用精密的計划和流程,風險會最小化。

這篇文章會談到:

  • “最小化日志”是什么意思
  • 在日志空間使用方面,最小化日志的優勢
  • 對於災難恢復,時間點恢復和尾日志備份,最小化日志的影響
  • 使用大容量日志恢復模式的最佳實踐

最小化日志操作

當數據庫運行在完整恢復模式里,所有的操作都會完整日志。這表示每個日志記錄存儲着回滾(撤銷),前滾(重做)操作描述的足夠信息。在給出的完整日志的日志文件里的所有日志記錄,我們有在時間軸上對數據庫做出修改的完整描述。這就是說,在恢復操作期間,SQL Server可以通過每個日志記錄前滾,可以把數據庫恢復到日志文件里存在的任何時間點的狀態。

當數據庫運行在大容量日志(或簡單)恢復模式里,SQL Server會最小化特定日志操作。在一些明顯最小化日志操作(BULK INSERT,bcp或索引重建)都是,其他不是。例如,在SQL Server 2008和后續版本,INSERT……SELECT在某些情況下會是最小化日志操作。(點擊了解更多:https://msdn.microsoft.com/zh-cn/library/ms191244%28v=sql.100%29.aspx

這里你會找到SQL Server會最小化日志的操作的所有清單:https://msdn.microsoft.com/zh-cn/library/ms191244.aspx 一些更通用的如下:

  • 大容量加載操作——例如通過SSIS,bcp或BULK INSERT
  • SELECT INTO操作
  • 創建和重建索引

“可以是”最小化日志和“會是”最小化日志是不一樣的,SQL Server理論上可以最小化日志,實際還是會完整日志大容量記載操作。取決於索引所在的位置和查詢優化器選擇的計划。主要是可恢復性需要,SQL Server只最小化日志大容量數據加載的,那是分配新區。例如,如果我們進行大容量加載到已有一些數據的聚集索引,加載會包含增加頁,分頁,分配新頁的混合操作,因此SQL Server不能最小化日志。同理,對SQL Server是可以最小化插入表,但插入非聚集索引是完整日志。來看數據加載性能指導白皮書來進一步了解(https://msdn.microsoft.com/en-us/library/dd425070.aspx)。

在線幫助描述最小化日志為“只記錄事務需要恢復的信息,不支持時間點恢復”。同理,Kalen Delaney,在她的書里,《SQL Server 2008內核剖析與故障排除》(第4章,第199頁),定義最小化日志操作為“日志只記錄事務回滾的足夠信息,不不支持時間點恢復”。

為了理解什么是用來“最小化日志”操作的區別,取決於數據庫是否使用完整或大容量日志恢復模式。我們來驗證下!

數據和備份文件位置

在這篇文章里所有例子都假定數據和日志文件位於“D:\SQLData”,所有備份位於“D:\SQLBackups”,不是同個地方的。當運行例子時,直接修改這些位置作為你系統的正確位置(在真實系統中,請記住我們不會存儲所有東西在同個硬盤!)

我們使用在SQL Server 2008里會最小化日志的SELECT...INTO語句插入ScmeTable表200條記錄,每條記錄2000字節。因為在SQL Server里頁大小是8kb,我們會得到4行一頁,總共50個數據頁(加上一些分配頁)。代碼6.1創建一個測試數據庫,完整恢復,請保證它運行在完整恢復模式,然后運行SELECT...INTO語句。

 1 USE master
 2 GO
 3 IF DB_ID('FullRecovery') IS NOT NULL 
 4     DROP DATABASE FullRecovery;
 5 GO
 6 
 7 -- Clear backup history
 8 EXEC msdb.dbo.sp_delete_database_backuphistory 
 9     @database_name = N'FullRecovery'
10 GO
11 
12 CREATE DATABASE FullRecovery ON
13 (NAME = FullRecovery_dat,
14   FILENAME = 'D:\SQLData\FullRecovery.mdf'
15 ) LOG ON
16 (
17   NAME = FullRecovery_log,
18   FILENAME = 'D:\SQLData\FullRecovery.ldf'
19 );
20 
21 ALTER DATABASE FullRecovery SET RECOVERY FULL
22 GO
23 
24 BACKUP DATABASE FullRecovery TO DISK = 'D:\SQLBackups\FullRecovery.bak'
25 WITH INIT
26 GO
27 
28 USE FullRecovery
29 GO
30 IF OBJECT_ID('dbo.SomeTable', 'U') IS NOT NULL 
31     DROP TABLE dbo.SomeTable
32 GO
33 
34 SELECT TOP ( 200 )
35         REPLICATE('a', 2000) AS SomeCol
36 INTO    SomeTable
37 FROM    sys.columns AS c;

(代碼6.1:在完整恢復模式數據庫上進行SELECT...INTO操作)

現在,在這個點,我們來看看日志內部,來理解由於SELECT...INTO語句的完整日志,SQL Server在日志里記錄了什么。有一些第三方日志讀取工具可以達到這個目的,但很少有工具對SQL Server 2005以上版本支持。但是,我們可以使用2個未公開和不支持的功能來查這個日志文件內容(fn_dblog)和日志備份(fn_dump_dblog),如代碼6.2所示。

1 SELECT  Operation ,
2         Context ,
3         AllocUnitName ,
4    --   Description ,
5         [Log Record Length] ,
6         [Log Record]
7 FROM    fn_dblog(NULL, NULL)

(代碼6.2:使用fn_dblog查看日志內容)

(插圖6.1:在完整恢復模式數據庫上SELECT...INTO后fn_dblog的輸出)

插圖6.1展示了8頁集合的輸出的一小部分(分配單元位置是dbo.SomeTable表)。注意每條記錄的Context是LCX_HEAP,因此這些是數據頁。我們也看到一些分配頁,這里是DCM(Differential Changed Map)頁,跟蹤自上次數據庫備份后修改過的分區,還有一些PFS(Page Free Space)頁,在頁上跟蹤頁分配和可用空閑空間。

描述對SomeTable做出改變的日志記錄都是LOP_FORMAT_PAGE類型;它們都是8個一起出現,每個是8276字節長。8個一起出現的事實實際上表示SQL Server處理插入是一次一個分區,在每一頁寫一條日志記錄。每條是8276字節表示每條包含整個數據頁的鏡像,包擴日志頭。換句話說,在大容量日志恢復模式里,對於INSERT...INTO命令和其他,SQL Server會最小化日志,當運行在完整模式里,SQL Server不會記錄每個單獨的行,而是記錄每個頁鏡像,實際上是填充的。

仔細看Log Record列顯示很多字節包含0x61的十六進制值,如插圖6.2所示。換做十進制是97,ASCII值是‘a’,因此這些是日志文件里的實際數據行。

(插圖6.2:仔細看日志記錄)

因此在完整恢復模式里,SQL Server只要通過讀取日志文件就知道那些分區改變和具體如何影響頁內容。現在我們對此比較下,在運行在大容量日志恢復模式里的數據上進行同樣的SELECT...INTO操作,看看日志記錄結果。

 1 USE master
 2 GO
 3 IF DB_ID('BulkLoggedRecovery') IS NOT NULL 
 4     DROP DATABASE BulkLoggedRecovery;
 5 GO
 6 
 7 EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'BulkLoggedRecovery'
 8 GO
 9 
10 CREATE DATABASE BulkLoggedRecovery ON
11 (NAME = BulkLoggedRecovery_dat,
12   FILENAME = 'D:\SQLData\BulkLoggedRecovery.mdf'
13 ) LOG ON
14 (
15   NAME = BulkLoggedRecovery_log,
16   FILENAME = 'D:\SQLData\BulkLoggedRecovery.ldf'
17 );
18 GO
19 
20 ALTER DATABASE BulkLoggedRecovery SET RECOVERY BULK_LOGGED
21 GO
22 
23 BACKUP DATABASE BulkLoggedRecovery TO DISK =
24           'D:\SQLBackups\BulkLoggedRecovery.bak'
25 WITH INIT
26 GO
27 
28 USE BulkLoggedRecovery
29 GO
30 IF OBJECT_ID('dbo.SomeTable', 'U') IS NOT NULL 
31     DROP TABLE dbo.SomeTable
32 GO
33 
34 SELECT TOP ( 200 )
35         REPLICATE('a', 2000) AS SomeCol
36 INTO    SomeTable
37 FROM    sys.columns AS c;

 (代碼6.3:在大容量日志恢復模式上進行SELECT...INTO操作)

BulkLoggedRecovery數據庫里再次運行代碼6.2的函數,這次我們獲得的是完全不同的記錄集。根本沒有LOP_FORMAT_PAGE日志記錄。

(插圖6.3:在大容量日志模式數據庫上SELECT...INTO后fn_dblog的輸出)

這次,對SomeTable做出修改的日志記錄出現GAM(Global Allocation Maps)和IAM(Index Allocation Maps)的上下文里,跟蹤分區分配,加上一些PFS頁。話句話說,SQL Server是記錄區分配(還有任何對元數據的修改,例如系統表,這在插圖6.3里並沒有顯示),但數據頁本身並不在。在日志里沒有什么數據在分配頁上的引用。這里對於FullRecovery數據庫,我們在對日志記錄里沒有看到0x61的式樣,大多數日志記錄是近100 byte大小。

因此,現在我們對於SQL Server最小化日志操作有了非常清晰的認識:這個操作是SQL Server記錄相關區的分配,不是這些區的實際內容分配(例如數據頁)。

這個影響是雙重的。首先,它意味着SQL Server會寫入更少的信息到事務日志,這與完整恢復模式里的等同操作的日志會明顯增長的更慢。這也意味着大容量加載操作會更快(在最小化日志和大容量日志恢復的優勢里會具體談到)。

其次,但是,對於最小化日志操作所屬的事務只有撤銷(回滾)的足夠信息,沒有重做(前滾)的信息。為了回滾包含SELECT...INTO操作的事務,SQL Server需要重新分配受影響的頁。因為頁分配被記錄了,如插圖6.3所示,那是可以的。為了前滾事務是另一回事,日志記錄不能用來重新分配頁,當操作是最小化日志時,是不行的,SQL Server不能使用這些日志記錄來重新分配頁的內容。

最小化日志與“僅分區重分配”

對於刪除表(DROP TABLE)和清空表(TRUNCATE TABLE)操作,作為大容量操作,SQL Server只記錄區的重分配。但是,前者不是真正的最小化日志操作,因為它們的行為在所有恢復模式里一樣。真正最小化日志操作的行為在完整恢復模式和在 大日志(簡單)模式里是不一樣的,在SQL Server日志方面。還有,對於真正最小化日志操作,當日志備份發生后,SQL Server捕獲由最小化日志操作影響的所有數據頁到備份文件(稍后我們會詳細討論這些),用於還原操作。這對DROP TABLE和TRUNCATE TABLE命令不會發生。

最小化日志和大容量日志恢復的優勢

在我們討論使用大容量日志恢復潛在的問題前,我們先來說下,對於DBA,它的主要優勢。

在簡單或大容量日志恢復模式里,操作可以被最小化日志。但是,如果我們從完整切換到簡單恢復模式,我們觸發檢查點(CHECKPOINT),它會截 斷日志,我們馬上中斷了LSN鏈。沒有可能進行日志備份直到數據庫切換回完整(或大容量日志)恢復模式,完整數據庫備份后,日志鏈重新開始,或者我們用差 異數據庫備份“橋接LSN斷片”。

從完整恢復模式切換回大容量日志模式,但是不會中斷日志鏈。從完整恢復模式切換到大容量恢復模式不需要進行數據庫備份,它是馬上生效。同樣,當切換 回完整恢復模式,也不需要進行數據庫備份。但是,在切換到大容量日志模式前立即進行一次日志備份,切換回也進行一次日志備份是個很好的做法。(閱讀下一部 分的大容量日志使用的最佳實踐有進一步的討論)。

雖然在切換到大容量日志模式時有很多問題要考慮,對此我們會馬上討論,在數據丟失風險方面是個更安全的選擇,因為日志鏈還是完整的。一旦數據庫運行在大容量日志模式,真正的優勢是在通過最小化日志操作使用的日志空間會下降,也提高了這些操作的潛在性能。

我們來看索引重建的一個例子,它的操作可以是最小化日志。在完整恢復模式里,索引重建操作需要與表相等或更大的空間。對於更大的表,會引起巨大的日志增長,這是論壇來自很多苦惱用戶的頭條問題根源——“我重建了索引,日志增長迅速/我們用完磁盤空間!”

在大容量日志恢復模式里,只有對新索引的頁分配會有日志,因此事務日志上的影響會比在完整恢復模式里大大減少。這同樣也適用於通過bcp或者BULK INSERT的數據加載,或通過SELECT...INTO或INSERT INTO...SELECT復制數據到新表。

為了理解這會帶來多大的區別,我們來看一個例子。首先,我們創建一個聚集索引表並插入數據(每行Filler列會增加1500字節用來保證我們的表會有很多頁)。

 1 USE FullRecovery  2 GO  3 IF OBJECT_ID('dbo.PrimaryTable_Large', 'U') IS NOT NULL  4 DROP TABLE dbo.PrimaryTable_Large  5 GO  6 CREATE TABLE PrimaryTable_Large  7  (  8 ID INT IDENTITY  9 PRIMARY KEY , 10 SomeColumn CHAR(4) NULL , 11 Filler CHAR(1500) DEFAULT '' 12  ); 13 GO 14 15 INSERT INTO PrimaryTable_Large 16  ( SomeColumn 17  ) 18 SELECT TOP 100000 19 'abcd ' 20 FROM msdb.sys.columns a 21 CROSS JOIN msdb.sys.columns b 22 GO 23 24 SELECT * 25 FROM sys.dm_db_index_physical_stats(DB_ID(N'FullRecovery'), 26 OBJECT_ID(N'PrimaryTable_Large'), 27 NULL, NULL, 'DETAILED');

(代碼6.4:創建並加載PrimaryTable_Large表)

現在我們在我們的完整恢復模式數據庫里重建聚集索引(根據sys.dm_db_index_physical_stats,包含20034個頁),用sys.dm_tran_database_transactions這個DMV來看看索引重建需要多少日志空間。

 1 --truncate the log  2 USE master  3 GO  4 BACKUP LOG FullRecovery  5 TO DISK = 'D:\SQLBackups\FullRecovery_log.trn'  6 WITH INIT  7 GO  8  9 -- rebuild index and interrogate log space use, within a transaction 10 USE FullRecovery 11 GO 12 BEGIN TRANSACTION 13 14 ALTER INDEX ALL ON dbo.PrimaryTable_Large REBUILD 15 -- there's only the clustered index 16 17 SELECT d.name , 18 -- session_id , 19  d.recovery_model_desc , 20 -- database_transaction_begin_time , 21  database_transaction_log_record_count , 22  database_transaction_log_bytes_used , 23 DATEDIFF(ss, database_transaction_begin_time, GETDATE()) 24 AS SecondsToRebuild 25 FROM sys.dm_tran_database_transactions AS dt 26 INNER JOIN sys.dm_tran_session_transactions AS st 27 ON dt.transaction_id = st.transaction_id 28 INNER JOIN sys.databases AS d ON dt.database_id = d.database_id 29 WHERE d.name = 'FullRecovery' 30 COMMIT TRANSACTION

(代碼6.5:當重建聚集索引時日志空間使用率)

當我在完整恢復模式里的數據運行這個代碼,從DMV里的輸出信息如插圖6.4所示。

(插圖6.4:在完整恢復模式里索引重建后的日志空間使用)。

它花費了近18秒來重建索引,對於20210條日志記錄,這個重建需要近166M日志空間;這已經忽略了回滾情況的日志,因此總的日志空間需要會更多。

如果在BulkLoggedRecovery數據庫運行同樣的例子,輸出如插圖6.5所示。

(插圖6.5:在大容量日志恢復模式里索引重建后的日志空間使用)

重建看起來會稍微快一點,但是,因為這個例子的索引非常小,數據和日志文件都在同一個硬盤,這一次看不出有太大的區別。這里的要點是日志空間使用率。在大容量日志里,索引重建只用近0.8M的空間,在完整恢復模式里卻有166M的日志空間使用。對於只有160M大小的表,這是最大的節約。

為了一些人想知道在簡單恢復模式里,這個操作會有沒有什么區別,執行代碼如下:

 1 /*Repeat example in SimpleRecovery*/
 2 USE master
 3 GO
 4 IF EXISTS ( SELECT  name
 5             FROM    sys.databases
 6             WHERE   name = 'SimpleRecovery' ) 
 7     DROP DATABASE SimpleRecovery
 8 GO
 9 
10 EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'SimpleRecovery'
11 GO
12 
13 CREATE DATABASE SimpleRecovery ON
14 (NAME = SimpleRecovery_dat,
15   FILENAME = 'D:\SQLData\SimpleRecovery.mdf'
16 ) LOG ON
17 (
18   NAME = SimpleRecovery_log,
19   FILENAME = 'D:\SQLData\SimpleRecovery.ldf'
20 );
21 
22 
23 ALTER DATABASE SimpleRecovery SET RECOVERY SIMPLE
24 GO
25 
26 USE SimpleRecovery
27 GO
28 IF OBJECT_ID('dbo.PrimaryTable_Large', 'U') IS NOT NULL 
29     DROP TABLE dbo.PrimaryTable_Large
30 GO
31 CREATE TABLE PrimaryTable_Large
32     (
33       ID INT IDENTITY
34              PRIMARY KEY ,
35       SomeColumn CHAR(4) NULL ,
36       Filler CHAR(1500) DEFAULT ''
37     );
38 GO
39 
40 INSERT  INTO PrimaryTable_Large
41         ( SomeColumn
42         )
43         SELECT TOP 100000
44                 'abcd '
45         FROM    msdb.sys.columns a
46                 CROSS JOIN msdb.sys.columns b
47 GO
48 
49 SELECT  *
50 FROM    sys.dm_db_index_physical_stats(DB_ID(N'SimpleRecovery'),
51                                        OBJECT_ID(N'PrimaryTable_Large'), NULL,
52                                        NULL, 'DETAILED');
53 
54 USE SimpleRecovery
55 GO
56 BEGIN TRANSACTION 
57 
58 ALTER INDEX ALL ON dbo.PrimaryTable_Large REBUILD
59  -- there's only the clustered index
60 
61 SELECT  d.name ,
62   --    session_id ,
63         d.recovery_model_desc ,
64   --    database_transaction_begin_time ,
65         database_transaction_log_record_count ,
66         database_transaction_log_bytes_used ,
67         DATEDIFF(ss, database_transaction_begin_time, GETDATE()) AS SecondsToRebuild
68 FROM    sys.dm_tran_database_transactions AS dt
69         INNER JOIN sys.dm_tran_session_transactions AS st ON dt.transaction_id = st.transaction_id
70         INNER JOIN sys.databases AS d ON dt.database_id = d.database_id
71 WHERE   d.name = 'SimpleRecovery'
72 COMMIT TRANSACTION

可以看下插圖6.6.

(插圖6.6:在簡單恢復模式里索引重建后的日志空間使用)

和預計的一樣,與大容量恢復模式的行為一樣,因為在簡單恢復模式里,最小化日志操作是和大容量日志恢復模式一樣的。

這是數據庫運行在大容量恢復模式里的主要原因;它提供完整恢復模式的數據庫恢復選項(馬上就會談到),但是它減少了特定操作的日志空間使用量。還有注意,如果數據庫是主日志傳送,我們不能切換數據庫到簡單恢復模式,因為索引重建后沒有必要重做日志傳送,但我們可以切換到大容量日志模式來做索引重建。

最后,注意數據庫鏡像只支持完整恢復模式,如果數據庫運行在主要鏡像里是不能使用大容量日志恢復模式。

最小化日志操作影響

 剛才,我們討論了當數據庫運行在大容量日志恢復模式時是如何操作的,日志只包含區分配(加上元數據),不是最小化日志操作的真實記錄(例如不是插入的實際數據)。這意味着日志本身只包含回滾事務的足夠信息,沒有重做的信息。為了接下來的操作,SQL Server需要讀取描述操作和操作影響到真實數據頁的日志記錄。

當任何時候SQL Server需要重做事務的時候,就會有影響,例如災難恢復(crash recovery),在數據庫恢復(restore)操作期間。對日志備份操作(log backup operation)也有影響,在這2個方面SQL Server必須復制到備份文件,但在這里SQL Server不能做到。

災難恢復

災難恢復,也叫做重新恢復,是SQL Server任何時候將數據庫重新上線的過程。例如,如果數據庫沒有正常關閉,數據庫重啟的時候會讀取數據庫事務日志。它會撤銷關閉時任何未提交的事務,重做已經提交但沒寫入硬盤永駐的事務。

這是可行的,因為在第一篇里討論的預寫式日志(Write Ahead Logging)機制保證數據修改相關的日志記錄在事務提交前或修改的數據寫入硬盤前,預先寫入硬盤。SQL Server可以在任何時候寫入更改到數據文件,在事務提交前或之后,通過檢查點或惰性寫入器。因此,對於一般的操作(例如完整記錄的),SQL Server在事務日志里有足夠的信息來表明一個操作是否要撤銷或重做,有足夠的信息來前滾或后滾。

對於最小化日志的操作,然而前滾是不可能了,因為在日志里沒有足夠的信息。因此當在簡單或大容量日志恢復模式里處理最小化日志時,另一個過程,勤奮寫入器(Eager Write),保證通過最小化日志的任何區修改的大容量操作,在事務完成前都硬寫入硬盤。這和在事務完成之前就寫入日志的普通操作,數據頁寫入是通過系統進程(惰性寫入器或檢查點)完成有鮮明的對比。

這意味着災難恢復從不重做最小化日志操作,因為SQL Server會保證數據頁修改會在事務提交前寫入硬盤,因此最小化日志對災難恢復過程毫無影響。

SQL Server寫日志記錄和修改數據頁在事務提交前寫入硬盤的要求的一個副作用是,與常規的事務相比,最小化日志操作會稍慢,如果數據文件不能處理大量的寫。最小化日志通常比常規操作快,但並不能保證。只有一個可以保證:它們寫入更少的信息到事務日志。

數據庫恢復

當恢復完整,差異或日志備份時,SQL Server也需要進行重做操作。我們已經談到,對於最小化日志操作,對於最小化日志操作,受影響的頁在事務完成時寫入硬盤,因此SQL Server直接復制這些頁到任何完整或差異備份文件,從這些備份里恢復未影響的頁。

然而從日志備份里恢復會更有趣。如果日志備份只包含相關區分配的日志記錄,那么在日志備份恢復上,沒有辦法重建最小化日志操作影響的分區內容。這是因為,如我們剛才所看到的,日志不包含插入的數據,只有區分配和原數據。

當有最小化日志操作時為了啟用日志恢復,包括在日志備份里不只是日志記錄,也有任何區(8頁一組)的鏡像,受最小化日志操作影響的。這不意味着它們的鏡像是最小化日志操作后的,但頁是日式備份時的。SQL Server維護一個位圖分配頁,叫做ML map或bulk-logged change map,每個區都有一個位。任何收最小化日志操作影響的區都會標記為1.日志備份操作讀取這個頁而知道在備份里應該具體包含哪些頁。然后日志備份會清空這個ML map。

因此,例如假設我們有如插圖6.7所示的時間點,日志備份發生在10:00,然后在1點最小化日志操作(例如一個BULK INSERT)影響了1308-1315頁,在2點一個UPDATE影響了1310頁和1311頁,在3點的時候另一個日志備份發生了。

(插圖6.7:數據庫操作和日志備份時間軸)

在10:30的日志備份,備份的日志記錄覆蓋了10:00-10:30的。因為在那個日志段有個最小化日志操作,它復制因最小化日志操作影響的區到日志備份里。復制了它們,因此它們會出現在日志備份里,因此它們會映射BULK INSERT和UPDATE的效果,加上在UPDATE和日志備份期間可能發生的任何進一步修改。

這影響到我們如何能恢復日志,也會影響日志備份的大小,在某些特定情況下,也會影響尾日志備份,這個我們在下一部分會詳細談到。

我們來看下最小化日志操作會如何影響時間點恢復。插圖6.8描述了2個數據庫的典型備份時間軸。綠色待變完整數據備份,黃色代表一系列的日志備份。2個數據庫的唯一區別是第一個數據庫運行在完整恢復模式,第二個數據庫運行在大容量日志恢復模式。

(插圖6.8:數據庫備份時間線)

第5個日志備份時間片是10:00-10:30。在10:10,一個BULK INSERT命令(1位置)加載了一系列數據。這個大容量數據加載一下子就完成了,但在10:20的時候有個不相關的事情發生,一個用戶運行了“流氓的”數據修改命令(2位置),關鍵數據丟失了。項目經理通知DBA團隊,要求他們恢復到導致數據丟失的事務前,在10:20前。

在完整恢復模式的數據庫里,這不是問題。大容量數據加載被完整記錄,我們可以恢復數據庫到日志文件里的任何時間點。我們直接恢復最近一次完整數據庫備份,使用without recovery,應用日志文件到不幸數據丟失事件發生前的時間點。使用RESTORE LOG命令,帶STOPAT參數,停止恢復操作到10:20前的某個時間點。

在大容量日志數據庫里,我們有個問題。我們恢復數據庫到第1個4個日志日志備份的任何時間點,但不能恢復到第5個日志備份,因為它包含最小化日志操作。記住對於這個日志備份,我們只有受最小胡日志操作影響的分區信息,它們存在於日志備份的時間上。第5個日志備份的恢復是“孤注一擲”:要么我們不應用這個日志文件的所有操作,停止恢復到第5個文件尾,或者應用日志文件的所有操作,恢復到文件尾,或者恢復第6個日志備份的任何時間點。

如果我們嘗試恢復到第5個日志備份,帶有STOP AT 10:15(這是最小化日志操作和流氓修改之間的時間點),SQL 不會繼續往下進行剩下的日志備份,找出頁上受最小化日志操作影響的哪些操作需要撤銷。它會更簡單的反饋:

Msg 4341, Level 16, State 1, Line 2
This log backup contains bulk-logged changes. It cannot be used to stop at an arbitrary point in time.

遺憾的是,如果我們應用整個第5個日志備份,它會挫敗恢復目的,因為錯誤進行的提交,它修改日志文件內部內容,因此我們直接刪除了我們嘗試恢復回來的數據!我們沒有選擇,只能恢復第4個日志尾,恢復數據庫,然后報告在這個時間后的任何數據丟失。

當數據庫選擇運行在大容量日志模式里時,如果日志段里有最小化日志操作,我們不能恢復數據庫到指定時間點,這是我們必須要考慮的,不管是短期還是長期運行。判斷日志備份里是否包含最小化日志操作是很容易的。RESTORE HEADERONLY返回備份的詳細問題信息,包括在HasBulkLoggedData列里。另外,在MSDB數據庫的backupset表里也有has_BULK_LOGGED_data列。如果這列值為1,那么日志備份包含最小化日志操作,可以全部恢復或全部丟失。那就是說,在計划或執行恢復時得到這個消息是個令人很不愉快的意外。

日志備份大小

最小化日志操作影響的頁需要拷貝到日志備份,影響日志備份的大小。基本上,在大容量恢復模式的數據庫里,對於大容量操作實際的日志增長會很少,與完整恢復模式的數據庫相比,日志備份大小不會更小,對於完整恢復模式來說,相比較的日志備份會更大。

為了驗證最小化日志操作的影響,還有大容量恢復模式在日志備份大小上。我們來看一個簡單的例子。

首先,重新運行代碼6.3片段,刪除很重建BulkLoggedRecovery數據庫,設置恢復模式為大容量恢復模式,再進行一次完整數據庫備份。

 1 USE master
 2 GO
 3 IF DB_ID('BulkLoggedRecovery') IS NOT NULL 
 4     DROP DATABASE BulkLoggedRecovery;
 5 GO
 6 
 7 EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'BulkLoggedRecovery'
 8 GO
 9 
10 CREATE DATABASE BulkLoggedRecovery ON
11 (NAME = BulkLoggedRecovery_dat,
12   FILENAME = 'D:\SQLData\BulkLoggedRecovery.mdf'
13 ) LOG ON
14 (
15   NAME = BulkLoggedRecovery_log,
16   FILENAME = 'D:\SQLData\BulkLoggedRecovery.ldf'
17 );
18 GO
19 
20 ALTER DATABASE BulkLoggedRecovery SET RECOVERY BULK_LOGGED
21 GO
22 
23 BACKUP DATABASE BulkLoggedRecovery TO DISK =
24           'D:\SQLBackups\BulkLoggedRecovery.bak'
25 WITH INIT
26 GO

接下來,運行代碼6.6在SomeTable表里創建50萬條記錄。

 1 USE BulkLoggedRecovery
 2 GO
 3 IF OBJECT_ID('dbo.SomeTable', 'U') IS NOT NULL 
 4     DROP TABLE dbo.SomeTable ;
 5 SELECT TOP 500000
 6         SomeCol = REPLICATE('a', 2000)
 7 INTO    dbo.SomeTable
 8 FROM    sys.all_columns ac1
 9         CROSS JOIN sys.all_columns ac2 ;
10 GO

(代碼6.6:在BulkLoggedRecovery數據庫里插入50萬條記錄)

然后就,我們檢查下當前日志空間使用率,然后備份日志。

 1 DBCC SQLPERF(LOGSPACE) ;
 2 --24 MB
 3 
 4 --truncate the log
 5 USE master
 6 GO
 7 BACKUP LOG BulkLoggedRecovery
 8 TO DISK = 'D:\SQLBackups\BulkLoggedRecovery_log.trn'
 9 WITH INIT
10 GO

(代碼6.7:為BulkLoggedRecovery數據庫備份日志)

日志大小只有24MB,看到這個備份日志的大小,你會很驚訝,在我的測試里近1GB!

對於完整恢復的數據庫,你會發現日志大小和日志備份大小都會近1GB!

尾日志備份

我們假設有硬件故障導致了數據損壞,但是數據庫還是在線的,我們希望恢復數據庫。使用BACKUP LOG...WITH NORECOVERY,會捕獲日志文件的剩余內容,把數據庫放入還原狀態,這樣的話沒有任何事務可以成功提交到數據庫,我們可以進行還原操作。這種尾日志備份,和通常的日志備份一樣,數據庫需要在線(這樣的話,SQL Server可以根據日志備份可以把信息標入數據庫頭)。

但是,假設對數據的損壞太厲害,數據庫變得不可用,嘗試無法將數據庫恢復在線。如果數據庫在完整恢復模式,有定期的日志備份,那么只要日志文件還是可用的,我們可以進行一次尾日志備份,但使用NO_TRUNCATE選項,例如BACKUP LOG...NO_TRUNCATE。這個選項會備份日志文件而不截斷它,而且不需要數據庫在線。

使用已有的備份(完整,差異(如果有的話),還有日志)和尾日志備份,我們可以恢復數據庫到失敗前的指定時間點。我們能這樣做的原因是日志包含足夠的信息來重建所有提交的事務。

但是,如果數據庫運行在大容量日志模式,在事務日志的活動部分有最小化日志,那么日志沒有包含足夠的信息來重建所有提交的事務,當我們進行尾日志備份時,實際的數據頁是需要的。如果數據文件不可用,那么尾日志備份不能復制需要的頁來恢復一致的數據庫。我們用BulkLoggedRecovery數據庫實際操作下。

 1 BACKUP DATABASE BulkLoggedRecovery 
 2     TO DISK = 'D:\SQLBackups\BulkLoggedRecovery2.bak'
 3 WITH INIT
 4 GO
 5 USE BulkLoggedRecovery
 6 GO
 7 
 8 IF OBJECT_ID('dbo.SomeTable', 'U') IS NOT NULL 
 9     DROP TABLE dbo.SomeTable ;
10 SELECT TOP 200
11         SomeCol = REPLICATE('a', 2000)
12 INTO    dbo.SomeTable
13 FROM    sys.all_columns ac1
14 GO
15 
16 SHUTDOWN WITH NOWAIT

(代碼6.8:創建BulkLoggedRecovery數據庫,執行SELECT INTO,然后關閉數據庫)

使用SQL Server服務關閉,進入數據文件夾,刪除BulkLoggedRecovery數據庫的mdf數據文件,然后重啟SQL Server。這不算硬盤故障的完美模擬,但對於我們的演示目的已經達到。

當SQL Server重啟時,數據庫是不可用的,這一點也不奇怪,因為主數據文件丟失。數據庫狀態是Recovery_Pending,表示SQL不能打開數據庫在它上面運行故障恢復。

USE master
GO
SELECT  name ,
        state_desc
FROM    sys.databases
WHERE   name = 'BulkLoggedRecovery'

(代碼6.9:BulkLoggedRecovery數據庫是RECOVERY_PENDING狀態)

在代碼6.10里,我們嘗試進行尾日志備份(注意NO_TRUNCATE表示COPY_ONLYCONTINUE_AFTER_ERROR(僅復制且忽略錯誤繼續))

1 BACKUP LOG BulkLoggedRecovery 
2 TO DISK = 'D:\SQLBackups\BulkLoggedRecovery_tail.trn' 
3 WITH NO_TRUNCATE

(代碼6.10:使用BACKUP LOG…WITH NO_TRUNCATE嘗試尾日志備份

很好,已經成功執行(盡管有警告,在錯誤日志沒有錯誤)。現在,我們嘗試恢復數據庫,如代碼6.11所示。

1 RESTORE DATABASE BulkLoggedRecovery
2 FROM DISK = 'D:\SQLBackups\BulkLoggedRecovery2.bak' 
3 WITH NORECOVERY
4 
5 RESTORE LOG BulkLoggedRecovery
6 FROM DISK = 'D:\SQLBackups\BulkLoggedRecovery_tail.trn'
7 WITH RECOVERY

(代碼6.11:嘗試恢復BulkLoggedRecovery)

1 RESTORE DATABASE BulkLoggedRecovery
2 FROM DISK = 'D:\SQLBackups\BulkLoggedRecovery2.bak'
3 WITH NORECOVERY
4 
5 RESTORE LOG BulkLoggedRecovery
6 FROM DISK = 'D:\SQLBackups\BulkLoggedRecovery_tail.trn'
7 WITH RECOVERY, CONTINUE_AFTER_ERROR

(代碼6.12:使用CONTINUE_AFTER_ERROR恢復日志備份)

成功執行,我們檢查下恢復數據庫的狀態。重新運行下代碼6.9,你會發現數據庫已經在線。

1 USE master
2 GO
3 SELECT  name ,
4         state_desc
5 FROM    sys.databases
6 WHERE   name = 'BulkLoggedRecovery'

我們再看看SELECT...INTO的SomeTable也存在,因此我們看看表里的數據(記住,頁分配是記錄了,但頁的內容是沒有記錄的)。

1 USE BulkLoggedRecovery
2 GO
3 IF OBJECT_ID('dbo.SomeTable', 'U') IS NOT NULL 
4     PRINT 'SomeTable exists'
5 
6 SELECT  *
7 FROM    SomeTable

(代碼6.13:嘗試查詢SomeTable表)

注意,這個信息是來自SQL Server 2008R2。如果你測試的其他版本的數據庫,錯誤信息可能會不同,這看起來並不好;我們運行DBCC看下數據庫狀態。

1 DBCC CHECKDB ('BulkLoggedRecovery') WITH NO_INFOMSGS, ALL_ERRORMSGS

(代碼6.14:使用DBCC CHECKDB檢查BulkLoggedRecovery狀態)

 

非常遺憾,這看起來非常糟糕(TempDB空間還沒用完)。現在唯一合理的選擇是重新恢復,但不恢復尾日志備份。這就意味着在正常日志備份和災難點的任何提交的事務都丟失了。

當決定數據庫運行在大容量日志恢復模式的時間長短時,這個另一個要考慮的重要因素。在完整恢復模式里,尾日志備份只需要訪問事務日志。因此,我們還可以備份事務日志即使由於例如硬盤故障導致MDF文件不可用。但是,在大容量日志模式里,如果字上次日志備份有最小化日志操作,這意味着我們不能進行尾日志備份,如果數據文件包含有最小化日志操作的數據變得不可用。這個原因是當在大容量日志恢復模式里進行事務日志備份時,SQL Server需要備份因大容量操作的修改的所有區事務日志備份文件,還有事務日志條目。換句話說,SQL Server需要訪問數據文件來進行尾日志備份。

大容量日志使用最佳實踐

最后,檢查下數據庫的SLA來看下數據丟失風險的可接受問題。如果不允許數據丟失,那么計划所有操作在完整恢復模式里,這會影響到日志的增長。如果你能在最大數據丟失范圍允許內用最小化完整想要的操作,即SLA指出的,那么你可以考慮使用大容量恢復模式。

當使用大容量恢復模式時,它的黃金法則是盡量短時間使用,且可能遠隔離最小化操作到它們的日志本身。因此,在切換到大容量日志模式前立即進行一次事務日志備份,切換回完整恢復模式后也立即進行一次日志備份。這可以保證在日志間隔里的最小化日志操作的事務,時間盡可能短的,數量盡可能數量小。

為了演示這個如何減少風險,假設下列情景:

  • 凌晨1:00 完整備份
  • 凌晨1:15 事務日志備份1
  • 凌晨2:15 事務日志備份2
  • 凌晨2:40 切換到大容量日志,大容量操作開始
  • 凌晨3:05 大容量操作結束
  • 凌晨3:10 ——災難——MDF文件不可用
  • 凌晨3:15 事務日志備份3

在這個情況下,凌晨3:15的日志備份會失敗,接下來可以做一次尾日志備份。我們只能恢復數據庫到完整備份和第一個2個日志備份,因此我們丟失了近55分鍾的數據。

換另一個情況,我們調整下,就會有更好的情況:

  • 凌晨1:00 完整備份
  • 凌晨1:15 事務日志備份1
  • 凌晨2:15 事務日志備份2
  • 凌晨2:35 事務日志備份3
  • 凌晨2:40 切換到大容量模式,大容量操作開始
  • 凌晨3:05 大容量操作結束
  • 凌晨3:05 切換回完整模式,進行事務日志備份4
  • 凌晨3:10 ——災難——MDF文件不可用
  • 凌晨3:15 事務日志備份5

這里,3:15的日志備份會失敗,但隨后我們能進行一次尾日志備份,因為日志備份4保證在活動日志里沒有最小化日志操作。那么我們恢復完整備份,4個事務日志備份,還有尾日志備份,恢復數據庫導凌晨3:15災難前的時間點。

即使有這些預防性的日志備份,最好還是正常備份之外進行任何的最小化日志操作,當很少有其他事務進行的時候。這個方法,如果出錯的話,我們可以直接重新進行大容量加載來恢復數據。

即使你通過在每個大容量操作前后進行額外的日志備份來最小化風險,還是不建議你連續運行數據庫在大容量模式。這個會非常困難,取決於你的環境,對誰可能在什么時候進行最小化日志操作進行完全控制。記住:任何表擁有者可以在表上進行創建和重建索引;任何可以建表的人也可以運行SELECT...INTO語句。

最后,我們建議閱讀下數據加載性能指南(https://msdn.microsoft.com/en-us/library/dd425070.aspx),它在獲得高速大容量數據修改提供很多建議,還有討論了使用跟蹤標記610如何衡量來自最小化日志的潛在效益。

小結

大容量恢復模式提供這樣一種方式:進行數據加載和一些例如索引重建數據庫維護操作,不會產生在完整恢復模式里事務日志增長過快的問題,但同時保持日志鏈是完整的。這個缺點包括如果災難發生的話會有很大風險丟失大容量操作期間的數據。換句話說,當還原的日志文件包含最小化日志操作,你不能使用STOPAT選項。還原整個事務日志備份來前滾數據庫還是可以的,或者恢復數據庫到不包含最小化操作的事務前的時間點。但是,在程序BUG里,或者用戶誤刪造成的數據丟失,在這最小化日志操作期間,是不能恢復到日志里的特定時間點里日志記錄,刪除的記錄也不能恢復。

當使用大容量日志恢復模式時,記住會增加數據丟失的風險,只在數據庫維護或大容量數據加載操作時使用這個模式,不是默認就使用大容量日志恢復模式。

盡管有潛在增加的風險,它還是個可行且有用的選項,當DBA計划大數據加載或索引維護時還是可以考慮的。

感謝

非常感謝《SQL Server Backup and Restore》的作者Shawn McGehee,為本篇數據庫恢復部分提供了額外材料。

也感謝您花這么長時間,這么耐心的看完這篇文章。

感謝關注!!!


免責聲明!

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



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