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

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

Рост файла лога и почему это необходимо, но не здорово.


366bef3a

Материал излагаемый в предыдущих частях цикла при всей свой неоспоримой важности и безусловной необходимости быть понятым каждым уважающим себя DBA грешил отчетливым «перекосом» в сторону теории. Не то что бы полученные знания нельзя было применить на практике, однако «точку» такого применения еще поискать надо было. Для большинства предыдущих разделом она не лежала на поверхности, часть информации (и изрядная притом) требовала «творческой обработки», дабы изложенные в ней идеи и концепции нашли свое отражение в командах отдаваемых вами серверу или хотя бы в изменении опций этих команд. Два же заключительных раздела цикла (их материал займет части с 10-й по 12-ю) будут гораздо более «практически-ориентированными», здесь вас ждут довольно много советов «вот так можно (лучше) делать», «а так — нельзя (хуже)». Однако, при чтении этих советов, во-первых, не забывайте главное IT-правило. А, во-вторых, обратите внимание, что без всей той самой теории, чье практическое приложение на первый взгляд затруднительно, большинство из советов были бы «книгой заклинаний», в которую требовалось просто верить. Ну или не верить, это уж по вкусу. А с нею, с теорией, вы вполне можете оценить применимость каждого из советов к конкретно вашим условиям. Что намного более ценно, чем слепая вера, а равно и слепое неверие.

Темы этих двух заключительных разделов очень просты (как и все темы связанные с логом они просты лишь на первый взгляд, разумеется):

  • как файл лога (LDF) растет
  • как файл лога (LDF) уменьшается

То есть мы рассмотрим изменения объема нашего физического журнала транзакций как в «плюс», так и в «минус». Для начала о «плюсе».

Как мы доподлинно установили, сразу при создании лог-файл «бьется» на свои логические составляющие, виртуальные лог-файлы (Virtual Log Files, VLF). Скажем в нашей тестовой базе с логом на ровно 1MB, этот самый 1MB был разбит на четыре VLF, каждый примерно по 250KB, в чем мы и убедились. Пытливый ум на этом месте задаст сразу кучу вопросов:

  • почему VLF 4, а не 25?
  • почему деление было 250-250-250-250, а не 970-10-10-10?
  • есть ли связь между начальным размером LDF-файла и числом VLF-ов на которые он будет поделен, а так же размерами последних?
  • а управлять числом/размерами VLF мы можем?
  • а когда LDF увеличивается в размерах потому что мы вручную «прирезаем» ему кусок дискового пространства, то вот этот кусок как на VLF-ы «бьется»?
  • а если кусок «прирезается» автоматически (была задействована опция FILEGROWTH) что меняется?
  • и т.д.

Ну что же — почти все любопытство подобного сорта поддается удовлетворению, поехали.

Начнем с нескольких «быстрых фактов»:

  • файл LDF не может быть менее 512KB;
  • а любой отдельно взятый VLF в нем не может быть менее 248KB;
  • в любом LDF файле не может быть менее двух VLF;
  • LDF размером ровно 512KB имеет ровно 2 VLF объемом 253952 и 262144 байт соответственно, что в сумме дает 516096, да плюс 8192 байт заголовка файла, что и составит искомые 512KB.

Далее, когда имеющийся объем HDD выделенный LDF-файлу делится на X частей (VLF-ов) менеджер этих самых виртуальных лог-файлов стремится к тому, что бы каждая часть имела размер идентичный любой другой части. То есть формула расчета объема каждого VLF будет L/X, где L — объем физического LDF-файла подлежащей распределению, X — число VLF-ов, на который этот LDF будет поделен. На практике это приводит обычно к тому, что все части (т.е. VLF-ы) кроме последней имеют размер идентичный с точностью до байта, и лишь часть последняя имеет размер слегка (обычно не принципиально) больший, чем все остальные, дабы «выбрать остатки» распределяемого дискового пространства. Скажем все тот же наш пример блестяще подтвердил эту практику. Три из четырех VLF-ов имели размер 253952 байт каждый, и лишь последний — 278528 байт.

