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

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


366bef3a

Итого, сосредоточившись только на значимой информации релевантной только исследуемой базе TstLog мы увидим в результатах работы команды DBCC SQLPERF что-то вроде такого:

Database Name   Log Size (MB)   Log Space Used (%)
TstLog          0.9921875       31.20079

Значения колонок самоочевидны. То, что первая из них содержит не ровно 1.0 (как должно согласно скрипту создания TstLog) объясняется очень просто: 1.0-0.9921875=0.0078125. Но это «дельта» в мегабайтах, а переведя ее в байты получим 8192=8Кб. Именно означенное количество килобайт занимает заголовок лога, он же header. В котором содержится много чего, но никак не записи лога, они же records. А нам интересно, в первую очередь, сколько места в логе именно под записи, ибо ради них лог и ведется. Поэтому вполне резонно, что SQLPERF под размером лога имеет в виду «чистый» размер под записи, без учета служебного заголовка. Вторая колонка вроде бы тоже очевидна — это занятое место в лог-файле. Однако, имейте в виду, что по некоторым причинам, объяснение которых лежит совершенно за рамками данной статьи, команда DBCC SQLPERF практически всегда завышает процент занятого места. То есть если принимать текущее значение этой колонки за «чистую монету», то прямо сейчас у нас примерно треть мегабайтового лога (OK, хорошо — почти мегабайтового) занята, а две трети, как не сложно догадаться — свободны. На самом же деле у нас занято места значительно меньше, чем треть мегабайта. Впрочем, с ростом общего размера LDF-файла эта погрешность становится все менее значимой. В целом же, если вам не требуется отчет о свободном месте с точностью «до сантима», просто считайте цифру колонки Log Space Used корректной, не ошибетесь. Тем более что завышение показателя занятого места это «беда» значительно меньшего масштаба чем занижение того же параметра.

Теперь — можно ли сказать что означенный «почти мегабайт» представляет из себя прямо вот такой монолитный кусок на диске в который записи «заливаются» одна за другой пока этот самый кусок не закончится? Ни в коем случае! Лог-файл (даже самый маленький) имеет четко выраженную структуру и она ни разу не монолитна, как минимум с логической точки зрения.

Ядром этой структуры являются виртуальные лог-файлы (Virtual Log Files, VLF). Ответ на вопрос «что это?» уместится в одну строку: это логические части на которые «нарезается» исходный лог-файл (т.е. физический файл LDF). Для исчерпывающего же ответа на вопрос «как это работает?» понадобится книга, если не пару. До некоторой степени мы этот второй вопрос разберем в статье, кое-что, как это обычно бывает с очень объемными и многогранными вопросами, останется «за бортом», увы. Возможно постепенно автор будет дополнять эту основополагающую статью новыми и новыми заметками по вопросам функционирования журнала — используйте RSS/почту (крупные кнопки вверху и справа любой страницы сайта) что бы быть «в курсе». ;)

Итак, логически, единый LDF-файл (а равно и LDF-файл составленный из нескольких физических — это ничего не меняет) «бьется» на участки называемые VLF. Даже в самом крошенном логе их минимум два, а значит вся структура уже не монолитна. Однако обычно их много больше, в экстремальных случаях — больше на порядки, т.е. в «рабочем» LDF-файле их число исчисляется десятками (очень часто), сотнями (реже) и даже тысячами (совсем редко, но не так что бы «один из миллиарда»). Что же до размера отдельно взятого VLF-а, то достоверно известна лишь его нижняя граница, она равна 31*8Кб=248 килобайт. Верхний же его границы, насколько это известно автору, пока никому достичь не удалось и цифра сия остается покрыта мраком. Исходя из того, что, скажем, один VLF в 4 гигабайта — это, не сказать что бы рядовой случай, но технически это более чем запросто — можно лишь оценить эту цифру как «весьма значительную». Одним словом, задолго до того как вы этот максимальный объем одного VLF достигните (это если он вообще существует), вы исчерпаете место на своих самых вместительных HDD, ведь как было отмечено число самих виртуальных логов может быть 1000 и более.

