Кэши, копии и управление памятью в базе данных Oracle

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

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

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

Но, прежде чем перейти к блокам и буферам, мы рассмотрим более крупные структуры. Для начала мы познакомимся с гранулами (granules) и затем посмотрим, как эти гранулы связываются между собой, образуя пулы буферов.

Затем последует короткое обсуждение разных видов пулов буферов и причин их создания. Мы посмотрим, как «нарезать» пулы буферов на рабочие наборы данных (working data sets), которые Oracle использует для решения проблемы освобождения буферов с целью повторного использования и чтения данных в память.

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

 

Управление памятью

Существует так много разных цепочек, пересекающих кэш данных, что порой очень сложно запомнить, где они начинаются, поэтому я отступлю на шаг назад и начну с самого простого. Обсуждение ниже не касается версий Oracle ниже 9i, потому что там идет речь о гранулах – крупных блоках памяти, которые в Oracle используются для областей разделяемой памяти.

 

Гранулы

В версии 10g разработчиками из Oracle Corp. была создана система автоматического управления памятью (Automatic System Memory Management, ASMM), целью которой было ликвидировать проблему выбора значения для параметров db_cache_size (размер кэша данных) и shared_pool_size (кэш кода). Официально (хотя и не всегда правильно), начиная с версии 10g, предлагалось не задумываться о настройке этих параметров. В 9i имелась возможность перераспределять память между ключевыми областями во время работы базы данных, но начиная с версии 10g предполагалось, что администратор настроит лишь размер системной глобальной области (System Global Area, SGA), а экземпляр сам определит, как лучше ее использовать, оценив, сколько времени будет сэкономлено да дисковых операциях ввода/вывода при увеличении кэша данных (db_cache_size), и сравнив его с временем экономии за счет увеличения кэша кода (shared_pool_size).

Чтобы повысить эффективность распределения памяти между db_cache_size и shared_pool_size, механизм поддержки SGA был переработан в версии Oracle 9i и ориентирован на использование блоков памяти фиксированного размера, которые стали называть гранулами (granules). Размер гранулы зависит от операционной системы, версии Oracle и размера SGA; для «маленьких» SGA гранулы имеют размер 4 Мбайт, тогда как для больших SGA гранулы имеют размер 8 Мбайт в Windows и 16 или 64 Мбайт в Unix. (В данном контексте под «маленькими» подразумеваются все размеры SGA до 1 Гбайт – в ранних версиях Oracle контрольной точкой был размер 128 Мбайт, и, возможно, что существуют также другие вариации ограничений и размеры гранул, с которыми я пока не столкнулся.)

Чтобы узнать размер гранулы в текущем действующем экземпляре, выполните запрос к v$sgainfo

select bytes from v$sgainfo where name = ‘Granule Size’;

Вполне возможно, что этот запрос станет неработоспособным в будущих версиях Oracle, тогда роль подсказки можно будет возложить на столбец granule_size в представлении v$sga_dynamic_components.

Oracle не поддерживает динамическое представление для получения информации об отдельных гранулах. Но, обладая привилегиями SYS, с помощью объекта x$ksmge можно получить список всех гранул – столбец grantype сообщит, для чего используется гранула, а столбец granstate расскажет, была ли данная гранула выделена в памяти, освобождена или является недействительной. В 9.2 можно выполнить соединение (join) по grantype с x$ksmgv, чтобы перевести значения grantype для выделенных в памяти гранул в описания; в 11.2 соединение должно выполняться с x$kmgsct. Например, следующий запрос покажет, какому компоненту SGA принадлежит каждая гранула и в каком порядке они связаны:

select
        ge.grantype, ct.component,
        ge.granprev, ge.grannum, ge.grannext
from
        x$ksmge ge,
        x$kmgsct ct
where
        ge.grantype != 6
and     ct.grantype = ge.grantype
order by
        ge.grantype,
        ct.component
;

Примечание. Вместе с гранулами в Oracle 9i появился параметр для администраторов, позволяющий вручную перераспределить память между ограниченным подмножеством элементов SGA во время работы экземпляра. В 10g появился параметр sga_target, позволяющий включить автоматическое перераспределение памяти. В Oracle 11g и 12c разработчики сделали еще один шаг вперед, заменив ASMM на AMM (Automatic Memory Management – автоматическое управление памятью), оставив единственный параметр memory_target для определения суммы SGA и PGA (ограничение размера памяти процесса, прежде определяемое параметром pga_aggregate_target). Прежде чем использовать возможность автоматического управления памятью в крупных и сложных системах, вам следует ознакомиться с некоторыми предупреждениями.

 

Гранулы и буферы

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

