存储引擎揭秘:深入理解幽灵清除
原文地址:
http://www.sqlskills.com/BLOGS/PAUL/post/Inside-the-Storage-Engine-Ghost-cleanup-in-depth.aspx
(译注: MSND 中将 ghost 翻译为虚影,本文翻译为幽灵,一个意思。)
正文:
在存储引擎小组的这几年,我看到很多关于幽灵清除的帖子。这个程序的以前版本中有一些bug (KB932115 和KB815594 ),而且关于它的信息很少。基于某些原因,我没有再以前的博文中讲述它,今天我想详细介绍它。
那么什么是幽灵清除呢?它是一个用来清除幽灵的后台程序,通常也被称为幽灵清除任务。什么是幽灵呢?正如上周我在《 存储引擎揭秘:基本结构之一 ——记录 》中进行简单描述的一样,幽灵是指刚被删除的索引记录。(实际上,如果是快照隔离级别可能更复杂,但就目前而言,说“索引记录”已经可以了。)这样的一个删除操作并不会将记录从页中物理删除——它只是将记录标明已经删除了(或者说变成幽灵了)。这种性能优化措施可以让删除更快速地完成,它也可以快速地回滚删除操作,因为要做的动作仅仅是将删除标志改回去,而不是重新插入被删除的记录。那条被删除的记录以后会被后台运行的幽灵清除任务物理清除。为了防止释放空的数据和索引页,幽灵清除任务会在页上留下一个记录(译注:这是在 SQL SERVER 2000 下, 2005 及以后版本不是这样了)。
幽灵清除任务直到删除事务被提交后才能物理删除这些幽灵记录,因为提交事务前,被删除记录是被锁住的,而且锁直到事务被提交才释放。顺便说一下,如果一个页中有幽灵记录,即使是 NOLOCK 或 READ UNCOMMITTED 方式下也不会返回这些幽灵记录,因为它们被标为幽灵记录了。
当一个记录被删除,除了将这个记录标为幽灵记录,记录所在页的头部以及分配该页的 PFS 中都会标明。在 PFS 页上标明某页有幽灵记录也会改变数据库的状态——标明数据库中某处有幽灵记录需要清除。当然系统是没有办法告诉幽灵清除任务哪些页发生了删除操作。只有下次扫描操作读到了这些页才会注意到这些页中有幽灵记录。(译注:根据上下文意思,“下次扫描”是指幽灵清除任务扫描数据库中的所有 PFS 页。)
幽灵清除任务不是一请求就运行,它是每 5 秒钟自动在后台运行来查找幽灵记录来清除。记住幽灵清除任务并不是由删除操作来指示清除一个指定页的——它是由后来的扫描操作指示的。当幽灵清除任务开始时,它会检查数据库中是否有页需要被清除,若有则清除。若没有,则选择标明有幽灵记录的数据库,扫描数据库中的所有 PFS 页检查是否有页需要清除。为了确保不会加重系统负担,每次运行时,幽灵清除任务只会检查并清除有限数量的页——我记得好像是 10 页。如果它处理了数据库且没有再发现任何幽灵记录,它就会将数据库标明无幽灵记录,这样下次就可以跳过检查了。
你如何辨别幽灵清除任务是否正在运行呢?在 SQL SERVER 2005 中,你可以运行下面的代码,在 sys.dm_exec_requests 中查看幽灵清除任务:
SELECT * INTO myexecrequests FROM sys.dm_exec_requests WHERE 1 = 0 ;
GO
SET NOCOUNT ON ;
GO
DECLARE @a INT
SELECT @a = 0 ;
WHILE ( @a < 1 )
BEGIN
INSERT INTO myexecrequests SELECT * FROM sys.dm_exec_requests WHERE command LIKE '%ghost%'
SELECT @a = COUNT (*) FROM myexecrequests
END ;
GO
SELECT * FROM myexecrequests ;
GO
在SQL Server 2000 中你需要使用sysprocesses( 当然,在SQL Server2005 也可以使用它,但是它是从DMV 中继承来的假视图):
SELECT * INTO mysysprocesses FROM master . dbo . sysprocesses WHERE 1 = 0 ;
GO
SET NOCOUNT ON ;
GO
DECLARE @a INT
SELECT @a = 0 ;
WHILE ( @a < 1 )
BEGIN
INSERT INTO mysysprocesses SELECT * FROM master. dbo. sysprocesses WHERE cmd LIKE '%ghost%'
SELECT @a = COUNT (*) FROM mysysprocesses
END ;
GO
SELECT * FROM mysysprocesses ;
GO
sys.dm_exec_requests 输出如下(去除了大部分没用的和无意义的列):
session_id request_id start_time status command
---------- ----------- ----------------------- ------------ ----------------
15 0 2007-10-05 16:34:49.653 background GHOST CLEANUP
那么你是如何判断一个记录是幽灵记录呢?让我们搭建一个环境然后使用DBCC PAGE 来查看——我已经去除了无意义的输出,并且将感兴趣的幽灵记录部分加深:
CREATE TABLE t1 ( c1 CHAR ( 10 ))
CREATE CLUSTERED INDEX t1c1 on t1 ( c1 )
GO
BEGIN TRAN PaulsTran
INSERT INTO t1 VALUES ( 'PAUL' )
INSERT INTO t1 VALUES ( 'KIMBERLY' )
DELETE FROM t1 WHERE c1 = 'KIMBERLY' ;
GO
DBCC IND ( 'ghostrecordtest' , 't1' , 1 );
GO
DBCC TRACEON ( 3604 );
GO
DBCC PAGE ( 'ghostrecordtest' , 1 , 143 , 3 );
GO
<snip>
m_freeData = 130 m_reservedCnt = 0 m_lsn = (20:88:20)
m_xactReserved = 0 m_xdesId = (0:518) m_ghostRecCnt = 1
m_tornBits = 0
<snip>
Slot 0 Offset 0x71 Length 17
Record Type = GHOST_DATA_RECORD Record Attributes = NULL_BITMAP
Memory Dump @0x6256C071
00000000: 1c000e00 4b494d42 45524c59 20200200 †....KIMBERLY ..
00000010: fc†††††††††††††††††††††††††††††††††††.
UNIQUIFIER = [NULL]
Slot 0 Column 1 Offset 0x4 Length 10
c1 = KIMBERLY
Slot 1 Offset 0x60 Length 17
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP
Memory Dump @0x6256C060
00000000: 10000e00 5041554c 20202020 20200200 †....PAUL ..
00000010: fc†††††††††††††††††††††††††††††††††††.
UNIQUIFIER = [NULL]
Slot 1 Column 1 Offset 0x4 Length 10
c1 = PAUL
让我们来看看在这个过程中事务日志是怎样的(记住这是非文档化的且不提供支持——确保只在测试数据库上执行)?我已去除了很多无意义列:
DECLARE @a CHAR ( 20 )
SELECT @a = [Transaction ID] FROM fn_dblog (null, null) WHERE [Transaction Name] = 'PaulsTran'
SELECT * FROM fn_dblog (null, null) WHERE [Transaction ID] = @a ;
GO
Current LSN Operation Context Transaction ID
------------------------ ----------------- ------------------- --------------
00000014:00000054:0011 LOP_BEGIN_XACT LCX_NULL 0000:00000206
00000014:0000005a:0012 LOP_INSERT_ROWS LCX_CLUSTERED 0000:00000206
00000014:0000005a:0013 LOP_INSERT_ROWS LCX_CLUSTERED 0000:00000206
00000014:0000005a:0014 LOP_DELETE_ROWS LCX_MARK_AS_GHOST 0000:00000206
在两条删除插入动作后跟着一条删除动作——删除的行被标为幽灵记录。但是哪儿是对PFS 页的更新呢?哈哈,在PFS 页中改变ghost 位并不是事务的一部分。我们需要通过其他途径来观察它(除了列出所有的事务日志并人工检查):
SELECT Description , * FROM fn_dblog (null, null) WHERE Context like '%PFS%' AND AllocUnitName like '%t1%' ;
GO
Description Current LSN Operation Context Transaction ID
------------------------- ------------------------ ---------------- --------- ----------------
Allocated 0001:0000008f 00000014:00000054:0014 LOP_MODIFY_ROW LCX_PFS 0000:00000208
00000014:0000005a:0015 LOP_SET_BITS LCX_PFS 0000:00000000
第一条是分配一个页,但是第二条正是我们寻找的——它修改了页中的位:表明页中有幽灵记录。现在让我们提交事务,再看看发生什么吧,过滤掉所有先前的事务日志:
SELECT MAX ( [Current LSN] ) FROM fn_dblog (null, null);
GO
-- 00000014:0000005e:0001
COMMIT TRAN
GO
SELECT [Page ID] , * FROM fn_dblog (null, null) WHERE [Current LSN] > '00000014:0000005e:0001' ;
GO
Page ID Current LSN Operation Context Transaction ID
--------------- ------------------------ ------------------ --------------- --------------
NULL 00000014:0000005f:0001 LOP_COMMIT_XACT LCX_NULL 0000:00000206
0001:0000008f 00000014:00000060:0001 LOP_EXPUNGE_ROWS LCX_CLUSTERED 0000:00000000
我们看到几乎是事务一被提交,幽灵清除任务就处理了该页。我们列出页并检查一下:页确实已被删掉了,但记录的内容还在(无关信息也已被删除)。
DBCC PAGE ( 'ghostrecordtest' , 1 , 143 , 3 );
GO
<snip>
m_freeData = 130 m_reservedCnt = 0 m_lsn = (20:94:1)
m_xactReserved = 0 m_xdesId = (0:518) m_ghostRecCnt = 0
m_tornBits = 0
<snip>
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP
Memory Dump @0x6212C060
00000000: 10000e00 5041554c 20202020 20200200 †....PAUL ..
00000010: fc†††††††††††††††††††††††††††††††††††.
UNIQUIFIER = [NULL]
Slot 0 Column 1 Offset 0x4 Length 10
c1 = PAUL
DBCC PAGE ( 'ghostrecordtest' , 1 , 143 , 2 );
GO
<snip>
6212C 040: 01000000 00000000 00000000 00000000 †................
6212C050: 00000000 00000000 00000000 00000000 †................
6212C060: 10000e00 5041554c 20202020 20200200 †....PAUL ..
6212C070: fc1c000e 004b494d 4245524c 59202002 †.....KIMBERLY .
6212C 080: 00fc0000 00000000 00000000 01000000 †................
6212C090: 00000000 13000000 01000000 00000000 †................
<snip>
所以即使记录已经不存在了,所做的仅仅是 slot 从行偏移表中删除了——记录的内容还在,直到空间被再次使用为止。