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

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









Физическая структура страниц данных.

Как всем давно и хорошо известно, .mdf/.ndf файлы логически состоят из страниц данных, содержащих, собственно, эти самые данные. Страница=8кБ, 8 последовательных страниц=экстент (extent), 1 extent=64кБ, и т.д. — все это вам, безусловно, известно. Если копнуть чуть глубже, и спросить себя «а как физически выглядит отдельная страница», то и на него ответить будет не столь затруднительно. Скажем вся наша тест-таблица T1 (уместившаяся на единственной странице, да еще кучу свободного места на ней оставившей) физически, на диске, выглядит вот как:

DataPage_Structure

Разумеется, вам страшно интересно знать — как автор столь безошибочно воспроизвел структуру указанной страницы? Ничего сложного, поверьте. Сначала командой

1
DBCC IND(TstLog, T1, -1)

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

PageFID PagePID IAMFID  IAMPID  PageType
1       80      NULL    NULL    10
1       79      1       80      1

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

  • PageFID и PagePID — вместе однозначно идентифицируют страницу данных в границах данной базы. Первое значение нужно поскольку файлов данных, в общем случае, может быть более одного. Понятно, что для нашей базы (как частный случай) здесь всегда будет 1, без вариантов. Ну а вторая идентифицирует, собственно, страницу. Так же заметьте себе, что в литературе (да и в отчетах самого движка) данную «сладкую парочку» принято записывать через двоеточие, к примеру 1:80. Слева — ID файла, справа — ID страницы. Все просто и понятно.
  • IAMFID и IAMPID — похоже на предыдущую пару, но дает не адрес самой этой страницы, а адрес ее «родительской» страницы. Страницы в куче (heap) распределяются согласно записям в специальной IAM-странице (Index Allocation Map). Если хотите — считайте такие страницы «служебными», поскольку они не содержат самих данных, а лишь указания откуда эти данные взять. Всегда есть связь между реальной страницей данных и указывающей на нее IAM-страницей. Предыдущая пара колонок и эта позволяют эту связь видеть. К примеру, для страницы 1:80 нашей тест-таблицы последняя пара содержит NULL в каждой колонке. Это означает что 1:80 и есть та самая «служебная» IAM. И «родителя» у нее нет. Что, кстати, полностью подтверждается значением последней колонки, см. следующий пункт. Страница же 1:79, напротив, содержит в той же паре колонок значение которое мы смело можем записать как 1:80. Прояснилась для вас означенная связь?
  • PageType — тип страницы. Может содержать несколько значений, но мы ограничимся описанием двух:
    • 10 — страница является «служебной», т.е. IAM-страницей;
    • 1 — страница является самой обычной страницей данных.

Итак, автор понял, что первичный его интерес — страница 1:79. IAM-страница тоже очень даже интересна, и заслуживает отдельной заметки, но в контексте текущего разговора она нам не нужна. Поэтому сосредотачиваемся на 1:79.

Если в вашем конкретном окружении страницей таблицы T1 будет не 79-я, а любая иная, вам придется не только делать «мысленные правки» в дальнейшем тексте статьи, но и вносить вполне физические изменения (хотя и совсем небольшие) в некоторые из последующих скриптов. Все дальнейшее повествование исходит из предположения, что данные указанной таблицы разместились на странице именно с 79-м номером.

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

1
2
DBCC TRACEON(3604)
DBCC PAGE(TstLog, 1, 79, 1)
Обратите внимание на третий параметр второй команды и соотнесите его значение с предупреждением чуть выше и с вашим конкретным окружением.

Строго говоря недокументированной является лишь вторая команда. Первая вполне «легальна» и ее назначение — позволить команде второй отправлять сгенерированный ею текст клиенту (студия, в нашем случае), а не в error log. Кстати, такое «переназначение» вывода будет продолжаться пока вы не закроете текущую вкладку редактора в студии (говоря языком более техническим, флаг 3604 устанавливается показанной командой для текущего соединения), и повторять TRACEON перед каждой DBCC PAGE нужды нет, достаточно сделать это один раз в начале новой сессии. Так вот собственно наблюдая вывод этой второй команды автор без проблем изобразил предыдущей иллюстрацией внутреннюю структуру 79-й страницы. Вот как это у него получилось (усеченный вывод команды DBCC PAGE показан в правой части рисунка):

