Библиотечный кэш в СУБД Oracle в деталях

Илья Дергунов

Илья Дергунов

Автор статьи. ИТ-специалист с 20 летним стажем, автор большого количества публикаций на профильную тематику (разработка ПО, администрирование, новостные заметки). Подробнее.

Как организован библиотечный кэш в базе данных OracleНа одну защелку приходится удивительно большое число хэш-блоков библиотечного кэша (тысячи, если не десятки тысяч), тогда как число цепочек в кэше буферов на одну защелку относительно невысоко (обычно в диапазоне 32–128). Операции поиска, заполнения, чтения и освобождения буферов данных сталкиваются с теми же проблемами, что и операции обслуживания библиотечного кэша (и других компонентов, находящихся в разделяемом пуле), поэтому неудивительно, что некоторые решения выглядят очень похожими.

Цепочки в кэше буферов и в библиотечном кэше служат, по сути, одной и той же цели – они позволяют Oracle быстро находить нужные элементы. Но, можно ли то же самое сказать о поиске доступной памяти, когда требуется загрузить в кэш новый элемент? Кэш буферов имеет цепочки LRU и список REPL_AUX, позволяющие эффективно решать задачу освобождения и повторного использования наиболее подходящих для этого областей памяти. Имеется ли подобный механизм в библиотечном кэше? Да, имеется! Но искать его нужно на уровне, располагающемся над библиотечным кэшем, – в разделяемом пуле. Разделяемый пул – под защитой защелки – следит за наличием свободной памяти, приобретает дополнительную память, когда это необходимо, и перераспределяет свободную память между областями.

Не пытаясь проводить слишком глубоких аналогий, можно нарисовать картину, подчеркивающую сходства – и некоторые отличия – между разделяемым пулом и кэшем буферов. На рис. 1 представлен один из вариантов такой картины.

Схематическое сравнение разделяемого пула и кэша буферов

Рис. 1. Схематическое сравнение разделяемого пула и кэша буферов

В верхней половине рис. 1 изображена основа кэша буферов – список LRU, в который здесь включены только списки буферов REPL_MAIN и REPL_AUX. Вообще говоря, REPL_MAIN – это множество буферов, в которых заинтересован сеанс, а REPL_AUX – это не коллекция пустых буферов, а множество буферов, которые могут без ущерба для сеанса использоваться для чтения новых блоков с диска или создания согласованных копий.

В нижней половине рис. 1 изображена упрощенная схема строения разделяемого пула, где также имеется список LRU (хранящий ссылки на библиотечный кэш и кэш словаря) и списки свободных фрагментов памяти. Я не опустил резервные списки свободных фрагментов памяти (объединяющие фрагменты свободной памяти в разделах разделяемого пула, известных как резервный пул (reserved pool) – определяется параметром shared_pool_reserved_size и скрытым параметром _shared_pool_reserved_pct). Я нарисовал схему так, чтобы подчеркнуть сходства.

  • Кэш буферов разбит на рабочие наборы данных – на рис. 1 показан один такой рабочий набор. Аналогично разделяемый пул также может быть разбит на несколько непересекающихся подразделов (sub heaps) – хотя, в версии 9i этот механизм содержал мелкие ошибки и многими просто отключался установкой скрытого параметра – и это выполняется автоматически, если в системе имеется достаточный объем памяти и достаточное число процессоров.
  • Каждому рабочему набору данных соответствует строка в x$kcbwds, а каждому подразделу – строка в x$kghlu.
  • Кэш буферов разбит на основной список (MAIN, определенно используемых) буферов и вспомогательный (AUX, кандидатов на повторное использование). Разделяемый пул разбит на список LRU и списки свободных фрагментов памяти (которые объединяют действительно свободные фрагменты).
  • Кэш буферов (MAIN) имеет список LRU (Least Recently Used – наиболее давно не использовавшихся) буферов и использует алгоритм вставки в середину для включения новых элементов. Разделяемый пул имеет список LRU и точку вставки, разбивающую его на рекуррентную и переходную части. Подробнее об этом мы поговорим чуть ниже.
  • Хотя это и не показано на схеме, тем не менее, кэш буферов (при использовании блоков с размерами по умолчанию) может быть разбит на три области – пул постоянного (keep) хранения, пул временного (recycle) хранения и пул по умолчанию. Начиная с версии 11g (такая возможность доступна также в версии 10g, но должна включаться установкой скрытого параметра), разделяемый пул разбивается на четыре раздела с различной «продолжительностью» хранения. Фактически оба механизма обеспечивают некоторый уровень изоляции в разных ситуациях. Кэш буферов управляется (иногда успешно) администратором базы данных, разделяемый пул – своими внутренними механизмами.

