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

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









И вот лишь теперь, когда мы «побороли» (будем надеяться) этот хитрый MinLSN можно дать совершенно точное и полное описание тех VLF-ов, что будут обладать статусом Active: этим статусом в каждый из моментов времени обладает любой VLF содержащий в тот же момент времени хотя бы одну запись между MinLSN и MaxLSN включая и эти самые границы. Особо подчеркну: VLF-у нет нужды быть заполненным такими записями (MinLSN-MaxLSN) на 100% или хотя бы 10%. Достаточно одной единственной такой записи и — дело в шляпе, VLF получает указанный статус вне зависимости чем заполнен его остаток и заполнен ли он вообще. Можно так же сказать (глядя на последнее графическое представление лога) что статусом Active обладают все VLF-ы составляющие «логический лог». Такое альтернативное определение почти верно, если не учитывать, что правая граница лога логического проходит по записи MaxLSN, а граница каждого VLF проходит, собственно, по его краям. :) Одним словом, на той же иллюстрации обсуждаемым статусом обладают VLF-ы 2-й и 3-й. Что, кстати, блестяще подтверждается и отчетом команды DBCC LOGINFO. Так же напомню, что диапазон (MinLSN-MaxLSN) принято называть активным логом. И, как вы догадываетесь, название такое возникло отнюдь не на пустом месте.

Наконец-то мы можем перейти к третьему (из четырех возможных) статусу VLF — Recoverable. Это, пожалуй, самый «неудобный» для пояснения статус и многие авторы описывающие работу журнала транзакций предпочитают сделать вид что его не существует (а может они и реально не в курсе о факте его существования). Мы, конечно, не будем им уподобляться, а возьмемся за дело со всей решимостью.

Итак, обсуждаемым статусом обладает VLF потерявший статус Active (а как он его «потерял», кстати? проверьте-ка себя! только учтите — в данный момент вас спрашивают не о времени когда такая «потеря» случается, этого вы пока можете и не знать, а о критерии или о признаке такой «потери» — вот это вы понимать к данному моменту уже обязаны! ответ чуть ниже, но пока приготовьте свой), но еще не получивший «одобрения» на перезапись существующих в нем записей? А почему не получивший? Кого/чего ждем? А ждет он, собственно, ближайшего бэкапа лога (но не данных!). А без этого «неудобного» статуса нельзя? Абсолютно нельзя! Если бы не он ваши бэкапы лога, скорей всего, не содержали бы «непрерывной цепочки» записей, а она и есть краеугольный камень всей системы резервных копий SQL Server. Вот тут-то и «неудобство»! Без этого статуса объяснил про MinLSN, сказал «все что слева усекаемо, все что справа — нет», и вопрос закрыт. А тут приходится открыть его вновь и признать, что «заградительных барьеров» для усечения лога не один, а целых два:

  • MinLSN — устанавливается на уровне отдельной записи, но «лочит» (не дает усекать) целый VLF;
  • статус Recoverable — устанавливается всегда на целый VLF и его же «лочит».

Второе «неудобство» того же статуса: приходится потратить некоторое число предложений на пояснение принципов его работы в различных моделях восстановления. Да, вы все прочитали правильно: это единственный из статусов работающих по разному в зависимости от того находится ли база в модели SIMPLE или же в двух альтернативных режимах. И разница эта заключается в том моменте, когда этот статус сменится на следующий — Reusable. Таблица идущая чуть ниже даст вам возможность эту разницу «прочувствовать». И прежде чем мы перейдем к финальному VLF-статусу, а вслед за ним и к той самой таблице — ответ на «задачку»: VLF теряет статус Active как только диапазон записей MinLSN-MaxLSN настолько «смещается вправо», что перестает «захватывать» хотя бы одну его запись. Да, я знаю — вы ответили именно так, молодцы! ;)

Наконец, статус финальный — Reusable. Пожалуй, к этой точке повествования, в отношении данного статуса можно обойтись практически без комментариев. Мы уже и так знаем, что VLF с этим статусом готов приступить к «записи поверх», что тут добавить?

Не раскрытым, пожалуй, остались два вопроса связанных со статусами:

  • как выглядит «цикл» смены статусов отдельно взятого VLF. В принципе, это уже практически очевидно, однако явно выражено до сих пор не было;
  • в какой момент времени VLF меняет статус на следующий. А вот об этом мы совсем не говорили.