В зависимости от версии Oracle и разрядности аппаратной части – 32- или 64-разрядной – строка в x$bh может занимать от 150 до 250 байт (кроме изменений в содержимом она включает множество указателей, имеющих размер 4 байта в 32-разрядной системе и 8 байт – в 64-разрядной).

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

На первый взгляд кажется странным, что Oracle использует отдельные массивы для буферов и их заголовков, и у вас может возникнуть законный вопрос, почему нельзя было объединить два массива в один. Мне на ум приходят две возможные причины. Во-первых, благодаря такой изоляции, начала буферов следуют в памяти друг за другом с постоянным шагом, благодаря чему можно получить некоторый выигрыш в производительности при загрузке данных с диска. Во-вторых, многое объясняется традициями: гранулы появились только в версии 9i, а прежде массив заголовков буферов располагался вообще в отдельной части SGA, отдельно от самих буферов данных, возможно даже в отдельном сегменте разделяемой памяти; подобное отделение заголовков, возможно, играло роль дополнительной гарантии от ошибок во время реализации механизма поддержки гранул.

Имея гранулу с размером 8 Мбайт и размером блока 8 Кбайт, можно предположить, что она содержит 1024 буфера; но, если вспомнить, что каждый буфер имеет заголовок, занимающий пару сотен байт, тогда для гранулы с размером 8 Мбайт логичнее предположить, что она содержит что-то около 1000 буферов. Как свидетельство зависимости размеров от внешних условий, ниже демонстрируются результаты выполнения двух запросов: в 32-разрядной версии 9.2.0.8 (где размер строки в x$bh составляет 186 байт) и в версии 11.1.0.7 (где размер строки в x$bh составляет 208 байт). Оба экземпляра выполнялись под управлением Windows XP. Сначала результаты для 9i: 

SQL> select current_size, granule_size
   2 from v$sga_dynamic_components
   3 where component = ‘buffer cache’;
CURRENT_SIZE GRANULE_SIZE
------------ ------------
    50331648      8388608
SQL> select block_size, buffers from v$buffer_pool;
BLOCK_SIZE BUFFERS
---------- ----------
      8192       6006

И затем результаты для 11g:

CURRENT_SIZE GRANULE_SIZE
------------ ------------
    50331648      8388608
BLOCK_SIZE    BUFFERS
---------- ----------
      8192       5982 

В обоих случаях я установил размер буферного кэша равным 48 Мбайт и выбрал достаточно большой размер SGA, чтобы получить гранулы по 8 Мбайт. Как видите, в версии 9.2.0.8 в буферном кэше оказалось 6006 буферов, а в версии 11.1.0.7 – только 5982. То есть, 1001 и 997 буферов на гранулу соответственно. На рис. 1 приводится упрощенная структура одной гранулы, с ее заголовком, массивом заголовков буферов и массивом буферов данных.

Упрощенное изображение структуры гранулы в базе Oracle

Рис. 1. Упрощенное изображение структуры гранулы

Примечание. Oracle широко использует указатели. При миграции из 32-разрядной в 64-разрядную версию Oracle, размеры указателей увеличиваются в два раза, из-за чего увеличивается объем памяти, необходимой для организации управления тем же объемом данных. Если вы захотите узнать число буферов в грануле непосредственно, эта информация доступна в столбце bp_bufpergran структуры x$kcbwbpd, лежащей в основе представления v$buffer_pool.

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

 

Несколько кэшей данных

База данных Oracle позволяет настроить до восьми разных кэшей данных: пять – чтобы обеспечить поддержку блоков данных разных размеров, и три (по крайней мере номинально) для обработки по разным схемам. В табл. 1 ниже  перечислены восемь параметров, с комментариями, определяющими настройки этих кэшей.

 