А теперь о различиях. Самое важное отличие заключается в том, что кэш буферов (или любой другой рабочий набор данных) состоит из фрагментов памяти одинакового размера – размер блока определяется для каждой части кэша. Это уменьшает сложность обслуживания свободной памяти – если потребуется выделить фрагмент памяти, он будет иметь тот же размер, что и любой другой фрагмент. В разделяемом пуле, напротив, могут использоваться фрагменты самых разных размеров, и не только в библиотечном кэше, который в своей работе опирается на механизмы разделяемого пула (см. рис. 2). Поэтому найти фрагмент памяти подходящего размера оказывается намного сложнее, и это объясняет, почему в разделяемом пуле так много списков свободных фрагментов памяти и резервных областей. Итак, давайте взглянем на строение разделяемого пула, резервной области в нем и списков свободных фрагментов памяти.

Разделяемый пул имеет единый список LRU для разных целей

 Рис. 2. Разделяемый пул имеет единый список LRU для разных целей

 

Организация разделяемого пула

Прежде всего, разделяемый пул состоит из множества гранул, точно так же, как пул буферов. Именно такая организация позволяет механизмам автоматического управления перераспределять память между различными частями SGA. Кроме того, разделяемый пул может быть разбит на несколько подразделов (подчиненных пулов), если объем SGA достаточно велик. Каждый такой подраздел состоит из множества гранул (которые в дампе кучи помечаются как экстенты (extents)). В 11g каждый подраздел может быть разбит на четыре «подподраздела» по продолжительности хранения (durations), и опять, каждый подподраздел состоит из множества непересекающихся гранул.

Увидеть такое «двухэтажное» деление можно, выполнив дамп кучи на уровне 2 (oradebug dump heapdump 2 – дамп наверняка получится очень длинным и для его вывода потребуется значительное время, поэтому не пытайтесь выполнить его на промышленной системе) и выделив все строки с текстом «sga heap». Ниже приводится пример вывода, полученный в системе с тремя подразделами (подчиненными пулами) и четырьмя подразделами по продолжительности хранения. (Я не знаю, почему Oracle начинает счет подразделов с 1, а подподразделов – с 0.) 

HEAP DUMP heap name="sga heap" desc=072CA0E8
HEAP DUMP heap name="sga heap(1,0)" desc=0AF66884
HEAP DUMP heap name="sga heap(1,1)" desc=0AF674BC
HEAP DUMP heap name="sga heap(1,2)" desc=0AF680F4
HEAP DUMP heap name="sga heap(1,3)" desc=0AF68D2C
HEAP DUMP heap name="sga heap(2,0)" desc=0AF6BDC4
HEAP DUMP heap name="sga heap(2,1)" desc=0AF6C9FC
HEAP DUMP heap name="sga heap(2,2)" desc=0AF6D634
HEAP DUMP heap name="sga heap(2,3)" desc=0AF6E26C
HEAP DUMP heap name="sga heap(3,0)" desc=0AF71304
HEAP DUMP heap name="sga heap(3,1)" desc=0AF71F3C
HEAP DUMP heap name="sga heap(3,2)" desc=0AF72B74
HEAP DUMP heap name="sga heap(3,3)" desc=0AF737AC

Если одновременно искать строки с текстом «EXTENT», можно будет увидеть, как много гранул выделяется для каждого подраздела – на рис. 7.4 представлен результат анализа подподразделов (duratins) в предыдущем файле.

Как видите, подразделы пула могут иметь разные размеры, также как и подподразделы, хотя (это может быть простым совпадением) все «подподразделы 0» значительно больше других подподразделов. Обратите также внимание, что на схеме имеется несколько гранул (или экстентов), доступных для использования, но еще не включенных ни в один из подподразделов – база данных только что была запущена и Oracle распределил достаточные объемы памяти, необходимые для начала, а остальную память будет выделять позднее, по мере необходимости.

Примечание. Когда происходит запуск экземпляра Oracle, он не стремится распределить всю память немедленно. Сначала выделяется несколько экстентов, а затем, постепенно, в списки добавляются новые экстенты. Если выполнить дамп кучи сразу после запуска, вы наверняка увидите, что перечисленный в дампе объем памяти намного меньше ожидаемого. В версии 11.2 ситуация обстоит иначе – там heapdump сообщит о наличии «резервных» экстентов. Однако, как только нагрузка на систему увеличится и потребуется дополнительная память, оставшиеся экстенты будут введены в дело.

Есть две причины, объясняющих такое разбиение разделяемого пула. Во-первых, с введением подразделов (в версии 9i) появилась возможность равномернее распределить действия с защелками, так как каждый подраздел имеет собственную защелку. Однако, в ранних версиях такое деление имело побочный эффект – когда распределялся фрагмент памяти, процесс запрашивал память только из одного подраздела и мог столкнуться с проблемой отсутствия свободного фрагмента нужного размера в этом подразделе. В результате процесс мог получить ошибку ORA-04031: unable to allocate %n bytes of memory(%s, %s, %s, %s) (невозможно выделить %n байт памяти (%s, %s, %s, %s)), даже при том, что в других подразделах могли иметься подходящие фрагменты.