Давайте оформим и «раскроем тему» в формате вот такой таблички, тем более автор обещал ее изобразить:

Текущий статус Значение колонки Status Следующий статус Когда меняется в simple Когда меняется в bulk/full
Unused 0 Active первая запись первая запись
Active 2 Recoverable отдельный механизм отдельный механизм
Recoverable 2 Reusable усечение лога бэкап лога
Reusable 0 Active первая перезапись первая перезапись

Вот вам расшифровка имен столбцов этой таблицы:

  • Текущий статус — какой статус имеет VLF в настоящий момент;
  • Значение колонки Status — какую цифру вернет для этого (текущего) статуса колонка Status команды DBCC LOGINFO;
  • Следующий статус — какой статус последует за обозначенным в первом столбце таблицы;
  • Когда меняется в simple — что именно приведет к смене статуса из первого столбца в статус из третьего столбца при условии что база данных находится в simple recovery model;
  • Когда меняется в bulk/full — тоже что и предыдущее, однако база находится в bulk-logged recovery model или в full recovery model.

Пожалуй, значения двух последних колонок приведенной таблицы тоже требуют своей расшифровки:

  • первая запись — первая же запись лога в него поступившая;
  • первая перезапись — практически как предыдущее, только запись «ложится» не на чистое место лога, а «поверх» уже имеющейся;
  • отдельный механизм — внутренний механизм движка периодически им запускаемый и проверяющий все VLF-ы заполненные записями на 100% к их готовности сменить статус на следующий. Почему проверяются только «полные» VLF-ы спрашиваете вы? Потому что только они являются валидными кандидатами на смену статусов от Active к Recoverable. VLF с любым иным процентом заполнения содержит, очевидно, MaxLSN и является «текущим» VLF-ом — смена статуса для него исключена по определению. Не менее понятно, что проверяются только VLF-ы со статусом Active, ибо данный механизм может изменить только этот статус и только на Recoverable;
  • усечение лога — тот самый механизм, большое обсуждение которого ждет нас с вами впереди;
  • бэкап лога — что-то не ясно? ;)

Интересно, что колонка Status из нашей таблицы (а, фактически, из отчета команды DBCC LOGINFO) могла бы сообщать нам обо всех 4-х возможных статусах, однако вместо этого объединяет их попарно и «делает вид» что статусов всего лишь два, а не четыре. Это, я бы сказал, «практический» подход к вопросу. Ведь по сути, если в нюансы не вдаваться, каждый VLF в каждый момент времени находится в одном из двух положений: «свободен» (т.е. готов принимать новые записи лога) и «занят» (т.е. не готов это делать). Колонка Status берет на вооружение именно такую логику и имеет на то полное право.

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

Вновь о «круговороте» журнала транзакций.

В очередной раз возвращаемся из «дальних странствий», где «водятся» маркеры MinLSN и меняются статусы VLF-ов, к основному течению беседы. Что бы восстановить «нить разговора» напомню вам, да и себе тоже, текущую «диспозицию»:

  • мы выяснили, что при одной большой транзакции и «залоченом» на 1MB файле лога больше 2000-2100 записей вставить в нашу таблицу мы не способны;
  • тогда мы решили реализовать идею «много коротких транзакций» и реализовали ее;
  • по новой методологии вставили 500 записей — OK;
  • повысили счетчик до 1000 записей — OK. При этом счетчике состояние нашего лог-файла такое (согласно отчету от инструкции DBCC LOGINFO), или такое (графическое представление, вариант «попроще»), или такое (графическое представление, вариант «посложнее»).

Вот на этом самом месте мы «уехали вбок», что бы вернуться обогащенные знаниями о MinLSN/VLF-статусах.

Стало быть, на 1000 записей лог успевает использовать 3 из 4-х своих VLF-ов причем первый из них успевает побывать во всех возможных статусах: UnusedActiveRecoverableReusable. Отлично, что насчет полутора тысяч записей? Тоже несложно, вот отчет:

FileId  FileSize    StartOffset FSeqNo  Status  Parity  CreateLSN
2       253952      8192        72      0       64      0
2       253952      262144      73      0       64      0
2       253952      516096      74      2       64      0
2       278528      770048      75      2       64      0

