收缩SQL Server日志不是那么简单

收缩SQL Server日志不是那么简单的(翻译)

 

原文地址: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日志不是那么简单

(图一)日志文件结构图

2、VLF

你可以通过DCC LOGINFO去分析数据库LDF中VLF(虚拟日志文件),LDF、VLF、日志的关系是:LDF包括多个VLF,每个VLF中包括多个日志记录。在VLF中,当事物日志增加时,日志的头部(首日志)不断向前移动,日志将占用越来越多的剩余空间,当这个VLF被占满后,新的日志写入到其他未被使用的VLF中,这个时候LDF并不会增大。当LDF中没有可用的VLF时,数据库会创建一个新的VLF。从而使得LDF文件物理增大,占用更多的磁盘空间。

收缩SQL Server日志不是那么简单

(图二)日志增长

三、解决方法详细阐述

1、日志的截断

上图演示了首日志向前移动的场景,结合图一和图二可以看到,当VLF2的空间被日志填满后,数据库扩充LDF文件(向操作系统申请更多的磁盘空间),并在扩充后的LDF中新建一个VLF3用来填充新的日志记录。尽管VLF1中存在剩余空间,但因为VLF1中存在活动日志(哪怕只有一条),所以数据库无法利用这个VLF的剩余空间,(详细原因可以参考这篇文章什么是LSN:日志序列号)。

这个时候做日志备份就会发生日志截断的现象。一般会将截断理解为"删除"一些日志记录(非活动),实际上它只是意味着尾日志的向前移动:尾日志序号会被刷新成最小的活动日志序号,而从原来尾日志的位置到新位置之间的空间被标记为"可重新利用"。这个过程并不会减少LDF已占用的磁盘空间。如下图,整个VLF1的和部分VLF2上的日志(非活动)被截断了。

收缩SQL Server日志不是那么简单

日志截断示意图(图三)

随着事务日志不断增加,VLF3中日志头部所在的位置将不断向前移动,当VLF3的空间被占满后,数据库会重新利用VLF1的空间,这种写入、截断、再写入的方式形成一个写日志的循环。在此期间LDF并不会物理上增大。

收缩SQL Server日志不是那么简单

日志循环使用示意图(图四)

2、为什么日志不能收缩

现在我们再来看一个日志无法收缩的场景:

图四中,VLF1中的日志不断增加,直到VLF1的所有空间都被填满(如图五),此时因为没有发生截断,尾日志都在VLF2上,且VLF2和VLF3都被标记为不可重新利用,数据库只能扩充LDF、新建一个VLF4用来记录新的日志,首日志的位置将出现在VLF4中,整个写日志的(从图一到图四)顺序为VLF2——>VLF3——>VLF1——>VLF4。这个过程会导致数据库的日志文件在物理上增大。

收缩SQL Server日志不是那么简单

日志增长示意图(图五)

这时我们再来截断事物日志,如上文所说,尾日志的会被更新,最后可能出现尾日志和首日志在同一个VLF上的场景。从日志文件记录的架构上来看,我们可以将这个过程简单地理解为:截断的顺序会按照首日志移动的顺序移动,从VLF2——>VLF3——>VLF1——>VLF4,最终尾日志和首日志出现在同一个VLF上。

收缩SQL Server日志不是那么简单

日志截断示意图二(图六)

如上图,这个LDF文件包括3个空的和1个只有小部分活动日志的VLF文件,首日志和尾日志在同一个VLF中,这种情况下,试图通过DBCC SHRINKFILE是不会减小LDF文件的大小的。

日志文件能被收缩的原因是该文件尾部的数据被清除了,使得该部分空间被释放,而不是逃过尾部去删除文件首部或者中间部分的内容。这点与MDF文件不同,MDF文件中的数据是不能被删除的,只能将文件尾部的数据迁移到其他区域的剩余空间上,然后释放尾部占用的空间。

在LDF中 ,日志是不能被迁移的,而且也没有迁移的必要,因为当事物被提交后,日志变为不活动状态,通过事物日志备份即可将其截断(特殊情况下日志备份不一定能截断,如发布订阅的环境)。

综上所述,日志文件能被收缩的前提是:日志文件的最后一个VLF必须是free状态,从后向前推,只要是free状态的VLF都会被收缩,据此可以估算一个日志文件可以释放的空间大小。

如下我们看一个实际的例子:

USE DBname

DBCC loginfo

收缩SQL Server日志不是那么简单

VLF状态示意图(图七)

从上图可以看到,这个数据库的日志文件共有13个VLF,其中有前12个处于free状态,最后1个处于活动状态,因此,我们可以推断首日志和尾日志的位置都在这个VLF上。这个时候执行文件收缩将看不到文件减小的效果。

3、如何解决这个问题

那么碰到这种情况,该怎么去收缩日志呢:尽可能多的执行一些能够产生大量日志的操作,这些日志将导致数据库重新利用startoffset靠前的非活动状态的VLF,将首日志的位置定位到这个startoffset,然后做一次事务日志备份,将尾日志也迁移到startoffset靠前的非活动状态的VLF中,如下图,最后再执行DBCC SHRINKFILE即可收缩日志文件。

收缩SQL Server日志不是那么简单

日志截断示意图三(图六)

四、重要说明

前文中一直在说通过日志备份即可解决日志截断的问题,其实这只是最简单的场景。在实际环境中可能有很多因素会影响日志的截断,如:

  • 活动的事物日志

日志备份只能截断非活动的日志,如果一个事物长时间运行,此时备份事物日志将不会引起截断发生。

  • 事物日志分发

事物日志分发中,只有当日志读取器代理已经读取完待分发的日志后,日志才能变得非活动状态。(之前我处理过一个类似问题,大家可以通过这个链接看看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;
    ms.Dispose();
    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

IOperationResult

Error, warning or informational message

StatusCode

IOperationResult

Nullable status code from server

InnerResult

IOperationResult

Nested result.  Populated by low-level I/O failures.

Value

INullableOperationResult

Extended by IGetOperationResult, where Value is item for given key.

HasValue

INullableOperationResult

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"?>
<configuration>
  <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>  
</configuration>

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

Nlog配置参考

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <sectionGroup name="enyim.com">
      <section name="log" type="Enyim.Caching.Configuration.LoggerSection, Enyim.Caching" />
    </sectionGroup>
    <section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog" />
  </configSections>
  <enyim.com>
    <log factory="Enyim.Caching.NLogFactory, Enyim.Caching.NLogAdapter" />
  </enyim.com>
  <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>
</configuration>

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

总结 :以上信息来源官方的Getting Started,另附一份自己整理的Demo。(通过office word 发布的文档格式有些变形)

Demo源码

着意耕耘,自有收获
上一篇:恒源云_云GPU服务器如何使用SpaCy?


下一篇:Linux中set -x 与 set +x命令的使用