Хорошо, тогда очевидно, что в формуле L/X делимое (L) полностью под нашим контролем. Сказали мы 1MB — L=1MB. А сказали бы 100GB — то и соответственно. А что насчет делителя, X? Вот он откуда берется? Тут ситуация довольно забавная. Мы не имеем никакого прямого контроля над этой цифрой. И в то же время имеем полный косвенный контроль над ней, через, опять же, значение L. Табличка «вытанцовывается» такая:

Если L... ..то X
< 1MB от 1 до 4
>= 1MB и <= 64MB 4
> 64MB и <= 1GB 8
> 1GB 16

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

Замечателен тот факт, что приведенная таблица полностью универсальна. Она работает и когда лог-файл создается изначально (командой CREATE DATABASE), и когда его размер прирастает в результате нашего «ручного» вмешательства (команда ALTER DATABASE ... MODIFY FILE), и когда тоже самое случается в автоматическом режиме (опция FILEGROWTH). В первом случае L — начальный размер лог-файла, во втором и третьем — размер «прирезаемого» куска, но не итоговый (т.е. новый) размер лог-файла. Пример: изначально лог был 25МВ (L=25MB), командой ALTER DATABASE увеличиваем его до 75MB. Тогда до увеличения размера число VLF будет 4 (вторая строка таблицы), а после — 8, поскольку «прирезаемый кусок» имеет L=75-25=50MB и, согласно все той же второй строке таблицы, будет разделен на 4 VLF, которые и будут добавлены к четырем существующим. Пользоваться третьей строкой таблицы было бы ошибкой, т.к. в распределении нуждаются лишь 50MB, а 25MB уже были распределены ранее. Ну и уж «для комплекта»: размер каждого из первых четырех VLF будет 25MB/4=6.25MB (примерно), а каждого из вторых, разумеется, вдвое больше — 12.5MB (примерно). Так же отметьте для себя, что при каждом увеличении размера LDF-файла общее число VLF-ов в нем может только увеличиться, и никак иначе.

Отдельного замечания заслуживает и распределение место в «составном» лог-файле, то есть таком, что состоит из нескольких физических LDF-файлов. Мы уже говорили, что в принципе в такой ситуации не будет ошибкой считать что у нас просто один «длинный» LDF-файл общего объема как сумма объемов файлов его составляющих. Однако когда дело доходит до распределения числа VLF-ов и объемов каждого из них, все составные LDF-файлы рассматриваются независимо. Если вам будет проще, можете считать что первый из LDF-файлов «основной», а все остальные являются «прирезаемыми кусками» к нему и рассчитываются ровно по той же методике что изложена в предыдущем абзаце. Вновь пример: наша база изначально содержит 2 физических LDF-файла, на 25MB и 50MB. Можем ли мы считать что у нас один LDF-файл на 75MB? Для целей практических — вполне! А можем ли мы утверждать, что все эти 75MB (согласно 3-й строке в таблице чуть выше) будут поделены на 8 VLF-ов и объем каждого составит примерно 75MB/8=9.375MB? Ни в коем случае! Распределение будет абсолютно такое же, как когда к единственному 25 мегабайтовому LDF-файлу добавляется 50 мегабайтовый «кусок», а именно: первые четыре VLF по 6.25MB, вторые четыре — по 12.5MB. Иными словами мы имеем 100% копию предыдущего примера, что лишний раз доказывает: в большинстве случаев идея «составного» лог-файла исходит исключительно из непонимания механики работы журнала транзакций, а так же из-за нерегулярности посещения данным DBA блога sqlCMD.ru. :roll: Нет, бывает конечно, что двух-файловый, трех-файловый и даже более «-файловый» лог вполне оправдан и делается более чем осознанно и продумано, но все же условия и конкретное окружение для оправдания дизайна подобного толка должны быть весьма специфическими.

