2.11.25

Логирование блокировок и быстрое восстановление

Автор: Paul Randal, Lock logging and fast recovery

Одна из тем, о которых я люблю рассказывать, — журнал транзакций и механизмы журналирования/восстановления. Я читал доклад на эту тему и на PASS, и на SQL Connections, и в обоих случаях обещал опубликовать серию статей о глубинных внутренних аспектах операций журналирования. Перед вами первая из них. Другие мои статьи, посвящённые некоторым деталям журналирования:

А теперь — к делу.

В SQL Server в редакции Enterprise существует возможность, называемая быстрое восстановление (fast recovery). Она позволяет базе данных стать доступной для работы сразу после завершения первой фазы восстановления (REDO), ещё до окончания второй (обычно более долгой) фазы (UNDO). Если не понимаете, о чём речь, загляните в мою статью в TechNet Magazine — Ведение журнала и восстановление в SQL Server. Но как SQL Server добивается этого?

Ответ — протоколирование блокировок (lock logging). Каждая журнальная запись описывает одно изменение, внесённое в базу. Для тех записей, которые могут быть использованы в UNDO (да, есть изменения, которые «в одну сторону» — например, изменения страниц PFS), начиная с 2005 года журнал дополнительно включает описание того, какие блокировки удерживались в момент внесения изменения. Эти блокировки были необходимы, чтобы защитить само изменение, когда исходная транзакция выполнялась (до сбоя), а значит, те же самые блокировки будут нужны для защиты анти‑операции, которая это изменение отменяет. О самих анти‑операциях подробнее расскажу в одной из следующих статей из цикла о журналировании.

Движок хранения (Storage Engine) делает три прохода по журналу в рамках восстановления после сбоя. Первый проход определяет, какие транзакции были зафиксированы, и инициирует упреждающее чтение (readahead) нужных страниц файлов данных в буферный пул. Второй проход выполняет REDO, а также читает те журнальные записи, которые будут обрабатываться на третьем проходе (UNDO), — разбирает описания блокировок и фактически захватывает эти блокировки. Для быстрого восстановления на этом этапе база поднимается онлайн. Это возможно потому, что система восстановления знает, что уже удерживает корректные блокировки, гарантирующие, что она безопасно сгенерирует и выполнит анти‑операции, необходимые для UNDO. Побочный эффект: хотя база уже доступна, запрос может упереться в одну из удерживаемых ради быстрого восстановления блокировок — и тогда ему придётся ждать, пока эта блокировка будет снята по мере продвижения UNDO.

Здорово, правда?

Это было введение, чтобы перейти к небольшой «спелеологии» по внутренностям :-) Я создам несколько коротких примеров, чтобы показать протоколирование блокировок в журнале. Не путайтесь: в журнал попадёт не «сама блокировка» (не память, занятая под неё), а лишь описание того, какие блокировки удерживались и в каких режимах.

Ниже скрипт для создания базы данных с простой таблицей. Я использую LOB‑столбец и специально настраиваю хранение вне строки (см. Importance of choosing the right LOB storage technique), чтобы можно было увидеть и блокировки текстовых страниц. Модель восстановления ставлю SIMPLE ради простоты (ха‑ха) — тогда можно очищать журнал контрольной точкой, не возясь с бэкапами лога. Вставлю первую строку и затем очищу журнал.

CREATE DATABASE LockLogging;
GO
USE LockLogging;
GO
CREATE TABLE LockLogTest (c1 INT, c2 INT, c3 VARCHAR (MAX));
GO
EXEC sp_tableoption 'LockLogtest', 'large value types out of row', 'on';
GO
INSERT INTO LockLogTest VALUES (1, 1, 'a');
GO
ALTER DATABASE LockLogging SET RECOVERY SIMPLE;
GO
CHECKPOINT;
GO

Теперь первый опыт — простая вставка — и посмотрим журнальные записи через fn_dblog (пропускаю записи контрольной точки):

