Процесс записи в журнал и на диск в базе данных Oracle в деталях

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

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

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

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

Определенную сложность для разработчиков Oracle представляло проектирование инфраструктуры, которая позволяла бы выполнить этап восстановления максимально быстро и, возможно, приостанавливала бы все обычные действия на время восстановления.

Ключом к возможности восстановления является «своевременная» запись журнала на диск. Ключом к минимизации затрат времени на восстановление является постоянная запись измененных блоков данных на диск, а ключом к минимизации затрат на запись блоков является сбор блоков в пакеты и запись их в правильном порядке, что подразумевает (в случае с Oracle) запись сначала более старых блоков.

Далее мы посмотрим, как действует процесс записи в журнал (log writer, lgwr) и я объясню, что означает «своевременная» запись. Мы также познакомимся с работой процесса записи данных (database writer, dbwr), я объясню, что означает «сначала более старые», и расскажу о недостатках этой стратегии.

Однако, передо мной стоит одна проблема – с чего начать свой рассказ. О чем рассказать в первую очередь – о lgwr или dbwr? Можно было бы привести доводы в пользу процесса записи в журнал, потому что он должен записывать описание изменений на диск до того, как будут записаны измененные блоки данных. С другой стороны, процесс записи данных иногда вызывает процесс записи в журнал и во время записи данных сам генерирует записи повторения изменений, поэтому есть веские причины сначала рассказать о dbwr.

Примечание. Одна из важнейших особенностей Oracle заключается в том, что процесс записи данных никогда не записывает измененные блоки данных на диск до того, как процесс записи в журнал не сохранит записи, описывающие изменения. Такая стратегия опережающего журналирования (write-ahead logging) играет важную роль для механизма восстановления. Фактически, файлы журнала (включая архивные) образуют актуальную версию базы данных – файлы данных сами по себе являются лишь последним моментальным снимком базы данных. (Разумеется, сюда не относятся любые нежурналируемые операции.)

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

 

Запись в журнал

