Sql Server 收縮日志文件原理及always on 下的實踐


一、准備知識

  1、LSN

  LSN用來標識特定日志在日志文件中位置(詳情請見什么是LSN:日志序列號),它由兩部分組成:一部分用來標識VLF(虛擬日志文件)的序列號,剩下的用來標識該日志在VLF中的具體的位置。

  根據LSN不同,日志一般分為兩類:首日志(最新的活動日志序號)和尾日志(保留時間最長的活動日志序號)。隨着數據庫的操作不斷增加(如數據庫中的update操作),首日志LSN序號不斷變化。尾日志的序號只有在日志備份后才會變化。

     

                                                        (圖一)日志文件結構圖

2、VLF

  你可以通過DCC LOGINFO去分析數據庫LDF中VLF(虛擬日志文件),LDF、VLF、日志的關系是:LDF包括多個VLF,每個VLF中包括多個日志記錄。在VLF中,當事物日志增加時,日志的頭部(首日志)不斷向前移動,日志將占用越來越多的剩余空間,當這個VLF被占滿后,新的日志寫入到其他未被使用的VLF中,這個時候LDF並不會增大。當LDF中沒有可用的VLF時,數據庫會創建一個新的VLF。從而使得LDF文件物理增大,占用更多的磁盤空間。

     

                                                           (圖二)日志增長

二、解決方法詳細闡述

  1、日志的截斷

  上圖演示了首日志向前移動的場景,結合圖一和圖二可以看到,當VLF2的空間被日志填滿后,數據庫擴充LDF文件(申請更多的磁盤空間),並在擴充后的LDF中新建一個VLF3用來填充新的日志記錄。盡管VLF1中存在剩余空間,但因為VLF1中存在活動日志(哪怕只有一條),所以數據庫無法利用這個VLF的剩余空間。

  這個時候做日志備份就會發生日志截斷的現象。一般會將截斷理解為"刪除"一些日志記錄(非活動),實際上它只是意味着尾日志的向前移動:尾日志序號會被刷新成最小的活動日志序號,而從原來尾日志的位置到新位置之間的空間被標記為"可重新利用"。這個過程並不會減少LDF已占用的磁盤空間。如下圖,整個VLF1的和部分VLF2上的日志(非活動)被截斷了。

     

                                                      (圖三)日志截斷示意圖

      隨着事務日志不斷增加,VLF3中日志頭部所在的位置將不斷向前移動,當VLF3的空間被占滿后,數據庫會重新利用VLF1的空間,這種寫入、截斷、再寫入的方式形成一個寫日志的循環。在此期間LDF並不會物理上增大。

     

                                                     (圖四)日志循環使用示意圖

2、為什么日志不能收縮

  現在我們再來看一個日志無法收縮的場景:

  圖四中,VLF1中的日志不斷增加,直到VLF1的所有空間都被填滿(如圖五),此時因為沒有發生截斷,尾日志都在VLF2上,且VLF2和VLF3都被標記為不可重新利用,數據庫只能擴充LDF、新建一個VLF4用來記錄新的日志,首日志的位置將出現在VLF4中,整個寫日志的(從圖一到圖四)順序為VLF2——>VLF3——>VLF1——>VLF4。這個過程會導致數據庫的日志文件在物理上增大。

     

                                                           (圖五)日志增長示意圖

      這時我們再來截斷事物日志,如上文所說,尾日志的會被更新,最后可能出現尾日志和首日志在同一個VLF上的場景。從日志文件記錄的架構上來看,我們可以將這個過程簡單地理解為:截斷的順序會按照首日志移動的順序移動,從VLF2——>VLF3——>VLF1——>VLF4,最終尾日志和首日志出現在同一個VLF上。

      

                                                          (圖六)日志截斷示意圖二

      如上圖,這個LDF文件包括3個空的和1個只有小部分活動日志的VLF文件,首日志和尾日志在同一個VLF中,這種情況下,試圖通過DBCC SHRINKFILE是不會減小LDF文件的大小的。

  日志文件能被收縮的原因是該文件尾部的數據被清除了,使得該部分空間被釋放,而不是逃過尾部去刪除文件首部或者中間部分的內容。這點與MDF文件不同,MDF文件中的數據是不能被刪除的,只能將文件尾部的數據遷移到其他區域的剩余空間上,然后釋放尾部占用的空間。

  在LDF中 ,日志是不能被遷移的,而且也沒有遷移的必要,因為當事物被提交后,日志變為不活動狀態,通過事物日志備份即可將其截斷(特殊情況下日志備份不一定能截斷,如發布訂閱的環境)。

  綜上所述,日志文件能被收縮的前提是:日志文件的最后一個VLF必須是free狀態,從后向前推,只要是free狀態的VLF都會被收縮,據此可以估算一個日志文件可以釋放的空間大小。

  如下我們看一個實際的例子:

  USE DBname

  DBCC loginfo

      

                                             (圖七)VLF狀態示意圖

      從上圖可以看到,這個數據庫的日志文件共有13個VLF,其中有前12個處於free狀態,最后1個處於活動狀態,因此,我們可以推斷首日志和尾日志的位置都在這個VLF上。這個時候執行文件收縮將看不到文件減小的效果。

 