Параметр Описание
db_cache_size Кэш по умолчанию для блоков данных с размером, который был указан в инструкции create database. Блоки этого размера используются в табличных пространствах system, sysaux и temporary.
db_keep_cache_size Кэш объектов, которые определяются со спецификатором storage (buffer_pool keep). В принципе, этот кэш можно использовать для хранения объектов, которые должны находиться в кэше постоянно. На практике (из-за особенностей работы механизма согласованного чтения) этот кэш должен иметь больший размер, чем сумма размеров объектов, предназначенных для хранения в этом кэше (от 1.5 до 6 раз) – хотя для хранения согласованных копий часто используется всего несколько процентов от размера кэша – чтобы свести к минимуму нежелательное влияние алгоритма кэширования LRU (Least Recently Used – наиболее давно не использовавшийся), который в Oracle применяется повсеместно.
db_recycle_cache_size Кэш объектов, которые определяются со спецификатором storage (buffer_pool recycle). Обычно этот кэш используют для хранения объектов,редко используемых повторно; как следствие, он имеет относительно небольшой размер. Данный кэш особенно хорош для хранения больших объектов (LOB). Если был настроен кэш постоянного хранения (keep cache, см. описание параметра db_keep_cache_size), большая часть согласованных копий объектов из того кэша будет создаваться здесь, что может потребовать задать больший размер, чем предполагалось, чтобы уменьшить потери времени по событиям free buffer waits. В свое время я обнаружил, что иногда пул временного хранения (recycle pool) выгоднее в использовании, чем пул постоянного хранения (keep pool), так как помогает воспрепятствовать вытеснению из кэша полезных данных, когда требуется обращаться к случайным объектами.
db_2k_cache_size Oracle дает возможность создавать табличные пространства с размером блоков, отличным от размера (в базе данных) по умолчанию. Если в некотором табличном пространстве используются блоки данных с размером 2 Кбайта и размер блоков по умолчанию не равен 2 Кбайтам, просто создайте этот кэш.
db_4k_cache_size Имеет то же предназначение, что и параметр db_2k_cache_size, но для размера блоков 4 Кбайта
db_8k_cache_size Имеет то же предназначение, что и параметр db_2k_cache_size, но для размера блоков 8 Кбайт. Однако, на большинстве платформ утилита Database Configuration Assistant (DBCA) предлагает размер блока 8 Кбайт по умолчанию и, вероятно, это самый распространенный размер в мире. Имейте в виду, что вы не сможете настроить кэш db_Nk_cache_size, если по умолчанию блоки имеют размер N Кбайт.
db_16k_cache_size Имеет то же предназначение, что и параметр db_2k_cache_size, но для размера блоков 16 Кбайт. В Oracle есть несколько интересных аномалий (ошибок), проявляющихся при использовании блоков размером 16 Кбайт и в большинстве своем связанных со счетчиками, имеющими предельные значения, по достижении которых блоки объявляются заполненными.
db_32k_cache_size Имеет то же предназначение, что и параметр db_2k_cache_size, но для размера блоков 32 Кбайт. Возможность выбора размера блоков 32 Кбайта ограничена узким кругом платформ. Примечание, сделанное для блоков с размером 16 Кбайт, в равной степени относится и к блокам с размером 32 Кбайта.
db_Nk_cache_size Первоначально поддержка кэшей блоков разных размеров была реализована с целью дать возможность переносить табличные пространства между базами данных, имеющими разные настройки по умолчанию. Однако поддержка нескольких размеров блоков в одной базе данных используется довольно редко (и, вероятно, незаслуженно). В моей практике были случаи, когда имело смысл изолировать таким способом пару конкретных сегментов IOT или LOB – увеличенный размер блока в случае с IOT позволил бы уместить несколько связанных строк в один блок; уменьшенный размер блока в случае с LOB, помог бы сэкономить значительные объемы памяти, в противном случае расходуемые впустую

Хотя в руководствах не говорится об этом явно, тем не менее бытует мнение, что размер блока по умолчанию должен быть: 2, 4, 8, 16 или (если поддерживается) 32 Кбайт, однако имеется техническая возможность задавать другие размеры. Мне приходилось создавать базы данных с размером блока по умолчанию 12, 5 и даже 4.5 Кбайта, правда, только чтобы убедиться в такой возможности. (В одном случае у меня действительно был мотив использовать блоки с размером 12 Кбайт и не было никакой информации, которая свидетельствовала бы о том, что такой размер не поддерживается.)

В своей практике мне чаще приходилось сталкиваться с базами данных, использующими только блоки с размером по умолчанию 8 Кбайт; некоторые пытались получить преимущества от использования кэшей постоянного (keep cache) и временного (recycle cache) хранения; и некоторые использовали блоки с размером по умолчанию 4 или 16 Кбайт. Лично я предпочитаю размер по умолчанию 8 Кбайт, так как это наиболее распространенный выбор, проверенный практикой, и маловероятно, что с этим размером будут наблюдаться какие-либо проблемы. Размер 4 Кбайта является хорошим запасным вариантом для некоторых платформ на базе операционной системы Linux.


Пулы буферов и история


Многие из вас наверняка обратили внимание на выражение buffer_pool в описаниях параметров db_recycle_cache_size и db_keep_cache_size в табл. 1. Это пример безжалостного давления истории. В ранних версиях Oracle размер кэша данных определялся с помощью параметра db_block_buffers, в котором задавалось число буферов, и не было никакой возможности настроить кэши постоянного или временного хранения (не говоря уже о возможности задать размер блоков, отличный от размера по умолчанию).

Когда в Oracle была добавлена поддержка кэшей постоянного и временного хранения, сначала появились два параметра, buffer_pool_keep и buffer_pool_recycle – откуда пошел синтаксис предложения storage, и соответствующие динамические представления v$buffer_pool и v$buffer_pool_statistics. Для многих все еще привычнее говорить о пулах буферов, подразумевая кэш данных, поэтому далее я буду использовать этот термин.