Как ни странно, второй причиной к введению подразделов было решение противостоять ошибке ORA-04031. В значительной степени ошибка ORA-04031 является побочным эффектом фрагментации памяти – в разделяемом пуле может иметься значительный объем свободной памяти, но ни одного свободного фрагмента достаточного размера. К сожалению, в процессе работы Oracle память выделяется и освобождается фрагментами самых разных размеров, в результате чего может возникнуть ситуация, когда имеются буквально десятки тысяч свободных фрагментов, но все они имеют размеры в пару сотен байт. Если потребуется выделить фрагмент в 4 Кбайта, вы не получите никакой выгоды от наличия 100 Мбайт свободной памяти, если наибольший из имеющихся фрагментов имеет размер всего 280 байт. За долгие годы были выработаны три стратегии борьбы с такими ограничениями, последней из которых является стратегия деления на подподразделы по продолжительности хранения (durations). Эти стратегии перечислены ниже, в порядке их появления:

  • Резервный пул: в момент запуска базы данных, Oracle изолирует фрагмент разделяемого пула от остального экстента и отмечает его как «R-free» (reserved free – свободный резерв). Чтобы защититься от случайного включения в другие списки при освобождении смежных фрагментов, Oracle также выделяет два 24-байтных фрагмента с обеих сторон и отмечает их как «reserved stopper» (ограничитель резервного фрагмента). Эти фрагменты образуют резерв разделяемого пула, определяемый параметрами: _shared_pool_reserved_pct, со значением по умолчанию 5%, и соответствующий ему shared_pool_reserved_size). Когда процессу требуется получить «большой» фрагмент памяти (понятие «большой» определяется скрытым параметром _shared_pool_reserved_min_alloc, имеющим значение 4400 байт по умолчанию), он сначала проверяет имеющееся свободное пространство, а затем обращается к резерву. Не найдя фрагмент достаточного размера в резерве, процесс переходит к списку и начинает выталкивать объекты из памяти. (Мы исследуем эту процедуру чуть ниже.)
  • Стандартизация: как было показано при обсуждении кэша словаря, Oracle пытается выделять для него память фрагментами трех основных размеров, даже при том, что разные элементы кэша имеют разные размеры. «Непроизводительно» расходуя память, Oracle надеется увеличить шанс, что фрагменты будут иметь более востребованные размеры и, соответственно, уменьшить вероятность дробления свободной памяти на слишком мелкие фрагменты. Кроме того, была переработана значительная часть кода реализации Oracle, запрашивавшего большие фрагменты памяти, чтобы память запрашивалась не цельными кусками, а коллекциями фрагментов небольшого, стандартного размера. Наиболее популярными стали размеры 1072 и 4096 байт.
  • Деление на подподразделы по продолжительности хранения (durations): для решения разных задач требуются фрагменты памяти разных размеров, которые используются по-разному. Разбивая разделяемый пул по функциональному признаку, можно изолировать задачи, вызывающие фрагментацию, от задач, при решении которых часто можно использовать недавно выделявшиеся фрагменты. Если внимательнее рассмотреть дамп кучи, можно заметить, что все данные в памяти, относящиеся к кэшу словаря (обычно фрагменты по 284, 540 и 1052 байт), находятся в подподразделе 1; данные курсора «Heap 0» (обычно на каждый курсор приходится по 4 Кбайта) находятся в подподразделе 2; а данные курсора «SQLArea» – выполняемая часть – в подподразделе 3 (как и в предыдущем случае память выделяется по 4 Кбайта на курсор, но есть возможность выделять фрагментами большего размера), где выделяемая память освобождается обычно быстрее, чем в «Heap 0».

Игнорируя любые другие стратегии борьбы с противоречивыми требованиями к памяти для разных функций, механизмы обслуживания свободной памяти в разделяемом пуле так же полностью отличаются от механизмов в кэше буферов. Конечно, если не выполнить команду alter system flush buffer_cache или не удалить (или уменьшить) некоторые объекты, вы нечасто будете видеть пустые буферы в кэше буферов, поэтому сравнивать вспомогательный список в кэше буферов со списками свободных фрагментов в разделяемом пуле можно только с большой натяжкой. Однако, если вы готовы принять эту аналогию, вы сможете заметить самое существенное отличие, а именно: в ответ на запрос разделяемый пул стремится максимально быстро выделить «лучший» фрагмент памяти. Кэш буферов, напротив, в ответ на запрос просто «выталкивает блок из буфера, находящегося в конце вспомогательного списка, и возвращает освободившийся буфер». Задача разделяемого пула состоит в том, чтобы «найти фрагмент памяти нужного размера – если он вообще существует – и создать такой фрагмент, если найти его не удалось». Итак, давайте поближе рассмотрим организацию разделяемого пула, чтобы понять, как ему удается эффективно решать поставленную задачу.

 