3、如何解決這個問題

  那么碰到這種情況,該怎么去收縮日志呢:盡可能多的執行一些能夠產生大量日志的操作,這些日志將導致數據庫重新利用startoffset靠前的非活動狀態的VLF,將首日志的位置定位到這個startoffset,然后做一次事務日志備份,將尾日志也遷移到startoffset靠前的非活動狀態的VLF中,如下圖,最后再執行DBCC SHRINKFILE即可收縮日志文件。

     

                                                 (圖八)日志截斷示意圖三

三、重要說明

  前文中一直在說通過日志備份即可解決日志截斷的問題,其實這只是最簡單的場景。在實際環境中可能有很多因素會影響日志的截斷,如:

  活動的事物日志

  日志備份只能截斷非活動的日志,如果一個事物長時間運行,此時備份事物日志將不會引起截斷發生。

  事物日志分發

  事物日志分發中,只有當日志讀取器代理已經讀取完待分發的日志后,日志才能變得非活動狀態。

  數據庫鏡像和AlwaysOn

  這兩種數據庫技術都需要將日志傳遞到接受端,在傳遞還沒有完成時,日志會一直保留,即使是備份日志也無法截斷。

四、Always on 環境下實踐

  先對數據庫進行完整備份:

      

EXEC sp_configure 'show advanced options', 1;

RECONFIGURE;

EXEC sp_configure 'xp_cmdshell', 1;

RECONFIGURE;

DECLARE @DbName NVARCHAR(1000);

DECLARE myCursor CURSOR LOCAL STATIC
FOR
SELECT [name]
FROM sysdatabases
WHERE [name] NOT IN ( 'master', 'model', 'msdb', 'tempdb' )
AND name NOT LIKE '%test%'
AND name NOT LIKE '%bak%'
AND name NOT LIKE '%demo%'
AND version IS NOT NULL
AND version <> 0
ORDER BY [name];
OPEN myCursor;
FETCH NEXT FROM myCursor INTO @DbName;
WHILE ( @@FETCH_STATUS = 0 )
BEGIN
DECLARE @strDate AS NVARCHAR(20),
@strDateBeforeSeven AS NVARCHAR(20),
@strFileName AS NVARCHAR(255),
@strFileNameBeforeSeven AS NVARCHAR(255),
@strCommand AS NVARCHAR(255)

SET @strDate = CONVERT(NVARCHAR(20),GETDATE(),112);
SET @strDateBeforeSeven = CONVERT(NVARCHAR(20),GETDATE()-3,112); 
SET @strFileName = 'E:\daybak\['+@DbName+']_bakup_'+@strDate; 
SET @strFileNameBeforeSeven = 'E:\daybak\['+@DbName+']_bakup_'+@strDateBeforeSeven; 

EXEC ('BACKUP DATABASE ['+@DbName+'] TO DISK = ''' + @strFileName + '.bak''')

SET @strCommand = 'DEL ' + @strFileNameBeforeSeven + '.bak'
EXEC master.dbo.xp_cmdshell @strCommand

FETCH NEXT FROM myCursor INTO @DbName;
END;
CLOSE myCursor;
DEALLOCATE myCursor;

  然后對數據庫進行事務日志備份並收縮:

EXEC sp_configure 'show advanced options', 1;

RECONFIGURE;

EXEC sp_configure 'xp_cmdshell', 1;

RECONFIGURE;

DECLARE @DbName NVARCHAR(1000);

DECLARE myCursor CURSOR LOCAL STATIC
FOR
SELECT [name]
FROM sysdatabases
WHERE [name] NOT IN ( 'master', 'model', 'msdb', 'tempdb' )
AND name NOT LIKE '%test%'
AND name NOT LIKE '%bak%'
AND name NOT LIKE '%demo%'
AND version IS NOT NULL
AND version <> 0
ORDER BY [name];
OPEN myCursor;
FETCH NEXT FROM myCursor INTO @DbName;
WHILE ( @@FETCH_STATUS = 0 )
BEGIN
DECLARE @strDate AS NVARCHAR(20),
@strDateBeforeSeven AS NVARCHAR(20),
@strFileName AS NVARCHAR(255),
@strFileNameBeforeSeven AS NVARCHAR(255),
@strCommand AS NVARCHAR(255)

SET @strDate = CONVERT(NVARCHAR(20),GETDATE(),112);
SET @strDateBeforeSeven = CONVERT(NVARCHAR(20),GETDATE()-3,112); 
SET @strFileName = 'E:\Log_daybak\['+@DbName+']_bakup_'+@strDate; 
SET @strFileNameBeforeSeven = 'E:\Log_daybak\['+@DbName+']_bakup_'+@strDateBeforeSeven; 

EXEC ('BACKUP LOG ['+@DbName+'] TO DISK = ''' + @strFileName + '.log'';USE ['+@DbName+'];DBCC SHRINKFILE(2,100);')

SET @strCommand = 'DEL ' + @strFileNameBeforeSeven + '.log'
EXEC master.dbo.xp_cmdshell @strCommand

FETCH NEXT FROM myCursor INTO @DbName;
END;
CLOSE myCursor;
DEALLOCATE myCursor;

 


免責聲明!

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



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