А вот и графика:

Log005

Думаю, что для внимательных читателей достаточно фразы «все поехало вправо» что бы описать происходящее. По сути мы задействовали 4-й VLF, но освободили 2-й. Таким образом, по сравнению со счетчиком «одна тысяча ровно»: транзакций у нас в полтора раза больше, записей в лог, очевидно, также больше, а сам лог — такой же! В том смысле, что длинна отрезка «логический лог» вообще не изменилась, он, отрезок этот, просто сместился вправо. Понимаете теперь все изящество решения по эффективному использованию места в LDF-файле?!

Отлично, посмотрим как справится новый подход (и справится ли?) со счетчиком 2000. Вот отчет команды DBCC LOGINFO:

FileId  FileSize    StartOffset FSeqNo  Status  Parity  CreateLSN
2       253952      8192        76      2       128     0
2       253952      262144      73      0       64      0
2       253952      516096      74      0       64      0
2       278528      770048      75      2       64      0

Хм. А «как это будет по-русски», то бишь в графическом представлении? А вот так:

Log006

Собственно говоря, если вы мысленно «склеите» конец LDF-файла с его началом, то повышение счетчика записей 1500→2000 ничем не будет отличаться от повышения предыдущего, 1000→1500. Все совершенно так же «уехало вправо» сохранив при этом все свои пропорции, например длинна отрезка «логический лог» вновь константа! Вот вам и обещанный «круговорот журнала в природе».

Разумеется, самые прозорливые читатели все уже поняли: при новом подходе 2300 записей вставляются без всяких проблем. И 25000 тоже. И больше — тоже. А отсюда мы делаем вывод переоценить значимость которого решительно невозможно:

Лог переполняет не «обильность» транзакций, т.е. их число. И даже не «масса» каждой отдельной транзакции, т.е. число инструкций ее составляющих. Эти факторы, конечно, влияют на переполнение, но лишь косвенно. Истинной причиной переполнения лога является невозможность проведения процедуры известной под названием усечение лога. Той самой, что автор все обещает разобрать подробно, но пока никак не найдет места для этого разбора.

Прежде чем закончить с «кольцом журнала» разберем вот какой вопрос. Когда у нас счетчик был 1000 журнал выглядел так:

Log004

А при повышении счетчика до 1500 стал выглядеть так:

Log005

А спрашивается в задаче — что мешало логу при повышении счетчика с тысячи до полутора стать таким:

Log007

Нет, ну действительно — VLF1 явно свободен, бери да пользуйся, оставь 4-й VLF «про запас»! Однако менеджер виртуальных файлов лога не только умный (автор даже не ставит кавычек — последнее определение следует понимать буквально). Он еще и «хитрый». :) Смотрите — что у нас «меряет» желтый горизонтальный отрезок, «логический лог»? Верно — расстояние от первой записи первого «нужного» VLF-а до MaxLSN. Что есть «нужный» VLF не забыли? OK, тогда мысленно «замерьте» величину обратную этому отрезку, т.е. поставьте палец на MaxLSN (не Min, а Max!) и ведите его вправо (ибо новые записи в лог поступают именно в этом направлении) до первого «нужного» VLF-а. Сделайте это для обоих альтернатив, не забывая, что если в одной из них ваш палец упрется в правый «бордюр» последнего VLF-а то это не повод бросать измерение. Просто перенесите палец на VLF1 (за вашим пальцем последуют и реальные записи лога) и продолжайте начатое. По окончании эксперимента признайте: на предпоследней иллюстрации (вариант менеджера виртуальных файлов лога) палец прошел большее расстояние, чем на иллюстрации последней (альтернатива предлагаемая автором). А стало быть именно в варианте менеджера, при прочих равных, пройдет больше времени, прежде чем «хвост» «логического лога» повстречается с его же «головой». А что такого особенного в возможности такой встречи? А то, что когда такое случается определенно происходит одно из двух:

  • при определении лог-файла LDF (что случается в рамках команды CREATE DATABASE) использовалась опция FILEGROWTH и на HDD есть потребное число KB/MB/GB. Тогда LDF расширяется (физически!) на указанную величину байтов, что приводит в возникновению новых (и пустых) VLF-ов, что дает возможность продолжить обработку транзакции;
  • при определении лог-файла LDF не использовалась опция FILEGROWTH, а равно использовалась, но HDD не готов выделить запрошенное число байт. Клиент получает ошибку 9002 (да-да, ту самую), транзакция безусловно откатывается.

