Header RSS-подписка на обновления сайта eMail-подписка на обновления сайта

Как перестать называть журнал транзакций SQL Server лог-файлом и прекратить борьбу за его размер. Часть 9/12.





Так вот, при заполнении буфера лога он выводится на диск, в LDF-файл, и никто его «разношерстностью» (т.е. принадлежностью разных записей разным транзакциям) не заморачивается. Просто потому, что нет тут никаких проблем. Да, будут разные слоты одного и того же блока содержать записи от разных транзакций — и что? Ведь каждая отдельная запись очень неспроста содержит (помимо прочего) столбец Transaction ID, думаю теперь вы и сами это понимаете. Единственный чрезвычайно важный критерий сводится к тому, что бы каждая новая запись имела строго уникальный LSN — это да, вот это правило абсолютно. А насчет транзакционной принадлежности это уже скорей забота каждой отдельной записи нежели лога в целом.

Вполне ожидаемо что и при CHECKPOINT-е весь буфер, со всем его неоднородным (с аспекте транзакционной принадлежности) содержимым сбрасывается во все тот же LDF-файл. Менее ожидаемо, что и при фиксации транзакции происходит ровно тоже самое, все что накопилось в буфере к этой точке выводится в LDF! Не важно, что не все накопленные записи относятся к фиксируемой транзакции. Не важно, что часть этих записей может принадлежать транзакции все еще остающейся активной, т.е. находящейся в процессе своего развития. Обсуждаемый нами в настоящий момент механизм RAM-буфер логаLDF-файл всегда работает по алгоритму:

  • вывести все(!) накопленные к текущему моменту записи в LDF;
  • начать аккумулировать новую «пачку» записей лога с байта 0 буфера, т.е. считать что буфер чист на 100%.

Причины запуска этого механизма (а их всего 3, как мы установили в конце предыдущей части) на алгоритм не влияют абсолютно.

Обратимся к нашему текущему примеру и избранным строкам отчета по нему и посмотрим — что у нас с практикой? Подтверждает ли последняя факт деления VLF на блоки, тех, в свою очередь, на слоты, а так же не будет ли нами замечено последствий «прогона» записей лога через буфер в памяти?

Даже самый поверхностный анализ упомянутого отчета показывает что слоты, в рамках одного блока (для примера возьмем все тот же 0x8c-блок; не забыли, что это не его номер, а это смещение этого блока?), просто идут по возрастанию. 1-й слот, 2-й слот и т.д. Что вполне ожидаемо. И вот так они растут, растут до слота с номером 0x2aa. И тут, несколько неожиданно (ведь транзакция наша продолжается!), вместо что бы создать очередной слот 0x2ab все того же блока 0x8c, последний закрывается, открывается блок новый, 0x104, и очередная запись лога поступает в первый слот этого нового блока. И мы снова приходим к вопросу оставшемуся без ответа в предыдущей части — почему бы не сделать один большой блок на весь VLF? Откуда берется это деление на блоки вообще? Давайте посчитаем.

У нас имеются 2 последовательных блока — 0x8c и 0x104. Это, как мы знаем, смещения блоков от начала VLF в 512-ти байтных «юнитах». Стало быть, первый блок у нас начинается по смещению (0x8c=140)*512=71680 байт, а второй — (0x104=260)*512=133120 байт. А разница в смещениях что должна символизировать? Да пожалуй что объем байт занятых первым блоком, согласны? Так, и сколько же это? 133120-71680=61440. Это байт. А килобайт? 61440/1024=60. Т.е. 60 килобайт! Думаете совпадение? Тогда прекращайте так думать — это абсолютно прямое и ничем не прикрытое следствие максимального размера RAM-буфера лога. В этой точке транзакции сработала одна из возможных причин запуска сброса буфера в LDF-файл, а именно первая. Теперь понимаете сакральный смысл термина «блок VLF-а»? Да, это именно оно: 1 блок=1 раз сработал механизм RAM-буфер логаLDF-файл. Новый блок в VLF образуется всякий раз, когда содержимое буфера лога «выталкивается» на диск. Ни причины к этому приведшие, ни объем записей на диск выводимых роли не играют.