Детали организации разделяемого пула

Чтобы понять, что происходит в разделяемом пуле, мы сначала подробно исследуем различные фрагменты дампа SGA, начав с дампа экстента, затем перейдем к спискам свободных фрагментов и, наконец, займемся списками LRU. Покончив с этим, вам проще будет понять, как перераспределяется память между различными списками свободных фрагментов и списком LRU.

 

Дамп экстента

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

EXTENT 0 addr=1A800000
Chunk 1a800038 sz= 24 R-freeable "reserved stoppe"
Chunk 1a800050 sz= 212888 R-free " "
Chunk 1a833fe8 sz= 24 R-freeable "reserved stoppe"
Chunk 1a834000 sz= 969800 perm "perm " alo=35876
Chunk 1a920c48 sz= 16068 free " "
Chunk 1a924b0c sz= 1072 recreate "Heap0: KGL " latch=00000000
ds 1b3e5d60 sz= 1072 ct= 1
Chunk 1a924f3c sz= 2056 freeable “parameter handl”
Chunk 1a925744 sz= 4096 freeable "sql area " ds=1A93000C
Chunk 1a926744 sz= 4096 freeable "sql area " ds=1A93000C
Chunk 1a927744 sz= 1072 freeable "CCursor " ds=1A9B9E44
Chunk 1a927b74 sz= 4096 recreate "sql area " latch=00000000
ds 1a93000c sz= 12288 ct= 3
1a925744 sz= 4096
1a926744 sz= 4096
Chunk 1a928b74 sz= 1072 freeable "CCursor " ds=1A9B9E44

Размер гранулы в данном экземпляре составляет 4 Мбайта, а в листинге представлен первый экстент разделяемого пула. Строки, начинающиеся со слова Chunk, относятся к фрагментам памяти в экстенте. Для каждого фрагмента приводятся начальный адрес, размер в байтах, его класс (free, freeable, recreate, perm, R-free, R-freeable, R-recreate, R-perm) а также комментарий в двойных кавычках.

Фрагменты класса free находятся в одном из списков свободных фрагментов разделяемого пула, фрагменты класса R-free находятся в одном из резервных списков свободных фрагментов. Обратите внимание на фрагмент класса R-free с размером 212 288 байтов в начале списка – это 5 процентов (примерно), выделяемых в каждом экстенте при его размещении в разделяемом пуле. Данный экземпляр был только что запущен, поэтому еще не выделял память из резерва.

Примечание. Стратегия резервирования 5 процентов в каждом экстенте используется в Oracle уже много лет, но только в версии 11.2 появилась возможность выделять весь экстент под резерв, и в этом случае фрагмент получает класс R-perm (я не помню, чтобы мне встречался этот класс в предыдущих версиях Oracle), а не R-free.

Вы можете видеть пару 24-байтных фрагментов R-freeable по обеим сторонам фрагмента R-free. Это те самые «ограничители», цель которых – препятствовать случайному присоединению фрагмента класса R-free к другому списку при освобождении соседних фрагментов памяти. Префикс R- в именах классов означает Reserve (резервный), класс freeable означает, что данный фрагмент теоретически можно освободить, но только если его «родитель» вызовет процедуру для его освобождения – что это означает, я расскажу чуть ниже.

Если просмотреть дальше, вниз по списку, можно увидеть несколько фрагментов класса freeable и несколько фрагментов класса recreate (от англ. recreatable – доступен для воссоздания). Имя данного класса кого-то может сбить с толку – дело в том, что фрагменты обоих типов доступны для воссоздания и освобождения (recreatable и freeable), но воссоздание и освобождение выполняется разным кодом в Oracle. Код, обслуживающий разделяемый пул (heap manager – диспетчер кучи) может выполнить вызов, чтобы разрушить воссоздаваемый (recreatable) фрагмент и освободить его – и этот вызов будет направлен владельцу фрагмента (например, диспетчеру библиотечного кэша). Но диспетчер кучи не может сделать вызов, чтобы разрушить освобождаемый (freeable) фрагмент – каждый такой фрагмент связан с воссоздаваемым фрагментом и владелец воссоздаваемого фрагмента ответственен за освобождение связанных освобождаемых фрагментов, когда он будет освобождать воссоздаваемый фрагмент.