Второе — это просто беда или катастрофа (определение степени трагичности этого исхода автор оставляет читателю). Первое, безусловно, получше, но тоже — далеко «не айс». И если вы продолжите чтение статьи то поймете почему первый вариант далеко не идеален. Пока же констатируем: если какое-то время можно обойтись без любого из двух указанных вариантов — несомненно нужно поступать именно так. А как бы это самое «можно обойтись» растянуть по времени? Так как раз отсрочить встречу «хвоста» и «головы»! Что менеджер делает не колеблясь, и поступает совершенно правильно. А в «экономии» 4-го VLF-а нет ровным счетом никакого резона. Место под него уже «забрано» у OS Windows. Будет ли это место «чистым», или будет содержать записи лога — ровным счетом ничего и ни для кого не меняет.





Формат идентификатора записи журнала (LSN). Буфер лога.

Казалось бы последние эксперименты не оставляют сомнений: идея «много коротких транзакций» наголову «побивает» идею «одна большая транзакция». Да и сам автор, помнится, в части 5-ой данного труда призывал вас прислушаться к фразе «транзакция должна быть настолько короткой насколько это вообще возможно». Не поймите же меня неправильно, фраза — в силе, призыв — в силе, но! Кабы наш сервер описывался словами «делай так, и будешь в шоколаде», то:

  • он был бы прямой как рельс;
  • имел бы 2 настройки (всего) — аутентификация Windows/аутентификация смешанная;
  • и админился бы без всяких проблем любым вменяемым бухгалтером. По совместительству с его/ее основной работой.

А как всем хорошо известно, настроек у сервера туча, а бухгалтеру физическое местоположение сервера лучше просто не сообщать — целее будут, причем как первый, так и второй.

То есть, автор ведет к тому, что в любой компьютерной системе категории сложности хотя бы «выше элементарной» нет «универсально правильных» решений. Но и нет «универсально ошибочных». Так и в SQL сервере: в любой практике признаваемой как «единственно верная» или «лучшая» гарантировано будут недостатки (скорей всего вторичные, но все же). И наоборот — в приемах работы с сервером общепризнанных как «неудачные» практически гарантировано скрываются рациональные зерна, только для специфических условий. Коротко и для себя автор формулирует этот тезис так:

В IT вообще и SQL Server в частности нет «правильных» и «неправильных» решений. Есть решения соответствующие задаче/проекту/условиям работы, и есть решения им несоответствующие. Все, иных критериев «хорошести» решения просто не существует.

Так и в нашем случае. Можно ли сказать что идея «много коротких транзакций» «лучше чем»? Нет, потому что такого критерия не существует. А можно сказать что она «более подходящая» нашим условиям? Ну, так — какие условия? Как у нас — файл лога в 1MB и «залоченый»? Тогда — однозначно да, как мы доказали нашими экспериментами. А если условия будут иными? Тогда, вполне возможно, поменяется и наша оценка. Ну а можно ли привести пример таких условий, когда идея «одна большая транзакция» будет более уместна и указать те преимущества, что она будет иметь перед своей конкуренткой? Вот это — запросто. Но с оговоркой. Мы оставляем «за бортом» нашего рассмотрения те вещи, что не относятся непосредственно к файлу лога хотя бы косвенно, то бишь блокировки, конкурентную работу с данными и все прочее этого плана. Так вот сделав эту оговорку мы без всяких проблем можем показать преимущества того подхода, что, вроде как, безусловно записан нами в безоговорочные аутсайдеры.

Начнем, как это водится в данном цикле статей о журнале транзакций, издалека. Зададимся вопросом: вот есть у нас LSN, он же Log Sequence Number, такого, допустим, вида — 00000043:0000001b:0002. Мы уже знаем, что это «навсегда увеличивающийся» счетчик и не более того. Но почему он записывается в виде этакого «трехчлена»? Все ли значения в этом числе произвольны и идут просто «по нарастающей»? Возможно каждый из трех компонентов входящий в LSN пытается сообщить нам важную информацию?

