Защелки в базе данных Oracle: подробный разбор механизма

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

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

Как можно заключить из комментариев выше, мьютексы (которые появились в 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, можно увидеть следующую запись (числовые значения могут отличаться в разных версиях):

ADDR      INDX    INST_ID    KSMFSNAM   KSMFSTYP   KSMFSADR   KSMFSSIZ
-------- ------- --------- ----------- ---------- ---------- ----------
035004B0    3923         1    kcbllsb_    ksqeq *   03D3C818          4  

Эта запись сообщает, что по адресу 0x035004b0 хранится значение 0x0d3c818, которое является элементом данных, имеющим размер 4 байта, и «указателем на некоторое значение типа ksqeq». Когда я вывел содержимое памяти по адресу 0x0d3c818, я обнаружил там значение 0x21a33960, которое является адресом первой строки в фиксированном массиве x$ksqeq. То есть, я нашел указатель на указатель, который ссылается на массив фиксированной длины – это может служить признаком, что я нашел сегментированный массив, где последний элемент сегмента указывает на первый элемент следующего сегмента.

Также существуют сегментированные массивы с двумя элементами в x$ksmfsv, один из которых хранит число сегментов, составляющих массив, а другой ссылается на массив указателей, указывающих на отдельные сегменты.

 

Связанные списки

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

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

TRN CTL:: seq: 0x0b02 chd: 0x0011 ctl: 0x001c inc: 0x00000000 nfb:
0x0001
TRN TBL:
  index  state  cflags  wrap#    uel        scn           dba
---------------------------------------------------------------
...
   0x11    9     0x00  0x2d25  0x001b 0x0000.041c818a 0x00805805
...
   0x19    9     0x00  0x2d24  0x002e 0x0000.041c81d1 0x00805c0b
...
   0x1b    9     0x00  0x2d23  0x0019 0x0000.041c81ce 0x00805c09
   0x1c    9     0x00  0x2d24  0xffff 0x0000.041c907c 0x00805c0d
...
   0x27    9     0x00  0x2d25  0x001c 0x0000.041c9072 0x00805c0d
...
   0x2e    9     0x00  0x2d1f  0x001a 0x0000.041c81d2 0x00805806

В разделе управления транзакциями указывается, что головой списка (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 имеется масса других мест, где используются двусвязные списки. И снова нам не нужно рыскать по всей памяти, чтобы найти их, так как одним из примеров таких списков может служить хорошо известная структура. Ниже приводится выдержка из дампа раздела заголовка индексного листового блока: 

Leaf block dump
===============
header address 116564572=0x6f2a25c
kdxcolev 0
KDXCOLEV Flags = - - -
kdxcolok 1
kdxcoopc 0x80: opcode=0: iot flags=--- is converted=Y
kdxconco 1
kdxcosdc 2
kdxconro 571
kdxcofbo 1178=0x49a
kdxcofeo 1190=0x4a6
kdxcoavs 12
kdxlespl 0
kdxlende 0
kdxlenxt 4194525=0x4000dd
kdxleprv 4194523=0x4000db
kdxledsz 6
kdxlebksz 8036

Обратите внимание на элементы 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), как список, прикрепленный к этой точке.

Библиотечный кэш в базе данных Oracle

Рис. 1. Приближенная структура очень небольшого
библиотечного кэша

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

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

Объяснение механизма защелок Oracle (latches) СУБД Oracle

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

Вставка объекта в двусвязный список в базе данных Oracle

Рис. 2. Вставка объекта в двусвязный список

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

Мантра, хорошо известная всем пользователям Oracle, гласит: «Читающий сеанс не блокирует пишущий, а пишущий не блокирует читающий», – но она истинна только на уровне данных. На уровне памяти бывают моменты, когда читающие сеансы должны блокировать пишущие, и единственный пишущий сеанс должен блокировать все другие операции.

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

 

Защелки

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

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

По своей сути защелка представляет собой комбинацию из ячейки памяти в SGA и атомарной операции, с помощью которой осуществляется проверка и изменение значения в этой ячейке. (Имеется также инфраструктура поддержки, занимающая в памяти от 100 до 200 байт; точный объем памяти, занимаемой этой инфраструктурой, зависит от версии Oracle.)

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

 

Логика работы защелок

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

Записать в регистр X адрес A ячейки памяти защелки

Если значение по адресу A равно нулю, записать туда 0xff ***

Если значение по адресу A равно 0xff, значит вы «владеете» защелкой

Если нет, вернуться обратно и повторить попытку, и так пару тысяч раз

Решение, что делать, если даже после пары тысяч попыток не удалось приобрести защелку, мы обсудим несколькими страницами ниже. Строка, отмеченная звездочками ***, описывает атомарную операцию – после нее сеанс может сказать: «если значение защелки было равно нулю, а стало равно 0xff, значит мне удалось приобрести защелку». Если бы эта операция, которая часто реализуется единственной машинной инструкцией «проверить и установить», могла быть прервана, тогда вы могли бы столкнуться со следующей последовательностью событий:

  • Сеанс A готовится к входу в цикл.
  • Сеанс B готовится к входу в цикл.
  • Сеанс A проверяет защелку, обнаруживает нуль и в этот момент прерывается.
  • Сеанс B проверяет защелку, обнаруживает нуль, устанавливает значение 0xff и в этот момент прерывается.
  • Сеанс A возобновляет работу, устанавливает значение (с опозданием) 0xff и полагает, что приобрел защелку.
  • Сеанс B возобновляет работу, обнаруживает значение 0xff и полагает, что приобрел защелку.

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

  • Сеанс A выполняется на процессоре 1, проверяет защелку и обнаруживает нуль.
  • Сеанс B выполняется на процессоре 2, проверяет защелку и обнаруживает нуль.
  • Сеанс A устанавливает значение 0xff и полагает, что приобрел защелку.
  • Сеанс B устанавливает значение 0xff и полагает, что приобрел защелку.

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

Примечание. Желающим больше узнать о путях взаимодействий процессоров в многопроцессорных системах, а также о побочных эффектах и опасностях таких взаимодействий, я рекомендую обратиться к книге эксперта по Oracle Джеймса Морли (James Morle) «Scaling Oracle8i», которая в настоящее время доступна бесплатно для загрузки по адресу: www.scaleabilities. co.uk/index.php/Books/. (Не обращайте внимания на номер версии 8i в названии – основные принципы почти не изменились с тех пор.)

Недостаток исключительной защелки, как можно догадаться, заключается в ее исключительности. В каждый конкретный момент времени такой защелкой может владеть только один сеанс, то есть, только один сеанс обладает доступом к защищенному ресурсу. Это плохо сказывается на масштабировании в высоконагруженных системах, где одновременно может выполняться множество сеансов, которым нужно лишь прочитать содержимое структуры в памяти и которые не собираются изменять его. По этой причине в версии 9i, для защиты особенно интенсивно используемых участков кода, были реализованы разделяемые защелки. Возможно, что их появлению способствовало появление инструкций «сравнения с обменом» (compare and swap) в наиболее распространенных процессорных архитектурах.
Разумеется, конкретные реализации зависят от особенностей аппаратных архитектур, но обычно все их можно выразить на псевдокоде, как показано ниже:

  • Установить флаг F в нулевое значение
  • Записать в регистр X адрес защелки L
  • Записать в регистр Y текущее значение, хранящееся по адресу L
  • Записать в регистр Z новое значение для ячейки памяти с адресом L
  • Если «Y» = «L», записать в L «значение Z» и установить флаг F в значение 1 ***
  • Если флаг F установлен в значение 1, значит сеанс изменил значение защелки

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

  • Цикл в пару тысяч итераций
  • Если бит записи установлен, вернуться в начало цикла
  • Присвоить защелке значение value+1 (получить право на чтение)
  • Если флаг установлен, покинуть цикл

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

Алгоритм приобретения защелки пишущим процессом (которому требуется исключительный доступ к объекту), с другой стороны, мог бы выглядеть так:

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

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

Примечание. Особенности поведения защелок в постоянно менялись с течением времени, но общие принципы остаются неизменными с версии 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).