Ну и совсем отдельная тема — работа автоприращения (опция FILEGROWTH) в многофайловых логах. Тут давайте сразу же к примеру. У нас двух-файловый лог (т.е. состоящий из двух физических LDF-файлов), конфигурация каждого идентична: начальный размер 10MB и автоприращение (FILEGROWTH) столько же. Разумеется, со старта у нас 8 VLF-ов (вы считаете что VLF-ов должно быть всего 4? тогда перечитайте предыдущий абзац), по четыре в каждом файле. Открываем условно-бесконечную транзакцию, которая непрерывно заполняет лог и не дает ему усекаться. Тогда события будут развиваться по такому сценарию:

  • записи лога последовательно заполняют все четыре VLF первого LDF-файла;
  • когда эти первые исчерпаны записи начинают поступать в VLF второго LDF-файла, приращения пока что нет;
  • когда заполнена и вторая четверка первый и только первый (!) LDF-файл увеличивается на 10MB, предоставляя в наше распоряжение третью четверку VLF-ов, в которые и продолжают «заливаться» записи лога;
  • когда исчерпана и эта третья четверка второй и только второй (!) LDF-файл увеличивается на 10MB, и процесс повторяется с четвертой четверкой;
  • см. пункт 3 и т.д. «по кругу».

И что у нас «на выходе»? Ну, если заменить одну бесконечную транзакцию несколькими очень большими, то, например, одна из последних могла первые свои операции (записи) вставлять еще в VLF, допустим, 76-й. По мере развития этой транзакции этот VLF был заполнен на 100% и последние записи той же транзакции «отправились» уже в VLF 77-й. А самое интересное, что 76-й VLF находится в файле log1.ldf, а 77-й — в файле log2.ldf. И вот так одна транзакция оказывается «размазана» куда круче чем масло по бутерброду. Нет, разумеется, все будет продолжать работать как должно, но... «Мораль сей басни»? Очень простая: сделать один лог файл с SIZE = 20MB, FILEGROWTH = 10MB (или 20MB — по вкусу) и закрыть вопрос.

Наконец, что-бы окончательно завершить рассуждения на тему «а как мне сделать что бы в файле лога было ровно 57 VLF-ов?», или «а я хочу что бы VLF первый и третий были по 18.85MB, а второй и четвертый по 7.408MB» просто процитирую BOL. Благо в обсуждаемом контексте последний нетрадиционно ясен и четок. Итак — цитата:

Виртуальные файлы журнала (VLF) не имеют фиксированных размеров. Не существует также и определенного числа виртуальных файлов журнала, приходящихся на один физический файл журнала. Компонент Database Engine динамически определяет размер виртуальных файлов журнала при создании или расширении файлов журнала. Компонент Database Engine стремится обслуживать небольшое число виртуальных файлов. После расширения файла журнала размер виртуальных файлов определяется как сумма размера существующего журнала и размера нового приращения файла. Администраторы не могут настраивать или устанавливать размеры и число виртуальных файлов журнала.

Последняя цитата снимает все вопросы, хотя, как мы убедились, косвенно и отчасти DBA все же способен влиять и на число VLF, и на размер каждого. Ну и конечно, что бы BOL был, а «косяков» не было... Редкая цитата обходится без редактирования. Вот и тут, правильная версия предпоследнего предложения означенной цитаты будет:

После расширения файла журнала размер виртуальных файлов определяется только размером нового приращения. Размер существующего журнала никак не влияет ни на размер существующих VLF, ни на размер добавляемых VLF.

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

Итак, снова берем уже полюбившийся нам процесс crash recovery. Напомню, что первый раз мы его «полюбили» тут, а здесь ознакомились с ним еще плотнее. Вообще, по оценкам автора, не менее 10% того что сервер в принципе умеет, и того чем он регулярно занимается направлено исключительно на обслуживание этого процесса. Да, а вы что хотели? Надежность, она знаете ли совсем не бесплатно достигается. Так и тут, для описания всех «ниточек» коими опутан этот самый crash recovery и за которые он «дергает» прямо или косвенно не хватит и еще одного цикла статей. Кое с какими механизмами из «свиты сопровождения» нашего «героя» мы ознакомились, большинство останутся за бортом повествования, увы. Но вот вам еще один механизм из того же набора — нахождение точек входа и выхода из журнала при crash recovery.