Для открытия «тайн» этого идентификатора записи лога, давайте выполним вот такой скрипт, в котором вновь реализуется идея «одна большая транзакция»:

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
BEGIN TRAN
    WHILE @i < 500 --loop counter
    BEGIN
        insert into T1 VALUES (17, REPLICATE('S', 20))
        set @i += 1
     END
COMMIT TRAN
go
DBCC LOGINFO
DBCC LOG(TstLog, 1)
--clean up
USE master
go
DROP DATABASE TstLog

Как мы уже знаем, 500 записей без проблем «прокачиваются» нашим «залоченым» журналом в рамках одной транзакции, а поэтому никаких ошибок мы не увидим, а увидим мы пару отчетов. Из отчета первого (DBCC LOGINFO) нам достаточно одной строки:

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

Отчет второй (от инструкции DBCC LOG) весьма объемен, но нам нужны лишь некоторые «репперные» его записи и только две колонки, собственно изучаемый нами в настоящий момент LSN (колонка Current LSN) и колонка операции зафиксированной данной записью лога (Operation):

Current LSN             Operation
00000048:00000052:008a  LOP_BEGIN_CKPT
00000048:0000008b:0001  LOP_END_CKPT
00000048:0000008c:0001  LOP_IDENT_NEWVAL
00000048:0000008c:0002  LOP_BEGIN_XACT
00000048:0000008c:0003  LOP_INSERT_ROWS
00000048:0000008c:0004  LOP_IDENT_NEWVAL
...
00000048:0000008c:02a9  LOP_INSERT_ROWS
00000048:0000008c:02aa  LOP_IDENT_NEWVAL
00000048:00000104:0001  LOP_INSERT_ROWS
00000048:00000104:0002  LOP_IDENT_NEWVAL
00000048:00000104:0003  LOP_INSERT_ROWS
00000048:00000104:0004  LOP_IDENT_NEWVAL
00000048:00000104:0005  LOP_INSERT_ROWS
...
00000048:00000104:0154  LOP_IDENT_NEWVAL
00000048:00000104:0155  LOP_INSERT_ROWS
00000048:00000104:0156  LOP_COMMIT_XACT

Не сомневаюсь, что читатели изучающий данный материал последовательно, вдумчиво и без пропусков чувствуют себя в показанных строках «как рыба в воде». Давайте возьмем первый (и единственный) из задействованных в настоящий момент VLF-ов и какую-нибудь запись из него. Скажем запись начала нашей большой транзакции, а именно 00000048:0000008c:0002 — вполне годится. Произведем «вскрытие» указанного виртуального лог-файла:

LSN_format

Давайте разбираться последовательно. Вполне очевидно, что каждый LSN (и отобранный нами 00000048:0000008c:0002 не исключение) имеет ярко выраженную трехкомпонентную структуру, причем каждый компонент в записи отделяется двоеточием. Выясняется, что компоненты имеют хорошо различимую иерархию и самый левый из них указывает на «общее», а самый правый — на «частное». На примере отобранной нами записи:

  • 00000048 — есть ничто иное как «логический» номер VLF. Описание колонки FSeqNo помните? Так это — ее значение. Почему не совпадает спрашиваете вы? 0x48=72 — так OK? ;)
  • 0000008c — далее, тонкий момент: VLF делится на блоки. Каждый блок имеет минимальный размер в 512 байт и должен начинаться только с этой границы. Но не думайте что VLF изначально нарезается такими 512-ти байтными «батончиками»! Отнюдь. Очередной блок начинается там, где ему судьбой выпадет начаться, разумеется максимально экономя имеющееся пространство внутри VLF. Т.е. он начнется максимально «плотно» к блоку предыдущему. Так что второй компонент это ни разу не «номер блока». Как таковой этот термин лишен смысла, число блоков заранее неизвестно и выясняется «на лету», по мере «залива» записей лога в данный VLF. На самом деле, второй компонент — это всего лишь смещение (offset) первого байта блока которому принадлежит данная запись. Смещение меряется от нулевого байта самого VLF-а и выражается в 512-ти байтных «юнитах». Итого, наша запись о начале транзакции принадлежит тому блоку 72-го VLF-а, что начинается с байта (0x8c=140)*512=71680, если считать от левой границы этого VLF-а.
  • 0002 — наконец, третий компонент. Для его понимания зададимся вопросом: когда возникает новый блок? Ну это просто — когда блок предыдущий «закрылся», а в лог приходит очередная запись. Вот под нее и «открывается» новый блок. Нет записи — нет нужды и с блоком заморачиваться. Поэтому абсолютно все блоки на последней иллюстрации (фиолетовые шестнадцатеричные цифры внизу ее показывают смещение каждого блока в «юнитах») содержат хотя бы по одной записи лога. Однако показаны в виде красных прямоугольников лишь записи того блока на котором мы решили сосредоточить свое внимание. А на самом деле подобные записи есть совершенно в каждом блоке. Различаются лишь число таких записей. Ну и размер (в байтах) каждой отдельной записи тоже, скорей всего, будет уникальным. Так вот запись (а это есть обычная лог-запись, с который мы по ходу развития текста статьи работали уже неоднократно), когда о ней говорят в смысле «относится к блоку», будет называться уже не записью, а слотом. И наш третий компонент LSN — номер слота. В границах, разумеется, того блока чье смещение символизирует второй компонент LSN.