В действительности эти параметры не являются полностью устаревшими. Если вы пользуетесь Oracle для 32-разрядной версии Windows, вы должны знать, что в этой операционной системе имеется встроенный механизм «оконного доступа к памяти» (memory windowing) или «страничной организации памяти» (memory paging), позволяющий обращаться к памяти, лежащей за границами 32-разрядного адресного пространства (4 Гбайта). Oracle может использовать это дополнительное адресное пространство для кэша данных, но только если размеры кэшей указаны с использованием старых параметров.


Кэши, копии и настройка памяти в базе данных Oracle 

Гранулы и пулы буферов

Единственное важное замечание, которое я хотел бы сделать о разных пулах буферов, касается использования гранул – каждая гранула размещается в определенном пуле. То есть, каждая конкретная гранула может содержать только блоки одного размера, что обеспечивает плотное и предсказуемое размещение буферов внутри гранулы. Стоит так же отметить, что как и большинство других структур в Oracle, гранулы представляют собой комбинацию массивов и связанных списков: гранулы объединены в массив, а элементы массива хранят указатели на другие элементы, связывая воедино гранулы, составляющих различные компонентов SGA. Убедиться в этом можно, взглянув на результаты типового запроса, представленного выше (на это раз ограничимся только кэшем постоянного хранения (keep cache)): 

GRANTYPE    COMPONENT            GRANPREV    GRANNUM   GRANNEXT
----------  ------------------ ---------- ---------- ----------
         8  KEEP buffer cache          26         27          0
                                       25         26         27
                                       24         25         26
                                       23         24         25
                                       22         23         24
                                       17         22         23
                                       16         17         22
                                       12         16         17
                                        0         15         14
                                       15         14         13
                                       14         13         12
                                       13         12         16

Второй столбец (GRANNUM) – это номер гранулы, первый столбец (GRANPREV) – номер предыдущей гранулы, и последний столбец (GRANNEXT) – номер следующей гранулы в компоненте. В первой строке можно заметить, что гранула 27 не имеет следующей за ней гранулы, то есть, это последняя гранула в списке, а предшествующая ей гранула имеет номер 26.

Следуя в обратном направлении можно перейти от гранулы 26 к грануле 25, от гранулы 25 к грануле 24,и так далее, до гранулы 22, которая ссылается на предшествующую ей гранулу с номером 17. Гранула 17 ссылается на гранулу 16, но за гранулой 16 опять следует «разрыв» – она ссылается на гранулу 12. Такие промежутки в связанных списках являются свидетельством выполнения операций изменения размеров, в ходе которых происходило добавление или удаление гранул из кэша. (Чтобы убедиться в этом, можно выполнить запрос к v$sga_resize_ops в 10g или, если настроен параметр memory_target, к v$memory_resize_ops в 11g.)

Мы можем взять за основу структуру, скопировать ее несколько раз и затем связать копии вместе, чтобы увидеть, как действует Oracle. На рис. 2 показано, как обрабатываются гранулы.

 

Пулы буферов

Деление больших объемов памяти на более мелкие гранулы производится, чтобы упростить управление. Когда память разбита на фрагменты одинакового размера, ее гораздо проще перераспределять по функциональному назначению (например, гранулу из кэша данных преобразовывать в гранулу разделяемого пула). Задолго до того, как появились гранулы, в Oracle использовалась другая стратегия разбиения большого кэша данных на небольшие фрагменты, но не для упрощения управления, а для повышения масштабируемости.

Организация гранул в виде комбинации массива и связанных списков в СУБД Oracle

Рис. 2. Организация гранул в виде комбинации массива
и связанных списков

За эти годы механизм неоднократно изменялся в разных направлениях и вне всяких сомнений продолжит изменяться в будущем. Наиболее широко он известен как список LRU (Least Recently Used list), но более уместно называть его как рабочий набор данных (working data set).

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

Рабочий набор данных описывает структура x$kcbwds; она недоступна непосредственно через динамическое представление, но некоторые статистики рабочей нагрузки из нее включены в представление v$buffer_pool_statistics. Если заглянуть в структуру x$kcbwds, можно увидеть, что один из первых столбцов в ней называется dbwr_num – каждый рабочий набор данных связан с единственным процессом записи (Database Writer, DBW), а каждый процесс записи может отвечать за несколько рабочих наборов данных. На рис. 3 изображена схема строения одного пула, отражающая взаимосвязи между гранулами, рабочими наборами данных и процессами записи. Этот шаблон распространяется на все пулы буферов, имеющиеся в экземпляре, отличаясь только числом гранул в каждом пуле.