DataPage_Structure_and_DBCC_PAGE

Как видите — ничего сложного. Основных компонента физической структуры страницы данных всего два: заголовок страницы (его размер фиксирован и для любой страницы равен 0x60=96 байт) и сами строки данных, называемые на этом уровне погружения в нюансы слотами (slots). Размер каждого слота, разумеется, совершенно произволен. Именно поэтому требуется вспомогательный компонент — «Таблица смещений», определяющая в какой точке страницы (относительно ее первого, или даже лучше сказать нулевого, байта) начнется тот или иной слот. Таблица эта хоть и интересна сама по себе и ее понимание никак вам не повредит, для целей именно нашей беседы совсем не важна. Более того, не столь существенными являются даже и строки с данными, хотя время от времени мы будем к ним обращаться. Выясняется, что при рассмотрении вопросов связанных с механизмом функционирования журнала транзакций со стороны data pages главенствующую роль играет заголовок страницы (тот что гарантировано занимает первые 96 байт). Поэтому автор частенько будет прибегать к разновидности той самой недокументированной команды DBCC PAGE, а именно такой:

1
DBCC PAGE(TstLog, 1, 79, 0)

Разница, как видите, лишь в четвертом аргументе. Его значение 0 говорит о том, что мы хотим видеть только информацию заголовка (пара прочих служебных записей, так же выводящихся при этом значении параметра, нам снова не интересны). Содержимое слотов и таблицы смещений нам ненужны. Поэтому при таком варианте команды DBCC PAGE гораздо легче найти интересующую нас информацию. Однако и заголовок сам по себе тоже не микроскопичен. При должном умении в 96 байт знаете сколько информации впихнуть можно? Поэтому сразу обозначим поля заголовка релевантные нашему разговору:

Page @0x00000001816C0000

m_pageId = (1:79)                    m_headerVersion = 1                  m_type = 1
m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x8000
m_objId (AllocUnitId.idObj) = 29     m_indexId (AllocUnitId.idInd) = 256  
Metadata: AllocUnitId = 72057594039828480                                
Metadata: PartitionId = 72057594038779904                                 Metadata: IndexId = 0
Metadata: ObjectId = 2105058535      m_prevPage = (0:0)                   m_nextPage = (0:0)
pminlen = 12                         m_slotCnt = 3                        m_freeCnt = 8030
m_freeData = 156                     m_reservedCnt = 0                    m_lsn = (66:81:3)
m_xactReserved = 0                   m_xdesId = (0:0)                     m_ghostRecCnt = 0
m_tornBits = 0

По сути нас будет интересовать единственное поле — m_lsn. Вы можете обнаружить его в третьей колонке того отчета, что предоставляется командой DBCC PAGE и пример которого вы наблюдаете прямо сейчас. В этом примере значение данного поля равно (66:81:3). Так вот это самое поле чрезвычайно важно для нормальной работы сервера да и в статье нашей будет играть роль если не центральную, то уж заметную как минимум. На текущий момент достаточно если вы можете ответить на вопрос «как увидеть текущее значение поля m_lsn из заголовка страницы». А физический смысл и интерпретация возможных значений такого поля будут даны позже, когда мы ознакомимся со структурой файла лога.

Теперь — первый (из большой кучи что нам предстоит исследовать) нюанс. Допустим страница 79-я указанной базы существует в двух версиях: версия на диске и «RAM-версия» (которая, как мы хорошо уже знаем, есть ничто иное как dirty page начиная с того момента как на ней изменился хотя бы один бит). Которую из двух покажет нам инструкция DBCC PAGE? Ну этот-то нюанс один из (если не самый) легчайших для верного ответа, даже и исследований особых не надо, ответ можно вывести из чисто логических рассуждений. Конечно же, нам всегда предъявляется версия из памяти. Ведь она по любому более «свежая», не так ли? А нас, как корректно рассудили авторы данной команды, интересует что там происходит с информацией сейчас, а не вчера. И даже не 5 секунд назад. Поэтому — только RAM, без вариантов и каких-либо опций по изменению такого поведения. Могу даже более сказать, если команда на исследование 79-й (а равно и любой иной) страницы дается в момент когда таковая находится лишь на диске, то тогда она будет сначала загружена в RAM (buffer/data cache), а уж потом мы увидим отчет по ней. Итак, обращаясь к инструкции DBCC PAGE мы можем быть уверены: информация о запрошенной странице — наисвежайшая, причем всегда.