Таким образом запись о начале нашей транзакции расположилась во втором слоте блока по смещению 71680 байт относительно начала 72-го VLF-а. Не сложно же? :) Следует понимать, что в отличии от блоков слоты не ограничены ни минимально, ни максимально (нет, ну конечно слот не может «выползти» за границы своего родительского блока, это-то понятно), ни по границе с которой они обязаны начинаться. По сути слоты это наборы байтов. Закончился один набор — пошел следующий. Закончился второй — пошел третий. И т.д. Конец набора выясняется очень просто: один из его (набора) байтов означает длину этого самого набора (мы не будем на этом заострять внимание). Где конец очередного набора там, очевидно, начало следующего. И, не менее очевидно, 1 обсуждаемый набор=1 запись в журнале транзакций=1 строка отчета инструкции DBCC LOG.

Пока все четко, и ясно, и выглядит логично. Не понятно лишь — а если «бабахнуть» 1 блок на весь VLF и в нем слоты? Так не «покатит»? Что вынуждает менеджер виртуальных файлов закрывать блоки? Нельзя ли просто новые слоты «закидывать» в блок существующий да и весь сказ? И тогда структура будет проще — если у нас весь VLF содержит ровно один блок, то и нужды в этой последней сущности нет, она же будет совпадать со своим «контейнером». Так вот, заданные вопросы являются, пожалуй, центральным моментом если и не всей статьи, то уж текущего раздела определенно. Дело у нас в следующем.

Еще в самом начале (пункт 6-й этого списка) мы установили, что прежде чем клиент получит уведомление об успешной фиксации транзакции соответствующие записи «упадут» в наш журнал транзакций, т.е. на HDD. И вы, разумеется, думаете что сгенеренная движком запись лога (наш набор байт) берется и «выпихивается» движком непосредственно на диск, в LDF-файл. Собственно так же думают 95% DBA, не меньше. И все они ошибаются. У лога есть свой buffer pool! Да — вот так, «физика» работы лога значительно ближе к «физике» «грязных» страниц чем это воспринимается подавляющим числом администраторов. И там, и тут записи (или, соответственно, данные) накапливаются в промежуточном буфере и лишь затем, «когда нужно», выводятся на диск в «один присест». То бишь одной командой дисковой подсистеме. А не пятью тысячами.

RAM-буфер лога есть совершенно независимая структура в памяти полностью «отвязанная» от реального (т.е. страничного) buffer pool. Точнее таких независимых RAM-буферов лога обычно несколько, а именно один на каждую базу данных размещенную на сервере/экземпляре. Размер одной такой структуры способен вызвать ваш здоровый смех: она способна вместить не более 60Kb! Именно K, не M, и уж точно не G! Однако тут у нас именно случай «мал — да удал», переоценить вклад этой «малипуськи» в дело оптимизации производительности сервера практически невозможно. Почему? Пример (немного натянутый, но зато сразу проставляющий все точки над ё): что быстрее — записать 60K файлов по одному байту или один файл на 60Kb? Так вот тут у нас аналогия если не полная, то весьма близкая.

