天天看點

收縮SQL Server日志不是那麼簡單的(翻譯).NET 使用CouchBase 基礎篇

原文位址:http://rusanu.com/2012/07/27/how-to-shrink-the-sql-server-log/

說明:本文為了更好的說明收縮的過程,在原文翻譯的基礎上增加了一些個人的了解,省略了部分内容,建議大家在閱讀本文時參考原文。

一、問題場景

我的資料庫日志檔案已經增大到200G了,我也嘗試去收縮資料庫,但大小沒有改變,請問該如何減小日志檔案的大小?這個問題實際上就是說執行DBCC SHRINKFILE沒有減小日志檔案的大小,到底是什麼原因導緻的呢?

二、準備知識

1、LSN

LSN用來辨別特定日志在日志檔案中位置(詳情請見什麼是LSN:日志序列号),它由兩部分組成:一部分用來辨別VLF(虛拟日志檔案)的序列号,剩下的用來辨別該日志在VLF中的具體的位置。

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

收縮SQL Server日志不是那麼簡單的(翻譯).NET 使用CouchBase 基礎篇

(圖一)日志檔案結構圖

2、VLF

你可以通過DCC LOGINFO去分析資料庫LDF中VLF(虛拟日志檔案),LDF、VLF、日志的關系是:LDF包括多個VLF,每個VLF中包括多個日志記錄。在VLF中,當事物日志增加時,日志的頭部(首日志)不斷向前移動,日志将占用越來越多的剩餘空間,當這個VLF被占滿後,新的日志寫入到其他未被使用的VLF中,這個時候LDF并不會增大。當LDF中沒有可用的VLF時,資料庫會建立一個新的VLF。進而使得LDF檔案實體增大,占用更多的磁盤空間。

收縮SQL Server日志不是那麼簡單的(翻譯).NET 使用CouchBase 基礎篇

(圖二)日志增長

三、解決方法詳細闡述

1、日志的截斷

上圖示範了首日志向前移動的場景,結合圖一和圖二可以看到,當VLF2的空間被日志填滿後,資料庫擴充LDF檔案(向作業系統申請更多的磁盤空間),并在擴充後的LDF中建立一個VLF3用來填充新的日志記錄。盡管VLF1中存在剩餘空間,但因為VLF1中存在活動日志(哪怕隻有一條),是以資料庫無法利用這個VLF的剩餘空間,(詳細原因可以參考這篇文章什麼是LSN:日志序列号)。

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

收縮SQL Server日志不是那麼簡單的(翻譯).NET 使用CouchBase 基礎篇

日志截斷示意圖(圖三)

随着事務日志不斷增加,VLF3中日志頭部所在的位置将不斷向前移動,當VLF3的空間被占滿後,資料庫會重新利用VLF1的空間,這種寫入、截斷、再寫入的方式形成一個寫日志的循環。在此期間LDF并不會實體上增大。

收縮SQL Server日志不是那麼簡單的(翻譯).NET 使用CouchBase 基礎篇

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

2、為什麼日志不能收縮

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

圖四中,VLF1中的日志不斷增加,直到VLF1的所有空間都被填滿(如圖五),此時因為沒有發生截斷,尾日志都在VLF2上,且VLF2和VLF3都被标記為不可重新利用,資料庫隻能擴充LDF、建立一個VLF4用來記錄新的日志,首日志的位置将出現在VLF4中,整個寫日志的(從圖一到圖四)順序為VLF2——>VLF3——>VLF1——>VLF4。這個過程會導緻資料庫的日志檔案在實體上增大。

收縮SQL Server日志不是那麼簡單的(翻譯).NET 使用CouchBase 基礎篇

日志增長示意圖(圖五)

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

收縮SQL Server日志不是那麼簡單的(翻譯).NET 使用CouchBase 基礎篇

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

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

日志檔案能被收縮的原因是該檔案尾部的資料被清除了,使得該部分空間被釋放,而不是逃過尾部去删除檔案首部或者中間部分的内容。這點與MDF檔案不同,MDF檔案中的資料是不能被删除的,隻能将檔案尾部的資料遷移到其他區域的剩餘空間上,然後釋放尾部占用的空間。

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

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

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

USE DBname

DBCC loginfo

收縮SQL Server日志不是那麼簡單的(翻譯).NET 使用CouchBase 基礎篇

VLF狀态示意圖(圖七)