Процесс записи в журнал решает одну критически важную задачу: копирует содержимое буфера журнала из памяти на диск. Однако объем работы, окружающей эту задачу, с годами только увеличивался, из-за увеличения объемов изменений и повышения уровня параллелизма. (Кроме того, с годами расширялись и требования, с целью обеспечить возможность записи в удаленную резервную базу данных посредством обращений к процессам RFS [Remote File Server – удаленный файловый сервер] в 10g, и NSS или NSA [синхронный/асинхронный транспорт журналов] в11g.

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

Примечание. Не так давно производители начали выпускать жесткие диски с размером сектора, равным 4 Кбайтам. По причинам, с которыми вы познакомитесь далее, использование блоков большего размера для файла журнала может вынуждать lgwr впустую расходовать значительное пространство в файле журнала. С другой стороны, использование блоков меньшего размера может приводить к ухудшению эффективности записи в журнал (из-за увеличения числа циклов записи/чтения). Начиная с версии 11.2, Oracle позволяет администраторам выбирать меньшее из двух зол, давая возможность настраивать размер блока для файла журнала из трех значений: 512 байт, 1 Кбайт или 4 Кбайта.

Файлы журнала автоматически разбиваются (форматируются) на блоки, размер которых совпадает с размером дискового сектора (обычно 512 байт), а сам буфер журнала отображается на текущий файл журнала как «скользящее окно», каждый блок в котором сопровождается 16-байтным заголовком. На рис. 1 изображен буфер журнала, имеющий размер 8 блоков.

Упрощенная схема строения буфера журнала Oracle

Рис. 1. Упрощенная схема строения буфера журнала

Буфер журнала на рис. 1 заполнялся и использовался повторно 11 раз (когда буфер был впервые отформатирован, он соответствовал блокам в файле с номерами от 1 до 8). Текущее состояние буфера соответствует 12 циклу использования буфера, именно это объясняет, почему последний блок в буфере имеет номер 88 (то есть, 8 блоков × 11 циклов). Процесс записи в журнал только что записал блоки с 89 по 91, вскоре запишет блоки с 92 по 94 и присвоит блоку 87 новый порядковый номер 95.

Примечание. В Интернете можно найти множество заметок о настройке размеров файлов журнала, но многие из них давно устарели. В действительности, начиная с версии 10g, не следует изменять параметр log_buffer; он автоматически настраивается экземпляром в момент запуска так, чтобы все общедоступные буферы журнала имели размер в несколько мегабайт. В особых случаях, чтобы обеспечить выделение памяти для нескольких гранул, может потребоваться присвоить этому параметру значение, равное 16 или 32 Мбайт, но это имеет смысл, только если наблюдаются существенные потери времени из-за ожидания доступа к пространству буфера.

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

Ключевые точки в буфере журнала базы данных Oracle

Рис. 2. Ключевые точки в буфере журнала

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

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

Как видите, описание простое, но вопросы, которые оно поднимает, очень важные. Какие события приводят к запуску lgwr? Как сеансам удается не затирать информацию друг друга? Что происходит, когда сеансам требуется больше пространства, чем имеется в наличии? Какие риски возможны из-за конкуренции между lgwr и другими сеансами? Начнем с самого простого – какие события приводят к запуску lgwr.

 

Цикл записи в журнал базы данных Oracle

Существует четыре основных причины, вынуждающие lgwr начать запись:

  • по таймеру, который устанавливается самим процессом lgwr и срабатывает каждые 3 секунды;
  • когда буфер журнала (общедоступный) заполнится на одну треть;
  • когда суммарный объем несохраненных данных в буфере журнала (общедоступном) превысит 1 Мбайт;
  • когда сеанс выполнит инструкцию commit; или rollback; (см. примечание ниже).

Примечание. Когда сеанс подтверждает транзакцию, он генерирует запись повторения, описывающую изменения в слоте таблицы транзакций, в заголовке undo-сегмента. Помещает эту запись в буфер журнала, применяет ее к заголовку undo-сегмента, вызывает процесс записи в журнал, чтобы вытолкнуть буфер на диск, и затем ждет, пока буфер фактически будет записан в файл на диске. Операция отката выполняется аналогично: после применения вектора отмены изменений (и журналирования изменений, выполненных в результате применения этого вектора) на последнем шаге также обновляется слот в таблице транзакций и вызывается процесс записи в журнал. Этот механизм подтверждения/отката соответствует четвертому пункту списка выше и обеспечивает надежность транзакций.

Ситуация, описанная в первом пункте, легко поддается наблюдению, особенно если включить трассировку состояний ожидания для процесса lgwr. Каждые 3 секунды вы будете видеть, что lgwr приостанавливается в ожидании сообщения rdbms ipc message (ожидание взаимодействия между процессами) с таймаутом 300 сотых долей секунды. То есть, если в течение этого времени ничего не произойдет, lgwr возобновит работу, выполнит некоторые подготовительные операции и запишет на диск данные, имеющиеся в буфере.

Чтобы проверить реакцию процесса lgwr на события, описанные в двух следующих пунктах (1 Мбайт или заполнение более, чем на одну треть), можно выполнить массив изменений известного размера из одного сеанса и проверить статистики: messages sent и redo size в сеансе пользователя, и messages received в сеансе lgwr. Если в параметре log_buffer установить значение меньше 3 Мбайт, передача сообщений из пользовательского сеанса в сеанс lgwr будет происходить при превышении redo size одной трети объема буфера журнала (можно видеть в структуре x$kcrfstrand). Но если размер буфера журнала сделать больше 3 Мбайт, передача сообщений будет происходить при превышении redo size величины 1 Мбайт.

Ограничения на объем несохраненных данных поднимают дополнительные вопросы. Процесс записи в журнал приостанавливается сразу на 3 секунды, как же тогда он узнает, какой объем буфера используется? Ответ прост: каждый раз, когда сеанс резервирует память в буфере, он проверяет общий объем зарезервированного пространства (то есть, за размером области между границами «Конец свободного пространства» и «Начало свободного пространства»). Если общий объем превысит тот или иной предел, сеанс посылает сообщение процессу lgwr сразу после копирования своих записей повторения в буфер.

Но тут возникает другой вопрос: не получится ли так, что в высоконагруженных системах, где имеется множество сеансов, очень быстро генерирующих записи повторения, процессу lgwr одновременно будет послано множество избыточных сообщений? Нет, не получится! Такого не происходит, благодаря флагу записи, о котором упоминалось выше.

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


Сообщения


Статистика redo synch writes подсчитывает, сколько раз сеанс отправил сообщение о подтверждении транзакции (статистика messages sent) процессу lgwr. Это не точное значение; в действительности «передача сообщений» может осуществляться без фактических сообщений.

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

Когда сеанс «посылает сообщение» процессу lgwr и тот не занят сохранением буфера на диск, lgwr находится вне очереди выполняющихся процессов, ожидая сообщения rdbms ipc message. То есть, сеанс просто вызывает операционную систему, чтобы вернуть процесс lgwr обратно в очередь выполняющихся процессов. Когда lgwr оказывается в голове очереди и возобновляет выполнение, ему не нужно знать, кто разбудил его или какую часть буфера следует записать. Он просто сохраняет столько, сколько может сохранить, и затем определяет, какие сеансы в настоящий момент ожидают сообщения log file sync, в которых значение buffer# (параметр parameter1 сообщения) ниже границы в буфере, где lgwr закончил запись.

Напротив, когда сеанс возобновляет работу по сообщению log file sync, он сначала проверяет, был ли удовлетворен его запрос на сохранение буфера, и только потом продолжает работу. (В версиях до 11.1 включительно таймаут ожидания сообщения log file sync жестко «зашит» в программный код и равен 1 секунде. В версиях 11.2.0.1 и выше таймаут настраивается и имеет значение по умолчанию 10 сотых долей секунды. То есть, сеанс может возобновить выполнение по истечении таймаута, а не в результате получения сообщения, сгенерированного процессом lgwr.) Проверка выполняется просто: сеанс знает, до какой позиции должен добраться lgwr, чтобы запрос мог считаться удовлетворенным (параметр buffer# сеанса) и он может проверить, где в данный момент начинается свободное пространство. Для такой проверки ему даже не нужно приобретать защелку redo allocation.


Четвертая причина, вынуждающая lgwr выполнить запись (когда сеанс вызывает инструкцию commit;), указана в списке правильно – здесь нет ошибки – если забыть о расширении в 10g, о котором я расскажу чуть ниже. Однако, как мы только что видели, если lgwr уже осуществляет запись, сеанс не отправит сообщение; он просто увеличит redo synch writes и перейдет в режим ожидания сообщения log file sync. Это влечет за собой еще одну причину заставить процесс lgwr произвести запись.

Когда lgwr завершит запись, он может проверить список активных сеансов, выяснить, какие из них ожидают сообщение log file sync и послать им его, чтобы те могли продолжить работу. (Я полагаю, что в ранних версиях Oracle для этого использовалась структура, на которой основано представление v$session, но в более поздних версиях Oracle могла быть введена еще одна структура, хранящая список таких сеансов.) Когда lgwr просматривает список сеансов, он может видеть, где в буфере находятся их записи подтверждения (столбец parameter1 представления v$session_wait в их buffer#) и послать им ожидаемое сообщение, а обнаружив все еще ожидающие сеансы, немедленно начать новый цикл записи. То есть, последней причиной, вынуждающей lgwr произвести запись буфера на диск, является обнаружение дополнительных записей подтверждения сразу после завершения записи.

 

Оптимизация PL/SQL

Как мы уже знаем, после сохранения записи подтверждения в буфере журнала, сеанс ждет, пока lgwr пошлет сообщение log file sync wait, чтобы продолжить работу. Таким способом Oracle гарантирует надежность транзакций. Но это не единственный путь. Существует малоизвестная оптимизация PL/SQL, из-за которой PL/SQL не всегда ждет завершения записи на диск.

Рассмотрим простой блок PL/SQL (см. сценарий core_commit_01. sql в пакете загружаемых примеров):

begin
     for r in (
             select id from t1
             where mod(id,20) = 0
     ) loop
             update t1
             set small_no = small_no + .1
             where id = r.id;
             commit;
     end loop;
end;
/

Этот простой фрагмент кода обновляет каждую 20-ю строку в таблице, хранящей 500 строк. То есть, всего выполняется 25 изменений и 25 подтверждений. Если выполнить этот код отдельно (в отсутствие других сеансов, генерирующих записи повторения и подтверждающих транзакции) и исследовать активность сеанса, можно было бы ожидать увидеть следующие значения статистик: 

user commits (session statistic) 25
messages sent (session statistic) 25
redo synch writes (session statistic) 25
log file sync (session events) 25
messages received (lgwr statistic) 25 redo writes (lgwr statistic) 25 log file parallel write (lgwr events) 25

Эти результаты можно было бы интерпретировать как 25 циклов, выполняющих следующую последовательность:

  • пользовательский сеанс подтверждает транзакцию;
  • посылает сообщение процессу lgwr и увеличивает redo synch writes;
  • переходит в режим ожидания сообщения (log file sync) от lgwr;
  • процесс lgwr возобновляет работу;
  • записывает буфер журнала на диск и ждет короткий промежуток времени после каждой записи.

Однако, когда я впервые запустил этот тест (примерно 12 лет тому назад, в Oracle 8i), я получил другие результаты: 

user commits (session statistic) 25
messages sent (session statistic) 6
redo synch writes(session statistic) 1
log file sync (session events) 1
messages received (lgwr statistic) 6 redo writes (lgwr statistic) 6 log file parallel write (lgwr events) 6

Совершенно очевидно, что пользовательский сеанс ведет себя не так, как ожидалось – он послал несколько сообщений процессу lgwr, но увеличил значение redo synch writes только один раз, откуда можно сделать вывод, что он не приостанавливался и не ждал, пока lgwr сгенерирует сообщение. Пользовательский сеанс нарушил требование к надежности; если бы экземпляр потерпел аварию где-то в середине цикла, подтвержденную транзакцию не удалось бы восстановить.

Такому поведению есть вполне разумное объяснение: пока весь блок PL/SQL не завершил выполнение, вы (как конечный пользователь) не можете знать, сколько транзакций подтвердилось. Поэтому Oracle не пытается обеспечить возможность их восстановления, пока блок PL/SQL не завершится и управление не вернется вызывающей программе. Это та самая точка, в которой сеанс увеличивает redo synch writes и переходит в ожидание сообщения log file sync. В действительности вы вообще можете не увидеть сообщение log file sync – в конце блока вы могли бы вставить вызов dbms_lock. sleep() и заставить сеанс приостановиться на несколько секунд, в результате чего мог бы истечь очередной 3-секундный таймаут и процесс lgwr автоматически перенес бы содержимое буфера на диск. Спустя установленный интервал, сеанс мог бы возобновить работу, проверить состояние lgwr и обнаружить, что он уже сохранил буфер. В этом случае увеличилось бы только значение статистики redo synch writes, а значения messages sent и log file sync остались бы прежними.

Примечание. Вообще говоря, если в рамках одного обращения к базе данных выполняется подтверждение нескольких транзакций, сеанс не приостанавливается после каждого из них, в ожидании сообщения от процесса lgwr. Переход к ожиданию log file sync (с увеличением redo synch writes) происходит только один раз, в конце вызова. Типичным примером могут служить подтверждения в цикле PL/SQL, когда теоретически возможно, что часть подтвержденных транзакций останется в состоянии, непригодном для восстановления, в случае аварии экземпляра. Отметив это обстоятельство, следует также заметить, что «окно возможностей» для подобного рода ситуаций, когда что-то пойдет не так, обычно меньше сотых долей секунды. Описанная стратегия не используется, когда обновления выполняются через связи между базами данных. По этой причине, чтобы обеспечить приостановку кода PL/SQL в ожидании сообщения log file sync после каждого подтверждения, я использовал в прошлом избыточные обратные петлевые связи (loopback database link).

Однако, данное обоснование имеет слабые стороны. Обычно нельзя точно сказать, сколько транзакций было подтверждено (к моменту запроса информации может оказаться слишком поздно, потому что другие сеансы тоже могут подтвердить множество транзакций). Поэтому Oracle предоставляет разные механизмы (такие как каналы, внешние процедуры или utl_file) для передачи сообщений из блока PL/SQL, а это означает, что есть возможность увидеть несоответствие.

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

Я считаю, что стоит потратить несколько минут, чтобы внимательнее исследовать результаты работы простого кода PL/SQL. Мы видим шесть сообщений, отправленных пользовательским сеансом процессу lgwr. В связи с этим возникает вопрос: нет ли в Oracle специального алгоритма, в цикле определяющего, как часто посылать сообщения. В действительности мы уже видели ответ на этот вопрос. Каждый раз, когда код подтверждает транзакцию, сеанс проверяет – что делает процесс lgwr. Если он занят записью буфера на диск, отправка сообщений стала бы пустой тратой ресурсов. Если он находится в состоянии ожидания, сеанс мог бы отправить сообщение. То есть, если вы попробуете повторить мой тест, число отправленных сообщений у вас будет зависеть от быстродействия процессора (как быстро ваш код будет выполнять каждую итерацию) и скорости жесткого диска (сколько времени будет тратить процесс lgwr на один цикл записи).

Примечание. Далее вы увидите, что статистика redo synch writes тесно связана с сообщениями log file sync; они означают почти одно и то же. Но есть еще более интересная статистика – redo synch write time (время в сотых долях секунды), – значение которой с точностью до ошибки округления соответствует времени, потраченному на ожидание сообщения log file sync. В версии 11.2.0.2 были добавлены еще две статистики: redo synch write time (usec) (время в микросекундах) и очень полезный индикатор redo synch long waits, сообщающий, сколько раз сеансу приходилось ждать запаздывающего сообщения log file sync. (Я не нашел упоминания точного значения предела, когда событие можно назвать «запаздывающим», но, по моим оценкам, оно находится где-то между 13 и 17 миллисекундами).

Механизм записи в буфер журнала и на диск в базе данных Oracle 

Аномалия ACID

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

  1. Создается вектор изменений для обновления таблицы транзакций.
  2. Вектор изменений копируется в буфер журнала.
  3. Применяется к заголовку undo-сегмента.
  4. Процессу lgwr посылается сообщение о необходимости выполнить запись.

Между шагами 3 и 4 другой сеанс сможет увидеть изменения, выполненные в транзакциях, даже при том, что записи подтверждения еще не были записаны на диск. Если с экземпляром случится авария между шагами 3 и 4, вы сможете увидеть в отчете (или в удаленной базе данных) результаты подтверждения транзакции, которая не будет восстановлена после перезапуска экземпляра. Этот эффект легко продемонстрировать:

  1. Сеанс 1: использует oradebug для приостановки lgwr.
  2. Сеанс 2: обновляет некоторые данные и подтверждает изменения – сеанс приостановится.
  3. Сеанс 1: запрашивает данные и обнаруживает изменения.
  4. Сеанс 1: завершается аварийно (останавливается).

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

Самое удивительное в этой аномалии (для меня, по крайней мере) заключается в том, что я много раз описывал работу этого механизма за последние 12 лет и не обращал внимания на последствия, о которых только что рассказал, пока Тони Хаслер (Tony Hasler) (http:// tonyhasler.wordpress.com) не написал небольшую статью в своем блоге, высвечивающую данную проблему.

 

Расширенные механизмы подтверждения

Теперь пришло время познакомиться с изменениями в версии 10g, обусловленными переходом от «секретной» оптимизации операций подтверждения к общедоступной возможности управления поведением подтверждениями с помощью параметра commit_write, появившегося в версии 10.2 и затем превратившегося в версии 11g в два параметра: commit_logging и commit_wait. В самом простом случае проблему надежности в циклах PL/SQL можно решить преобразованием commit в commit write batch wait.

Примечание. Команда commit допускает четыре возможные комбинации: commit write [batch|immediate] [wait|nowait]. В 11g вариант batch/immediate можно выбрать с помощью параметра commit_logging, а вариант wait/nowait – с помощью параметра commit_wait. В 10g оба варианта выбираются с помощью единственного параметра commit_write.

Самый быстрый и простой способ показать эффект новых возможностей commit – привести значения нескольких важнейших статистик после выполнения описанного выше цикла PL/SQL с оригинальной командой commit и с четырьмя разными новыми комбинациями. В табл. 1 приводятся результаты, полученные в экземпляре Oracle 11.2.0.2.

Статистика Простая
команда
commit
commit
write immediate
wait
commit
write immediate
nowait
commit
write
batch
wait
commit
write
batch
nowait
messages sent 7 25 7 25 0
redo entries 25 50 50 25 25
redo size 11500 12752 11876 12068 11012
redo synch writes 1 25 0 25 0
redo wastage (lgwr) 820 12048 1468 3804 0
redo blocks written (lgwr) 25 50 27 32 0

Отмечу некоторые важные моменты в этих результатах. Если указан параметр wait, сеанс будет увеличивать redo synch writes с каждой командой подтверждения и ждать события log file sync. Обычно это приводит к выполнению большого числа циклов записи маленькими порциями и увеличению статистик redo wastage (о которой я расскажу чуть ниже), redo size (потому что каждый цикл записи сопровождается собственным пакетом управляющей информации, когда используется множество буферов журнала) и redo blocks written (число блоков журнала, записанных на диск). Многие из вас наверняка обратили внимание, что статистика redo entries принимает два значения: 25 и 50. Число 25 – ожидаемое; оно соответствует стандартной оптимизации в версии 10g, когда векторы изменений, соответствующие маленьким транзакциям, собираются в единую запись повторения.

Параметр batch имеет два ключевых отличия от параметра immediate: вектор подтверждения изменений (commit change vector) превращается в отдельную запись повторения (redo record, я не могу понять почему; возможно с целью уменьшения временного окна, когда может проявиться аномалия ACID, упоминавшаяся выше), что дает небольшое увеличение значения redo size, и – если вы не указали параметр wait – сеанс вообще не будет посылать сообщения процессу lgwr; он просто будет считать, что какое-то другое событие в ближайшее время запустит цикл записи.

Новые параметры команды commit просты и понятны: если вам действительно необходимо обеспечить восстановимое подтверждение каждой транзакции (как, например, в банковских или медицинских приложениях), тогда используйте параметр wait. Комбинация batch wait даст вам небольшой прирост эффективности, когда все транзакции имеют небольшой размер. Если вы допускаете возможность потери нескольких подтвержденных транзакций в случае аварии экземпляра (например, в приложениях социальных сетей), тогда подумайте о комбинации параметров batch nowait, применение которых поможет уменьшить размеры записей повторения, непроизводительные потери памяти в буфере и время, которое сеансы проводят в ожидании.

 

Механика

В последних нескольких страницах мне удалось достаточно подробно рассказать о том, как процесс старается записать как можно больше за один цикл, уменьшить непроизводительные потери памяти и свести к минимуму конфликты с или между пользовательскими сеансами. Теперь пришло время внимательнее рассмотреть, как используется буфер журнала. Сначала мы исследуем механику работы процесса записи в журнал, затем уделим внимание непроизводительным потерям и потом займемся приватными буферами повторений (private redo) – новейшим средством борьбы с конфликтами в Oracle.

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

Интенсивно используемый буфер журнала в базе данных Oracle

Рис. 3. Интенсивно используемый буфер журнала

За мгновение до возникновения состояния, изображенного на рис. 3, процесс записи в журнал был инициирован сообщением redo write, которое отправил сеанс, сгенерировавший запись подтверждения c1. Прежде чем lgwr включился в работу, другие сеансы успели добавить в буфер еще несколько записей повторения, включая два сеанса, создавших записи подтверждения c2 и c3, и послали свои сообщения redo write. Как следствие, в данный момент имеется три сеанса, ожидающих сообщения log file sync. Имейте в виду, что между моментом отправки сообщения и моментом активизации lgwr может пройти какое-то время – именно в этот промежуток другие сеансы имеют возможность послать свои (избыточные) сообщения процессу lgwr, даже при том, что Oracle стремится избегать избыточных вызовов с помощью флага записи.

Сразу после возобновления, процесс lgwr должен приобрести защелку redo writing, установить флаг записи и освободить защелку, чтобы в дальнейшем все сеансы смогли увидеть, что процесс занят выполнением цикла записи. Затем он должен приобрести защелку redo allocation, чтобы выяснить верхнюю границу памяти в буфере, зарезервированной на данный момент (то есть, начало свободного пространства), переместить указатель в конец блока – эта точка отмечена на рис. 3 подписью «Текущий цикл записи процесс lgwr завершит здесь» (важность этого действия я объясню в следующем разделе, «Непроизводительные потери памяти в буфере») – освободить защелку и начать копирование буфера на диск. Обратите внимание: даже при том, что цикл записи был инициирован созданием записи подтверждения c1, процесс lgwr сохранит также записи c2 и c3.

Итак, на рис. 3 изображено состояние буфера журнала, когда lgwr только приступил к записи. Но, когда цикл записи закончится, в буфере появятся дополнительные записи, включая новые записи подтверждения (c4, c5). Сеансы, создавшие их, приобретут защелку redo writing, обнаружат, что lgwr уже выполняет цикл записи, и не будут посылать свои сообщения (отправка этих сообщений все равно ничего не даст), но увеличат свои счетчики redo synch writes и точно так же приостановятся в ожидании сообщения log file sync. На рисунке также видно, что в буфере было зарезервировано некоторое пространство сеансом, который еще не скопировал туда подготовленную запись повторения.

Как только lgwr завершит цикл записи, он сбросит флаг записи (приобретя и освободив защелку redo writing), переместит указатель «Конец свободного пространства» в точку, где только что была закончена запись (приобретя и освободив защелку redo allocation), выполнит обход списка сеансов, ожидающих сообщения log file sync и оповестит операционную систему о необходимости вернуть в очередь выполняющихся процессов все сеансы, записи которых только что были сохранены. (Я мог неправильно указать последовательность действий, но то, что все эти действия выполняются, не вызывает никаких сомнений.)

Примечание. Все процессы, ожидающие сообщения log file sync, возобновляют работу одновременно. Поэтому, если таких процессов достаточно много, может возникнуть проблема «инверсии приоритетов» (priority inversion), когда lgwr вытесняется из очереди выполняющихся процессов разбуженными им сеансами – процесс lgwr до данного момента работал очень активно, а сеансы – нет, поэтому, после возврата в очередь выполняющихся процессов, они получат более высокий приоритет, чем lgwr. Как результат, иногда можно заметить ухудшение производительности системы из-за того, что lgwr не имеет возможности поработать. В редких случаях данную проблему можно исправить, увеличив приоритет процесса lgwr или запретив операционной системе применять стандартное правило понижения приоритета к процессу lgwr.

Далее lgwr замечает, что имеется еще несколько сеансов, ожидающих сообщения log file sync, записи которых еще не были сохранены на диске. В результате он повторяет цикл записи (устанавливает флаг записи, передвигает указатель «Начало свободного пространства» и приступает к записи в файл), но на этот раз обнаруживает, что некоторый сеанс зарезервировал место в буфере и пока не скопировал туда свою запись повторения. Теперь давайте на мгновение переключимся на этот сеанс и посмотрим, какие действия он выполняет, чтобы скопировать свою запись в буфер. Вот что он делает:

  1. Приобретает защелку redo copy. Так как число защелок равно 2 × cpu_count, сеанс может по очереди попробовать каждую из них приобрести немедленно и перейти в режим готовности к ожиданию, только если все они окажутся заняты. Выбор первой защелки происходит случайным образом, поэтому разные сеансы не будут вынуждены все сразу ждать на одной из них.
  2. Приобретает защелку redo allocation. (При наличии нескольких общедоступных потоков журналирования, защелка redo copy определяет выбор буфера, который определяет выбор защелки allocation latch; я остановлюсь подробнее на случае использования нескольких буферов журналирования далее в этой статье.)
  3. Перемещает указатель начала свободного пространства.
  4. Освобождает защелку redo allocation.
  5. Копирует запись.
  6. Освобождает защелку redo copy.
  7. Если в результате резервирования в буфере оказалось занято более одной трети его объема или более 1 Мбайта, или если запись повторения является записью подтверждения, посылает процессу lgwr сообщение (но перед этим приобретает защелку redo writing, чтобы проверить флаг записи).
  8. Если запись повторения является записью подтверждения, увеличивает значение redo synch writes и переходит в режим ожидания сообщения log file sync с 1 секундным таймаутом.

Примечание. Когда сеанс приобретает защелку redo allocation, чтобы зарезервировать место в буфере журнала, может обнаружиться, что места недостаточно. В этом случае сеанс освободит защелку redo allocation и приобретет защелку redo writing, чтобы проверить, не выполняет ли процесс lgwr цикл записи, и при необходимости пошлет ему сообщение. Затем сеанс освободит защелку redo writing и приостановится в ожидании сообщения log buffer space. Соответственно, закончив цикл записи, процесс lgwr должен также проверить наличие сеансов, ожидающих сообщения log buffer space и активировать их. Если в момент проверки флага записи сеансом процесс lgwr уже выполняет цикл записи, тогда сеанс просто вновь приобретает защелку redo allocation и выполняет повторную попытку зарезервировать место в буфере, потому что к этому моменту в буфере могло освободиться достаточное пространство.

Итак, сеанс резервирует место в буфере журнала и копирует туда свою запись, удерживая защелку redo copy. В теории это означает, что lgwr должен только приобрести (и затем освободить) все защелки redo copy для проверки зарезервированных пространств перед началом записи, потому что после приобретения всех защелок все зарезервированные пространства в буфере будут гарантированно заполнены. Однако, как отмечает Стив Адамс (Steve Adams) (http://www.ixora.com.au/tips/tuning/redo_latches.htm), описанный алгоритм работы изменился – в версиях Oracle выше 8.0 появился механизм, позволяющий проверить состояние защелок redo copy без их приобретения (с использованием внутреннего эквивалента v$latchholder), и если хотя бы одна защелка удерживается каким-то сеансом, процесс lgwr переходит в режим ожидания сообщения LGWR wait for redo copy.

Наблюдая за активностью вокруг защелок, можно увидеть нечто подобное, но я не знаю точно, какой процесс посылает сообщение LGWR wait for redo copy (по всей видимости это должен быть сеанс, удерживающий защелку) и я не знаю, как сеанс определяет, что он должен послать это сообщение. Однако, поскольку параметр parameter1 сообщения LGWR wait for redo copy хранит значение copy latch #, и каждый сеанс знает, какую дочернюю защелку redo copy он удерживает, несложно догадаться, что имеется некоторый код, который всегда вызывается сеансом непосредственно перед освобождением защелки и проверяет – не является ли защелка той, освобождения которой ожидает процесс lgwr, и посылает ему ожидаемое сообщение.


Сообщение log file sync


Увидев, что сеансы приостанавливаются в ожидании сообщения log file sync, многих начинает волновать вопрос производительности (еще бы: «нам приходится ждать, пока lgwr запишет буфер на диск!»). Если вы принадлежите к их числу, могу сообщить, что значительную долю ожидания сообщения log file sync составляет ожидание сообщения log file parallel write.

Взгляните еще раз на рис. 3 и обратите внимание на сеанс, копирующий в буфер запись подтверждения c4. Вы увидите, что между моментом, когда сеанс приостановился в ожидании сообщения log file sync, и моментом, когда он возобновит работу, выполняется масса операций не имеющих отношения к данной конкретной записи.

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

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

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

То есть, если периоды ожидания log file sync кажутся слишком продолжительными, это необязательно обусловлено длительностью записи в файлы журнала – они могут быть вызваны высокой нагрузкой на систему.


 

Непроизводительные потери памяти в буфере

Выше я отмечал, что самым первым действием процесса lgwr в ответ на сообщение log file sync является приобретение защелки redo allocation и перемещение указателя начала свободной области в конец текущего блока. На рис. 4 изображено состояние того же буфера журнала, что и на рис. 3, в момент перехода от одного цикла записи к другому.

Границы блоков и непроизводительные потери памяти Oracle

Рис. 4. Границы блоков и непроизводительные потери памяти

На рис. 4 я снова изобразил небольшой буфер журнала, занимающий всего восемь блоков. Процесс записи в журнал закончил сохранение записей подтверждения c1, c2 и c3, и обнаружил, что за это время еще пара сеансов успела записать в буфер свои записи подтверждения.

На этой схеме я также изобразил границы блоков, чтобы вы могли видеть, что недавно зарезервированное пространство не достигает конца блока. Почему тогда lgwr приобретает защелку allocation latch и перемещает указатель на начало свободной области до границы блока, впустую расходуя свободное пространство в последнем занятом блоке?

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

Разработчики Oracle решили выбрать более простую стратегию: переместить указатель в конец блока, записать блок, никогда не читать блоки с диска и не хранить их копии. Цель такой стратегии – сохранить простоту реализации и обеспечить непрерывную запись на диск без приостановок на чтение.

Пространство, израсходованное впустую из-за перемещений указателя до конца блока, измеряется статистикой redo wastage. Так как lgwr является единственным процессом, выполняющим сохранение буфера на диск, только он перемещает указатель подобным образом и потому только он вносит ненулевой вклад в redo wastage. Статистически, средний размер непроизводительных потерь на один цикл записи, вероятно, будет стремиться к половине размера буфера, если только все транзакции не имеют один и тот же размер. Хуже ситуация с транзакциями «одного размера» обстоит в приложениях, которые выполняют огромное число очень маленьких изменений, – выполняющих обработку данных построчно и не использующих дополнительные возможности базы данных.

Фактически в примере, результаты которого приводятся в табл. 1, я невольно продемонстрировал проблему. Взгляните на столбец со значениями статистик для команды commit write immediate wait. Здесь вы увидите, что при общем объеме записей повторения (redo size) около 12.5 Кбайт, объем непроизводительных потерь (redo wastage) составляет 12 Кбайт. Каждая моя транзакция резервировала всего несколько байтов в блоке (если соберетесь посчитать, не забудьте про 16-байтный заголовок, который не требуется сохранять в каждом блоке), а остальная часть каждого второго расходовалась впустую.

Эта проблема заставляет меня вернуться к замечанию о появлении новых дисков с размером сектора 4 Кбайта. В системе с таким диском мой демонстрационный пример мог бы показать суммарный объем redo size 12 Кбайт и redo wastage около 89 Кбайт – то есть, около 510 байтов redo size и 3.5 Кбайта redo wastage в конце блока для каждой из 25 транзакций. Вам наверняка будут попадаться приложения, где понадобится уделить особое внимание статистике redo wastage и сделать выбор. Либо использовать блоки размером 512 или 1024 байт, и тем самым уменьшить непроизводительные потери памяти (redo wastage), но ценой повторного чтения блоков с диска, либо использовать блоки по 4 Кбайта и снизить накладные расходы на повторное чтение, но ценой увеличения redo wastage и, соответственно, увеличения размеров файлов журнала.

 

Приватные буферы журнала

Пришло время рассказать о расширениях в версии 10g для работы с журналом. В основной своей массе эти расширения реализуют поддержку приватных буферов журнала (их обычно называют приватными журнальными потоками (private redo threads)) и возможность использования нескольких общедоступных буферов. К счастью, нововведений здесь не так много.

Первое, что хотелось бы отметить, – число журнальных потоков, обслуживающих приватные или общедоступные буферы, изменяется динамически и Oracle пытается удержать это число на минимальном уровне, увеличивая его, только когда возникает сильная конкуренция за защелки redo allocation и начинает увеличиваться время, проводимое сеансами в ожидании. Так, например, когда моя система с 4 общедоступными и 20 приватными потоками была нагружена шестью весьма активными сеансами в течение получаса, я обнаружил, что большую часть времени использовался только 1 общедоступный и 6 приватных потоков. (Второй общедоступный поток был загружен лишь самую малость и, благодаря наличию различных фоновых заданий, незначительно оказался загружен седьмой приватный поток.)

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

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

Еще одно ключевое отличие состоит в том, что когда сеанс начинает транзакцию, он пытается приобрести приватный поток журналирования. Начальное (и максимальное) число приватных потоков определяется как transactions / 10, то есть в большинстве систем сеанс часто будет находить свободный приватный поток. Если приватный поток недоступен, сеанс будет использовать один из общедоступных потоков традиционным способом (поддерживавшимся в версиях до 10g).

Каждый приватный поток имеет собственную защелку redo allocation, поэтому, чтобы получить доступ к приватному потоку, сеанс должен приобрести соответствующую защелку. Прежде я полагал, что Oracle приобретает защелки в непосредственном режиме. Учитывая наличие нескольких приватных потоков, каждый из которых имеет собственную защелку, в случае неудачной попытки получить доступ к первому потоку Oracle может попробовать следующий (как это делается при попытке получить защелку redo copy) и использовать режим готовности к ожиданию, только после невозможности немедленно приобрести последнюю защелку. Однако, результаты простого эксперимента выглядели так, как если бы сеансы всегда приобретали защелки в режиме готовности к ожиданию. Это навело меня на мысль, что сеанс выполняет обход структур приватных потоков журналирования, не приобретая никаких защелок, находит первый неиспользуемый поток, и только тогда приобретает защелку, чтобы отметить поток, как используемый. (Такое решение все еще оставляет окно возможностей, когда два сеанса могут одновременно попытаться приобрести одну и ту же защелку, но в целом оно все же позволяет сэкономить на операциях с защелками.)

И последнее, что я хотел бы сказать о сохранении буфера журнала на диск. Когда lgwr начинает цикл записи, он последовательно обходит все (активные) общедоступные потоки журналирования. Это означает, что в течение одного цикла может потребоваться неоднократно приобретать защелки и ожидать на них. Это объясняет, отчасти, почему Oracle стремится удерживать число активных буферов на минимальном уровне.

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

Oracle и непроцедурный доступ ...
Oracle и непроцедурный доступ ... 8512 просмотров Antoni Tue, 21 Nov 2017, 13:32:50
Как устроен поиск блоков данны...
Как устроен поиск блоков данны... 4495 просмотров Дэн Wed, 03 Jan 2018, 17:39:13
Язык SQL в Oracle
Язык SQL в Oracle 4282 просмотров Ирина Светлова Tue, 21 Nov 2017, 13:26:01
Listener Oracle
Listener Oracle 33067 просмотров Stas Belkov Tue, 21 Nov 2017, 13:18:05
Войдите чтобы комментировать

ildergun аватар
ildergun ответил в теме #10057 2 года 9 мес. назад
Хороший материал. Спасибо!
VaaPa аватар
VaaPa ответил в теме #8966 6 года 2 нед. назад
Фундаментально! Узнал много нового о работе процесса lgwr в БД Oracle...