Данный дамп очень неплохо демонстрирует принцип организации экстента. Взгляните внимательнее на фрагмент с адресом 1a927b74 – это воссоздаваемый фрагмент, но фрагменты с адресами 1a925744 и 1a926744 являются освобождаемыми. Если взглянуть на фрагмент, следующий за фрагментом с адресом 1a927b74, вы увидите метку ds (data segment – сегмент данных), размер 12 288 и счетчик 3. Эта информация говорит о том, что фрагмент по адресу 1a927b74 является первым из группы, начитывающей три фрагмента и имеющей общий размер 12 288 байт. К этой же группе относятся следующие два фрагмента, с адресами 1a925744 и 1a926744 (это те же фрагменты, что находятся в списке двумя фрагментами выше и обозначены как freeable). То есть, диспетчер кучи может выполнить вызов, чтобы разрушить фрагмент 1a927b74 и вернуть память в список свободных фрагментов, но, как следствие разрушения фрагмента 1a927b74, произойдет освобождение еще двух фрагментов, которые также вернутся в список свободных фрагментов. Адрес в строке с меткой ds находится в середине освобождаемого фрагмента, снабженного комментарием «PCursor» (parent cursor – родительский курсор) и находящегося в листинге ниже. И этот родительский курсор будет уведомлен, что он потерял свою область SQL. К слову сказать, это чистое совпадение, что все взаимосвязанные фрагменты оказались в одном экстенте, обусловленное тем, что экземпляр был только что запущен.

Примечание. Я не помню, чтобы в прошлом мне встречался класс R-recreate фрагментов. Возможно, это просто статистическая аномалия, потому что нет никаких очевидных причин, почему это не должно было случиться. Но, только приступив к исследованию свежего экземпляра 11.2, я тут же наткнулся на фрагмент размером 4 Мбайта этого класса с комментарием «KSFD SGA I/O b». По аналогии с классом R-freeable, единственное значение этого имени класса обозначение принадлежности к резервному пулу. Интересно отметить, что аналогичные фрагменты принадлежат (если опираться на сходство комментариев) классу recreate в экземпляре 10.2.

Мы пока не закончили исследование представленной выдержки из дампа – осталось еще рассмотреть класс perm (permanent – постоянный) фрагментов. Такие фрагменты необязательно являются постоянными, как можно заключить из названия класса. Некоторые действительно являются таковыми и выделяются в момент запуска экземпляра. Но некоторые фрагменты могут размещаться процессами динамически для фиксации некоторого объема памяти, который не может быть освобожден до завершения процесса. Очень часто постоянные (perm) фрагменты служат универсальными контейнерами для хранения самой разной информации – в 10.2, например, постоянные фрагменты в библиотечном кэше используются для хранения массивов блокировок и закреплений KGL.

 

Списки свободных фрагментов

Предыдущая выдержка из дампа информирует о распределении фрагментов памяти в экстентах, то есть, свободные фрагменты оказываются разбросанными по всему списку. Следующий раздел в дампе – список свободных фрагментов, где Oracle собрал и отсортировал все свободные фрагменты памяти. На рис. 1 я указал, что имеется 255 списков свободных фрагментов и отметил, что не включил в схему резервные списки свободных фрагментов (которых 14 штук). Я не собираюсь приводить здесь весь дамп, но я сделал выборку из него:

FREE LISTS:
Bucket 0 size=16
Bucket 1 size=20
Chunk 1b49f330 sz= 20 free " "
Chunk 1b0fb8d8 sz= 20 free " "
Chunk 1b492fe4 sz= 20 free " "
Bucket 2 size=24
Chunk 1b4796dc sz= 24 free " "
Chunk 1ae6c930 sz= 24 free " "
Chunk 1b041994 sz= 24 free " "
...
Bucket 67 size=284
Bucket 68 size=288
Bucket 69 size=292
Bucket 70 size=296
Bucket 71 size=300
...
Bucket 187 size=812
Chunk 1a9a02d0 sz= 872 free " "
Bucket 188 size=876
Chunk 1a9a27c0 sz= 880 free " "
...
Bucket 251 size=12324
Chunk 1a9293d4 sz= 14672 free " "
Chunk 1a920c48 sz= 16068 free " "
Bucket 252 size=16396
Chunk 1a930374 sz= 18864 free " "
Bucket 253 size=32780
Bucket 254 size=65548
Total free space = 92620
RESERVED FREE LISTS:
Reserved bucket 0 size=16
Reserved bucket 1 size=4400
Reserved bucket 2 size=8204
...
Reserved bucket 13 size=65548
Chunk 1a800050 sz= 212888 R-free " "
Chunk 1ac00050 sz= 212888 R-free " "
...
Total reserved free space = 5747976

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

В первых 176 списках (с 0 по 175) размер блока увеличивается на 4 байта от списка к списку. В следующих нескольких списках шаг увеличения составляет уже 12 байт, затем следует примерно 60 списков с шагом увеличения 64 байта (с парой непонятных исключений), и последние несколько списков предназначены для хранения очень больших фрагментов. Резервный список устроен похожим образом, но шаг изменения размеров фрагментов в нем растет гораздо быстрее – на то есть свои причины, о которых я расскажу ниже.