Итак, пункт 6-й упомянутого списка должен быть «по хорошему» переписан нами так: записи инструкций составляющие транзакцию вносятся сначала в RAM-буфер, аккумулируются там, а затем, в подходящие моменты времени, «выталкиваются» в LDF-файл. Вот так будет совсем хорошо. Только не понятно, какой из моментов времени считать «подходящим»? Таких моментов 3 и, на самом деле, хотя это задача для истинных «SQL-монстров», все 3 выводятся методом логической дедукции из информации изложенной в данной статье к текущей точке. Для желающих решить по настоящему «взрослую» задачу предлагаю найти все три пункта самостоятельно (думать придется много!), для остальных же решение с пояснением:

  • Заполнение RAM-буфера на 100%, т.е. до 60Kb. Единственный пункт из трех не требующий никаких доказательств необходимости сброса буфера на диск. Место под новые записи в лог-буфере нам надо? Надо! Куда имеющиеся девать? Правильно.
  • CHECKPOINT — куда ж без него. Доказательство необходимости сброса RAM-буфер логаLDF-файл в этой точке «от противного». Допустим такого сброса не происходит. Тогда у нас на диске в MDF-файле оказываются зафиксированными данные для которых в LDF нет «истории изменения». А если в этот момент отключение глобальное во всем здании? И данные зафиксированные в MDF принадлежат «зависшей» транзакции и подлежат безусловному откату при следующем включении? А у нас-то в логе и нет ничего! Вывод: допущение неверно, фиксация в LDF обязательна. Автор даже больше вам скажет: понятие «грязный» относится не только к страницам данных, но и к записям транзакционного лога! Да, записи последнего тоже бывают «грязные» и «чистые». Так вот говоря коротко, каждый CHECKPOINT обязан «очистить» всю «грязь», а не только относящуюся к данным.
  • Фиксация (commit) транзакции (но не ее откат!). Доказательство необходимости сброса RAM-буфер логаLDF-файл и в этой точке и тоже «от противного». Допустим такого сброса не происходит. Тогда, сразу после исполнения команды COMMIT TRAN, клиент получит подтверждение ее успешности, но на сервере при этом не будет записей на HDD о произошедшем. Т.е. вообще никаких, как будто такой успешной транзакции не было вовсе. А если снова «свет»? Как redo фазу при повторном включении делать будем? Да и попросту принцип write-ahead logging никто не отменял. Вывод: допущение неверно, фиксация в LDF обязательна.

К чему приводит вся только что изложенная механика? А приводит она к паре-тройке оооочень любопытных следствий. Ну, к примеру. У нас на сервере исполняется 5 транзакций от 5 различных пользователей. Каждая, разумеется, генерит кучу лог-записей. Как они не «подерутся» за RAM-буфер лога, он же один? Или, быть может, первая по времени транзакция «захватывает» этот буфер и просто вынуждает остальных ждать пока она его не «отпустит»? Нет, конечно. Такой подход стал бы безусловным «приговором» для многопользовательского сценария работы, ни о каких десятках/сотнях одновременно открытых сессиях и речи быть бы не могло. На самом деле никто никого не «лочит» и никто ничего не «ждет». А принцип очень простой: надо тебе (т.е. транзакции, а в конечном счете клиенту) что-то записать в буфер лога — пиши! Каждая из пяти (в нашем примере) транзакций вставляет в буфер свои собственные записи совершенно не сообразуясь с тем же процессом происходящим в транзакциях конкурирующих. И, вполне может быть, что наш лог-буфер приобретет вот такой «веселый» вид:

  • запись относящаяся к транзакции 5;
  • запись относящаяся к транзакции 5;
  • запись относящаяся к транзакции 2;
  • запись относящаяся к транзакции 1;
  • запись относящаяся к транзакции 1;
  • запись относящаяся к транзакции 5;
  • запись относящаяся к транзакции 3;
  • запись относящаяся к транзакции 3;
  • запись относящаяся к транзакции 3;
  • ...

И это — нормально, вот такая вот «чересполосица»! И когда буфер будет заполнен на 100% все они, одной командой, будут выведены в лог-файл LDF, а точнее в один из VLF-ов этого файла, а даже еще более точнее — в новый блок этого VLF-а. Почему же блок непременно новый, спросите вы? Чем плох блок имеющийся? Продолжайте чтение, совсем скоро правда откроется и вам.