Рабочие наборы данных служат двум целям. Во-первых, они позволяют распределить операции ввода/вывода между несколькими процессами записи; во-вторых, они помогают уменьшить конфликты между процессами, пытающимися читать блоки данных в пул, разрешая им выбирать буферы из разных рабочих наборов данных. Тот факт, что в действительности совсем необязательно иметь множество процессов записи (особенно в системах, реализующих асинхронные операции ввода/вывода), объясняет, почему Oracle позволяет каждому процессу записи обслуживать сразу несколько рабочих наборов данных из каждого пула буферов.

Схема строения пула с гранулами, рабочими наборами данных и процессами записи

Рис. 3. Схема строения пула с гранулами,
рабочими наборами данных и процессами записи

Алгоритм определения числа процессов записи и рабочих наборов данных (на каждый пул) зависит от версии Oracle и числа процессоров, определяемых экземпляром, а также с некоторыми дополнительными сложностями при использовании технологии NUMA. В системе 10.2.0.5 без использования NUMA, число рабочих наборов данных на пул определяется как cpu_count / 2, но в версии 11.2.0.2 оно совпадает с cpu_count; в обеих версиях число процессов записи определяется как ceil(cpu_count / 8). Например, в системе 10.2.0.5 с 32 процессорами вы будете иметь 16 рабочих наборов данных на пул и 4 процесса записи, каждый из которых будет отвечать за обслуживание 4 рабочих наборов данных в каждом пуле. Ради эксперимента можно попробовать изменить число процессов записи с помощью параметра db_writer_processes, но, вопреки множеству отзывов в Интернете, этот параметр редко требуется изменять в промышленных системах.

Примечание. Если у вас появится желание посмотреть, как меняется распределение памяти в зависимости от числа процессоров, измените значение параметра cpu_count и перезапустите экземпляр; имейте в виду, что в версии 11.2 необходимо также присвоить параметру _disable_cpu_check значение false.

 

Рабочие наборы данных

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

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

Чтобы понять, как осуществляется физический ввод/вывод, достаточно рассмотреть единственный рабочий набор данных (working data set), представляющий весь кэш, поэтому я начну с короткого обзора части структуры x$kcbwds, которая определяет рабочий набор данных. Общее число столбцов в структуре существенно отличается в разных версиях Oracle, но сейчас я хочу заострить внимание только на восьми из них: 

Name                          Null?    Type
----------------------------- -------- --------------------
CNUM_SET                               NUMBER
SET_LATCH                              RAW(4)
NXT_REPL                               RAW(4)
PRV_REPL                               RAW(4)
NXT_REPLAX                             RAW(4)
PRV_REPLAX                             RAW(4)
CNUM_REPL                              NUMBER
ANUM_REPL                              NUMBER

Столбец cnum_set – это число буферов в рабочем наборе данных, а столбец set_latch – адрес соответствующей защелки цепочки LRU, охватывающей данный рабочий набор. Следующие шесть столбцов имеют в своих именах слово REPL; эти столбцы описывают список замены (replacement list), который часто называют (не совсем правильно) цепочкой LRU. В обсуждаемой структуре имеется еще несколько групп по шесть столбцов, но они имеют отношение к записи, а не к чтению.

В определениях столбцов можно найти две важные подсказки, касающиеся природы списка замены. Во-первых, судя по именам столбцов, список состоит из двух частей: REPL (основной список замены – replacement list) и REPLAX (вспомогательный список замены – auxiliary replacement list). Во-вторых, данный список является еще одним образцом двусвязных списков, так широко используемых в Oracle: это можно видеть по префиксам NXT (next – следующий) и PRV (previous – предыдущий) в именах столбцов, а так же по типу raw(4) (который в 64-разрядной системе должен превратиться в raw(8)).

Мы знаем, что рабочий набор данных пересекает несколько гранул и каждая гранула хранит массив заголовков буферов, как было показано на рис. 2. А теперь мы видим конечные точки пар связанных списков в x$kcbwds, и если заглянуть в x$bh (в сами заголовки буферов) можно увидеть еще пару столбцов (nxt_repl и prv_repl), подсказывающих, как устроен связанный список. С их помощью можно перемещаться вперед и назад по массивам заголовков буферов из пары гранул, распутывать хитросплетения из двух простых списков и просматривать результаты под разными углами, как показано на рис. 4.

Блок в верхней части рис. 4, над множеством заголовков буферов, в многих изданиях называют «список LRU», столбец nxt_repl которого указывает на конец списка MRU (Most Recently Used – последние использовавшиеся), а столбец prv_repl – на конец списка LRU.

Прежде чем приступать к исследованию списка LRU, я хочу отметить, что последние два столбца структуры x$kcbwds, перечисленные выше, хранят число буферов в списках: cnum_repl представляет общее число буферов в двух списках, а anum_repl – число буферов во вспомогательном списке замены (REPL_AUX).

 