Стало быть, при каждом (ре-)старте сервера первая же фаза означенного процесса (имя этой фазы нам хорошо известно — analysis) должна, безусловно, считать записи лога данной базы (вообще-то всех баз, но для простоты положим что у нас на сервере ровно одна база данных). Задумаемся вот над каких вопросом: если в текущий момент времени физический размер LDF-файла 5TB (ну — допустим, у нас оооочень объемные транзакции, и их очень много) то, очевидно, в составе его VLF будут:

  • совершенно чистые, т.е. не содержащие никаких записей лога;
  • «устаревшие», т.е. содержащие такие записи «затирание» которых уже одобрено движком;
  • «актуальные», т.е. содержащие свежие записи как раз и необходимые для успешного crash recovery.

Как поступим? Будем читать «от края до края», все 5TB? Это при условии что «актуальными» являются записи значительно меньшего объема, скажем, для определенности, 75MB? Конечно нет, давайте определимся с границами «актуальности» и считаем только эти 75MB! Давайте. Ну, на логическом уровне нет никаких проблем, «актуальные записи», содержатся, собственно, в «актуальном же логе», он же лог активный. Переводя рассуждения в более техническое русло, нам нужны лишь записи из промежутка MinLSN-MaxLSN, т.к. именно такой промежуток называется активным логом, не забыли об этом? Так, отлично, встаем на место фазы analysis в момент старта сервера: вот у нас mdf-файл, а вот у нас ldf-файл. Как приступить к поиску этих двух границ? Для нахождения MinLSN можно было бы «пробежаться» по всем VLF-ам в LDF-файле оценивая их статус. VLF с наименьшим номером и со статусом Active , очевидно, будет содержать левую границу диапазона, MinLSN. Однако потом ее еще нужно найти уже внутри этого «VLF с наименьшим номером». В общем — алгоритм возможный, но не оптимально. Поступают иначе: из файла mdf (да, именно из файла данных) считывают специальную служебную загрузочную страницу (boot page). Это неизменно девятая страница первого файла данных (т.е. ее адрес всегда 1:9), и, как обычно, для описания всех «интересностей» что она содержит нужна отдельная пара статей. Пока же ограничимся таким фактом: поле dbi_checkptLSN указанной страницы всегда содержит LSN последней зафиксированной контрольной точки, т.е. нашего last checkpoint. «Пройдя» в LDF-файл по указанному адресу (это гораздо быстрее, чем «шерстить» каждый VLF на предмет его статуса, а потом еще сканировать один из отобранных VLF-ов) и прочитав указанную запись мы без всяких проблем вычисляем MinLSN. Иногда им будет является сам last checkpoint, иногда нет. Важно, что если мы «встали» на последнюю контрольную точку то от нее до истинного MinLSN по любому «рукой подать».

Хорошо, левая граница есть, что с границей правой, MaxLSN? Вот тут начинается «засада». Boot page помочь нам не в состоянии, такая отметка в ней отсутствует. И то сказать, если частота смены last checkpoint в высоко нагруженной системе «разумная» (обозначим это так), то частота смены MaxLSN просто «за гранью». Она ж меняется с каждой новой записью в логе! Это даже не микросекунды, а их доли, этак мы и «дырку» на HDD протрем, если будем в какое-то место (допустим в отдельное поле в той же boot page) постоянно эту информацию обновлять!