Несколько быстрых фактов, прямо вытекающих из нашего последнего открытия:

  • записи относящиеся к одной транзакции могут принадлежать нескольким различным блокам. Правда с учетом того, то они могут и разным VLF-ам принадлежать данное замечание не кажется столь уж сногсшибательным. Подчеркиваю, что речь идет о разных записях лога относящихся к одной транзакции. Одна индивидуальная запись, разумеется, никак не может нарушить границу блока и даже границу контейнера более мелкого — слота;
  • если в блок 0x0 выводится сколько угодно записей размером (суммарным) 512 байт, то блок следующий будет иметь смещение 0x1 — пространство используется максимально эффективно;
  • но и если суммарный размер записей будет 1 байт (ну, допустим), то блок следующий снова будет иметь смещение 0x1 — 511 байт «испарились»;
  • если в блок 0x0 выводится сколько угодно записей размером (суммарным) 513 байт, то блок следующий будет иметь смещение 0x2 — 511 байт блока 0x1 снова «испарились»;
  • и т.д.

Т.е. эффективность использования места в блоках зависит от одного фактора: кратности суммы всех записей этого блока числу 512. Не то что это можно было бы как-то использовать практически (длина любой записи, как и их сумма, вне нашего контроля), но знать об этом не повредит. Как и не повредит осознать такой факт: наихудший сценарий работы лога — обслуживание кучи мелких (ага!) транзакций, каждая из которых генерит 1 байт записи и после этого фиксируется (commit). А наилучший — обслуживание одной большой транзакции чьи записи по объему будут максимально близки к 60кБ*X, где X — любое целое число от единицы. В общем: мелкие транзакции оставляют значительные «вымоины» в 512-ти байтных блоках, транзакции большие тоже их оставляют, но в гораздо меньших количествах. Просили пример? Получите.

Однако давайте проверим наши умозрительные (пока что) заключения практикой. Стало быть, если мы наш текущий пример перепишем в «парадигме» «много мелких транзакций», то, по идее, мы должны увидеть большую кучу блоков, а вот число слотов в каждом должно изрядно сократиться. Пробуем?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
USE master
go
CREATE DATABASE TstLog
ON PRIMARY (
    NAME = 'TstLog_Data',
    FILENAME = 'c:\sqlCMD.ru\TstLog_Data.mdf',
    SIZE = 5 MB,
    MAXSIZE = 5 MB )
LOG ON (
    NAME = 'TstLog_Log',
    FILENAME = 'c:\sqlCMD.ru\TstLog_Log.ldf',
    SIZE = 1 MB,
    MAXSIZE = 1 MB )
GO
ALTER DATABASE TstLog SET RECOVERY SIMPLE
GO
USE TstLog
GO
CREATE TABLE T1 (C1 INT IDENTITY, C2 INT NOT NULL, C3 VARCHAR(20) NOT NULL)
GO
INSERT INTO T1 VALUES (10, 'A')
INSERT INTO T1 VALUES (20, 'K')
INSERT INTO T1 VALUES (30, 'S')
CHECKPOINT
GO
declare @i int = 0
    WHILE @i < 500 --loop counter
    BEGIN
        BEGIN TRAN
        insert into T1 VALUES (17, REPLICATE('S', 20))
        set @i += 1
        COMMIT TRAN
     END
go
DBCC LOGINFO
DBCC LOG(TstLog, 1)
--clean up
USE master
go
DROP DATABASE TstLog

Отчет команды DBCC LOGINFO (выборочно):

FileId  FileSize    StartOffset FSeqNo  Status  Parity  CreateLSN
2       253952      8192        72      2       64      0
2       253952      262144      73      2       64      0

Отчет команды DBCC LOG (выборочно):