Алгоритм LRU/TCH

Итак, у нас имеются связанные списки заголовков буферов, каждый заголовок ссылается на соответствующий ему буфер, и каждый буфер хранит копию блока. Теперь нам нужен алгоритм, определяющий, какой буфер затереть, если потребуется прочитать в память новый блок. В Oracle для этой цели используется модифицированный алгоритм LRU (Least Recently Used – наиболее давно не использовавшийся) со счетчиком обращений (Touch Count, TCH).

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

Преобразование хитросплетений из указателей в простые списки

Рис. 4. Преобразование хитросплетений
из указателей в простые списки

Впервые алгоритм LRU/TCH был реализован в версии 8i, с целью уменьшения числа операций с защелками, сопутствующих «чистому» алгоритму LRU. Типичные алгоритмы LRU перемещают объекты ближе к началу списка при каждом обращении к ним; но такие объекты, как буферные кэши, используются слишком часто, из-за чего цена их перемещения оказывается слишком высокой и растет число конфликтов. Чтобы решить эту проблему, в Oracle Corp. добавили в заголовок буфера счетчик (и отметку времени (timestamp) – столбец tim). Теперь при каждом обращении к буферу значение его счетчика увеличивается на единицу и обновляется отметка времени, при условии, что с момента предыдущего обновления прошло не менее 3 секунд; сам заголовок буфера никуда не перемещается.

Но, если обновление счетчика – это все, что делает данный алгоритм, тогда, в какой момент начинает работу механика LRU? Ответ на этот вопрос: «в самый последний момент». Иными словами: в момент, когда потребуется принять решение о том, какой буфер использовать для нового содержимого. Давайте рассмотрим пару примеров, чтобы понять суть.