Ладно, а вот так если. На MinLSN мы уже «стоим», как выяснилось это-то достаточно быстро. «Поехали» считывать записи лога, последовательно. Читаем и сразу же анализируем их LSN — это же не сложно, запись по любому должна быть считана полностью для ее всестороннего анализа? Ну так вот, пока LSN-ы идут по «нарастающей» мы — в границах MinLSN-MaxLSN. Последней «нарастающей LSN» будет как раз MaxLSN. А после нее мы «сваливаемся» на записи «предыдущего слоя», т.е. те что были помещены в данный VLF «когда-то». После этого VLF успел последовательно сменить статусы на Recoverable, затем на Reusable, и вот теперь он вновь находится в статусе Active и заполняет записями новый «слой». Одной (и последней) из записей этого нового слоя как-раз и будет наш искомый MaxLSN. Так вот, «свалившись» на слой ниже мы сразу замечаем что LSN уменьшился, вместо что бы увеличиться. А значит вот эта запись — лишняя, предыдущая и была MaxLSN. Весь активный лог считан, можно приступать к его анализу. Ну и — как план? По-моему — блестяще? Блестяще-то он блестяще, но вот если подумать — то не очень. Смотрите, что может получиться.

Сценарий такой: VLF с номером 99, его блок 0x100. Находится указанный VLF всего во втором цикле записи, т.е. в первом цикле («слой 1») он был заполнен полностью, во втором («слой 2») — частично. При заполнении второго слоя случился «крэш», запись MaxLSN находится именно в этом VLF, именно во втором слое. В «слое 1» блок 0x100 был заполнен сразу большой транзакцией, на 20KB. Стало быть блок начался по смещению (0x100=256)*512=131072 байт, а закончился по смещению 131072+20*1024=151552 байт. В «слое же 2» блок 0x100 начался с набора коротких транзакций объемом записей до 512 байт каждая, так что «нарезка блоков» пошла куда как «гуще» чем в первом слое: 0x100 → 0x101 → 0x102 → ... . Допустим что блок 0x111 содержит как раз слот с записью MaxLSN. Вновь считаем — на каком смещении эта запись находится? (0x111=273)*512=139776. И «свалившись» с этой записи «вниз», в «слой 1», мы отнюдь не оказываемся в начале какой либо записи этого предыдущего слоя. Мы, скорей всего, просто оказываемся в середине какого-то большого слота первого слоя. Понять где мы, что мы, и «где тут LSN» решительно невозможно, перед нами просто «мусорный набор» байтов. Хуже того, «мусорный набор» может выстроиться таким образом, что мы засчитаем его за валидную запись лога, и начнем вносить модификации вообще никем из пользователей не санкционированные! Разумеется, это должно сильно «повезти», что бы случайные байты выстроились определенным порядком, но, с учетом сколько в мире SQL серверов, сколько на них баз, и, особенно, сколько транзакций в них происходит — это случится непременно, вопрос лишь «рано/поздно» и «где».

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

Прикинув так и эдак мы, к своему ужасу, осознаем, что на логическом уровне MaxLSN не обнаружим, то есть в принципе. То есть структура лога и его записей такова, что «в холодную» (на базе в off-line) MaxLSN не «детектится», хоть тресни! А поддерживать его «твердую копию» на HDD, это, как мы выяснили, прямой путь к «продырявливанию» последнего. Тогда — что? Правильно, спускаемся на уровень ниже, физический.

Здесь нас встречают всякие «фишки» наших HDD: треки, цилиндры, кластеры и, особенно, сектора. Что такое сектор? Это, как всем хорошо известно, минимально адресуемая область данных на жёстком диске. Для нас важно то, что размер сектора традиционно равен 512 байт. Но и размер блока лог-файла столько же! Думаете простое совпадение?

Возможно вы знаете, что современные жесткие диски идут по пути увеличения размеров своих секторов. Уже не редкость диски с секторами в 1 и 2KB, а к дискам с секторам размером в 4KB даже в некоторой степени применимо прилагательное «популярные». Тем не менее, «основным сектором», на момент написания статьи, остается, несомненно, сектор «классический», на 512 байт. SQL Server без проблем работает с дисками практически любых размеров, однако о том как он это делает надо, несомненно, писать отдельную статью. Которая в планах автора значится наряду с прочими возможными кандидатами. В любом случае, в рамках статьи текущей мы не будем рассматривать сектора иных размеров кроме классического.