Определение «чистоты» страницы.

Следующий вопрос по которому нам необходимо определиться и без которых изыскания в области «физики» журнала транзакций немыслимы — как мы можем знать, та или иная страница находящаяся в памяти (буферном кэше) — она в настоящий момент рассматривается движком как «чистая» или как «грязная» (dirty page)? Установление этого факта чрезвычайно важно для дальнейшего изложения и подтверждения наших догадок и исследований. Мы в конце предыдущего под-раздела однозначно выяснили, что DBCC PAGE точно возвращает нам страницу именно из RAM, всегда и безусловно. К сожалению, она не сообщает нам сведений о «чистоте» странице, а лишь о ее содержимом. Точнее — как: у автора есть все основания полагать, что поле m_flagBits из заголовка страницы (вы можете видеть это поле в той же 3-й колонке отчета чуть выше) одним из своих битов (флагов) как раз «кодирует» эту информацию. Но! Во-первых, это не более чем «частные» изыскания автора, не подтвержденные хотя бы репликами форумов (я уж молчу про официальную документацию от Microsoft). Во-вторых, даже если автор прав, для четкого ответа на вопрос «чиста ли страница?» придется «вычленять» один бит из двух байт, коими представлено упомянутое поле. Ничего особо сложного в этом нет, за исключением того, что значение поля доступно нам лишь как обычный текст, а не как содержимое ячейки резалт-сета. А это уже много хуже, если попытаться вычленять нужный бит программно, а не «в уме». Ну и, наконец, третья, самая главная причина, по которой мы от данного поля легко откажемся: создатели SQL Server предлагают совершенно «легальный» и хорошо документированный способ разрешения нашего вопроса. Сей способ зовется «динамическое административное представление sys.dm_os_buffer_descriptors». А более точно, колонка is_modified указанной «вьюхи» своим нулем сообщает нам что страница «чиста», а своей единицей — что страница модифицирована и является в настоящий момент dirty page. Как легко себе представить, данное представление анализирует отнюдь не исключительно 79-ю страницу нашей тест-базы, а вообще все страницы находящиеся в момент ее запуска в буферном пуле, т.е. в RAM. Элементарный фильтр заставит ее «сосредоточится» на странице нужной нам:

1
2
SELECT CASE WHEN is_modified=1 THEN 'YES' ELSE 'NO' END AS 'T1_Page_Dirty?'
FROM sys.dm_os_buffer_descriptors WHERE page_id=79 AND db_name(database_id)='TstLog'

Отметим себе, что результатом запуска такого скрипта может быть три (а не два, как кажется на первый взгляд) значения итогового резалт-сета:

  • итоговый резалт-сета возвращает YES — страница 79 была модифицирована с момента считывания с диска и находится в состоянии dirty page;
  • итоговый резалт-сета возвращает NO — противоположно предыдущему, страница «чиста», т.е. не менялась с последнего считывания, либо с последнего своего сброса на диск;
  • итоговый резалт-сета пуст, т.е. содержит 0 строк — 79-я страница не загружена в память. Ведь представление sys.dm_os_buffer_descriptors работает исключительно с памятью, не так ли?

Несмотря на свою простоту данный скрипт будет весьма полезен в наших дальнейших экспериментах и автор имеет все основания полагать, что и не только в них. Включите его в свой арсенал администратора/разработчика — пригодится!

Физическая структура записей журнала транзакционного лога.

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

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

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

1
DBCC SQLPERF(LOGSPACE)

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