Итак, если сеансу потребуется фрагмент памяти размером 80 байт, он сможет вычислить, что должен обратиться к списку с номером 16. Если сеанс освобождает фрагмент размером 184 байта, он должен будет вернуть его в список с номером 42. В жизни, однако, не всегда все складывается как надо и в разделяемом пуле нельзя создать отдельный список для каждого размера – обратите внимание, что список 187 отмечен как size = 812, но в нем хранится фрагмент размером 872 байта. Выполнив очередное увеличение на размер шага, вы получаете нижнюю границу размеров фрагментов в списке.

Возможно вам любопытно узнать, почему я включил в пример списки с 67 по 71 (размеры с 284 по 300), не имеющие ни одного фрагмента. Вернемся мысленно к кэшу строк – одним из размеров выделяемых фрагментов в кэше строк как раз является размер 284 байта. Но, что случается, когда в списке не обнаруживается свободного фрагмента нужного размера? Выполняется проверка следующего списка, потом следующего, и так далее, пока не будет найден подходящий фрагмент. В одних случаях найденный фрагмент используется целиком, в других – от него «откусывается» кусок нужного размера, а остаток возвращается в список с соответствующим размером фрагментов. Вспомните, как ранее я говорил, что когда вычислил объем памяти, занимаемый фрагментами в кэше строк, я обнаружил расхождение между теоретическими и фактическими значениями. Это было обусловлено тем, что некоторые 284-байтные фрагменты фактически размещались во фрагментах размером 288, 292, 296 или 300 байт. Ни в одном случае не использовались фрагменты размером 304 байта или больше, потому что 304 – 298 = 161 и в такой ситуации Oracle отделяет от найденного свободного фрагмента фрагмент размером 284 байта и возвращает остаток в подходящий список.

Есть одна важная деталь, которую мы пока не охватили. Где в памяти Oracle имеет – или может создать – упорядоченный список всех фрагментов в каждом экстенте, поэтому каждый раз когда освобождается фрагмент памяти, Oracle может проверить, не попадает ли освобождаемый фрагмент между двумя уже свободными фрагментами, и при возможности объединяет смежные фрагменты (удаляя их из соответствующих списков), помещая в соответствующий список.

Примечание. Структура x$ksmsp является списком, содержащим все фрагменты в разделяемом пуле, и могла бы, в принципе, использоваться механизмом, проверяющим наличие смежных свободных фрагментов. Не пытайтесь запрашивать этот объект на промышленной системе, и не стоит пробовать даже запрос, опубликованный на My Oracle Support – побочные эффекты, вызванные удержанием защелки, могут оказаться катастрофическими.

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

Теперь самое время перейти к спискам LRU, но перед этим вернитесь назад и сравните дамп экстентов с дампом списков свободных фрагментов – я отбирал списки для демонстрации в книге так, чтобы фрагменты free и R-free из дампа экстента оказались в дампе списков свободных фрагментов.

 

Список LRU

Я пока не пришел к определенному мнению – считать ли LRU как один список или как два. Взглянув на следующую выдержку из дампа кучи, вы поймете, почему. Обратите внимание на строку SEPARATOR – возможно, что в действительности имеется два списка и разделитель (separator) является некоторой кодовой конструкцией в дампе LRU.

С другой стороны, возможно, что (по аналогии со списком REPL_MAIN в кэше буферов) все эти фрагменты объединены в единый связанный список с указателем в x$kghlu, отмечающем разрыв: 

:
UNPINNED RECREATABLE CHUNKS (lru first):
Chunk 203fffd4 sz= 44 recreate "fixed allocatio" latch=05987BB8
Chunk 1ef5b480 sz= 1072 recreate "PCursor " latch=00000000
...
Chunk 1a927b74 sz= 4096 recreate "sql area " latch=00000000
Chunk 1ee5fddc sz= 284 recreate "KGL handles " latch=00000000
SEPARATOR
Chunk 1ef0ac28 sz= 540 recreate "KQR PO " latch=1FDFD814
...
Chunk 1af9545c sz= 4096 recreate "PL/SQL MPCODE " latch=00000000
Chunk 1a924b0c sz= 1072 recreate "Heap0: KGL " latch=00000000
Unpinned space = 10827744 rcr=3921 trn=8164

В разделяемом пуле имеются фрагменты разных классов – постоянные, воссоздаваемые и освобождаемые – но только воссоздаваемые включаются в список LRU. Однако даже они исключаются из списка LRU на время, пока используются (например, закрепляются). Постоянные фрагменты не могут использоваться повторно (если только сеанс не освободит их явно), а освобождаемые фрагменты могут освобождаться, только как побочный эффект повторного использования связанного с ними воссоздаваемого фрагмента. В нижней части списка можно видеть, что он занимает примерно 10.8 Мбайт памяти – это не совсем точное значение, если освободить все воссоздаваемые фрагменты, системе будет возвращено больше памяти, потому что попутно будут освобождены все (или большинство) освобождаемых фрагментов.