Что же до роли VLF-ов играемых ими в обеспечении оптимальной работы журнала транзакций, то, скажем так — они, роли эти, разнообразны и многочисленны Однако несомненно, что ведущая роль сводится к обеспечении «эластичности» самого журнала. То есть сделать мир таким, что бы наш журнал, как физический LDF-файл, когда нам нужно — расширялся и занимал большее место на диске, а когда наоборот — сжимался и занятое место возвращал OS Windows. Первое нам нужно безусловно (объем компьютерных данных возрастает с каждым годом, это уже аксиома), насчет второго многие заблуждаются, однако не будем забегать вперед: размер лога и управление им (размером, а не логом) будет разбираться нами в рамках данного материала крайне подробно. Но — отдельно. Пока же просто констатируем факт: одно из главных назначений VLF — быть единицей работы (для IT-шников понятнее термин «юнитом») по изменению размера физического LDF-файла. То есть не бывает так что бы захотели мы увеличить размер лог-файла на 3 байта — увеличили. А захотели его же сократить на 295409 байт — «вырезали» точно указанное их число. Нет, и в ту, и в другую стороны лог меняется строго «кусочно», «по-VLF-но». Вот другое дело что размер каждого VLF (как и их число, заметьте!) может быть совершенно произвольным (но не менее 248-и Кб, как было отмечено) — это да. Но последнее замечание не отменяет того факта, что при каждом изменении размера LDF файл увеличивается / уменьшается на целое число VLF-ов, и никак иначе.

И как же на текущий момент выглядит лог нашей тестовой базы TstLog если его разложить на VLF-ы? А вот как:

Log_with_4_VLFs

Собственно — комментировать тут особенно нечего. Четыре VLF-а, из которых используется только первый. То есть записи о произошедших до сих пор транзакциях пока все умещаются в VLF1, а VLF со 2-го по 4-й даже не начинали использоваться — нет нужды, нам бы VLF1 сначала до конца «добить». Самое интересное тут, опять же — как автор все это рисовал, то есть информацию для рисунка откуда черпал? Встречайте, очередная недокументированная «фича» — DBCC LOGINFO. Инструкция предназначена как раз для анализа текущего состояния виртуальных файлов лога той базы данных, в контексте которой будет исполнена эта команда. Иными словами не забывайте перед ее применением переключиться в нужную вам базу инструкцией USE. В нашим случае выполнив несложный набор команд

1
2
3
USE TstLog
GO
DBCC LOGINFO

мы увидим вот такой примерно «расклад»:

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

