latch free oracle что это
Русские Блоги
Оптимизация Oracle 03-Latch и ждать
картографирование ума
серии
Обзор защелки
События ожидания, вызванные блокировкой, и блокировка, вызванная блокировкой, представляют собой две разные концепции: если проблемы производительности, вызванные этими двумя факторами, можно будет различить при оптимизации производительности, наше суждение об анализе производительности будет значительно улучшено.
Прежде всего, давайте представим сценарий: блок данных считывается в память с диска сессией. Обратите внимание, что он читается. В это время другой блок также нуждается в этом блоке данных. Что нам делать?
Чтобы поддерживать согласованность данных, при нормальной логике обработки другой сеанс должен ждать, пока этот блок данных не будет считан в память, так как же сеанс, который читает блок данных, не позволяет другим сеансам продолжать чтение этого блока данных? Метод заключается в том, что ему нужно получить ресурс, похожий на блокировку, который в Oracle называется Latch.
Защелка Оракул для защиты Структура памяти И ресурс изобрел.
В сложной структуре памяти Oracle, такой как SGA, различные данные многократно читаются с диска в память, а затем перезаписываются на диск. Если параллельные пользователи делают то же самое, Oracle должен использовать механизм Чтобы гарантировать, что данные могут быть завершены только одним сеансом при чтении, этот механизм защиты использует Latch.
После прояснения этих двух понятий мы можем объективно оценить, когда производительность системы снижается.Например, когда мы обнаруживаем, что причина медленной работы системы связана с многочисленными конфликтами защелок, мы должны рассмотреть, есть ли проблемы в дизайне системы и самой базы данных. Такие факторы, как связывание переменных, наличие горячего блока и обоснованность разработки параметров хранилища данных.
Зафиксируйте конфликт и подождите по многим причинам, многие ресурсы в памяти могут существовать в этом конфликте. Два наиболее распространенных типа состязания защелки описаны ниже.
Конфликт блокировки в общем пуле
Если в общем пуле имеется много SQL-кода, который неоднократно анализируется, это вызовет много конфликтов и длительных ожиданий. Наиболее распространенным явлением является отсутствие связанной переменной.
Наиболее распространенные типы Latch в общем пуле:
Oracle Database 11g Enterprise Edition Release 11.2.0.4.0
При анализе производительности системы, если вы видите конфликты защелок, такие как библиотечный кеш, вы можете сделать вывод, что существует проблема в общем пуле, которая в основном вызвана операторами SQL, такими как отсутствие переменных привязки или некоторых хранимых процедур. Повторный анализ.
Конфликт защелок пула данных
Блок данных с очень высокой частотой доступа называется «горячим» блоком. Когда многие пользователи обращаются к определенному количеству блоков данных вместе, это вызовет некоторый конфликт Latch.
Наиболее распространенные случаи защелки:
Конфликт между этими двумя защелками происходит в разное время при доступе к блокам данных.
Причины ожидания буфера заняты
Когда сеансу требуется доступ к блоку данных, и этот блок данных считывается в память другим пользователем с диска, или этот блок данных изменяется другим сеансом, текущий сеанс должен ждать, и буфер будет занят ждет, подождите.
Непосредственная причина этих конфликтов защелок заключается в том, что слишком много сеансов обращаются к одному и тому же блоку данных. Горячий блок Проблема, причина горячего блока может быть вызвана настройками базы данных или повторным выполнением SQL, часто обращающимся к некоторым из тех же блоков данных
Причины кеширования буфера chian
Когда сеансу необходимо получить доступ к блоку памяти, он сначала должен перейти к структуре, подобной связанному списку, для поиска того, находится ли блок данных в памяти. Когда сеанс обращается к связанному списку, ему необходимо получить защелку. Если сбой получения завершится, он будет Ожидание цепочки буферов кэша защелки, причина этого ожидания в том, что слишком много сеансов обращаются к одному и тому же блоку данных или этот список слишком длинный (если слишком много данных считывается в память, список хэшей блока данных, которым нужно управлять, будет очень длинным, Таким образом, время для списка сканирования сеансов увеличится, время для удержания защелки цепочки буферов chache увеличится, шанс других сеансов получить эту защелку уменьшится, а время ожидания увеличится).
Причина теплового блока
Причины создания горячих блоков различны: в зависимости от типов блоков данных их можно разделить на следующие типы: Различные типы горячих блоков обрабатываются по-разному.
Блок данных таблицы
Например, в системе OLTP для некоторых небольших таблиц некоторым блокам данных будут выполняться частые операции запроса или изменения. В это время эти часто используемые блоки данных станут горячими блоками, что приведет к конфликту защелок в памяти.
Метод обработки:
Если возникает такой горячий блок и таблица не слишком велика, одним из способов может быть распределение данных таблицы по большему количеству блоков данных, чтобы уменьшить частоту доступа к блокам данных в большинстве сеансов одновременно.
Функция: максимальное количество строк, размещаемых во всех блоках текущей таблицы, и это число будет записано в словаре данных, любые будущие вставки, которые приводят к тому, что число строк блока превысит это число, будут отклонены
Недостатки:
Мы распределяем данные по большему количеству блоков данных, что значительно снижает вероятность повторного чтения блока данных. Но так Недостаток очевиден, то есть он снижает производительность данных.В этом случае доступ к одним и тем же данным означает, что необходимо прочитать больше блоков данных, и производительность будет снижена.
Индексный блок данных
Такая ситуация обычно возникает в архитектуре RAC, и значение ключа индекса определенной таблицы имеет типичное явление «наклона вправо».
В этой ситуации использование обратных индексов может смягчить это противоречие.
Принцип. Таким образом, значения ключей, которые первоначально были помещены в один и тот же индексный блок данных, теперь распределяются по разным блокам данных, так что значение первичного ключа, вставляемое пользователем в разные экземпляры RAC, распределяется по разным блокам данных, поэтому Это не вызывает генерацию горячих блоков, что является в основном единственным случаем, когда используется обратный индекс.
Причина, по которой использование обратного индекса настолько ограничено, заключается в том, что он отбрасывает одну из наиболее важных функций индекса B-Tree:
Index range scan
Среди методов доступа к индексу этот метод является наиболее распространенным, но обратный индекс не может использовать эту функцию, потому что обратный индекс уже перетасовал порядок значений ключа. При поиске диапазона в порядке значения ключа, в обратном индексе, Поскольку значения ключей хранятся в обратном порядке, эти значения больше не сохраняются последовательно. Таким образом, метод сканирования диапазона индекса не имеет значения в обратном индексе. В обратном индексе это может быть достигнуто только путем полного сканирования таблицы или полного сканирования индекса. Это очень серьезный недостаток обратного индекса.
Индексный корневой блок данных
Горячие блоки также могут появляться в корневом блоке данных индекса.
В индексе B-Tree, когда Oracle обращается к ключу индекса, он сначала обращается к корню индекса, затем к ветви индекса и, наконец, к листовому блоку индекса.
Значение ключа индекса хранится в листовом блоке.
Когда корневые данные и данные ветви индекса сконцентрированы на нескольких блоках данных, таких как блок данных ветви, где расположены D и G,
Когда пользователь получает доступ к диапазону из A-F, будет доступен блок данных.Если многие пользователи часто обращаются к ключу индекса этого диапазона, это может привести к тому, что этот блок данных ветви станет горячим блоком.
Когда возникает это явление, можно рассмотреть возможность разделения индекса, чтобы упростить использование этих корней, блоки данных ветвлений распределяются по разным сегментам данных (разделам), что снижает плотность параллельного доступа к блокам данных, тем самым избегая корней и ветвей индекса Блоки данных слишком концентрированы, что приводит к появлению горячих блоков.
Блок заголовка сегмента
Начиная с Oracle 9i, была представлена технология автоматического управления сегментами ASSM (Automatic SegmentSpace Management: ASSM), которая позволяет Oracle автоматически управлять «Свободным списком». На самом деле, в ASSM нет такой структуры, как свободный список, Oracle использует битовые карты для определения доступности блоков данных.Этот метод управления пространством для блоков данных более эффективен, чем использование списка для управления ими.
Для систем OLTP операции с таблицами DML очень интенсивны. Для этих таблиц использование метода ASSM для управления более удобно и точно, чем ручное управление, которое может эффективно предотвратить превращение головки сегмента в горячий блок.
Для системы OLAP этот параметр не имеет большого практического значения, потому что в такой базе данных есть несколько таблиц, которые часто изменяются.Основная задача системы OLAP состоит в пакетной загрузке отчетов и массивных данных.
Проверьте связанный с защелкой SQL
Просмотреть горячий блок, вызвавший событие ожидания LATCH BUFFER CACHE CHAINS
Latch free oracle что это
Экземпляр Oracle состоит из ряда процессов, обращающихся к общим сегментам памяти (SGA и другие общедоступные ресурсы). Поэтому они могут испортить информацию друг друга. Следовательно, во многих случаях требуется обеспечить механизм, который при использовании одним процессом общедоступных ресурсов (например, участки памяти) запрещает другим процессам изменять эти данные. Таким механизмом в СУБД Oracle являются блокировки, то есть специальные переменные, показывающие, занят или свободен некоторый ресурс.
В данной статье под блокировками будем понимать внутренние блокировки сервера и защелки.
В СУБД Oracle блокировки делятся на два непересекающихся класса: защелки (latch) и очереди (enqueues).
Защелки в СУБД Oracle могут запрашиваться в двух режимах: “willing-to-wait” и “no-wait” (= immediate). Если процесс имеет возможность продолжать работу, не получив запрашиваемую защелку, то это запрос no-wait (например, redo copy latch). Если процесс не может продолжать работу, не получив запрашиваемую блокировку, то это режим willing-to-wait.
В отличие от защелок, очереди запросов (enqueue) действительно образуют упорядоченную очередь FIFO. Каждый запрос в очереди, кроме порядкового номера, отражает еще и режим запроса (share, exclusive). Например, запросы на чтение могут выполняться одновременно, не блокируя друг друга. Если запрос на блокировку enqueue не может быть удовлетворен, то он ставится в очередь. Порядковые номера в очереди запрашиваются через системные вызовы ОС (семафоры).
С блокировками типов enqueues и latches всегда связана процедура, которая возвращает блокировку к предыдущему состоянию, если процесс, удерживающий блокировку, зависнет или аварийно завершится. В СУБД Oracle эту функцию выполняет процесс PMON.
Проблема
Вопрос: можно ли программным путем гарантированно заблокировать ресурс?
Ответ: нет, невозможно! Например, два процесса могут одновременно опросить одну и ту же переменную и, убедившись, что ее значение равно 0, установят ее значение в 1. Такой сценарий не редкость в многопроцессорных ЭВМ.
Возможен и другой сценарий. Допустим, что один процесс считывает значение переменной блокировки и обнаруживает, что она равна 0. Но прежде, чем первый процесс успевает изменить ее на 1 (отвлекся на обработку прерывания или был снят с процессора по истечении отведенного ему кванта времени), управление получает второй процесс, который тоже считывает значение переменной блокировки и изменяет ее на 1. Когда первый процесс снова получит управление, он тоже заменит переменную блокировки на 1, и оба процесса будут считать себя исключительными владельцами ресурса.
Таким образом, надежного программного решения, которое исключало бы одновременный доступ, не существует.
Механизмы блокирования
Процессор, выполняющий эту команду, блокирует шину памяти так, чтобы остальные процессоры не могли обратиться к оперативной памяти, и затем выполняет команду ‘test’, читая соответствующую ячейку памяти. Если возвращаемое значение равно нулю (false), то это значит, что переменная свободна, и процессор выполняет команду ‘set’, которая записывает в эту переменную значение 1 (true), и шина памяти разблокируется. Освобождение блокировки выполняется путем записи 0 (false) в переменную блокировки.
Если другой процессор позже попытается запросить блокировку, то команда ‘test’ возвратит ему значение 1 (true), означающее, что блокировка уже установлена. В этом случае второму процессу придется подождать некоторое время, а затем снова запросить блокировку. При выполнении каждой TSL-команды происходит блокирование шины ЭВМ.
Таким образом, команда типа TSL аппаратно обеспечивает неделимость обращения к переменной блокировки, ибо процесс может быть снят с выполнения либо до начала команды, либо после ее окончания. В результате чего блокировки СУБД ORACLE спускаются на уровень аппаратного обеспечения и блокируют шину ЭВМ. Блокирование шины сервера означает, что во время выполнения команды TSL все остальные процессоры и процессы не могут получить доступ к оперативной памяти и вынуждены ждать завершения операции (однако они могут обращаться к данным в своем локальном кеше).
В общем, блокировки представляют собой чрезвычайно затратный механизм поддержания целостности и непротиворечивости системы, но другого механизма поддержки непротиворечивости пока не существует.
Механизм разблокировки
Первый способ, очевидно, является достаточно затратным, с точки зрения потребления ресурсов ЦПУ, потому что он загружает холостой работой все процессоры, на которых выполняются процессы, запрашивающие блокировку. Достоинство spin-подхода в том, что в этом случае отсутствует простой процесса (процесс получает блокировку сразу же, как только она освободится). Кроме того, отсутствует переключение контекста (переключение процессора с одного процесса на другой). Переключение контекста является длительной операцией, поскольку требует сохранения контекста текущего процесса (сохранение регистров процессора в стеке), загрузки нового контекста (загрузки в регистры процессора значений нового процесса). Кроме того, новый процесс начнет выполнение с непопадания в кеш, потому что кеш хранит данные старого процесса.
Второй способ является более экономным для ЦПУ, но время ожидания освобождения блокировки здесь будет больше. Достоинство второго подхода в том, что занятый процессор освобождается и может быть загружен полезной работой, но взамен происходит переключение контекста, что долго и дорого.
В общем, жертвовать придется всегда, либо общей производительностью ЭВМ, либо временем отдельного процесса, и главная задача здесь оптимальным образом сбалансировать запросы на блокировки, выполняемые тем или другим способом.
Влияние на производительность
Механизм блокирования системной шины фактически замораживает функционирование сервера на короткий период времени. А это означает, что если теоретически в одном часе имеется 3600 секунд, то в результате блокирования шины сервер фактически функционирует не 3600 секунд в час, а 3599, 3598, … и, возможно, менее. То есть, слишком часто блокируемый сервер работает не все отведенное для работы время. Причем частота блокирования растет пропорционально количеству процессоров и процессов. В результате чего добавление очередного процессора может не приводить к увеличению производительности всего сервера в целом.
Для полноты картины попробуем численно оценить влияние блокировок на производительность сервера, для чего рассмотрим типичный отчет Statspack, секцию “Latch Activity for DB”. Понятно, что этот расчет довольно приблизительный, но, на мой взгляд, довольно показательный.
Управление поведением
Документация по СУБД Oracle не дает информации о вычислении значения _spin_count. Для версий 8, 9 и 10 это значение равно 2000. Интересно, как получено это значение?
Методики анализа блокировок могут быть статическими (анализ кода), либо динамическими (анализ работы программы).
С точки зрения исследования исполняемого кода, если разработчики СУБД Oracle знают, что процесс, работающий под защитой блокировки, в среднем выполняет не более Х команд, то и параметр _spin_count можно было бы установить в значение, покрывающее этот промежуток. Однако реально сложно однозначно сказать, сколько времени займет выполнение 2000 команд. Команды процессора различаются по длительности, иногда довольно сильно, от нескольких тактов, например, инкремент – 1 такт, до нескольких десятков – деление требует 42 такта.
Удовлетворительный результат можно получить только в том случае, если только код, выполняющийся под блокировкой, специально написан так, чтобы он выполнялся за время, не большее, чем _spin_count. А еще и прерывания оказывают свою роль. В общем, статический механизм дает очень приблизительное значение для _spin_count.
Перейдем к динамике. Если установить для этой переменной слишком высокое значение, то процессоры сервера будут перегружены холостой работой. Если _spin_count установить слишком низким, например 1, то после одной неуспешной попытки процессы, запрашивающие защелку, будут приостанавливаться (уходить в сон), и в результате процесс потеряет много времени на ожидание.
Очевидными границами здесь являются переключение контекста и среднее время использования защелки. Для того чтобы снять процесс с процессора, а затем возвратить процесс на процессор, требуется некоторое количество команд и соответствующее для их выполнения время. Поэтому _spin_count можно безопасно выставить на время, которое меньше или равно длительности двух переключений контекста. Если среднее время ожидания защелки меньше, чем два переключения контекста, то для сервера дешевле выполнить spin. Если среднее время ожидания освобождения защелки больше, чем два переключения контекста, то дешевле снимать процесс с процессора, а затем вернуть обратно.
Способы оптимизации
Возможно, в будущем мы можем стать свидетелями появления нового процесса в Oracle типа “прогнозировщик блокировок”, а также нового узла в ЦПУ, целью которого является динамически идентифицировать начало и конец критической секции кода, выполняемой под блокировкой. Идея этого метода в том, продолжать вычисления, не обращая внимания на блокировку. Ключевым моментом здесь является то, что значительная часть исполнимого кода может не зависеть от значения этой переменной блокировки. И после каждой выполненной команды (или после каждой сотни команд) можно будет опрашивать переменную блокировки. Таким образом, запрашивающий процесс будет выполнять не холостой цикл, а полезную работу. В результате влияние блокировок на производительность будет минимальным. Главная задача при этом состоит в определении в исполнимом коде процесса максимально длинной последовательности команд, которые можно безопасно выполнить независимо от значения блокировки.
Вторым по порядку и простейшим “тупым” методом повышения быстродействия, на мой взгляд, является создание отдельной быстрой шины и выделенной только под переменные блокировки памяти. Тогда блокировки можно будет быстро ставить и быстро снимать, а все остальные процессоры будут обращаться к общей памяти по неблокируемой шине.
В ОС Solaris, начиная с 7 версии, появилось понятие adaptive mutex. В этом случае процесс, прежде чем запросить блокировку, проверяет, удерживает ли ее какой-нибудь другой процесс, и если да, то первый процесс проверяет, находится ли удерживающий процесс на процессоре или нет. Если процесс, удерживающий блокировку, выполняется на процессоре, то запрашивающий процесс переходит к стандартному алгоритму – в цикле выполняет TSL. Если удерживающий процесс не находится на процессоре, то запрашивающий процесс освобождает свой процессор и переходит в состояние ожидания.
В качестве еще одной оптимизации в ОС Solaris процессу, удерживающему блокировку, дается дополнительный квант процессорного времени, чтобы он как можно быстрее освободил эту блокировку. Используются ли эти технологии в СУБД Oracle мне пока не известно.
Справедливым возражением, относительно единого и универсального параметра _spin_count будет сказать, что не все блокировки одинаково длительны. Для того чтобы правильно настроить ожидание за защелку, следует знать особенности каждой конкретной защелки, то есть разные защелки могут иметь существенно разное среднее время удержания. В связи с этим вызывает сомнение, что один единственный параметр будет достаточным для всех защелок в СУБД. На месте разработчиков Oracle я бы, вероятно, для каждой защелки (или для каждого класса защелок с похожим поведением) определил свой параметр _spin_count.
Заключение
Автор выражает благодарность сотруднику компании“Открытые технологии” Александру Иванову за внимание и полезные советы при подготовке данной статьи.
Защелки в базе данных Oracle: подробный разбор механизма
В Oracle используется четыре механизма, гарантирующих защиту ресурсов от разрушения конфликтующими операциями чтения и записи. Эти механизмы могут ухудшать производительность, гарантируя безопасность операций, однако, некоторые из них способны, напротив, экономить время при многократном выполнении повторяющихся операций.
Как можно заключить из комментариев выше, мьютексы (которые появились в Oracle Database 10g в основном для замены закреплений (pins) в библиотечном кэше (library cache) оказываются где-то посередине между блокировками и защелками: сеансы конкурируют за владение мьютексами точно так же, как они конкурировали бы за владение защелками, с той лишь разницей, что мьютексы могут удерживаться достаточно продолжительное время (так же как блокировки и закрепления (pins)).
В первую очередь (вводная часть).
Прежде чем заняться исследованием защелок и особенностями их работы, необходимо сначала получить хотя бы общее представление о том, как в Oracle используются массивы, указатели, связанные списки и хэш-таблицы для хранения информации в памяти, потому что именно такие структуры чаще всего требуют защиты, когда в системе одновременно работает несколько пользователей.
Массивы
Массивы в Oracle фактически являются списками объектов одного типа и размеров, а так как все объекты имеют один и тот же размер, массивы легко можно обойти, просматривая объекты по очереди. Например, x$ksuse (структура с информацией о пользовательских сеансах в виде v$session) является фиксированным массивом со строками, имеющими размер 11 360 байт в 32-разрядной версии Oracle 11.2.0.2 для Windows. Чтобы обратиться к любому элементу этого массива, достаточно знать лишь местоположение первой записи в массиве и порядковый номер искомой записи – все остальное, лишь вопрос простых арифметических операций.
В некоторых случаях массив может быть «сегментированным» массивом. То есть изначально Oracle выделяет сегмент памяти для хранения фиксированного числа элементов массива, и затем выделяет дополнительные сегменты, по мере необходимости. В этом случае в Oracle должны храниться адреса всех сегментов, то есть, должен иметься список сегментов или в каждом сегменте должна храниться ссылка на следующий сегмент в списке. Структура x$ktatl (элемент с меткой temporary_table_locks в v$resource_limit) как раз является примером такого массива. В небольшом тесте, который я запускал в экземпляре 10g, эта структура создавалась как массив с 16 элементами по 144 байта в каждом, а затем к ней добавлялись сегменты, также по 16 элементов, разбросанные в памяти случайным образом. Аналогично устроены структуры x$ksqrs и x$ksqeq, только начальные сегменты имеют в них значительно больший размер и в дальнейшем они прирастают сегментами по 32 элемента.
Примечание. Элемент enqueue locks в представлении v$resource_limit реализован с ошибкой. В своем небольшом тесте я указал, что память для enqueues locks должна выделяться сегментами по 2500 элементов (при этом первоначальное ограничение limit_value имело значение 1130). После этого представление v$resource_limit по-прежнему показывало значение 1130 в limit_value и значение 1129 в current_allocation. Обнаруживая подобные небольшие аномалии, я постепенно составляю для себя общую картину работы Oracle. В частности, данная конкретная аномалия вполне может говорить о том, что элемент с порядковым номером 1130 используется как ссылка на следующий сегмент массива.
Указатели
Теперь познакомимся с идеей указателей. Указатель – это всего лишь область памяти, где хранится адрес другой области памяти с некоторой полезной информацией. Например, если взглянуть на массив x$ksmfsv переменных в фиксированном разделе SGA, можно увидеть следующую запись (числовые значения могут отличаться в разных версиях):
Эта запись сообщает, что по адресу 0x035004b0 хранится значение 0x0d3c818, которое является элементом данных, имеющим размер 4 байта, и «указателем на некоторое значение типа ksqeq». Когда я вывел содержимое памяти по адресу 0x0d3c818, я обнаружил там значение 0x21a33960, которое является адресом первой строки в фиксированном массиве x$ksqeq. То есть, я нашел указатель на указатель, который ссылается на массив фиксированной длины – это может служить признаком, что я нашел сегментированный массив, где последний элемент сегмента указывает на первый элемент следующего сегмента.
Также существуют сегментированные массивы с двумя элементами в x$ksmfsv, один из которых хранит число сегментов, составляющих массив, а другой ссылается на массив указателей, указывающих на отдельные сегменты.
Связанные списки
Имея возможность сослаться из одного местоположения на другое, легко можно уйти от «фиксированных структур», таких как массивы. С помощью указателей можно создавать связанные списки элементов данных разных форм и размеров, просто обеспечив включение в каждый элемент указателя на следующий элемент в списке, и этот подход широко используется в Oracle. Фактически, многие списки в Oracle реализованы как двусвязные списки, то есть каждый элемент списка хранит два указателя, один из которых ссылается на следующий элемент в списке, а другой – на предыдущий.
Нам даже не нужно заниматься поисками в памяти, чтобы найти примеры связанных списков, потому что мы уже видели их в этой статье, в таблице транзакций (и в разделе управления транзакциями), в заголовке undo-сегмента. Ниже приводится одна и таких структур, сокращенная до минимума:
В разделе управления транзакциями указывается, что головой списка (chd) является элемент 0x0011, а хвостом (ctl) – элемент 0x001c. Если заглянуть в строку 0x11 (см. столбец index), можно заметить, что столбец uel ссылается на строку 0x001b; строка 0x1b ссылается на строку 0x0019, строка 0x19 ссылается на строку 0x002e, строка 0x2e ссылается на строку 0x001a. и так далее (здесь я пропущу 30 (или что-то около того) ссылок), пока не встретится строка, ссылающаяся на строку 0x27, которая в свою очередь ссылается на строку 0x1c (ctl), завершающую список, о чем говорит значение 0xffff в столбце uel.
Примечание. В предыдущем примере можно видеть, что данные в столбцах index/uel выводятся то как 1-байтные, то как 2-байтные. Никогда не полагайтесь на формат вывода дампов, если вам нужна правда, только правда и ничего кроме правды. Если требуется точно знать, хранится ли значение в виде одного байта или двух (и вообще, хранится ли, как в случае со столбцом index, значение которого в действительности нигде не хранится), всегда обращайтесь к фактическим данным.
Интересно отметить, что в данном случае можно было бы использовать массив (элементы одного типа и одного размера). Но в действительности используется связанный список, что позволяет максимально быстро выбрать следующий свободный элемент (голова списка), вернуть элемент в конец (хвост) списка, когда он станет не нужен, и гарантировать, что предыдущий элемент не будет использован повторно, пока в списке остаются свободные элементы. Этот способ дает удобную возможность множеству сеансов извлекать элементы из списка, удерживать их неопределенно долго и затем возвращать в список.
Это был пример односвязного списка – допускающего обход его элементов только в одном направлении. Получив один элемент, легко узнать, какой элемент будет использоваться следующим, потому что на него ссылается текущий элемент. Но очень непросто узнать, какой элемент предшествует текущему, так как для этого необходимо проверить все остальные элементы, пока не будет найден ссылающийся на текущий. (Обратите также внимание, что Oracle хранит отдельную ссылку на хвост списка, чтобы упростить добавление новых элементов в его конец, без необходимости выполнять обход всех элементов, начиная с начала списка.) Такой список можно было бы назвать списком FIFO (First In, First Out – первым пришел, первым вышел); в некоторых случаях (например, для управления свободными блоками)
Oracle использует связанные списки для представления стеков (или LIFO; Last In, First Out – последним пришел, первым вышел).
Несмотря на то, что односвязные списки с успехом используются для объединения записей в таблице транзакций, в Oracle имеется масса других мест, где используются двусвязные списки. И снова нам не нужно рыскать по всей памяти, чтобы найти их, так как одним из примеров таких списков может служить хорошо известная структура. Ниже приводится выдержка из дампа раздела заголовка индексного листового блока:
Обратите внимание на элементы kdxlenxt (next leaf block – следующий листовой блок) и kdxleprv (previous leaf block – предыдущий листовой блок) ближе к концу дампа. Если вдруг потребуется обойти большое число индексов, очевидно, что самый простой способ сделать это – перемещаться от одного листового блока к другому, не поднимаясь и не опускаясь по дереву. Поэтому указатель на следующий листовой блок можно считать отличной находкой; а так как Oracle позволяет также выполнять обход индексов в обратном направлении, указатель на предыдущий блок можно считать не менее полезным решением.
Хэш-таблицы
Массивы отлично подходят для случаев работы со структурами одного типа, а связанные списки могут пригодиться для работы с относительно небольшим числом элементов. Но в обоих случаях приходится платить потерей производительности, когда требуется обойти массив или список в поисках требуемого элемента, хотя цена невысока, если элементов немного.
Но как быть, если необходимо выполнить поиск среди большого числа элементов, которые, к тому же, постоянно, то исчезают, то появляются? Конечно, нам известна идея использования B-деревьев индексов для быстрого доступа к данным, но эта концепция не позволяет эффективно работать с большим числом маленьких структур в памяти. Однако, B-деревья – это не единственный механизм быстрого поиска данных. Некоторые администраторы Oracle используют хэш-кластеры, и этот механизм отлично уживается с другими механизмами Oracle, широко используемыми для работы с памятью.
Концепция хэшей удивительно проста. Задается фиксированное число групп (buckets) (чаще всего используются степени двойки, которые позволяют получить наибольшую эффективность, как показывают многочисленные математические исследования функций хэширования). Затем выбирается алгоритм хэширования, реализацию которого можно применить к объекту и получить число в диапазоне от 1 до числа групп (или, если хотите, от нуля до «число_групп – 1»).
Например, можно реализовать распределение информации о друзьях по десяти группам, используя алгоритм «связать информацию с группой N, где N – последняя цифра в номере мобильного телефона». Можно поступить и по-другому: реализовать распределение информации о друзьях по 16 группам в соответствии с алгоритмом: «связать информацию с группой N, где N – остаток от деления числа детей на 16».
Однако есть некоторые тонкости, которые нужно прояснить: хэширование информации о друзьях по последней цифре в номере телефона с большой долей вероятности даст достаточно равномерное распределение по десяти группам, но будет непросто найти информацию о конкретном друге, не зная последнюю цифру в номере его телефона. Кроме того, у вас может быть много друзей, из-за чего с каждой группой может быть связано большое их число, то есть вам придется приложить дополнительные усилия, чтобы найти нужную информацию даже после выбора правильной группы. (Можно конечно задать 100 групп и хэшировать информацию по двум последним цифрам в номере, но в этом случае потребуется вспомнить уже две цифры, чтобы найти нужную группу.)
При хэшировании по числу детей наверняка приведет к тому, что группы с большими порядковыми номерами останутся незанятыми и все сведения будут связаны с первыми несколькими группами, хотя, с другой стороны, найти нужную группу будет проще, потому что обычно мы легко запоминаем у кого из друзей сколько детей.
В обоих случаях хэш-значение для друга может изменяться – друг может сменить номер телефона или у него может родиться еще один ребенок.
Ниже приводится несколько важных замечаний, касающихся хэшей:
В качестве примера хэширования возьмем библиотечный кэш (library cache) в Oracle. В момент начала работы с экземпляром, имеющим очень маленький объем SGA (System Global Area – системная глобальная область) и пустой библиотечный кэш второго уровня (см. Приложение), я обнаружил, что кэш имеет 131 072 групп, из которых 5880 используется. С большинством из используемых групп связано по одному объекту, 136 групп имели по два объекта, и одна группа – три. (К слову сказать, из 6000 объектов, примерно 800 – это дочерние курсоры (child cursors), которые отображаются в v$sql.)
Примечание. По всей видимости, в последних версиях Oracle максимальное число хэш-групп в библиотечном кэше (131 072) жестко зашито в коде. В более ранних версиях его можно было увеличить, манипулируя скрытыми параметрами, но в последних версиях его можно только уменьшить.
Когда вы передаете в Oracle инструкцию SQL и говорите: «найди это» (или «добавь это») в библиотечном кэше, Oracle интерпретирует инструкцию как поток чисел (кодов символов) и выполняет некоторый алгоритм, чтобы получить номер группы – информацию об инструкции, которая сообщит Oracle, где искать. А алгоритм приложит все усилия, чтобы как можно равномернее распределить инструкции по разным группам (хотя некоторые реализации, в районе версии v7, использовали не самый лучший алгоритм).
Итак, мы определили следующие ключевые положения: «равномерное распределение», «хороший алгоритм» и «не очень большое число элементов в группе». Но нерешенными остались еще два важных вопроса: что такое «группа» (bucket) и как действует Oracle, обнаружив две инструкции в одной группе? Ответы на эти вопросы объясняют, почему я начал этот раздел с обсуждения массивов, указателей и связанных списков. Группа (bucket), или хэш-блок, – это всего лишь элемент сегментированного массива, играющий роль головы двусвязного списка объектов. На рис. 1 показано (весьма упрощенно), как выглядит библиотечный кэш базы данных Oracle.
Примечание. Вы часто будете видеть, что термины «хэш-группа» (hash bucket) и «хэш-цепочка» (hash chain) используются взаимозаменяемо. Мне не хотелось бы занудствовать, объясняя, почему люди используют тот или иной термин, но, если хотите уловить грань между ними, представляйте группу, как фиксированную точку начала списка, а цепочку (chain), как список, прикрепленный к этой точке.
Рис. 1. Приближенная структура очень небольшого
библиотечного кэша
Чтобы загрузить новый объект в библиотечный кэш, Oracle должен определить группу, которой будет принадлежать этот объект, и затем добавить его в соответствующий список. То есть, выбрать два существующих объекта, которые в текущий момент ссылаются друг на друга, установить указатели в новом объекте, чтобы они указывали на эти объекты, и затем изменить обратный указатель в одном объекте и прямой указатель в другом объекте так, чтобы они указывали на новый объект. Для добавления нового объекта требуется изменить два существующих объекта, как показано на рис. 2.
Иногда могут возникать ситуации, когда имеющейся памяти оказывается недостаточно (ни одного свободного фрагмента подходящего размера) для создания нового объекта в библиотечном кэше. Тогда Oracle использует алгоритм «наиболее давно не использовавшийся» (Least Recently Used, LRU), чтобы выбрать несколько «случайных» объектов, которые можно удалить из соответствующих хэш-цепочек, и освободить память для повторного использования. И снова возникает необходимость изменить два объекта, соседних с исключаемым, чтобы переадресовать их указатели друг на друга.
Теперь вам должно быть понятно, что нам нужен высокоскоростной механизм, чтобы предотвратить неприятности при работе со сложными структурами в памяти. Представьте, что ваш сеанс решил использовать объект из первой хэш-группы (см. рис. 1). В этот же момент мой сеанс решил, что нужно освободить память, чтобы сохранить какую-то другую информацию, и выбрал тот же самый элемент в той же хэш-группе для удаления.
Рис. 2. Вставка объекта в двусвязный список
Если обоим сеансам будет позволено одновременно работать с содержимым хэш-группы, ваш сеанс сможет добраться до выбранного объекта по указателям только потому, что мой сеанс не успел исправить указатели в соседних объектах – имейте в виду, что операционная система может приостановить мой сеанс в любой момент, то есть, легко может сложиться ситуация, когда приостановка произойдет как раз в тот момент, когда мой сеанс сделал только полдела. Так или иначе, нужно каким-то способом гарантировать, что мой сеанс не переупорядочит список, пока ваш сеанс выполняет его обход, и на-оборот, ваш сеанс не сможет приступить к обходу элементов списка, пока мой сеанс занимается его переупорядочением.
Мантра, хорошо известная всем пользователям Oracle, гласит: «Читающий сеанс не блокирует пишущий, а пишущий не блокирует читающий», – но она истинна только на уровне данных. На уровне памяти бывают моменты, когда читающие сеансы должны блокировать пишущие, и единственный пишущий сеанс должен блокировать все другие операции.
Примечание. Для устранения проблем, которые могут возникать при одновременном изменении общей памяти несколькими процессами, в Oracle используются защелки; приемы их использования, могут отличаться, но суть остается неизменной. Один из приемов: блокировка доступа к списку, чтобы предотвратить конфликты между операциями поиска в связанном списке и изменения содержимого этого списка. Другой, более простой прием, связан с «изоляцией» счетчиков или указателей (таких как управляющие указатели в буфере журнала повторения), чтобы в каждый момент времени изменять их мог только один процесс.
Защелки
Существует два вида защелок – исключительные, или монополные (exclusive) и разделяемые для чтения (shared read) (защелки, разделяемые для чтения, как я узнал недавно, относительно широко стали использоваться только в версии 9i), но, что часто вызывает путаницу, разделяемую для чтения защелку можно приобрести в монопольном режиме – поэтому с данного момента я буду называть их просто разделяемыми защелками.
Существуют также две характеристики, описывающие процессы, взаимодействующие с защелками: готовые ждать получения защелки (большинство) и требующие немедленного ее приобретения. И снова мы вступаем в область перекрывающихся понятий, потому что некоторые взаимодействия начинаются, как требующие немедленного приобретения, а затем повторяются, демонстрируя поведение, характерное для взаимодействий, когда процесс готов ждать, но мы пока не будем затрагивать эту тему.
По своей сути защелка представляет собой комбинацию из ячейки памяти в SGA и атомарной операции, с помощью которой осуществляется проверка и изменение значения в этой ячейке. (Имеется также инфраструктура поддержки, занимающая в памяти от 100 до 200 байт; точный объем памяти, занимаемой этой инфраструктурой, зависит от версии Oracle.)
Примечание. Понятие «атомарная операция» играет особенно важную роль в многопользовательских системах, где большинство операций могут прерываться планировщиком операционной системы, и в многопроцессорных системах, где разные процессоры способны одновременно изменять значения в памяти. В идеале операция «проверки и изменения» должна выполняться единственной машинной инструкцией, чтобы она не могла быть разбита на две части и прервана планировщиком в середине ее выполнения. Но самое главное, что полная безопасность при работе с защелкой может быть обеспечена, только если единственный процессор сможет гарантировать невозможность прерывания операции, а в многопроцессорной системе будет обеспечена невозможность одновременного доступа к одной и той же ячейке памяти несколькими процессорами. Чтобы удовлетворить последнее требование, необходима особая машинная инструкция, которая запирает шину доступа к памяти, гарантируя ее использование только одним процессором в данный момент времени.
Логика работы защелок
В своей основе, логика работы защелки проста: «если есть возможность записать в память защелки некоторое значение N, значит можно выполнить какие-либо операции со структурой, защищенной этой защелкой». (Естественно, после выполнения защищенной операции значение защелки должно быть возвращено в исходное состояние.) Принцип действия исключительной, или монопольной защелки можно представить следующим псевдокодом:
Записать в регистр X адрес A ячейки памяти защелки
Если значение по адресу A равно нулю, записать туда 0xff ***
Если значение по адресу A равно 0xff, значит вы «владеете» защелкой
Если нет, вернуться обратно и повторить попытку, и так пару тысяч раз
Решение, что делать, если даже после пары тысяч попыток не удалось приобрести защелку, мы обсудим несколькими страницами ниже. Строка, отмеченная звездочками ***, описывает атомарную операцию – после нее сеанс может сказать: «если значение защелки было равно нулю, а стало равно 0xff, значит мне удалось приобрести защелку». Если бы эта операция, которая часто реализуется единственной машинной инструкцией «проверить и установить», могла быть прервана, тогда вы могли бы столкнуться со следующей последовательностью событий:
Аналогично, в отсутствие возможности заблокировать доступ к памяти в многопроцессорной системе, мы могли бы столкнуться с похожей последовательностью, в которой последние четыре события можно изложить так:
В любом случае у нас имеется два процесса, которые могут выполнить операции, разрушительные друг для друга, и уверенные, что обладают исключительным доступом к некоторому критическому ресурсу.
Примечание. Желающим больше узнать о путях взаимодействий процессоров в многопроцессорных системах, а также о побочных эффектах и опасностях таких взаимодействий, я рекомендую обратиться к книге эксперта по Oracle Джеймса Морли (James Morle) «Scaling Oracle8i», которая в настоящее время доступна бесплатно для загрузки по адресу: www.scaleabilities. co.uk/index.php/Books/. (Не обращайте внимания на номер версии 8i в названии – основные принципы почти не изменились с тех пор.)
Недостаток исключительной защелки, как можно догадаться, заключается в ее исключительности. В каждый конкретный момент времени такой защелкой может владеть только один сеанс, то есть, только один сеанс обладает доступом к защищенному ресурсу. Это плохо сказывается на масштабировании в высоконагруженных системах, где одновременно может выполняться множество сеансов, которым нужно лишь прочитать содержимое структуры в памяти и которые не собираются изменять его. По этой причине в версии 9i, для защиты особенно интенсивно используемых участков кода, были реализованы разделяемые защелки. Возможно, что их появлению способствовало появление инструкций «сравнения с обменом» (compare and swap) в наиболее распространенных процессорных архитектурах.
Разумеется, конкретные реализации зависят от особенностей аппаратных архитектур, но обычно все их можно выразить на псевдокоде, как показано ниже:
И снова, строка, отмеченная звездочками ***, определяет непрерываемую (атомарную) операцию. Преимущество такой защелки «размером в слово» заключается в возможности определить алгоритм, который будет позволять читающим сеансам отмечать «приобретение и освобождение» защелки, а пишущим сеансам – блокировать новые читающие (и другие пишущие) сеансы, установкой единственного бита в слове, играющего роль признака «исключительного доступа на запись». Обработка запроса на приобретение защелки от читающего сеанса может выглядеть так:
И снова, решение, что делать, если даже после пары тысяч попыток не удалось приобрести защелку, мы обсудим несколькими страницами ниже. Как только читающий процесс закончит чтение объекта, он должен выполнить аналогичный цикл, чтобы уменьшить значение защелки на единицу – только в этот раз не требуется проверять бит записи.
Алгоритм приобретения защелки пишущим процессом (которому требуется исключительный доступ к объекту), с другой стороны, мог бы выглядеть так:
Как видите, применение двух разных стратегий позволяет пишущему сеансу захватить защелку в «монопольное» пользование, пока читающие сеансы пользуются ресурсом, и затем просто дождаться, пока они не освободят ее. В то же время новые читающие процессы не смогут получить доступ к ресурсу, пока пишущий сеанс удерживает бит записи установленным, и только один пишущий сеанс может оперировать битом записи. Такой подход улучшает масштабируемость, позволяя одновременно обращаться к ресурсу сразу нескольким читающим сеансам, и минимизирует задержки для пишущих сеансов.
Примечание. Особенности поведения защелок в постоянно менялись с течением времени, но общие принципы остаются неизменными с версии 8.0. Желающим поближе познакомиться с внутренним устройством защелок я могу порекомендовать статью «Latch, Mutex and Beyond», опубликованную Андреем Николаевым (Andrey Nikolaev) в своем блоге: http://andreynikolaev.wordpress.com/, которого я хотел бы еще раз поблагодарить за помощь в подготовке этой статьи, и особенно за рецензирование моих комментариев о защелках и мьютексах. (Как бы то ни было, любые ошибки, которые вы найдете здесь, допущены исключительно мною, хотя, некоторые из них являются преднамеренными упрощениями.)
В действительности разрешение конфликтов между читающими и пишущими сеансами реализовано в Oracle немного сложнее, чем я описал. Вероятно, более доходчиво будет объяснить суть на примерах значений, которые приводятся в табл. 1, полученных мною вызовами oradebug (см. приложение).
Значение | Интерпретация |
0x00000005 | В настоящее время пять читающих сеансов удерживают разделяемую защелку. |
0x40000003 | В настоящее время три читающих сеанса удерживают защелку и один пишущий сеанс (пока по значению защелки нельзя сказать, что это за сеанс) установил бит блокировки, чтобы запретить доступ новым читающим сеансам. |
0x20000014 | Процесс 0x14 (v$process.pid) захватил защелку для монопольного использования с целью записи в защищенный объект. |
Как видно из табл. 1, если пишущий процесс конкурирует с читающими процессами за обладание защелки, для ее приобретения он должен выполнить два шага: сначала установить битовый флаг, объявив защелку «заблокированной», а затем, когда все читающие процессы освободят ее, установить другой битовый флаг, объявив защелку доступной только для записи, и записать в нее свой идентификатор процесса. Изначально пишущий процесс не предполагает, что имеются конкурирующие читающие процессы, и первым действием сразу пытается захватить защелку.
Статистики по операциям с защелками
Прежде чем рассказать, что происходит, когда попытка приобрести защелку оканчивается неудачей, я хочу представить несколько статистик по операциям с защелками. Наибольший интерес для нас представляют статистики, перечисленные в табл. 2, которые можно получить, обратившись к представлению v$latch (базовое представление со статистиками по защелкам, но не единственное – существуют также представления v$latch_parent и v$latch_children).
Неудачные попытки приобретения защелок
В описании выше я упоминал о «цикле, выполняющем пару тысяч итераций». Теперь пришло время узнать, что происходит, если попытка приобрести защелку не увенчалась успехом, даже после выполнения этого цикла.
Сеанс может установить таймер, чтобы разбудить себя через короткий интервал времени, и удалить себя из системной очереди выполняющихся процессов. Когда системный планировщик вернет его в очередь выполнения, процесс снова может войти в цикл, пытаясь приобрести защелку, и снова приостановиться в случае неудачи. Время приостановки увеличивается по экспоненте с каждой попыткой – чем больше неудачных попыток было выполнено, тем длиннее период приостановки процесса – в результате чего время приостановки может стать очень большим. Ниже приводится один из экстремальных примеров, который я однажды видел в системе Oracle 8i. Тогда сеансу потребовалось примерно 8 секунд, чтобы приобрести одну из защелок библиотечного кэша:
Прошедшее время (elapsed time, ela= nnnn, измеряется в сотых долях секунды) должно удваиваться после каждого второго периода ожидания, пока не достигнет максимального значения, равного 2 секундам1, но высокая нагрузка на систему не позволила точно выдержать это требование.
Примечание. Интересно отметить, что минимальный интервал ожидания, равный 1/100 секунды, был введен в Oracle 6 (или в более ранней версии), когда тактовая частота самых «быстрых» процессоров не превышала нескольких мегагерц. Теперь, когда тактовая частота процессоров измеряется гигагерцами, время ожидания 1/100 секунды оказалось в сотни раз больше, чем могло бы быть.
Важно отметить, что теперь такие ситуации невозможны: процесс, который не может приобрести защелку почти немедленно, включает себя в список ожидания защелки и приостанавливается. Понятие
«почти» отличается для разных типов защелок – Андрей Николаев, о котором я говорил выше, с помощью DTrace в системе Solaris исследовал некоторые детали и пришел к выводам, перечисленным в табл. 3.
Операция с защелкой | Используемый метод |
Приобретение исключительной (монопольной) защелки. | Попытка немедленного приобретения, вход в пустой цикл (в данном случае цикл выполняет 20 000 итераций), включение в список ожидания, попытка немедленного приобретения, приостановка. |
Приобретение разделяемой защелки в исключительном режиме, когда другой процесс удерживает ее в любом режиме (разделяемом, исключительном или блокирующем). | Вход в пустой цикл (в данном случае цикл выполняет 2000 итераций), включение в список ожидания, повторение пустого цикла, приостановка в случае неудачи. |
Приобретение разделяемой защелки в разделяемом режиме (для чтения), когда другой процесс удерживает ее в исключительном или блокирующем режиме. | Вход в пустой цикл (в данном случае цикл выполняет 2000 итераций), включение в список ожидания, повторение пустого цикла, приостановка в случае неудачи. |
Приобретение разделяемой защелки в разделяемом режиме (для чтения), когда другой процесс намеревается приобрести ее в исключительном или блокирующем режиме. | Время на выполнение пустого цикла не тратится – сразу происходит простановка. |
Приобретение разделяемой защелки в разделяемом режиме (для чтения), когда другой процесс удерживает ее в разделяемом режиме. | Цикл выполняет cpu_count + 2 итераций перед приостановкой. |
Здесь важно отметить, что когда попытка приобретения защелки оканчивается неудачей, процесс включает себя в список и затем ждет, пока система возобновит его выполнение. Механизм возобновления относится к числу механизмов, претерпевших наибольшие изменения в последних версиях Oracle – процесс, который в данный момент удерживает защелку, пошлет сообщение процессу, находящемуся на вершине списка, после освобождения защелки, и такой порядок работы может иметь некоторые интересные побочные эффекты.
Примечание. Механизм приостановки/возобновления для защелок был доступен и в ранних версиях Oracle, но применялся далеко не ко всем защелкам. Усовершенствованные его реализации в новых версиях Oracle зависят от доступности новейших особенностей в операционных системах и могут (но не обязаны) регулироваться с помощью скрытого параметра _enable_reliable_latch_waits.
Если защелка используется не очень интенсивно и никакие другие процессы не пытаются приобрести ее, мы сможем увидеть, что процессы, попадающие в список ожидания, фактически выполняются по очереди, потому что когда процесс освобождает защелку, он активизирует следующий за ним ожидающий процесс.
Конечно, если защелка используется не очень интенсивно, маловероятно, что для нее будет создан список ожидания, но вы вправе спросить, что произойдет, если защелка используется интенсивно, имеет список ожидания, и все больше процессов будет пытаться приобрести ее. В этом случае есть вероятность, что новый процесс приобретет защелку еще до того, как будет возобновлен процесс из списка – механизм очереди не отличается справедливостью!
В случае с исключительными защелками выбрано большее число итераций пустого цикла, чтобы уменьшить проблемы, связанные с приостановкой процесса. Можно принять за истину, что процессы стараются освободить удерживаемые защелки как можно быстрее, поэтому 20 000 итераций пустого цикла, как предполагается, будут выполняться дольше, чем любой код, удерживающий защелку. Тем не менее, у нас нет никаких статистик, которые могли бы нам рассказать о следующем сценарии:
Возможно, имеется какой-то хитрый код, который мешает подобному развитию ситуации; возможно, что подобные ситуацию случаются настолько редко, что разработчиками было решено не регистрировать их.
Теоретически число «периодических остановок» (recurrent sleeps) можно определить, опираясь на тот факт, что неудачи (misses) обычно заканчиваются приобретением защелки (spin_get) или приостановкой (sleeps) процесса, то есть, misses = spin_gets + sleeps или, если перенести правую часть уравнения в левую, sleeps + spin_gets – misses = 0. Но, если описанный выше сценарий действительно имел место, тогда число sleeps окажется больше ожидаемого, то есть выражение sleeps + spin_gets – misses будет возвращать результат больше нуля.
Похоже, что попытка приобрести единственную защелку может затянуться и мне хотелось бы знать, что в этом случае происходит: вернется ли сеанс 2 в предыдущем примере в начало очереди или будет перемещен в ее конец? Нам остается только надеяться, что этот механизм более эффективен, чем предыдущий, позволявший процессам устанавливать таймеры, чтобы они могли выполнять повторные попытки приобретения защелки.
Примечание. На протяжении всей этой статьи я говорю о сеансах, приобретающих защелки. Технически, под сеансом подразумевается процесс, попытка которого получить защелку оканчивается успехом, неудачей или переходом в режим ожидания, а также процесс, который в конечном итоге удерживает защелку (как можно видеть в динамическом представлении v$latch_holder, основанном на структуре x$ksuprlatch).
Можно надеяться, что попытки приобретения исключительной защелки встречаются относительно редко и потому не должны приводить к значительным потерям времени, но подход на основе списков ожидания для разделяемых защелок действительно может ухудшать производительность.
Если разделяемая защелка приобретена и удерживается в исключительном (монопольном) режиме, запрос на ее приобретение в разделяемом режиме тут же поставит запрашивающий процесс в конец очереди, а запрос на приобретение в исключительном режиме поставит процесс в конец очереди после выполнения короткого цикла. То есть, в результате единственного запроса на приобретение защелки в исключительном режиме, процесс может оказаться в конце длинной очереди. После освобождения защелки, ее сможет приобрести процесс из очереди или любой другой процесс, не успевший попасть в очередь. Если большая часть процессов приобретает защелку только для чтения (в разделяемом режиме), очередь должна опустеть очень быстро (каждый процесс, освобождающий защелку, позволяет приобрести защелку одному или более процессам из очереди). Но, как только защелка вновь будет приобретена в исключительном режиме, очередь вновь начнет наполняться процессами, пытающимися приобрести защелку в разделяемом режиме, в результате чего легко может получиться длинная очередь читающих процессов с редкими вкраплениями пишущих процессов. В подобных случаях относительно небольшое число пишущих процессов может вызывать значительные
задержки.
В вашем распоряжении не так много средств борьбы с подобными явлениями – это побочный эффект реализации механизма параллельного доступа к критическим областям памяти в Oracle. Имеется всего три стратегии, позволяющие ослабить отрицательное влияние:
На рис. 3 изображена (весьма упрощенно) структура библиотечного кэша; она отличается от структуры на рис. 1 наличием защелки библиотечного кэша «library cache latch».
Рис. 3. Второе приближение структуры небольшого
библиотечного кэша, включающей защелку
Это изображение, с небольшими вариациями, можно использовать в качестве схемы устройства библиотечного кэша, разделяемого пула списков свободных блоков, кэша буферов данных, кэша словаря (строк) и многих других механизмов управления памятью в Oracle.
Мы познакомились с возможностями создания сложных коллекций структур в памяти и с некоторыми начальными сведениями, благодаря которым мы теперь способны организовать обход элементов связанных списков или реализовать арифметику для поиска желаемых элементов. Мы можем найти все, что потребуется, выполняя арифметические операции или следуя по указателям, а механизм защелок гарантирует безопасное использование этих элементов.
Примечание. В версии Oracle 11g значительно изменился способ работы с библиотечным кэшем. Описание, представленное в этой статье, в основном относится к версиям Oracle, вплоть до 10g.
Защелки и масштабируемость
Выше в этой статье я упоминал, что в своей копии Oracle обнаружил наличие 131 072 хэш-блоков в библиотечном кэше. Если я захочу выполнить инструкцию SQL из командной строки SQL*Plus, первое, что сделает серверный процесс, управляющий моим сеансом, – попытается найти эту инструкцию в библиотечном кэше. Для этого он выполнит некоторые арифметические операции, чтобы преобразовать текст инструкции в хэш-код и получить номер хэш-блока, а затем обойдет элементы связанного списка (хэш-цепочки). Не менее важным этапом в этой процедуре является поиск защелки, защищающей данный хэш-блок, то есть, Oracle приобретет защелку, обойдет элементы связанного списка, выполнит необходимые операции с найденным объектом и освободит защелку.
Степень конкуренции за защелку определяется тремя важными аспектами:
До версии 10g число защелок, охватывающих библиотечный кэш, было очень невелико. В моей скромной системе доступ к 131 072 хэш-блокам защищали всего три защелки. Число защелок зависит от числа процессоров (примерно совпадает с числом в параметре cpu_count), но не превышает 67. Это удивительно маленькое число, правда, учитывая вероятность конфликтов даже при наличии небольшого числа часто выполняемых инструкций. Два процесса, выполняющие разные инструкции и даже не обращающиеся к одному и тому же хэш-блоку, могут конфликтовать в споре за обладание защелкой – для этого достаточно, чтобы они обращались к разным хэш-блокам, защищенным одной и той же защелкой.
Учитывая небольшое число защелок, вы едва ли удивитесь, узнав, что существуют механизмы, способные уменьшить число обращений к кэшу с целью найти некоторый объект. Мы можем связать объект с блокировкой KGL (KGL lock) один раз и использовать эту блокировку постоянно, как более короткий путь к объекту, а показать, что объект занят, можно, связав его с закреплением KGL (KGL pin). (Обе эти структуры изменились с введением мьютексов в версии 10g и почти всегда используются при работе с библиотечным кэшем в версии 11g.).
Что касается продолжительности удержания защелок: похоже, что в Oracle Corp. упорно работают в направлении уменьшения времени удержания защелок, потому что в этой области постоянно что-то изменяется с каждой новой версией, выпуском, исправлением. Иногда это выражается в дроблении задач на более мелкие фрагменты и введении новых типов защелок, защищающих эти фрагменты. Имеется (или существовало) несколько разных типов защелок, имеющих отношение к библиотечному кэшу. Чтобы показать, как изменяются подходы, в табл. 4 приводится список защелок, имеющихся в разных версиях Oracle.
Защелка | 8.1.7.4 | 9.2.0.8 | 10.2.0.5 | 11.2.0.2 |
Library cache load lock | X | X | X | X |
Library cache pin | X | X | X | |
Library cache pin allocation | X | X | ||
Library cache lock | X | X | ||
Library cache lock allocation | X | |||
Library cache hash chain | X |
Я не собираюсь подробно описывать, для чего служат все эти защелки и структуры, которые они защищают – тем более, что большая их часть исчезла в версии 11g.
Я считаю, что пришло время дать небольшой комментарий относительно мьютексов, потому что особенностями реализации и использования они очень похожи на защелки.
Мьютексы были введены в реализацию поддержки библиотечного кэша в версии Oracle 10.2, с целью заменить закрепления (pins). По своей сути мьютексы – это «приватные мини-защелки», являющиеся частью объектов библиотечного кэша. Это означает, что вместо небольшого числа защелок, охватывающих большое число объектов – с сопутствующим риском задержек из-за конкуренции – теперь в нашем распоряжении имеются мьютексы, по одному для каждого хэш-блока в библиотечном кэше и по два – на каждый родительский и дочерний курсор (один для замены KGL pin, а другой предназначен для обработки зависимостей), что должно способствовать улучшению масштабируемости часто используемых инструкций.
Недостаток такого решения состоит в том, что теперь мы получим меньше информации, если возникнет какая-нибудь проблема. Код поддержки защелок поставляет массу информации, отвечающую на вопросы: «кто?», «что?», «где?», «когда?», «почему?», «как часто?» и «как много?». Код поддержки мьютексов действует быстрее, но несет меньше подобной информации. Тем не менее, когда вы узнаете, как (и почему) Oracle использует блокировки (locking) и закрепления (pinning) в библиотечном кэше, вы оцените преимущества мьютексов.