Current LSN
00000048:0000008c:0001
Current LSN             Operation
...
00000048:0000008c:0001  LOP_IDENT_NEWVAL
00000048:0000008c:0002  LOP_BEGIN_XACT
00000048:0000008c:0003  LOP_INSERT_ROWS
00000048:0000008c:0004  LOP_COMMIT_XACT
00000048:0000008d:0001  LOP_IDENT_NEWVAL
00000048:0000008d:0002  LOP_BEGIN_XACT
00000048:0000008d:0003  LOP_INSERT_ROWS
00000048:0000008d:0004  LOP_COMMIT_XACT
00000048:0000008e:0001  LOP_IDENT_NEWVAL
00000048:0000008e:0002  LOP_BEGIN_XACT
00000048:0000008e:0003  LOP_INSERT_ROWS
00000048:0000008e:0004  LOP_COMMIT_XACT
00000048:0000008f:0001  LOP_IDENT_NEWVAL
...
00000048:000001e9:0001  LOP_IDENT_NEWVAL
00000048:000001e9:0002  LOP_BEGIN_XACT
00000048:000001e9:0003  LOP_INSERT_ROWS
00000048:000001e9:0004  LOP_COMMIT_XACT
00000048:000001ea:0001  LOP_IDENT_NEWVAL
...
00000048:000001ef:0001  LOP_IDENT_NEWVAL
00000048:000001ef:0002  LOP_BEGIN_XACT
00000048:000001ef:0003  LOP_INSERT_ROWS
00000049:00000010:0001  LOP_COMMIT_XACT
00000049:00000011:0001  LOP_IDENT_NEWVAL
00000049:00000011:0002  LOP_BEGIN_XACT
00000049:00000011:0003  LOP_INSERT_ROWS
...
00000049:000000a2:0003  LOP_INSERT_ROWS
00000049:000000a2:0004  LOP_COMMIT_XACT
00000049:000000a3:0001  LOP_IDENT_NEWVAL
00000049:000000a3:0002  LOP_BEGIN_XACT
00000049:000000a3:0003  LOP_INSERT_ROWS
00000049:000000a3:0004  LOP_COMMIT_XACT

Ну что тут скажешь? Какие еще более красноречивые доказательства требуются? Теперь каждый блок состоит из 4-х слотов (всего!) суммарной емкостью (по прикидкам автора) примерно 250 байт. Т.е. на каждой транзакции мы теряем половину места в блоке. Что же удивляться, что где-то начиная с последней трети отчета записи начинают поступать в следующий VLF, 0x49=73? А ведь в предыдущем подходе, при том же значении счетчика, и VLF 72-й был еще далек от исчерпания! Кстати, интересно, почему в данном случае тот же 72-й VLF был «выведен из эксплуатации» именно по заполнению блока 0x1ef? Что мешало «припахать» к делу блок очередной, 0x1f0? Снова берем в руки «шестнадцатеричные счеты»: (0x1f0=496)*512=253952. Именно с этого значения байт от левого «бордюра» 72-го VLF-а мог бы начаться блок 0x1f0. Но не начнется. Потому что как раз на этом числе байт заканчивается сам VLF (см. выдержку из первого отчета, колонка FileSize). А начинается VLF следующий, 73-й. И начинается он, заметим на секундочку, не блоком 0x0, а 0x10-м. Иными словами, (0x10=16)*512=8192=8кБ — как «корова языком». На вопрос «кто взял?» ответить очень просто: в первом же абзаце 3-й части статьи упоминалось, что лог-файл (LDF) имеет как-раз таки 8-ми килобайтный заголовок файла. Так вот каждый VLF по отдельности тоже не дурак завести свой персональный, «VLF-ный», заголовок идентичного размера. Под свои частные интересы. Но никак не под записи лога. Вот вам и «усушка».

В качестве резюме раздела — преимущества парадигмы «одной большой транзакции» (преимущества парадигмы альтернативной настолько очевидны, что просто не нуждаются в очередном подчеркивании):

  • значительно снижается нагрузка на дисковую подсистему. Снова вспомните пример предыдущей части: «60К файлов по одному байту» vs. «один файл на 60Кб». Большая транзакция есть файл на 60Кб (условно, конечно);
  • место в физическом файле лога потребляется намного эффективнее.

Вывод? Все тот же: в IT нет заведомо «плохих» решений. Как и заведомо «хороших».

Проблема разноформатных LSN

Автор совсем уж собрался было закруглятся с разделом подробного описания идентификаторов записи журнала (LSN), их форматом, ну и буфером лога, «до кучи». Да вспомнил про вопрос пусть не часто, но определенно регулярно обсуждаемый на форумах. Вопрос такой: «у меня есть LSN в десятичном формате, как соотнести его с LSN-ом в формате шестнадцатеричным»?