Сразу же запоминаем простое и удобное правило: каждая строка резал-сета изучаемой команды DBCC LOGINFO выводит информацию ровно об одном VLF, или, иными словами, число строк в резалт-сете равно числу виртуальных логов в LDF файле. Всегда выводится информация о всех VLF, сколько б их ни было. Теперь назначение колонок в резалт-сете:

  • FileId — идентификатор физического файла в котором расположен данный VLF. У нас физический файл лишь один, все VLF в нем и расположены. Если бы у нас была пара LDF файлов в данной колонке фигурировало бы дополнительно несколько строк с цифрой, допустим, 3. Если бы файлов было 3 — добавились бы строки с идентификатором 4, и т.д.
  • FileSize — размер виртуального файла, т.е. размер VLF. Не обманитесь названием колонки — это НЕ размер LDF-файла! Это именно размер каждого отдельного VLF причем в байтах.
  • StartOffset — поскольку, как мы уже знаем, размер каждого VLF совершенно произволен, то у нас (а равно и движка сервера) нет иного пути узнать в какой точке физического файла начинается данный VLF, кроме как «в лобовую» «замерить» расстояние между байтом 00 физического файла и первым байтом данного VLF. Иными словами это смещение начала каждого VLF от начала LDF, причем снова в байтах. Нюанс: если итоговый лог «склеен» из нескольких физических LDF-файлов смещение измеряется от начала того LDF-файла которому принадлежит данный VLF. Можно и так сказать: поскольку каждый LDF-файл составляющий многофайловый лог начинается обязательно с уже упомянутого ранее заголовка лога (header), и с учетом того обстоятельства что последний имеет фиксированный размер в 8кБ, первый VLF каждого (а не только первого!) LDF-файла будет иметь в этой колонке значение 8192. По этому значению можно легко понять, что вот в этой точке случается «переключение» на очередной физический файл, хотя быстрей всего эта информация извлекается из первой колонки, FileId. Кстати обсуждаемый резалт-сет всегда сортируется (по возрастанию) сначала по колонке FileId, а затем по этой — StartOffset. Так что мысленно нарисовать себе «зеленые квадратики» (подобно тому как это сделал автор на последней иллюстрации) нет никаких проблем: первая строка рисуется как самый левый квадратик, вторая — как второй слева и т.д. Вот и пожалуйста — наглядное представление физического расположения VLF-ов внутри LDF файла / файлов.
  • FSeqNo — в отличии от комбинации колонок FileId/StartOffset дающих нам представление о физическом расположении данного VLF-а, значение этой колонки говорит нам о логическом порядке использования того же VLF. Иными словами, очередная запись журнала поступит в тот VLF, что имеет максимальное значение в данной колонке. А до того как начал использоваться этот VLF, использовался тот, что имеет в данной колонке предыдущее значение. А до него — с еще более предыдущим, и т.д. Иными словами, если отсортировать резалт-сет по этой колонке по убыванию, то получится хронология заполнения VLF-ов в обратном порядке — внизу будут те из них кто заполнен записями уже давным-давно, а сверху будет всегда VLF который заполнен лишь частично и имеет место для новых записей. Разумеется, за время «жизни» базы один и тот же VLF может иметь в этой колонке сотни и тысячи различных значений. Но абсолютное значение данной колонки лишено какого либо смысла. Смысл появляется лишь при соотнесении значений данной колонки принадлежащих разным строкам (т.е. VLF-ам). Значение 0 данной колонки говорит нам о том, что на момент вызова инструкции DBCC LOGINFO данный VLF не содержит ни единой записи, и, более того, не содержал их никогда с момента образования базы. Можно и так сказать: с момента внесения первой записи в данный VLF значение колонки FSeqNo для него меняется с 0 на какое-то иное (но гарантировано положительное и больше нуля) и после этого вернуться к нулю уже никак не может, т.к. каждый новый «апдейт» этой колонки может лишь увеличить ее текущее значение, но никак не уменьшить его.
  • Status — эта колонка может содержать всего два значения: 0 и, как ни странно, 2 (а не ожидаемая единица). Как мы узнаем далее, каждый VLF в любой данный момент времени находится в одном из 4-х состояний:
    1. Active
    2. Recoverable
    3. Reusable
    4. Unused
    Смысл каждого состояния будет нами разобран в свое время, а пока ограничимся замечанием, что цифра 0 в обсуждаемой колонке означает что данный VLF находится в состоянии 3 или 4, а цифра 2 означает для него же, соответственно, состояние 1 или 2. Если сказать очень коротко (нюансы, напомню, последуют далее), то VLF со цифрой 0 в этой колонке готов к первому или повторному его использованию, а с цифрой 2 уже используется в настоящее время.
  • Parity — контроль по чётности, как вы понимаете из названия колонки. Снова всего два возможных значения но уже не 0/1, и даже не 0/2, а 64/128. Нюансы, как вы правильно догадались, последуют далее, а пока отметим что при каждом повторном использовании VLF его значение в данной колонке меняется на противоположное.
  • CreateLSN — время создания данного VLF, но не в формате день-час-минута, а в формате LSN (Log Sequence Number). Про нюансы повторяться не буду, а скажу лишь что значение 0 в данной колонке говорит нам о том, что данный VLF был сформирован как результат работы команды CREATE DATABASE, то есть еще до того как наша база «приняла» на себя свою самую первую транзакцию. Если же VLF был сформирован позже, как реакция движка на ту или иную транзакцию приведшую к разрастанию размера физического LDF-файла, то вот для такого VLF данная колонка будет содержать как раз ID той самой команды что привела к росту лога. Обычно движок распределяет сразу несколько VLF и тогда все они будут иметь в этой колонке идентичное значение. Это, кстати, общее правило: все VLF имеющие в данной колонке одно и тоже значение «родились» в одно и тоже время.

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

1
DBCC LOG(TstLog, 1)

Вообще-то общий синтаксис команды DBCC LOG довольно «развесист», но мы для наших исследований ограничимся всего двумя возможными параметрами:

  • имя той базы чьи записи журнала мы хотим посмотреть;
  • уровень «детальности» отчета. Здесь можно указать любую цифру из диапазона -1 — 4, т.е. всего 6-ть вариантов. Обычно чем цифра выше — тем более детальная информация будет нам представлена. Только -1 с этой точки зрения идет не перед 0, а после 4, т.е. -1 дает нам максимально подробный отчет. «Рабочими лошадками» являются цифры 0 и 1.

Сильно усеченный результат работы показанной выше команды может выглядеть так:

