четверг, 14 апреля 2016 г.

Ядро oracle

на основе одноименной книги Дж. Льюиса

1. Согласованное чтение
2. Восстановление данных
3. Принцип кэширования блоков в память при чтении
4. Парсинг запросов
5. RAC - кластер экземпляров oracle
6. Блокировки и защелки


1. согласованное чтение
общий принцип:

a. транзакция 1 изменяет данные:
 * в буферный кэш скидывается грязный блок
  ** в блоке данных записывается список недавних транзакий ITL (включая сейчас выполняющуюся)
  ** если транзакция сейчас выполняется, то также проставляется признак блокировки строки в блоке
 * процесс lgwr пишет:
  ** в redo журнал повтора - новое значение, новый scn, список ITL
  ** в undo журнал отката - старое значение и старый scn
 * процесс dbwr пишет:
  ** асинхронно с задержкой записывает данные в блок базы
   *** сразу после commit записывается не более 30% данных
   *** остальная часть записывается отложенно при следующем select из строк этой таблицы.
   Детектировать можно через события "db block change, consisten gets - examination"
   Т.е. не стоит удивляться, что первый select после большого update будет выполняться очень долго.

b. транзакция 2 читает данные:
 * считываются данные из буферного кэша или напрямую с диска
 * просматривается список ITL на наличие незавершенных транзакций
 * в любом случае (не зависимо от ITL) сверяется SCN транзакции и SCN блока (ITL незавершенной транзакции)
  ** если SCN блока/ITL оказывается больше запрашиваемого, то данные берутся из сегментов отката UNDO через ссылку из ITL
   *** поиск по ITL может продолжиться и дальше рекурсивно, если SCN запроса опять меньше SCN из undo
   *** если данные в undo не находятся, то это является причиной ошибки "snapshot too old"
 * в случае недавнего обновления блока, строка может оказаться помеченной как сейчас обновляемая и со старым SCN в списке ITL выполняется операция отложенная очистка:
  ** берутся данные из UNDO сегмента, смотрится, что транзакция подтверждена
  ** сбрасывается флаг блокировки в строке блока и ITL (что повторно генерирует redo логи при select таблицы)
  ** в случае отсутствия блока в буферном кэше и чтения с диска дополнительно изменяется номер SCN на максимальный (т.к. отсутствие блока в кэше говорит об однозначно последней версии на диске)

Примечание: как многоверсионность сделана в Postgree:
Устаревшие строки (после delete/update) хранятся в том же месте, где основная таблица.
Определение версии используются дополнительные идентификаторы в самой строке.
При update выполняются действия:
* Создается строка с новыми данными
* От старой строки создается указатель на новую
* У старой строки проставляется:
 ** xmin - идентификатор транзакции от старой версии строки
 ** xmax - идентификатор транзакции от новой версии строки

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

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


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

Redo лог также может использоваться в репликации. В зависимости от вида лога - текст или бинарные данные, будут разные преимущества и недостатки:

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

Текстовая репликация (можно добиться в Oracle через log miner - логический standby):
+ меньше данных передавать по сети (только сам текст, без зависимых изменений)
+ меньше блокировок (обрабатывается как обычный запрос)
+ возможность репликации между разными версиями
- ресурсоемко при применении (обрабатывается как обычный запрос, обновляя зависимые структуры)

3. Общий принцип кэширования блоков в память при чтении
В общем виде кэш - это хэш массив связанных списков.
Список внутри выстраивается по частоте обращения к блоку и имеет указатель на середину, названную холодной (cold) точкой.
Просмотр мест под новый прочитанный блок начинается с конца списка, проверяется что буфер не грязный (уже сброшен на диск) и не закреплен (сейчас не редактируется).
Варианты последующих действий с последним, самым редкоиспользуемым блоком:
 а. в конце оказывается буфер со счетчиком обращений = 1, то он открепляется от списка, а новый блок помещается в среднюю точку cold. Для этого ссылки соседей открепляются друг от друга и перенацеливаются на новый блок, который становится новой серединой.
 б. в конце буфер со счетчиком > 1, тогда последний блок открепляется, счетчик обращений делится на 2 и этот блок перемещается в начало списка.
Середина списка также смещается влево, одновременно делая число обращений = 1 у новой элемента-середины, если к ней никто не обращался за время смещения от начала до середины.
Новый блок помещается в центр списка cold.
Более подробное описание можно видеть тут: Oracle: Lru буферный кэш

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

Отдельно упомяну оптимизации, которые проводит oracle при полном сканировании таблицы (full table scan).
Если их не производить, то огромная таблица способна быстро вытолкнуть все буферные данные из sga.
Существует 3 стратегии
 * Маленькие таблицы < 2% размера буфера (определяется параметром "_small_table_threshold") - стандартная работа буфера
 * средние таблицы < 10% - при первом чтении не увеличивает счетчик обращений у уже загруженных ранее блоков. Но после этого работа с этими буферами проходит стандартно, с увеличением счетчика.
 * большие таблицы >= 10% - данные таблицы загружаются в кэш, но не более 25% его размера. Счетчик устанавливается в минимальное значение и больше не увеличивается, за счет чего блоки таблицы быстро уходят из буфера.