Сначала немного подробнее о самой проблеме — откуда берутся эти LSN-ы различных систем счисления? Ну со вторыми (hex-форматными) все понятно: это наши трехкомпонентные LSN берущиеся много откуда (из сообщений сервера, из резалт-сетов разных команд и т.п.), но прежде всего, конечно же, из колонки Current LSN инструкции DBCC LOG. В тоже время, часть сообщений, команд языка T-SQL, выходных резалт-сетов оперируют десятичными LSN-ами. Ну, допустим:

  • очень хорошо нам знакомая инструкция DBCC LOGINFO значение колонки CreateLSN выводит именно в формате dec, а не hex;
  • опции WITH STOPATMARK/WITH STOPBEFOREMARK инструкции RESTORE LOG ожидают в качестве параметров именно dec LSN. Пример такой записи можно видеть в BOL-статье Восстановление до номера LSN, более точно — в разделе «Примеры» указанной статьи. Вот так и выглядит LSN если записать его в dec-формате.

Так что проблема соотнесения LSN-ов различных форматов имеет место быть. Вопрос «кто виноват» отбрасываем как несущественный. Переходим сразу к «что делать».

Ну что тут делать — нужен, очевидно, «dec/hex-LSN-конвертор» или типа. Вот так можно провести конвертацию LSN с которым мы работали в прошлой части статьи (а работали мы, напомню, с LSN=00000048:0000008c:0002) в направлении hexdec:

1
2
3
4
5
6
7
8
DECLARE @hexLSN binary(10)
declare @h numeric(25,0), @m numeric(25,0), @l numeric(25,0)
SET @hexLSN=0x000000480000008c0002

SET @h=CONVERT(bigint, CONVERT(binary(4), @hexLSN))*1000000000000000
SET @m=(CONVERT(binary(8), @hexLSN) & CONVERT(bigint, 0xffffffff))*100000
SET @l=@hexLSN & CONVERT(int, 0xffff)
PRINT @h+@m+@l

Как видите, все не так уж сложно. Надо лишь записать LSN в его hex-представлении в виде единого шестнадцатиричного числа, т.е. безо всяких двоеточий. На выходе получаем:

72000000014000002

Это и есть тот же самый LSN в dec-записи.

Ну а обратно? Это будет посложнее, но тоже вполне решаемо:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
DECLARE @numLSN numeric(25,0)
DECLARE @h bigint, @m bigint, @l int
DECLARE @c tinyint, @r tinyint

SET @numLSN=72000000014000002

SET @h=0
SET @m=0
SET @l=0
SET @c=0
SET @r=1
WHILE @numLSN>0
BEGIN
    IF (@r=1)
    BEGIN
        IF (@c<5)
            SET @l=@l+(@numLSN % 10 * POWER(10, @c))
        ELSE
        BEGIN
            SET @r=2
            SET @c=0
        END
    END
    IF (@r=2)
    BEGIN
        IF (@c<10)
            SET @m=@m+(@numLSN % 10 * POWER(10, @c))
        ELSE
        BEGIN
            SET @r=3
            SET @c=0
        END
    END
    IF (@r=3)
        SET @h=@h+(@numLSN % 10 * POWER(10, @c))

    SET @numLSN=FLOOR(@numLSN / 10)
    SET @c=@c+1
END
PRINT CONVERT(varchar(10), CONVERT(binary(4),@h), 2)+':'+
CONVERT(varchar(10), CONVERT(binary(4),@m), 2)+':'+
CONVERT(varchar(10), CONVERT(binary(2),@l), 2)

На выходе имеем вполне ожидаемое:

00000048:0000008C:0002

Было бы, конечно, удобнее, если б LSN в принципе допускал запись лишь в одном формате. Но тут уж ничего не поделать, формата два и с этим нужно как-то жить. По счастью, вы теперь знаете как. ;)

А теперь — вместе!

Пожалуй, не будет самонадеянностью сказать, что к текущей точке изложения материала большинство читателей узнало столько нового (и интересного, будем надеяться), что где-то уже могла пойти «потеря картинки», когда нюансы проработаны настолько хорошо, что сознание цепляется за них, оставляя без внимания вопросы их взаимодействия друг с другом. Обычно такое состояние выражается словами «за деревьями леса не видно». Так вот давайте немного остановится в наших порывах «копания вглубь», и направим нашу энергию на собирание из отдельных частей пазла целостной картины. Это позволит нам еще раз вспомнить кое-что из пройденного, а так же закрыть пару-другую вопросов остающихся к текущему моменту в «неопределенном» состоянии. Вот, скажем, во второй части статьи автор упоминал что поле m_lsn из заголовка страницы (не лога!) якобы дюже важно для сервера, и что в данном материале оно так же заметную роль сыграет. Пока что, согласитесь, не похоже! Из той же части статьи мы знаем как увидеть текущее значение этого поля для любой страницы, но зачем нам это — решительно не понятно. Так вот давайте «соберем пазл».