INSERT INTO LockLogTest VALUES (2, 2, 'b');
GO
SELECT [Operation], [Context], [Page ID], [Slot ID], [Number of Locks] AS Locks, [Lock Information]
FROM fn_dblog (NULL, NULL);
GO
Operation        Context       Page ID        Slot ID  Locks Lock Information
---------------- ------------- -------------- -------- ----- -------------------------------------------------------------------------------------------------------
LOP_BEGIN_XACT   LCX_NULL      NULL           NULL     NULL  NULL
LOP_INSERT_ROWS  LCX_TEXT_MIX  0001:00000098  1        2     ACQUIRE_LOCK_IX PAGE: 18:1:152; ACQUIRE_LOCK_X RID: 18:1:152:1
LOP_INSERT_ROWS  LCX_HEAP      0001:0000009a  1        3     ACQUIRE_LOCK_IX OBJECT: 18:2073058421:0;ACQUIRE_LOCK_IX PAGE: 18:1:154; ACQUIRE_LOCK_X RID: 18:1:154:1
LOP_COMMIT_XACT  LCX_NULL      NULL           NULL     NULL  NULL

Видим IX‑блокировку страницы и X‑блокировку строки для LOB‑значения, вставляемого на текстовую страницу, а также IX‑блокировку таблицы, IX‑блокировку страницы и X‑блокировку строки для записи данных, вставляемой в кучу. Расшифровка ресурсов блокировок:

  • 18:1:152 — страница 152 в файле 1 базы с ID 18;
  • 18:1:152:1 — слот 1 на странице 152 в файле 1 базы ID 18;
  • 18:2073058421:0 — объект с ID 2073058421 (ID таблицы LockLogTest) в базе ID 18.

Обратите внимание и на LOP_BEGIN_XACT, и на LOP_COMMIT_XACT: хотя я не начинал явной транзакции, SQL Server обязан стартовать её не явно, чтобы была граница для отката на случай сбоя внутри операции.

Далее — обновление (с контрольной точкой перед ним, чтобы очистить журнал):

CHECKPOINT;
GO
UPDATE LockLogTest SET c1 = 3;
GO
SELECT [Operation], [Context], [Page ID], [Slot ID], [Number of Locks] AS Locks, [Lock Information]
FROM fn_dblog (NULL, NULL);
GO
Operation        Context       Page ID        Slot ID  Locks Lock Information
---------------- ------------- -------------- -------- ----- -------------------------------------------------------------------------------------------------------
LOP_BEGIN_XACT   LCX_NULL      NULL           NULL     NULL  NULL
LOP_MODIFY_ROW   LCX_HEAP      0001:0000009a  0        3     ACQUIRE_LOCK_IX OBJECT: 18:2073058421:0;ACQUIRE_LOCK_IX PAGE: 18:1:154; ACQUIRE_LOCK_X RID: 18:1:154:0
LOP_MODIFY_ROW   LCX_HEAP      0001:0000009a  1        3     ACQUIRE_LOCK_IX OBJECT: 18:2073058421:0;ACQUIRE_LOCK_IX PAGE: 18:1:154; ACQUIRE_LOCK_X RID: 18:1:154:1
LOP_COMMIT_XACT  LCX_NULL      NULL           NULL     NULL  NULL

Ровно то, чего и ожидали: IX‑блокировка таблицы, IX‑блокировка страницы и две X‑блокировки строк на этой странице.

А теперь что‑нибудь посложнее, например TRUNCATE TABLE. Слышали миф, что он «не журналается»? Вот именно — миф:

CHECKPOINT;
GO
TRUNCATE TABLE LockLogTest;
GO
SELECT [Operation], [Context], [Page ID], [Slot ID], [Number of Locks] AS Locks, [Lock Information]
FROM fn_dblog (NULL, NULL);
GO
Operation        Context       Page ID        Slot ID  Locks Lock Information
---------------- ------------- -------------- -------- ----- ----------------------------------------------------------------------------
LOP_BEGIN_XACT   LCX_NULL      NULL           NULL     NULL  NULL
LOP_LOCK_XACT    LCX_NULL      NULL           NULL     1     ACQUIRE_LOCK_SCH_M OBJECT: 18:2073058421:0
LOP_MODIFY_ROW   LCX_IAM       0001:0000009b  0        1     ACQUIRE_LOCK_X RID: 18:1:155:0
LOP_MODIFY_ROW   LCX_PFS       0001:00000001  0        1     ACQUIRE_LOCK_X PAGE: 18:1:154
LOP_MODIFY_ROW   LCX_PFS       0001:00000001  0        1     ACQUIRE_LOCK_X PAGE: 18:1:155
LOP_MODIFY_ROW   LCX_IAM       0001:00000099  0        1     ACQUIRE_LOCK_X RID: 18:1:153:0
LOP_MODIFY_ROW   LCX_PFS       0001:00000001  0        1     ACQUIRE_LOCK_X PAGE: 18:1:152
LOP_MODIFY_ROW   LCX_PFS       0001:00000001  0        1     ACQUIRE_LOCK_X PAGE: 18:1:153
LOP_SET_BITS     LCX_SGAM      0001:00000003  1        NULL  NULL
LOP_SET_BITS     LCX_GAM       0001:00000002  1        NULL  NULL
LOP_COUNT_DELTA  LCX_CLUSTERED 0001:00000014  89       NULL  NULL
LOP_COUNT_DELTA  LCX_CLUSTERED 0001:00000011  78       NULL  NULL
LOP_COUNT_DELTA  LCX_CLUSTERED 0001:00000014  90       NULL  NULL
LOP_COUNT_DELTA  LCX_CLUSTERED 0001:00000041  164      NULL  NULL
LOP_COUNT_DELTA  LCX_CLUSTERED 0001:00000041  165      NULL  NULL
LOP_COUNT_DELTA  LCX_CLUSTERED 0001:00000041  166      NULL  NULL
LOP_HOBT_DDL     LCX_NULL      NULL           NULL     NULL  NULL
LOP_MODIFY_ROW   LCX_CLUSTERED 0001:00000014  89       2     ACQUIRE_LOCK_IX OBJECT: 18:7:0;ACQUIRE_LOCK_X KEY: 18:458752 (0000c2681664)
LOP_HOBT_DDL     LCX_NULL      NULL           NULL     NULL  NULL
LOP_MODIFY_ROW   LCX_CLUSTERED 0001:00000014  90       2     ACQUIRE_LOCK_IX OBJECT: 18:7:0;ACQUIRE_LOCK_X KEY: 18:458752 (00007a581379)
LOP_MODIFY_ROW   LCX_CLUSTERED 0001:00000011  78       2     ACQUIRE_LOCK_IX OBJECT: 18:5:0;ACQUIRE_LOCK_X KEY: 18:327680 (00001df3833b)
LOP_COMMIT_XACT  LCX_NULL      NULL           NULL     NULL  NULL

Много журналирования и много блокировок. Если присмотреться к столбцу Context, видно, что операция изменяет карты распределения (IAM, PFS, SGAM, GAM), но блокировки берутся на страницы таблицы, а не на сами карты распределения — их лишь кратко блокируют (latch — внутренний, гораздо более лёгкий механизм синхронизации). Всё это происходит в момент, когда страницы таблицы освобождаются; движок хранения поступает так потому, что таблица небольшая, и он выбирает немедленно освободить всё место, вместо того чтобы складывать работу в очередь фоновой задачи отложенного удаления. См. мою статью Когда после TRUNCATE страницы станут доступны для повторного использования?, там это тоже обсуждается.

Никаких реальных операций над строками самой пользовательской таблицы не выполняется. Единственные строковые операции — внизу, на таблицах с object_id 7 и 5 (sys.sysallocunits и sys.sysrowsets), где обновляются счётчики страниц, ссылки на первую IAM и первую страницу для данной таблицы.

Надеюсь, это было полезно. В следующей статье обсудим компенсирующие журнальные записи (compensation log records) и то, как работает откат (rollback).




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

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