Так вот в SQL сервере «просто так» и «случайно» совпадения случаются редко, почти всегда совпадение делается с умыслом и с далеко идущими планами. Так и тут: такое положение вещей при котором каждый из блоков лога аккуратненько укладывается ровно в границы HDD сектора имеет под собой хитроумный план, не лишенный, я бы сказал, даже некоего остроумия и изящества. План этот сводится к пометке всего сектора (не блока! не слота! сектора) специальным флагом, способным принимать ровно два значения (как это и полагается любому уважающему себя флагу). Обратите еще раз внимание на принципиальное различие: помечается не блок (могущий занимать сразу несколько секторов, но не меньше одного), не слот (эта сущность вообще от сектора отвязана и работает совершенно независимо), а именно сектор. Когда производится пометка сектора? Как только в данный сектор идет запись очередного «слоя». То есть, если у нас транзакция такая, что она умещается полностью в блок 0x100 (т.е. суммарный размер всех записей транзакции меньше 512 байт), то в момент записи этого блока флаг устанавливается только для того сектора, что «принимает на себя» этот самый блок. А если суммарный размер транзакции начинающейся в том же блоке 0x100 будет 2000 байт? Тогда флаги будут установлены сразу для четырех секторов: 0x100 (первые 512 байт), 0x101 (512+512=1024 байт), 0x102 (1024+512=1536 байт), 0x103 (байты оставшиеся до 2000). Да, блок будет один. И выведен он будет на диск одной командой на запись. А пометок будет проставлено четыре, по числу секторов в которых этот блок разместит свои записи. Ну а суть-то этих пометок к чему сводится? В чем их «кармическое предначертание»? Думаю, самые внимательные читатели все уже поняли: различать записи «своего» «слоя» и «слоя» предыдущего! Ну а с таким-то «раскладом» у нас совсем иные «песни» начнутся! Вот теперь мы можем «встать» на запись MinLSN, из заголовка VLF к которому она относится узнать какой нынче флаг означает «свой слой» и «шпарить вперед» пока очередной считанный секторне блок!) не «маякнет» нам флагом противоположного значения. Вот тут — стоп, прибыли: слот с MaxLSN записью завершился в предыдущем секторе.

Ну а можно ли как-то это «изящное решение» «руками пощупать»? А как же! Наш традиционный уже скрипт с «много мелких транзакций»:

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

Вот отчет команды DBCC LOGINFO:

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

Очевидно, что сейчас у нас идет «залив первого слоя». Так вот обратите внимание на колонку Parity — видите там 64? Если хотите, считайте что это «ноль-флаг». Нас, в общем-то, устроит любая несовпадающая пара значений, 0/1, 2/4 — годится все. Вас, конечно, интересует такой набор вопросов: «ведь одна строка в последнем отчете это, по сути, VLF?» Верно. «А VLF — это набор блоков?» Снова верно. «А блоки могут содержать множество секторов?» И вновь вы правы. «Ну а как же VLF может отчитаться по флагам всех своих блоков и, уж тем более, секторов?» А вот это вы хорошо подметили! Не может, разумеется. Но в том и нужды нет. По сути, колонка Parity VLF-а 73-го (это для примера) говорит нам буквально следующее: если флаг сектора относящегося к VLF73 имеет значение 64 — записи в нем принадлежат «текущему слою», т.е. актуальны; если значение флага сектора противоположно — запись относится к слою «предыдущему», т.е. не актуальны.

Поднимем счетчик цикла до 1700, тогда:

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

Будем считать что «первый слой» заполнился до конца или почти до конца. Поднимем счетчик до 2500:

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