Примечание. Команда heapdump перечисляет незакрепленные объекты – я не знаю, означает ли это, что при закреплении объектов, они исключаются из списка (и после открепления, вероятно, возвращаются в начало списка LRU), или код, формирующий дамп, просто пропускает закрепленные элементы. Я полагаю, что это в значительной степени зависит от того – обслуживает ли Oracle список LRU с использованием истинного алгоритма LRU или применяет комбинацию алгоритмов LRU/TCH, как в кэше буферов.

Внизу списка приводится еще одна важная информация: всего воссоздаваемых фрагментов 12 085, из них 3921 являются рекуррентными (recurrent, rcr) и 8164 – переходными (transient, trn). Переходные фрагменты находятся в списке под строкой SEPARATOR, а рекуррентные – над ней. Это разбиение чем-то напоминает концепцию «средней точки» (или cold_head) в кэше буферов – когда в список LRU добавляется новый элемент, он включается в голову временного списка, но как только фрагмент будет использован более одного раза, он переместится в голову рекуррентного списка. При таком подходе в переходном списке окажутся все объекты, не представляющие большого интереса, а все самое интересное окажется в рекуррентном списке.

В действительности (хотя счетчики обращений есть во всех объектах) я не знаю, использует ли Oracle алгоритм подсчета обращений для поддержки рекуррентного списка, чтобы решить, когда следует переместить объект в голову списка, или же просто применяет стандартный алгоритм LRU (например, перемещает объект в голову очереди, когда он перестает использоваться). Чуть ниже я покажу, как перемещаться по списку, и расскажу о двух очередях, влияющих на способ удаления объектов и освобождения памяти.

Если вам интересно узнать число переходных и рекуррентных элементов, а также посмотреть, как изменяется размер списка с течением времени без использования (медленной и опасной) команды heapdump, выполните запрос к x$kghlu, например:

select kghluidx, kghlurcr, kghlutrn from x$kghlu order by 1;
KGHLUIDX     KGHLURCR   KGHLUTRN
---------- ---------- ----------
         1       1593       2138
         2        645       1263
         3       1084       1150

Этот запрос (и команда heapdump) сообщает важные детали о списках LRU, когда используется механизм подподразделов (durations). Данные результаты были получены в системе с включенными подподразделами и тремя подразделами, но они содержат только три строки, а не двенадцать – списки LRU связаны с подразделами, но не с подподразделами. В связи с этим возникает интересный вопрос, касающийся особенностей работы списка LRU.

 

Работа библиотечного кэша

До сих пор мы рассматривали статическое изображение списка LRU. Теперь настала пора рассмотреть его в динамике и выяснить, какие действия выполняются со списком при попытке сеанса использовать память из разделяемого пула. Далее мы рассмотрим три сценария.

 

Сценарий 1: удачный случай

Допустим, что сеансу требуется получить дополнительный фрагмент памяти объемом точно 1 Кбайт (1024 байт). Список с номером 190 как раз предназначен для хранения свободных фрагментов объемом от 1004 до 1064 байт, поэтому сеанс приобретает защелку для этого списка. Если в списке обнаруживается фрагмент нужного размера, он отсоединяется от списка и присоединяется к голове переходного (transient) списка, затем сеанс закрепляет фрагмент (потому что собирается использовать его для каких-то своих целей) и освобождает защелку. (В зависимости от целей, для которых приобретался фрагмент, сеанс может попробовать приобрести другую защелку, например защелку библиотечного кэша или защелку кэша словаря, чтобы подключить фрагмент к соответствующему хэш-блоку.)

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

Если и в этом списке не удается найти свободный фрагмент, выполняется переход к следующему списку и поиск повторяется. Эта процедура продолжается, пока не будет найден нужный фрагмент.

 

Сценарий 2: неудачный случай

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

Затем выполняется повторная проверка списков свободных фрагментов. Если на этот раз требуемый фрагмент будет найден, дальше выполнение пойдет по сценарию 1. В противном случае сеанс снова попробует освободить несколько элементов из списка LRU.

В этой точке у меня нет однозначного понимания, что именно происходит. Несколько лет тому назад я побывал на презентации Стива Адамса (Steve Adams), широко известного консультанта из Oracle, где рассказывалось, что алгоритм, реализованный в Oracle, освобождает элементы из списка LRU пакетами по восемь штук, переключаясь между переходным и рекуррентным списками и освобождая (если я правильно помню) два пакета из переходного списка на каждый пакет из рекуррентного списка. Мне также встречались утверждения в статьях на сайте Oracle, согласно которым список LRU сканируется пять раз. Я точно знаю, что одна опасная инструкция или блок PL/SQL способна вызвать перемещение огромных объемов из библиотечного кэша в списки свободных фрагментов и, если вам не повезет, привести к ошибке ORA-04031: unable to allocate %n bytes (невозможно выделить %n байт памяти).