Current LSN             Operation       Transaction ID  Previous LSN
00000043:0000001b:0001  LOP_BEGIN_XACT  0000:00000555   00000000:00000000:0000
00000043:0000001b:0002  LOP_MODIFY_ROW  0000:00000555   00000043:0000001b:0001
00000043:0000001b:0003  LOP_MODIFY_ROW  0000:00000555   00000043:0000001b:0002
00000043:0000001c:0001  LOP_DELETE_ROWS 0000:00000555   00000043:0000001b:0003
00000043:0000001c:0002  LOP_MODIFY_H... 0000:00000000   00000000:00000000:0000
00000043:0000001c:0003  LOP_SET_BITS    0000:00000000   00000000:00000000:0000
00000043:0000001c:0004  LOP_INSERT_ROWS 0000:00000555   00000043:0000001c:0001
00000043:0000001c:0005  LOP_SET_BITS    0000:00000000   00000000:00000000:0000
00000043:0000001c:0006  LOP_DELETE_ROWS 0000:00000555   00000043:0000001c:0004
00000043:0000001c:0007  LOP_SET_BITS    0000:00000000   00000000:00000000:0000
00000043:0000001c:0008  LOP_INSERT_ROWS 0000:00000555   00000043:0000001c:0006
00000043:0000001c:0009  LOP_SET_BITS    0000:00000000   00000000:00000000:0000
00000043:0000001c:000a  LOP_COUNT_DELTA 0000:00000000   00000000:00000000:0000
00000043:0000001c:000b  LOP_COUNT_DELTA 0000:00000000   00000000:00000000:0000

Каждая строка, как легко понять, является очередной записью лога. Коротко опишем смысл «избранных» колонок:

  • Current LSN — снова Log Sequence Number который уже наблюдался нами при разборе результатов предыдущей команды. Это, собственно, уникальный номер данной записи, ее ID. Любой последующий LSN гарантировано больше любого существующего в рамках всего журнала (а не только отдельного VLF!). Обратное тоже верно: любая запись с большим LSN произошла гарантированно позже чем любая запись с меньшим LSN. С логической точки зрения (и только с ней!) данная колонка напоминает колонку типа timestamp в наших с вами таблицах. Только последняя меняется при каждом «апдейте» соответствующей строки, а LSN закрепляется за данной записью журнала «намертво», на всю жизнь! Разумеется к часам и минутам LSN совсем никак не относится, т.к. реальный тип этой сущности не datetime, и не timestamp (который до некоторой степени ей бы подошел), а просто numeric(25,0). Т.е. это «навсегда увеличивающийся» счетчик и не более того. Данная колонка является ведущий в отчете любой степени подробности и всегда будет самой первой. О LSN мы будем говорить очень много.
  • Operation — «низкоуровневая» операция. Вполне очевидно, что когда мы пишем одну строку на языке T-SQL выполняется целая «пачка» команд, о части которых мы знаем (я надеюсь!) из чтения BOL/книг/блогов подобных этому, а о части лишь догадываемся или вовсе не подозреваем. Скажем простой однострочный INSERT может быть «разложен» на такие элементарные операции: начать транзакцию → наложить блокировку → вставить строку → зафиксировать транзакцию. В зависимости от обстоятельств (наличие индексов, наличие свободного места на странице данных и т.п.) число подобных операций может быть и больше показанных. Так вот данная колонка указывает код такой элементарной операции. Это как ассемблерная команда, только не для CPU, а для движка сервера. Код из этой колонки всегда начинается с префикса LOP (Log OPeration).
  • Transaction ID — каждая из описанных в предыдущем абзаце элементарных операций может выполнятся или в рамках той или иной транзакции, или выполняться вне всех транзакций. В последнем случае эта колонка будет содержать 0000:00000000. Любое значение отличное от нуля скажет нам, что операция транзакционна, а само это значение обозначит ID этой самой транзакции. Обыкновенно (хотя и вовсе не обязательно) несколько следующих друг за другом операций имеют в этой колонке одинаковое значение, т.к. выполняются в рамках одной и той же транзакции.
  • Previous LSN — как мы узнаем из дальнейшего изложения записи журнала могут образовывать так называемые «цепочки» (chains). Но могут их и не образовывать. Так вот значение 0 (более формально 00000000:00000000:0000) в этой колонке означает одно из двух:
    • данная запись «открывает» новую цепочку и является ее первым звеном;
    • данная запись «сама по себе», ни в какие цепочки не входит и не собирается.
    Любое иное значение будет ссылкой на LSN предыдущего звена цепочки. В подавляющем большинстве случаев команды исполняющиеся в рамках той или иной транзакции в цепочки входят, а исполняющиеся вне их являются как раз «сами по себе». Однако правило это не универсально. Та же инструкция языка T-SQL CHECKPOINT исполняется вне любых транзакций, однако в логе отмечается обязательно, и при этом запись об окончании этого процесса (код операции LOP_END_CKPT) непременно «сошлется» на запись о его начале (код операции LOP_BEGIN_CKPT).

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