За годы работы в команде Storage Engine я наблюдал много холивара на различных форумах по поводу задачи ghost cleanup. В предыдущих версиях с ней было несколько проблем (см. например, статью базы знаний — KB932115), и небыло доступно достаточно информации об этом. По какой-то причине я не добрался до публикации об этом в своём старом блоге, но сегодня я хочу подробно разобраться со всем этим.
Итак, что такое очистка фантомных записей? Это фоновый процесс, который очищает фантомные строки (записи) — обычно его называют "ghost cleanup task". Что такое фантомная запись? Как я упомянул в статье "Анатомия страницы", фантомная запись — это та, которая только что была удалена в индексе таблицы (ну, на самом деле всё становится сложнее, если включена какая-либо форма изоляции снимков, но пока запись в индексе — хорошее начало). Такая операция удаления никогда физически не удаляет записи со страниц — она только помечает их как удалённые, или "фантомные". Это оптимизация производительности, которая позволяет операциям удаления завершаться быстрее. Также это позволяет операциям удаления быстрее откатываться, поскольку всё, что нужно сделать, — это снять пометку у записей как удалённых/фантомных, вместо необходимости повторно вставлять удалённые строки. Удалённая запись будет физически удалена позже фоновой задачей очистки фантомных записей (ну, её слот будет удалён — данные записи фактически не перезаписываются). Задача очистки фантомных записей оставит одну запись на странице, чтобы избежать необходимости освобождать пустые страницы данных или индексов.
Очистка фантомных записей не может физически удалить фантомные строки до тех пор, пока не зафиксируется удалившая их транзакция, поскольку удалённые записи заблокированы, а блокировки не снимаются до фиксации транзакции. Кстати, когда на странице есть фантомные записи, даже просмотр с NOLOCK или READ UNCOMMITTED не вернёт их, поскольку они помечены как фантомные.
Когда запись удаляется, помимо того, что она помечается как фантомная запись, страница, на которой находится запись, также помечается как содержащая фантомные записи в одной из карт размещения — странице PFS (статья скоро появится!) — и в её заголовке страницы. Пометка страницы как содержащей фантомные записи на странице PFS также изменяет состояние базы данных, указывая, что есть некоторые фантомные записи для очистки — где-то. Ничто не говорит задаче очистки фантомных записей очистить конкретную страницу, на которой произошло удаление — пока. Это происходит только тогда, когда следующая операция сканирования читает страницу и замечает, что на странице есть фантомные записи.
Задача очистки фантомных записей не просто запускается, когда ей это говорят — она запускается в фоновом режиме каждые 5 секунд и ищет фантомные строки для очистки. Помните, что операция удаления не скажет ей идти очищать конкретную страницу — это делает последующее сканирование, когда оно случится в следующий раз. Когда задача очистки фантомных записей запускается, она проверяет, не сказали ли ей очистить страницу — если да, она идёт и делает это. Если нет, она выбирает следующую базу данных, которая помечена как имеющая фантомные строки, и просматривает страницы карты размещения PFS, чтобы посмотреть, есть ли какие-либо фантомные записи для очистки. Она проверит или очистит некоторое количество страниц каждый раз, когда просыпается — я припоминаю, что это будет 10 страниц — чтобы не перегружать систему. Итак — фантомные записи в конечном итоге будут удалены — либо задачей очистки фантомных записей, обрабатывающей базу данных на наличие фантомных записей, либо когда ей специально скажут удалить их со страницы. Если она не находит у базы фантомных строк, она помечает базу данных как не имеющую страниц с фантомными строками, и поэтому в следующий проход задачи она будет пропущена.
Как можно понять, что она работает? В SQL Server вы можете использовать следующий сценарий, чтобы увидеть задачу очистки фантомных записей в 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
Вывод из 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
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
00000014:0000005a:0016 LOP_DELETE_ROWS LCX_MARK_AS_GHOST 0000:00000206
Итак, здесь есть две вставки, за которыми следуют два удаления — с записями, помеченными как фантомные записи. Но где обновление страницы PFS? Ну, изменение фантомного бита на странице PFS не выполняется как часть транзакции. Нам нужно искать это другим способом (кроме простого дампа всего в журнале транзакций и ручного поиска):
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>
6212C040: 01000000 00000000 00000000 00000000 †…………….
6212C050: 00000000 00000000 00000000 00000000 †…………….
6212C060: 10000e00 5041554c 20202020 20200200 †….PAUL ..
6212C070: fc1c000e 004b494d 4245524c 59202002 †…..**KIMBERLY** .
6212C080: 00fc0000 00000000 00000000 01000000 †…………….
6212C090: 00000000 13000000 01000000 00000000 †…………….
<snip>
Итак, даже несмотря на то, что запись больше не существует, всё, что произошло, — это то, что слот был удалён из массива слотов в конце страницы — содержимое записи останется на странице до тех пор, пока место не будет повторно использовано.
Комментариев нет:
Отправить комментарий