Статистика Описание
gets Число попыток, предпринятых процессом для приобретения защелки в режиме «готовности к ожиданию». Эта статистика увеличивается только после приобретения защелки, независимо от числа неудач и периодов ожиданий.
misses Число попыток, предпринятых процессом для приобретения защелки в режиме «готовности к ожиданию» и потерпевших неудачу в самой первой операции «проверить и установить»/«сравнить и заменить». Неудача обычно оканчивается удачей, поэтому misses фактически является подмножеством gets.
spin_gets Число попыток, предпринятых процессом для приобретения защелки в режиме «готовности к ожиданию» и потерпевших неудачу в первой операции «проверить и установить»/«сравнить и заменить», но завершившихся удачей в последующих циклах. Большое значение misses может свидетельствовать о больших потерях процессорного времени, даже если неудачи в конечном итоге завершаются удачей. Значение spin_gets является подмножеством misses.
sleeps Сколько раз попытка приобрести защелку в режиме «готовности к ожиданию» оканчивалась неудачей, даже после выполнения всех итераций в цикле. В зависимости от версии, Oracle может «пробудиться» после приостановки и попытаться выполнить дополнительные итерации, поэтому единственная неудача может приводить к множеству приостановок (sleeps). Такой способ приобретения (с несколькими приостановками) защелок не поддерживается в самых новых версиях Oracle.
sleep1 ... sleep11 В эти столбцы Oracle записывает число попыток, когда процесс приостанавливался, пытаясь приобрести защелку в режиме «готовности к ожиданию». Начиная с версии 8.0, Oracle больше не изменяет значения столбцов sleepN, где N > 3, то есть, выше sleep3 (более того, эти столбцы вообще отсутствуют в физической структуре); если процесс приостанавливался более трех раз, он не будет заполнять другие столбцы, выше sleep3. Начиная с версии 10.2, исчезли и первые три столбца sleepN (вместе с множеством других столбцов).
immediate_gets Число попыток, предпринятых процессом для приобретения защелки в режиме «без ожидания», и увенчавшихся успехом после первой операции «проверить и установить»/«сравнить и заменить».
immediate_misses Число попыток, предпринятых процессом для приобретения защелки в режиме «без ожидания», и потерпевших неудачу после первой операции «проверить и установить»/«сравнить и заменить». Обратите внимание, что immediate_misses не увеличивает счетчик immediate_gets. Примером может служить попытка приобретения защелки «redo allocation» (по крайней мере, в 10g): представьте, что в сеансе несколько приватных потоков повторения пытаются по очереди приобрести защелку и возвращают управление сразу после неудачной попытки приобрести защелку.
wait_time Общее время, затраченное процессом на ожидание защелки. Имеет отношение только к попыткам приобрести защелку в режиме «готовности к ожиданию». Время отображается в микросекундах, но (в зависимости от версии, платформы и наличия ошибок) может отображаться в других единицах измерения, поэтому я всегда сравниваю его с временем ожидания latch free из v$system_event.

 

