SQL Server 2000是一个可以自增长的可复写的循环文件,最小增长大小为248K(256K-8K),而一个数据库的多个日志文件,是被轮流选择写入日志的,也就是同时循环使用,所以日志文件如果要增长,也是轮流依次增长。
每一个事务日志文件都被分成若干个小的部分,称为虚拟日志文件(VLF),它是SQL Server日志的最小单元,当整个VLF中的所有记录不在包含活动事务(未提交事务)的信息的时候,比如存在这个VLF的事务刚好提交了,那么这个VLF就处于不活动的状态,能够被SQL Server所复用。
日志文件刚创建时,被分割成如干个相同大小的VLF文件(最少2个,最多16个),创建时,日志文件如果大于1MB,VLF的大小总是64K的倍数,下表提供了日志文件刚创建时的分配情况:
日志创建时大小 | VLF数量 | 每个VLF的大小 |
小于1MB | 2-4个 | 248KB-334KB |
1MB-64MB | 4个 | 256K-16MB |
64MB-1GB | 8个 | 8MB-128MB |
1GB以上 | 16个 | 64MB或更大 |
日志文件的前8K用来存放日志头信息,后面依次存放VLF,所以,对于一个1MB大小的日志文件,先存放一个8K的日志头以后,显然是放不下4个256K的VLF的,其实SQL Server是存了3个256K的VLF和1个256K-8K=248K的VLF。
日志头信息
这8K的文件头也并非填满了信息,其实只有前42个字节是有用的信息,从42-8192这部分字节都是补0的。下面介绍着42个字节究竟存放了哪些信息。
数据类型 | 名称 | 大小 | 描述 |
UCHAR | lfh_fixedValue | 1 | 用于保护以下区域的重复映射.在SQL Server 2000中,被设为0xAB |
UCHAR | lfh_parity | 1 | 在0x40和0x80之间切换,以防止这段VLF被重用.在做位填充操作时,在每512字节的小边界处会填充这个值,以便在恢复时判断日志的结尾 |
USHORT | lfh_versionNo | 2 | 日志文件的格式版本,SQL Server 7时,此值为1,SQL Server 2000时,此值为2 |
ULONG | lfh_fSeqNo | 4 | VLF的序列号,如果是不活动的,则为0 |
ULONG | lfh_status | 4 | 活动的是0x02,否则为0 |
ULONG | lfh_writeSeqNo | 4 | 每次更新时的VLF序列号自增长计数器 |
ULONGLONG | lfh_fileSize | 8 | VLF的大小 |
ULONGLONG | lfh_startOffset | 8 | 物理文件开始到这个头文件的位移 |
LSN | lfh_createLSN | 10 | 此时需要创建的LSN |
日志块结构
每个VLF会包含若干个日志块(log block),而每个日志块依次存放以下信息:
日志块头信息,日志记录(用一个4字节的边界隔开),位置槽数组,一段空白区(用于伸缩填充,以确保后面剩余512个字节作为边界),512字节的边界
日志块总是512字节的倍数(NTFS的sector的大小),最大不会超过60K,一个日志块是一次日志I/O的基本单元. 通常情况下,VLF中的日志块是被填的满满的,但也有时候,VLF中的最后一个日志块是空的,如果VLF剩余的空间装不下这个记录的时候,会保留这个空日志块,而直接去填充下一个VLF。
日志块头信息
名称 | 大小 | 偏移量 | 描述 |
lbk_version | 2 | 0x0 | 日志块的格式版本,SQL Server 7是1,SQL Server 2000是2 |
lbk_noOfRecords | 2 | 0x2 | 日志块中日志记录的个数 |
lbk_offsetSlotArray | 2 | 0x4 | 位置槽数组的偏移量 |
lbk_totalSize | 2 | 0x6 | 日志块的总大小 |
lbk_status | 2 | 0x8 | 被填充为0x01,非填充为0 |
lbk_prevBlkSize | 2 | 0xA | 上一个日志块的起始位置的偏移量,这样可以快速地定位前一个日志块的位置,因此SQL Server是可以反向扫描日志块的 |
lbk_startLSN | 10 | 0xC | 块中第一条记录的LSN |
lbk_pad | 2 | 0x16 | 用于补齐头信息为四字节的整数倍 |
位置槽数组
位置槽数组是一串2字节偏移量值的数组,反向记录每条记录的偏移量.如下图
字节填充
由于日志块是日志I/O的最小单元,所以日志块占用的磁盘段要么全部写入磁盘,要么全都不写。未能在读取日志块的时候能够检查这种状态,采用一种在这些sector的开头标识一个相同的小标记,这个标记在一个VLF中公用同一个,就是lfh_parity的值,每次重用的时候,在0x40和0x80之间切换,即上次使用了0x40这标记,这次就采用0x80(是不是日志块只有一部分被写到了磁盘中,因为日志块是512字节的倍数,它包换若干个sector,而磁盘的写入是以sector为单位的。)
当然,光这么标记也不行,更重要的是记录这个块的开头和结尾,于是在开头sector多加0X10,在结尾sector多加0x08。在NTFS中,读取一个被重映射的sector,可能会读到一段被0xFE填充的字节,但并不返回错误报告,虽然0xFE的0x40和0x80位都是1,并且0x10和0x08位也都是1,但是显然它是个有问题的,怎么办呢,我们通过0x20位来进行检验(我们之前无论怎么标记,0x20位一定是0,但是如果是0xFE是1,那就说明有问题了)
具体规则如下,假设sector开头的那个小标记叫B
if(B AND 0x02==1),说明这个字节很可能是0xFE,可能文件系统经历过磁盘硬件故障
if(B AND 0x10==1),日志段的第一个sector
if(B AND 0x08==1),日志段的最后一个sector
if(B AND lfh_parity == 0),旧的日志块
字节填充技术也用于在启动时检测日志的真实结尾,找到最后一个VLF的最后一个日志块,由于日志文件头并没有提供这个VLF中有多少个日志块,所以需要另外的表示方法来确定当前读取的日志块是否属于当前的VLF。
逻辑日志文件(LLF)
VLF以逻辑日志文件LLF的形式放在日志中,LLF是用于表示一个VLF或存放在备份设备中的临时对象,下表显示了LLF与VLF的关系
日志文件 | 日志备份 |
VLF 11 | LLF1 |
VLF 12 | LLF2 |
VLF 13 | LLF3 |
VLF 14 | LLF4 |
VLF 9 | LLF5 |
VLF 10 | LLF6 |
LLF7 | |
LLF8 |
日志备份存放了原来的VLF1-VLF8,其实一个LLF就是一个VLF,他们的结构是相同的,只不过LLF会存放在日志备份中,而VLF只存放在日志文件中