Стало быть, что касается упомянутого поля. Из его названия (m_lsn) и из примеров значения этого поля (их можно так же увидеть во второй части статьи) складывается четкое ощущение что мы вновь имеем дело с LSN. И ощущение совершенно верное — это и есть «какой-то» LSN. Прежде чем ответить на вопрос «какой именно» ответим на вопрос «в каком формате». Тут выясняется забавная вещь. Несмотря на то, что, казалось бы, предыдущий раздел рассмотрел оба возможных формата LSN (и hex, и dec), обсуждаемому полю удалось изобрести формат третий, «комбинированный». Т.е. значение поля m_lsn состоит из привычных для hex-варианта трех компонентов. И каждый отделен от «соседа» двоеточием. Вот только каждый отдельный компонент это не heх-число, а dec-число. То есть для перевода его в привычный LSN-hex нужно независимо перевести каждое из трех чисел из десятичной основы, в основу шестнадцатиричную. Что в большинстве случаев можно проделать едва ли не «в уме». Пример: поле m_lsn 66:81:3 из второй части соответствует «нормальному» LSN 42:51:3.

Теперь к более интересному вопросу — а в чем его предназначение? Поставим вот такой опыт:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
USE master
go
CREATE DATABASE TstLog
ON PRIMARY (
    NAME = 'TstLog_Data',
    FILENAME = 'c:\sqlCMD.ru\TstLog_Data.mdf',
    SIZE = 5 MB,
    MAXSIZE = 5 MB )
LOG ON (
    NAME = 'TstLog_Log',
    FILENAME = 'c:\sqlCMD.ru\TstLog_Log.ldf',
    SIZE = 1 MB,
    MAXSIZE = 1 MB )
GO
ALTER DATABASE TstLog SET RECOVERY SIMPLE
GO
USE TstLog
GO
CREATE TABLE T1 (C1 INT IDENTITY, C2 INT NOT NULL, C3 VARCHAR(10) NOT NULL)
GO
INSERT INTO T1 VALUES (10, 'A')
INSERT INTO T1 VALUES (20, 'K')
CHECKPOINT
DBCC TRACEON(3604)
DBCC PAGE(TstLog, 1, 79, 0)
INSERT INTO T1 VALUES (30, 'S')
DBCC LOG(TstLog,1)
DBCC PAGE(TstLog, 1, 79, 0)
GO
--clean up
USE master
go
DROP DATABASE TstLog

Все «крутится» вокруг вставки третьей строки (которая, разумеется, случится в рамках транзакции, только неявной). Сначала мы вызовем заголовок 79-й страницы до этой транзакции, и запомним значение поля m_lsn. Затем, после завершения транзакции, посмотрим на последние записи в журнале. А так же еще раз оценим значение того же поля m_lsn.

Вот значение m_lsn до транзакции (в скобках — тоже значение приведено к «нормальному» hex-варианту):

m_lsn: 72:80:3(48:50:3)

Вот часть колонок из отчета команды DBCC LOG. Напомню, что к этому моменту транзакция уже завершена:

Current LSN             Operation       Transaction ID  Previous LSN
...
00000048:0000008b:0002  LOP_BEGIN_XACT  0000:0000057e   00000000:00000000:0000
00000048:0000008b:0003  LOP_INSERT_ROWS 0000:0000057e   00000048:0000008b:0002
00000048:0000008b:0004  LOP_COMMIT_XACT 0000:0000057e   00000048:0000008b:0002

А вот и значение m_lsn той же страницы после транзакции:

m_lsn: 72:139:3(48:8b:3)

То есть мы видим, что разбираемое поле ссылается на запись лога с номером LSN=48:8b:3. А что за запись такая? Да это же запись последней из инструкций (а именно, с кодом операции LOP_INSERT_ROWS) повлиявшей на содержимое 79-й страницы! Вот вам и смысл поля m_lsn: оно должно изменяться каждый раз когда меняется хотя бы 1 байт информации на данной странице и фиксировать — какая именно запись лога это изменение отражает.