Неудачные попытки приобретения защелок

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

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

WAIT #4: nam=’latch free’ ela= 1 p1=-1351741396 p2=62 p3=0
WAIT #4: nam=’latch free’ ela= 1 p1=-1351741396 p2=62 p3=1
WAIT #4: nam=’latch free’ ela= 1 p1=-1351741396 p2=62 p3=2
WAIT #4: nam=’latch free’ ela= 3 p1=-1351741396 p2=62 p3=3
WAIT #4: nam=’latch free’ ela= 3 p1=-1351741396 p2=62 p3=4
WAIT #4: nam=’latch free’ ela= 7 p1=-1351741396 p2=62 p3=5
WAIT #4: nam=’latch free’ ela= 9 p1=-1351741396 p2=62 p3=6
WAIT #4: nam=’latch free’ ela= 18 p1=-1351741396 p2=62 p3=7
WAIT #4: nam=’latch free’ ela= 15 p1=-1351741396 p2=62 p3=8
WAIT #4: nam=’latch free’ ela= 55 p1=-1351741396 p2=62 p3=9
WAIT #4: nam=’latch free’ ela= 33 p1=-1351741396 p2=62 p3=10
WAIT #4: nam=’latch free’ ela= 69 p1=-1351741396 p2=62 p3=11
WAIT #4: nam=’latch free’ ela= 100 p1=-1351741396 p2=62 p3=12
WAIT #4: nam=’latch free’ ela= 150 p1=-1351741396 p2=62 p3=13
WAIT #4: nam=’latch free’ ela= 151 p1=-1351741396 p2=62 p3=14
WAIT #4: nam=’latch free’ ela= 205 p1=-1351741396 p2=62 p3=15

Прошедшее время (elapsed time, ela= nnnn, измеряется в сотых долях секунды) должно удваиваться после каждого второго периода ожидания, пока не достигнет максимального значения, равного 2 секундам1, но высокая нагрузка на систему не позволила точно выдержать это требование.