Стоит отметить:
 * размер таблицы берется из ее статистики и отсутствие статистики может сыграть злую шутку.
 * если таблица значительно больше буфера или нужна разово, можно указать oracle необходимость читать таблицу напрямую в приватную память процесса pga, минуя буферный кэш sga "_serial_direct_read = always"

4. Парсинг запросов
приблизительное описание было тут: http://blog.skahin.ru/2016/02/oracle-bind.html
буферное кэширование планов происходит примерно также, как кэширование таблиц, описанное в п.3


5. RAC - кластер экземпляров oracle
В общем виде rac представляет из себя кластер экземпляров oracle с собственными процессами записи логов (lgwr), данных бд (dbwr), разделяемыми кэшами sga, но с общим файлом логов и базы данных.

Отсюда плюсы:
+ Масштабируемость при увеличении числа пользователей в oltp системе.
Достигается за счет распределния множества небольших запросов на разные кластеры rac.
+ Надежность в случае отказа. При отказе одной из нод кластера: незавершенные и новые запросы подхватятся другими.
Реализуется это за счет:
 * Общего dns адреса для группы ip, для выполнения запроса выбирается менее загруженная нода
 * Контроль за доступностью за каждой нодой путем опроса соседей и записи состояния в общий диск кворума.
  ** Если оказывается, что одна нода не пишет в кворум или не отзывается, то она отсоединяется от сети
  ** Если сама нода перестает видеть большую часть соседей (>50%), то она самостоятельно отключается от сети. Если она видит больше 50% нод, то отключается меньшая часть.

Минусы какие я вижу:
- Плохо подходит для olap систем с небольшим числом тяжелых запросов.
 Частично может решаться за счет параллельной обработки на разных нодах, но что тоже только частичное решение, т.к. это влечет увеличения трафика на обмен данными между потоками разных нод.

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

Последовательности (sequence) приобретают новые свойства в распределенной системе:
 * По умолчанию сиквенс разносится по нодам по размеру кэша.
Допустим размер кэша = 1000 и число нод 4, тогда нода 1 получает значения 1-1000, нода 2: 1001-2002 и т.д.
Если сиквенс сделать не кэшируемым, то это породит гигантский трафик синхронизации счетчика между нодами.
Т.е. в кластерной системе нельзя надеяться на непрерывность сиквенса.
 * Если непрерывность все таки нужна, то можно указать параметр "ordered" при создании и сделать сиквенс не кэшируемым. В этом случае выдача нового значения будет происходить всегда в 1 месте и при инкременте будет получаться защелка.
Этот вариант очень плох, на него не стоит надеяться, т.к. сиквенс становится узким местом распределенной системы, и также не гарантирует непрерывность счетчика при rollback, аналогично некластерным системам.


6. Блокировки и защелки
1. Защелки - ограниченный ресурс БД. Используется для защиты внутренних объектов бд от непреднамеренного изменения.
Защелки бывают 2 видов:
* исключительные - захват защелки блокирует любое одновременное обращение к этой области памяти.
* разделяемые - могут использоваться одновременно несколькими потоками чтения и одним записи.
Принцип работы построен на атомарной операции. Поток перед обращением к области памяти пытается захватить защелку атомарной операцией:
- если защелка уже занята, то процесс уходит в цикл ожидания
- если нет, то установка флага и начала работы (проверка и установка флага проходит атомарно - за один такт процессора)
В разделяемых защелка флаг расширяется из просто флага до цифры: 0x1000005. В нем старший бит означает блокировку защелки на запись (0x1), а младшие биты число читающих сеансов (0005).
- новые читающий сеанс увеличивает младший бит, при окончании чтения бит уменьшается
- записывающий сеанс устанавливает старший бит и блокирует новые чтения (они становятся в очередь) и ждет когда счетчик читающих опустится до 0
Для понимания ограниченности ресурса защелок: на весь буферный кэш выделяется защелок = количество cpu / 2.
Защелки используется в Library cache, буферном кэш и Redo. В представлении v$latch можно видеть статистику использования защелок, сколько запрошено, сколько занято сразу, а сколько ушло в ожидание (v$lathholder - информация о том кто держит защелку)

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

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

События связанные с защелками и мьютексами:
Блокировки библиотечного кэша:
* Cursor: mutex ... - блокировка курсоров при сборе статистики
* Cursor: pin S ... - поиск субплана и парсинг
Блокировки буферного кэша:
* latch: cache buffer chains - конкуренция за горячий блок
* latch: cache buffer chains LRU chains - кэш слишком маленький, блоки быстро замещаются из LRU
* Buffer busy waits - множество процессов конкурируют за чтение/вставку в один блок в кэше
* Free buffer waits - дбврайтер медленно скидывать грязные блоки на диск

Комментариев нет:

Отправить комментарий