27.9.25

Подробно об очистке фантомных строк

Автор: Paul Randal, Inside the Storage Engine: Ghost cleanup in depth

За годы работы в команде 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>

Итак, даже несмотря на то, что запись больше не существует, всё, что произошло, — это то, что слот был удалён из массива слотов в конце страницы — содержимое записи останется на странице до тех пор, пока место не будет повторно использовано.



Комментариев нет:

Отправить комментарий