Примечание. Интересно отметить, что минимальный интервал ожидания, равный 1/100 секунды, был введен в Oracle 6 (или в более ранней версии), когда тактовая частота самых «быстрых» процессоров не превышала нескольких мегагерц. Теперь, когда тактовая частота процессоров измеряется гигагерцами, время ожидания 1/100 секунды оказалось в сотни раз больше, чем могло бы быть.

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

1 Steve Adams «Oracle8i Internal Services for Waits, Latches, Locks and Memory»
(Sebastopol, CA: O’Reilly Media, 1999).

«почти» отличается для разных типов защелок – Андрей Николаев, о котором я говорил выше, с помощью DTrace в системе Solaris исследовал некоторые детали и пришел к выводам, перечисленным в табл. 3.

Операция с защелкой Используемый метод
Приобретение исключительной (монопольной) защелки. Попытка немедленного приобретения, вход в пустой цикл (в данном случае цикл выполняет 20 000 итераций), включение в список ожидания, попытка немедленного приобретения, приостановка.
Приобретение разделяемой защелки в исключительном режиме, когда другой процесс удерживает ее в любом режиме (разделяемом, исключительном или блокирующем). Вход в пустой цикл (в данном случае цикл выполняет 2000 итераций), включение в список ожидания, повторение пустого цикла, приостановка в случае неудачи.
Приобретение разделяемой защелки в разделяемом режиме (для чтения), когда другой процесс удерживает ее в исключительном или блокирующем режиме. Вход в пустой цикл (в данном случае цикл выполняет 2000 итераций), включение в список ожидания, повторение пустого цикла, приостановка в случае неудачи.
Приобретение разделяемой защелки в разделяемом режиме (для чтения), когда другой процесс намеревается приобрести ее в исключительном или блокирующем режиме. Время на выполнение пустого цикла не тратится – сразу происходит простановка.
Приобретение разделяемой защелки в разделяемом режиме (для чтения), когда другой процесс удерживает ее в разделяемом режиме. Цикл выполняет cpu_count + 2 итераций перед приостановкой.

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

Примечание. Механизм приостановки/возобновления для защелок был доступен и в ранних версиях Oracle, но применялся далеко не ко всем защелкам. Усовершенствованные его реализации в новых версиях Oracle зависят от доступности новейших особенностей в операционных системах и могут (но не обязаны) регулироваться с помощью скрытого параметра _enable_reliable_latch_waits.

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

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

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

  • Сеанс 1 приобретает исключительную защелку.
  • Сеанс 2 пытается приобрести исключительную защелку и приостанавливается.
  • Сеанс 1 освобождает защелку и активизирует сеанс 2.
  • Сеанс 3 приобретает защелку до того, как сеанс 2 успевает возобновить работу.
  • Сеанс 2 возобновляется, выполняет пустой цикл и снова приостанавливается.

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

Теоретически число «периодических остановок» (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. Имеется всего три стратегии, позволяющие ослабить отрицательное влияние:

  • Как проектировщик/разработчик/программист, избегайте большого числа операций с защелками; старайтесь, например, разбивать задачи на большое число маленьких этапов.
  • Как администратор, ищите (документированные) возможности увеличить число защелок, охватывающих определенный тип деятельности. Классической защитной мерой может служить выявление «горячих» данных и поиск путей их распределения по большому числу блоков. Также желательно следить за выходом исправлений и своевременно применять их.
  • Как проектировщик/разработчик в Oracle Corp., создавайте механизмы, уменьшающие зависимость от защелок. (Например, шире используйте закрепления (pins) и мьютексы.)

На рис. 3 изображена (весьма упрощенно) структура библиотечного кэша; она отличается от структуры на рис. 1 наличием защелки библиотечного кэша «library cache latch».

библиотечный кэш с защелкой в базе данных Oracle

Рис. 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) в библиотечном кэше, вы оцените преимущества мьютексов.

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

Создание базы данных Oracle
Создание базы данных Oracle 34401 просмотров Александров Попков Wed, 14 Nov 2018, 12:44:39
Видеокурс по администрированию...
Видеокурс по администрированию... 10719 просмотров Илья Дергунов Mon, 14 May 2018, 05:08:47
Oracle и непроцедурный доступ ...
Oracle и непроцедурный доступ ... 8522 просмотров Antoni Tue, 21 Nov 2017, 13:32:50
СУБД Oracle: обзор характерист...
СУБД Oracle: обзор характерист... 15813 просмотров Antoni Fri, 24 Nov 2017, 07:35:05
Войдите чтобы комментировать