Хорошо, а где проявляется важность этого поля? Почему автор его столь упорно выделяет? А вот для этого нам нужно обратиться еще к одному «кусочку пазла» — процессу crash recovery. Помните его трехфазную природу? OK, тогда давайте к фазе первой — analysis. Тем более что это, опять же, один из незакрытых вопросов — автор обещал кое-какие нюансы данной фазы осветить, но обещания до сих пор не сдержал. Пора исправляться.

Итак, что нам уже известно? Нам известно, что analysis готовит «стройплощадку» на которую «приходят» затем две последующие фазы crash recovery и «возводят» на ней здание под названием «база данных в статусе on-line». Каков один из главнейших элементов такой «стройплощадки»? Это просто (но и сложно, в тоже время): выяснить — какие из страниц данных потенциально могли бы быть на момент «крэша» «грязными». А зачем нам это выяснять? А затем, что фазе второй и третьей предстоит работать с этими, и только этими страницами. Но! Что значит «работать»? А это значит — вносить в них изменения. А как вносятся изменения в любую data page? Правильно, только в buffer pool и ни в коем случае не «прямой записью» в MDF-файл. А значит нам нужно, как минимум, определить перечень тех страниц что будут загружены в этот самый пул. Потому как предложение «а давайте считаем все страницы базы, на всякий случай» ничего кроме здорового смеха вызвать не может. У нас, к тому же, через crash recovery проходит не одна, а все базы данного сервера. Предлагаете считать в буфер «все-все-все» страницы? Так вот фаза обсуждаемая подобными «проектами» голову себе не забивает, а принимается за дело: построение «таблицы грязных страниц» (dirty page table, DPT). Центральная часть этой таблицы — PageFID / PagePID «потенциально грязных страниц». Ну а как определить-то, подлежит данная страница включению в эту таблицу или нет? Вот! Тут и начинается «представление».

Смотрите — у нас до «крэша» обязательно была контрольная точка. Была ли она за час до него, или за секунду — не важно совершенно. Важно что вот этот CHECKPOINT был, а следующего — не было, вместо него «крэш» случился. Назовем такую «особенную» точку последней (last checkpoint). Тогда, зная «физику» работы контрольных точек (а мы с читателями знаем ее уже ой как хорошо), мы можем утверждать: страницы не менявшиеся после last checkpoint не могут быть «грязными», даже потенциально. Ведь суть CHECKPOINT-а — перевести все страницы из «грязных» в «чистые», не так ли? А отсюда следует, что фазе analysis нужно, прежде всего, найти в журнале тот LSN, что символизирует собой окончание (LOP_END_CKPT) last checkpoint-а. Что элементарно: максимальное не-NULL значение колонки Minimum LSN (ее можно видеть в отчете команды DBCC LOG при условии что второй аргумент этой инструкции будет не меньше трех) возвращает требуемое.

При анализе указанной колонки не впадайте в очередную «мисконцепцию» широко разошедшуюся в SQL-сообществе. Эта колонка не есть MinLSN, хотя название ее в точности соответствует одному из имен последнего. Однако в данном случае Minimum LSN != MinLSN. Указанная колонка содержит исключительно метки окончаний CHECKPOINT-ов, что в принципе не может являться MinLSN. В крайнем случае таковым может стать начало контрольной точки, но не факт что именно оно будет назначено «маркером MinLSN». Как знают читатели из прошлых частей статьи — «возможны варианты».

Стало быть, LSN last checkpoint-а (LSN_LC) у нас (а равно и у фазы analysis) «на руках». Дальше рассуждаем логически: в заголовке любой данной страницы данных поле m_lsn будет или больше LSN_LC, или меньше его. Окончание контрольной точки не вносит никаких изменений ни на одну из страниц, а значит LSN этой операции не может стать значением обсуждаемого поля. Поэтому — либо «до», либо «после». Если m_lsn < LSN_LC (что означает — последние изменения были до last checkpoint), то может ли такая страница быть «потенциально грязной»? Нет, как это было неопровержимо доказано абзацем ранее. А если m_lsn > LSN_LC (последние изменения были после last checkpoint)? То ровно наоборот, разумеется. Такая страница подлежит занесению в dirty page table и последующей загрузке в RAM для возможных модификаций. Таким образом, благодаря полю m_lsn, львиная доля страниц каждой базы отбрасывается еще на этапе анализа и рестарт сервера ускоряется на порядки. Важно нам это, сами как считаете? Вот и автор считает так же.