Примечание. Существует распространенное мнение, что определить, какой блок вызывает наибольшую конкуренцию за защелку в заданной цепочке буферов можно выявлением самых больших значений счетчика обращений (TCH) среди буферов, охватываемых данной защелкой. К сожалению, это не самый надежный метод. Буфер, обращения к которому происходили в среднем раз в секунду в течение получаса, будет иметь значение 600 в счетчике обращений. Буфер, к которому было 10 миллионов обращений за последние 5 минут, будет иметь счетчик обращений со значением, что-то около 100. Счетчик обращений может играть роль ориентира, но он не должен иметь решающего значения. (Для отчаявшихся в Oracle имеется механизм, помогающий выявлять проблемы с «горячими» блоками, который можно включить установкой скрытого параметра _db_hot_block_tracking, но я все же рекомендую воспользоваться инструментом latchprofx, созданным Танелом Подером (Tanel Poder) и доступным по адресу: http://blog.tanelpoder.com.)

 

Алгоритм LRU/TCH в действии

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

Примечание. На самом деле нет необходимости волноваться по поводу присутствия на диаграмме единственного рабочего набора данных, когда речь идет о чтении данных с диска. Каждый раз, когда сеансу требуется прочитать блок, он будет выбирать один рабочий набор, чтобы получить свободный буфер. Сначала сеанс случайным образом выбирает подходящий набор (то есть, из соответствующего пула буферов) и пытается приобрести защелку цепочки LRU без ожидания. Если попытка увенчалась успехом, сеанс использует данный набор; в противном случае он начинает проверять все наборы по очереди, пока не найдет такой, что сможет приобрести защелку немедленно.

Рабочий набор на рис. 5 содержит всего шесть буферов и пару новых для нас столбцов: x$bh хранит счетчик обращений tch, и x$kcbwds – указатель «середины» cold_hd. На рисунке изображены две последовательности событий: сценарий (a) соответствует чтению нового блока в рабочий набор данных, и сценарий (b) соответствует первому этапу загрузки второго блока.

Схема работы алгоритма LRU/TCH без учета REPL_AUX в базе данных Oracle

Рис. 5. Схема работы алгоритма LRU/TCH без учета REPL_AUX

Сценарий (a): если нужно загрузить в рабочий набор новый блок, сначала выполняется поиск свободного буфера. Поиск начинается с конца LRU списка замены (в этом утверждении содержится ошибка, к которой я вернусь чуть ниже). К счастью, буфер с этого конца имеет счетчик обращений, равный 1 (этот блок не пользовался особой популярностью и буфер не отмечал особой активности). Далее проверяется, не закреплен ли буфер в данный момент (об этом тоже чуть ниже) и его содержимое не нужно записать обратно на диск. Допустим, что все проверки были успешно преодолены. Далее буфер закрепляется для монопольного использования (чтобы гарантировать, что никакой другой сеанс ничего не сможет с ним сделать). Выполняется чтение блока. Обновляется информация в заголовке буфера. Заголовок отсоединяется от конца списка, вставляется в среднюю точку (то есть, добавляется в цепочку, на которую указывает x$kcbwds.cold_hd) и затем буфер открепляется.

Это довольно большой объем работы, но я еще не закончил; в заголовке буфера имеется еще несколько ссылок, которые следует изменить.

 

Переустановка ссылок в буфере

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

Ниже коротко перечислены все изменения, без соблюдения фактического порядка, которые выполняет Oracle:

Теперь перейдем к сценарию (b). Допустим, что буфер со счетчиком tch = 1 после загрузки нового блока был вставлен в середину списка, а буфер со счетчиком tch = 4 оказался в конце списка. Какое значение будет иметь величина счетчика обращений, если теперь попытаться прочитать другой блок в этот же рабочий набор данных?

Буфер хранит «популярный» блок; с момента загрузки блока в буфер к нему уже было несколько обращений, поэтому система не должна спешить очистить этот буфер и использовать его для сохранения другого блока. Сначала Oracle отсоединит буфер от конца LRU списка, присоединит к концу MRU – попутно разделив счетчик обращений пополам – и перейдет к проверке следующего блока в списке. То есть, популярный блок перемещается в конец MRU списка в самый последний момент, непосредственно перед тем как «уйти» в конец LRU, а не каждый раз, когда он используется. Такой подход существенно улучшает масштабируемость.

Обработка счетчика обращений в Oracle может казаться странной в данный момент. Очевидно должна быть стратегия, гарантирующая блоку возможность продолжать зарабатывать право на место в кэше, и именно эта стратегия определяет необходимость деления счетчика пополам при перемещении буфера в конец MRU списка. Но, просмотрев на все скрытые параметры, можно обнаружить, что Oracle имеет множество путей тонкой настройки алгоритма. Одна из самых больших странностей проявляется, когда другие буферы перемещаются в конец MRU и только что перемещенный буфер (значение счетчика в котором уменьшилось с 4 до 2) неуклонно начинает приближаться к концу LRU. Как только буфер пересечет среднюю точку и окажется «под прицелом» указателя cold_hd, его счетчик сбросится до 1, а это
означает, что если никто не обратится к нему до того, как он достигнет конца LRU списка, буфер будет уничтожен.

Примечание. Я не видел, чтобы где-то обсуждался точный механизм определения момента пересечения буфером средней точки, хотя в документации часто утверждается, что такой механизм имеется. Я могу лишь догадываться, что после вставки блока в конец MRU списка Oracle обращается к срединному буферу через указатель cold_hd, переходит по ссылке к следующему буферу, уменьшает счетчик посещений до нуля и затем сохраняет в указателе cold_hd ссылку на этот буфер.

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

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

 

REPL_AUX

Описание списка замены, которое я дал, нельзя назвать полным. Это не является серьезной ошибкой, и если вы читали описание не очень внимательно, то запросто могли подумать, что это все, хотя в действительности это не так. Когда в сеансе возникает потребность найти буфер, чтобы прочитать новый блок, Oracle начинает поиски не с конца LRU списка REPL_MAIN (о котором рассказывалось до сих пор), а использует список REPL_AUX (auxiliary replacement list – вспомогательный список замены), который существует как источник буферов, почти наверняка готовых к немедленному повторному использованию. Благодаря этой гарантии сеансы получают дополнительные выгоды при поиске буферов – им не приходится впустую растрачивать ресурсы, имея дело с «грязными» и закрепленными буферами, и и другими сложностями.

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

В момент запуска экземпляра все буферы оказываются в списке REPL_AUX, а список REPL_MAIN не содержит ни одного буфера (эту ситуацию можно воспроизвести с помощью команды alter system flush buffer_cache, которая запишет в столбец state всех заголовков буферов значение 0 [признак свободного буфера] и затем добавит их в список REPL_AUX). Когда сеансу потребуется буфер, чтобы загрузить блок с диска, он проверит список REPL_AUX с конца LRU (хотя не совсем правильно говорить о конце «LRU» списка, для которого не действует алгоритм LRU), чтобы найти подходящий буфер, отсоединит найденный буфер от списка REPL_AUX и добавит его в список REPL_MAIN. Именно так и попадают буферы в REPL_MAIN с течением времени.

На данный момент остается нерешенным еще один вопрос: почему список REPL_AUX не опустошается в первые же минуты с момента запуска экземпляра. Если перемещение буфера из списка REPL_AUX в список REPL_MAIN происходит при чтении каждого блока, понадобится совсем немного времени, чтобы переместились все буферы. (Разумеется, если бы список REPL_AUX действительно опустошался, описание порядка поиска подходящего буфера было бы полным.) Соответственно, должен быть некоторый механизм, перемещающий буферы из REPL_MAIN обратно в REPL_AUX.

Мне встречались заметки, в которых утверждалось, что процесс записи (database writer, dbwr) перемещает буферы в REPL_AUX после записи их содержимого на диск. Но я знаю, что это неполный ответ, поскольку легко воспроизвести ситуацию, когда блоки постоянно возвращаются в список REPL_AUX, даже когда в экземпляре нет «грязных» буферов, требующих сохранения на диск. Более того, я могу создать такую ситуацию, когда число буферов в REPL_AUX будет увеличиваться даже после остановки dbwr. (Если первый случай еще можно объяснить наличием разных веток кода в dbwr, проверяющих буферы с целью выявить «грязные» блоки, то второй случай можно объяснить только наличием какого-то другого процесса.)

Похоже, что по умолчанию список REPL_AUX должен иметь размер, соответствующий 25% размера рабочего набора данных; значение anum_repl постепенно снижается в процессе выполнения запросов, но в периоды бездействия вновь растет, стремясь к указанному пределу. Данный предел можно изменить, и этот факт укрепляет меня во мнении, что постоянная перезагрузка REPL_AUX является частью обдуманной стратегии, а не просто побочным эффектом каких-то других действий. В Oracle имеется параметр с именем _db_percent_hot_default, который определяет процент по умолчанию от объема пула буферов, выделяемый для хранения популярных (или «горячих») данных. Иными словами, он влияет на выбор цели для указателя cold_hd. По умолчанию этот параметр имеет значение 50. Если его уменьшить (и перезапустить экземпляр), можно будет заметить, что целевое значение anum_repl также уменьшилось и составляет половину значения параметра, то есть, если присвоить параметру _db_percent_hot_default значение 40, целевым значением для anum_repl станет число 20.

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

Что я еще заметил: когда выполняется код, постоянно выполняющий операции чтения, вспомогательный список замены постепенно сокращается, но, достигнув определенного уровня, он почти мгновенно увеличивается. Это заставляет предположить, что имеется какой-то код, выполняемый в процессе чтения с диска, который сканирует REPL_MAIN с конца LRU и переносит часть буферов в конец MRU списка REPL_AUX.

Если мое предположение верно, тогда должны существовать некоторые ограничительные правила, определяющие, какие буфера могут переноситься, сколько буферов можно переносить за один раз и какой объем работы допустим в процессе перемещения. Очевидно, что «грязные» буферы не должны перемещаться, как не должны перемещаться закрепленные буферы и буферы со счетчиком обращений больше 1. Уместно также предположить, что согласованные копии блоков – особенно когда их больше шести (предел, определяемый параметром _db_block_max_cr_dba) – являются первыми кандидатами на перемещение. Я также склонен предположить, что буфер будет перемещен, только если процедура чтения сможет немедленно приобрести соответствующую защелку цепочки кэшей буферов. Возможно также, что существует правило, ограничивающее перемещение в единственный раздел списка REPL_MAIN – таким способом можно уменьшить число ссылок между буферами, которые подлежат обновлению.

Но, размышляя о том, что должен сделать Oracle, чтобы обеспечить постоянное пополнение списка REPL_AUX, следует задуматься так же над тем, какие выгоды принесут эти действия. На перемещение буферов из REPL_MAIN в REPL_AUX расходуются вычислительные ресурсы, следовательно эти действия должны давать экономию где-то в другом месте, но где? Эта экономия, скорее всего, получается в процедуре планирования.

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

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

Настройка памяти базы данных O...
Настройка памяти базы данных O... 19197 просмотров Stas Belkov Sat, 07 Jul 2018, 15:44:14
Создание БД Oracle 12c с макси...
Создание БД Oracle 12c с макси... 4268 просмотров Андрей Васенин Mon, 20 Aug 2018, 13:43:20
Мониторинг Oracle через метрик...
Мониторинг Oracle через метрик... 5060 просмотров sepia Tue, 21 Nov 2017, 13:18:05
Oracle ADDM - автоматическая д...
Oracle ADDM - автоматическая д... 7669 просмотров Алексей Вятский Tue, 21 Nov 2017, 13:18:05
Печать
Войдите чтобы комментировать

apv аватар
apv ответил в теме #8931 6 года 1 мес. назад
Ага, достойный звания глубокого профи материал. Ваши изыскания или подсмотренные? ;-)
В любом случае спасибо за публикацию!
1dz аватар
1dz ответил в теме #8896 6 года 2 мес. назад
Просто глобально по косточкам разобрали механику работы памятью Oracle и управление ею! Автору огромная благодарность. Для профессионалов интереснейший материал!! Спасибо за статьЮ!!! :-)