Понимаете, да? В первых двух VLF-ах пошел на запись «второй слой» и вот для них «флаг актуальности» стал равен 128. Опять же, можете рассматривать это значение как «единица-флаг», если вам от того проще. Но второй «слой» еще не достиг последней пары VLF и для них флаг не сменился на противоположный. Вот когда записи этого второго слоя «прибудут» и туда — их флаги тоже поменяются на 128. Затем лог-файл сделает очередной «полный оборот» и в первый VLF начнут поступать записи третьего «слоя» (собственно нумерация «слоев» совершенно избыточна, у нас в любой момент времени и в любом VLF «слоя» всего два — текущий и предшествующий). И тогда флаг этого первого VLF вновь сменится на что? Верно — вновь на 64. И так «пока не надоест». :)

Все ли понятно? Отлично! Вот только не ясно — а почему в самом первом отчете у последней пары VLF флаг не 64, не 128, а ноль — это как, вообще? Третье значение флага могущего принимать ровно два значения? Да, с этим стоит разобраться... Вновь давайте думать.

Мы в нашем последнем скрипте «заказали» лог-файл на 1MB. OS Windows указала где этот файл разместится, примерно как «от этого колышка до вот этого». Может ли движок сервера просто взять и начать заливать первый «слой» записей в указанное место («от первого колышка») без всякой подготовки онного места? Смоделируем ситуацию: лог-файл состоит из 10 тыс. секторов (цифры условны, важно что мы вновь обсуждаем сектора, а не блоки/слоты), началась работа с базой, записи лога заполнили 2000 секторов — «крэш». Стало быть, при ближайшем crash recovery сектор 2001-й должен четко заявить: «я — из другого слоя». Просто потому что таков наш алгоритм чтения записей лога, он «верит» биту четности сектора. Если никаких предварительных обработок места под LDF-файла (тех самых «колышков») не проводить — кто может гарантировать верные показания сектора 2001? Никто! В 2001-м секторе может оказаться все что угодно: нули, обрывки удаленного ранее файла и т.п. В том числе там может оказаться и такой набор бит и байт что их сочетание скажет как-раз таки о принадлежности этого «типа блока» «активному слою». И что тогда? Ничего хорошего, это уж как минимум. А поэтому нужно что? Правильно — совершенно явно «прочесать» каждый (!) сектор лога и пометить его как «не из активного слоя». А иными словами обработать весь лог. В принципе для подобной пометки подошла бы любая цифра кроме 64 (флаг первого активного «слоя») но выбор пал на банальный ноль. Его, ноль этот, мы и видим в колонке Parity первого из последней группы отчетов. Логически такой ноль говорит что данный сектор не содержит записей активного «слоя», не содержит записей «слоя» предшествующего, а, попросту говоря, не содержит вообще ничего. И в нашей смоделированной ситуации фаза analysis процесса crash recovery «тормознет» где надо — ведь 2001-й сектор не будет содержать «правильного» флага, и уж вот на этот раз — гарантировано! Ну а технически процедура обсуждаемого «прочесывания» реализована просто до примитивности: каждый новый файл лога заполняется нулями «от края до края» — и все дела. Сама же эта процедура известна в литературе под кодовым названием «зануление лога» (zero-initialize of log file). И в ней, в целом, нет ничего плохого. За исключением принудительной записи пятка гигабайт (допустим) нулей. Ну или пятка терабайт, в зависимости каким вы видите свой будущий LDF-файл. А что совсем грустно так это тот факт, что и приращение (причем как ручное, так и автоматическое) должно пройти ту же самую процедуру «принудительного зануления». И причины те же — мы не можем рисковать работой с «мусором». Поэтому: увеличиваете свой LDF-файл на 100MB? Получите эквивалентное число нулей «в нагрузку». Понимаете теперь почему автор назвал процесс приращения размера LDF-файла выходом, безусловно, лучшим, чем критическая ошибка переполнения того же файла, но лучшим не намного? Потому что это процесс ресурсозатратный и даже чрезвычайно. Хуже всего то, что практически 100% нагрузки им генерируемой ложится на и так самое слабое место большинства систем — Disk I/O. Ведь требуется «тупо прокачать» большое (иногда очень большое) число нулей: на CPU при этом нагрузки ноль, на сетевую карту — ноль, но вот диск...