Внимательно изучая x$kghlu (см. выше) в системе, нагруженной выполнением опасного блока PL/SQL, я смог заметить, что переходные и рекуррентные фрагменты освобождаются именно так, как описывал Стив – Oracle удаляет фрагменты по несколько штук сразу перед повторной попыткой найти свободный фрагмент нужного размера. Если вы захотите установить точную последовательность событий, вам, вероятно, потребуется воспользоваться таким инструментом, как dtrace или SystemTap, чтобы получить перечень вызываемых процедур.

Идея обхода списка LRU с целью освободить незакрепленные фрагменты поднимает несколько вопросов. Я уже упоминал о проблеме, порождающей ошибку ORA-04031. Если разделяемый пул в системе разбит на несколько подразделов, сеанс будет пытаться получить память в одном из подразделов и начинает обход списка LRU. В целом возможно получить ошибку ORA-04031, когда подраздел сильно фрагментирован, даже если в другом подразделе имеется достаточно свободной памяти.

Похожая проблема может возникать и в подподразделах. Мы знаем (или, по крайней мере, полагаем), что разные подподразделы используются для хранения разных категорий информации, но список LRU действует на уровне подраздела. Поэтому, если сеансу потребуется выделить память для Heap 0 (например), он будет использовать для этого подподраздел (duration) 2. Соответственно, есть вероятность, что обход списка LRU и освобождение большого числа элементов из кэша строк, которые хранятся в подподразделе 1, ничем не поможет. (Мне кажется, что разработчики Oracle должны были подумать о таком варианте развития событий.)

 

Сценарий 3: выделение фрагмента большого размера

Нам осталось рассмотреть еще один сценарий – выделение фрагмента памяти, размер которого превышает значение _shared_pool_reserved_min_alloc (4400 байт). В этом случае Oracle пытается сначала пойти по сценарию 1 – проверяет стандартный список свободных фрагментов в поисках фрагмента достаточного размера. Если попытка не увенчается успехом, тогда проверяются резервные списки. Далее, если в резервных списках не нашлось подходящего фрагмента, выполняется цикл освобождения элементов из списка LRU с объединением смежных свободных фрагментов. Технически возможно, что некоторые фрагменты будут принадлежать классу R-recreate, при встрече которых Oracle будет приостанавливать обход списка LRU для проверки обычных и резервных списков.

 

ORA-04031: Unable to Allocate %n Bytes

На сайте My Oracle Support (Metalink) имеется масса информации об ошибке ORA-04031 и о приемах выявления причин ее возникновения. Как было показано в сценариях 2 и 3, эта ошибка возникает, когда сеанс пытается выделить память и не может найти или создать фрагмент достаточно большого размера после обхода списка LRU (согласно утверждениям, Oracle выполняет пять попыток) и освобождения памяти. Причины могут быть самые разные, но обычно эта ошибка говорит о недостатках в архитектуре приложения – оно выполняет слишком много отдельных инструкций SQL или блоков PL/SQL. Большой объем операций может приводить к значительной фрагментации памяти и появлению множества маленьких свободных фрагментов, которые не могут быть объединены. Для устранения этой проблемы обычно применяют следующие решения:

  • Использование разделяемых курсоров для уменьшения числа отдельно выполняемых инструкций. Это решение может сработать, но, к сожалению, влечет за собой ряд других проблем.
  • Использование dbms_pool.keep() для закрепления больших объектов в библиотечном кэше в момент запуска экземпляра, чтобы избежать худшего случая развития событий, когда возникнет необходимость получить фрагмент большого размера.

В конечном счете, все решения сводятся к исправлению приложения, чтобы оно не генерировало большое число отдельных инструкций SQL, или, в очень редких случаях, к выделению большего объема памяти для разделяемого пула.

Вас заинтересует / Intresting for you:

Видеокурс по администрированию...
Видеокурс по администрированию... 10460 просмотров Илья Дергунов Mon, 14 May 2018, 05:08:47
Обновление до Oracle Database ...
Обновление до Oracle Database ... 5490 просмотров Илья Дергунов Tue, 21 Nov 2017, 13:18:05
Поддерживаемые Oracle типы дан...
Поддерживаемые Oracle типы дан... 5653 просмотров Валерий Павлюков Wed, 24 Oct 2018, 08:00:37
Oracle и непроцедурный доступ ...
Oracle и непроцедурный доступ ... 7382 просмотров Antoni Tue, 21 Nov 2017, 13:32:50
Войдите чтобы комментировать