從上圖可以看到,這個資料庫的日志檔案共有13個VLF,其中有前12個處于free狀态,最後1個處于活動狀态,是以,我們可以推斷首日志和尾日志的位置都在這個VLF上。這個時候執行檔案收縮将看不到檔案減小的效果。

3、如何解決這個問題

那麼碰到這種情況,該怎麼去收縮日志呢:盡可能多的執行一些能夠産生大量日志的操作,這些日志将導緻資料庫重新利用startoffset靠前的非活動狀态的VLF,将首日志的位置定位到這個startoffset,然後做一次事務日志備份,将尾日志也遷移到startoffset靠前的非活動狀态的VLF中,如下圖,最後再執行DBCC SHRINKFILE即可收縮日志檔案。

收縮SQL Server日志不是那麼簡單的(翻譯).NET 使用CouchBase 基礎篇

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

四、重要說明

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

  • 活動的事物日志

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

  • 事物日志分發

事物日志分發中,隻有當日志讀取器代理已經讀取完待分發的日志後,日志才能變得非活動狀态。(之前我處理過一個類似問題,大家可以通過這個連結看看http://www.cnblogs.com/i6first/p/3281437.html。)

  • 資料庫鏡像和AlwaysOn

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

.NET 使用CouchBase 基礎篇

2011年2月,CouchOne和memebase合并後,改名為Couchbase,官網位址(www.couchbase.com)。membase最後一個版本為1.7.2,可在Couchbase的官網下載下傳(http://www.couchbase.com/downloads-all)。

這裡不介紹couchbase的安裝,隻介紹.NET Client Librarye 使用。

  1. 獲得CouchBase For Net的SDK有兩種方式
    1. 通過nuget,Install-Package CouchbaseNetClient
    2. 通過官網下載下傳http://www.couchbase.com/communities/net
  2. 開始CouchBase之旅
    1. 建立項目,這裡需要提醒的是,在vs2010中我們建立類Console應用程式時項目預設使用.NET Framework Client Profile,我們需要手動切換至full .NET Framework。
    2. 在程式中配置CouchBase,CouchBase提供編碼配置和配置檔案配置,當然使用app.config是最靈活的,這裡是我們的首選,添加以下資訊至你的配置檔案,

    <?xml version="1.0"?>

    <configuration>

      <configSections>

        <section name="couchbase" type="Couchbase.Configuration.CouchbaseClientSection, Couchbase"/>

      </configSections>

      <couchbase>

        <servers bucket="default" bucketPassword="">

          <add uri="http://192.168.0.2:8091/pools"/>

          <add uri="http://192.168.0.3:8091/pools"/>

        </servers>

      </couchbase>

    </configuration>

    這裡使用了叢集配置的url清單,當然在你本地調試隻需要一個位址,預設CouchBase在安裝時會建立一個沒有密碼的default的緩存桶(bucket),你可以自由修改這塊的資訊。(如果對bucket不太明白,請自行google)。
    1. 添加引用

using Couchbase;

using Couchbase.Configuration;

using Couchbase.Extensions;

using Enyim.Caching;

using Enyim.Caching.Configuration;

using Enyim.Caching.Memcached;

根據實際引用添加引用

  1. 建立執行個體及使用

var client = new CouchbaseClient(); // 建立執行個體

client.Store(StoreMode.Add, "somekey", "somevalue"); //存儲資料

var someValue = client.Get("somekey") as string; //擷取資料

var someValue = client.Get<string>("somekey"); //擷取資料

以上是簡單基本類型的使用,下面我們介紹一下複雜類型。先申明一個類

[Serializable]

public class Beer {

    public Guid Id { get; set; }

    public string Name { get; set; }

    public string Brewery { get; set; }

}

var key = Guid.NewGuid();

var beer = new Beer {

    Id = key,

    Name = "Old Yankee Ale",

    Brewery = "Cottrell Brewing Company"

};

client.Store(StoreMode.Add, "beer_" + key, beer);

var beer = client.Get<Beer>("beer_" + key);

在CouchBase2.0正式版就開始支援json,這一點讓人激動人心。

存儲json資料

public static bool StoreJson<T>(this CouchbaseClient client, StoreMode storeMode, string key, T value) where T : class {

    var ms = new MemoryStream();

    var serializer = new DataContractJsonSerializer(typeof(T));

    serializer.WriteObject(ms, value);

    var json = Encoding.Default.GetString(ms.ToArray());

    ms.Dispose();

    return client.Store(storeMode, key, json);            

擷取json資料

public static T GetJson<T>(this CouchbaseClient client, string key) where T : class {    

    var json = client.Get<string>(key);    

    var ms = new MemoryStream(Encoding.Default.GetBytes(json));

    var serializer = new DataContractJsonSerializer(typeof(T));                            

    var obj = serializer.ReadObject(ms) as T;

    return obj;                        

Client使用方法

var key = Guid.NewGuid();

var beer = new Beer {

    Id = key,

    Name = "American Ale",

    Brewery = "Thomas Hooker Brewing Company",

    Type = "beer"

client.StoreJson<Beer>(StoreMode.Add, "beer_" + key, beer);

var beer = client.GetJson<Beer>("beer_" + key);

  1. 檢查和操作結果

    官方的說明

    For check and set operations, the return values are wrapped in a CasResult instance.  The success of the operation is still determined by a Boolean and detailed failures still require logging. 

var result = client.GetWithCas("foo");

var bar = "bar"; 

var result = client.Cas(StoreMode.Set, "foo", bar, result.Cas);

if (result.Result) {

   Console.WriteLine("CAS operation was successful");

  1. 擷取詳細操作結果

    如果你需要擷取運作時的詳細資訊,你可以使用IoperationResult API方法,下面是官方給的API屬性的說明。

    Each of these methods shares its name with a method from the single-value return API, but prefixed with "Execute." For example, Get() becomes ExecuteGet() and Store() becomes ExecuteStore().

Property Interface Description
Success IOperationResult Whether the operation succeeded
Message Error, warning or informational message
StatusCode Nullable status code from server
InnerResult Nested result.  Populated by low-level I/O failures.
Value INullableOperationResult Extended by IGetOperationResult, where Value is item for given key. 
HasValue Shortcut for null Value check. 
Cas ICasOperationResult Extended by IGetOperationResult, IMutateOperationResult, IConcatOperationResult and IStoreOperationResult.  Contains possible CAS value for operations.

var getResult = client.ExecuteGet<Beer>("beer_heady_topper");

if (getResult.Success && getResult.HasValue) {   

   var beer = getResult.Value;

   beer.Brewery = "The Alchemist";

   var casResult = client.ExecuteCas(StoreMode.Set, "beer_heady_topper", beer, getResult.Cas);

   if (casResult.Success) {

       Console.WriteLine("CAS operation was successful");

   }

} else {

   Console.WriteLine("Get operation failed with message {0} and exception {1} ", 

                          getResult.Message, getResult.Exception);

  1. 配置日志

    CouchBase支援Log4Net和Nlog,你可以自己現在或者從CouchBase提供的SDK擷取。

    Log4Net 配置參考。

<?xml version="1.0" encoding="utf-8"?>

  <configSections>

    <sectionGroup name="enyim.com">

      <section name="log" type="Enyim.Caching.Configuration.LoggerSection, Enyim.Caching" />

    </sectionGroup>

    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />

  </configSections>

  <enyim.com>

    <log factory="Enyim.Caching.Log4NetFactory, Enyim.Caching.Log4NetAdapter" />

  </enyim.com>

  <log4net debug="false">

    <appender name="LogFileAppender" type="log4net.Appender.FileAppender,log4net">

      <param name="File" value="c:\\temp\\error-log.txt" />

      <param name="AppendToFile" value="true" />

      <layout type="log4net.Layout.PatternLayout,log4net">

        <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] &lt;%X{auth}&gt; - %m%n" />

      </layout>

    </appender>

    <root>

      <priority value="ALL" />

      <level value="DEBUG" />

      <appender-ref ref="LogFileAppender" />

    </root>

  </log4net>  

更多Log4Net配置可參考:http://logging.apache.org/log4net/release/manual/configuration.html.

Nlog配置參考

    <section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog" />

    <log factory="Enyim.Caching.NLogFactory, Enyim.Caching.NLogAdapter" />

  <nlog>

    <targets>

      <target name="logfile" type="File" fileName="c:\temp\error-log.txt" />

    </targets>

    <rules>

      <logger name="*" minlevel="Info" writeTo="logfile" />

    </rules>

  </nlog>

  <startup>

    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />

  </startup>

更多Nlog配置可參考:http://nlog-project.org/wiki/Configuration_file

總結 :以上資訊來源官方的Getting Started,另附一份自己整理的Demo。(通過office word 釋出的文檔格式有些變形)

Demo源碼

着意耕耘,自有收獲