Однако трудолюбивый analysis на этом не успокаивается, а, продолжая строительные аналогии, приступает к «рытью котлована». Вот как он это делает:

  • те транзакции, чьи LSN о их завершении меньше LSN_LC можно спокойно игнорировать. Причем исход транзакции совершенно не важен, что фиксация, что откат — все надежно сохранено в data page в момент последней точки;
  • те транзакции, чьи LSN о их завершении больше LSN_LC подлежат обработке второй фазой, redo. Причем исход транзакции снова совершенно не важен, как мы уже знаем redo обязан обработать и те, и другие;
  • те транзакции, у которых есть LSN о их начале, но нет LSN хоть о каком-то окончании (мы с вами договорились называть такие транзакции «подвисшими») — явные «клиенты» фазы третьей, undo.

Из всего этого труда рождается вторая вспомогательная таблица — активных транзакций. С нею две основных фазы процесса crash recovery работают куда как эффективнее.

Наконец, разберем и такой нюанс. Допустим наша база находится в модели восстановления SIMPLE. Можно ли утверждать, что после каждой контрольной точки (а, очевидно, пока не случится точка следующая именно описываемая точка получает статус last checkpoint) абсолютно все записи лога с LSN-ами < LSN_LC могут быть из лога безопасно удалены, т.е. очищены? Подумайте! Да нет, конечно же! Вот представьте: транзакция началась в 00:01, в 00:02 произошел last checkpoint=LSN_LC, а в 00:03 — «крэш». Всю эту пару минут транзакция что-то активно меняла в таблицах, но по итогу так завершится и не успела, т.е. осталась «подвисшей». Очевидно, у этой транзакции в журнале полно записей более чем удовлетворяющих условию LSN < LSN_LC. Что случится, если мы такие записи удалим? Да то, что при рестарте сервера фаза undo будет не способна выполнить возложенные на нее обязательства! Как, скажите на милость, она сможет «отмотать назад» всю транзакцию если останется протоколирование лишь половины из ее активных действий? А ведь отменить надо именно всю транзакцию, а не только ту ее часть что случилась после last checkpoint-а. Уже по одной этой причине мы не можем себе позволить стирать все записи с LSN-ми меньшими LSN_LC. Как ни удивительно, но «против» будет и фаза redo. Перепишем наш сценарий вот так:

  • 00:01 — старт транзакции
  • 00:02 — last checkpoint=LSN_LC
  • 00:03 — фиксация транзакции
  • 00:05 — «крэш»

Казалось бы, уж redo мог бы начать свою деятельность с записей LSN случившихся после LSN_LC. Ведь все что произошло «до» уже отражено на странице данных! Ан нет, и тут «отправной точкой» является 00:01, а не 00:02. И причина тому — желание (да и просто необходимость) «воспроизвести» транзакцию «шаг-в-шаг» в буквальном смысле. Например, как уже было упомянуто в статье ранее, redo при «переигрывании» транзакции наложит совершенно те же блокировки, что и транзакция оригинальная. А поэтому и эта фаза должна начинать свою активность не на last checkpoint, а чуть раньше, на самой первой из активных транзакций. А что такое первая (другими словами — старейшая; кстати, иногда к ней так и адресуются, oldest transaction) активная транзакция? Да это же один из претендентов на «владение» «маркером MinLSN»! И если за «владение» им «спорят» last checkpoint и oldest transaction то «побеждает» тот, кто «раньше начался» (по другому — у кого LSN меньше). А вот уж «слева от маркера» вы можете урезать совершенно все что вам заблагорассудится. Поэтому правильный «критерий урезания» не LSN<LSN_LC, а LSN<MinLSN. Что, впрочем, не исключает что в ряде моментов оба неравенства будут эквивалентны в виду эквивалентности их правых частей.

Ну как, начал «пазл складываться»? ;)

Добавить комментарий

Имя и простая капча - это все что требуется для принятия вашего комментария



Для отправки сообщения щелкайте по кнопке с цифрой три

Ваш комментарий отправляется. Одну секунду...