|
|
|||||||||||||||||||||||||||||
|
Алгоритм "мини-откатов" в Oracle или еще раз о Write Consistency. Часть 1Источник: oracle
Введение
Как-то раз, бродя по просторам Интернета, я в очередной раз увидел ссылку на сайт Тома Кайта [1]. Обнаружив, что Том продолжил дискуссию циклом небольших статей на эту тему, я тоже решил заняться исследованием этой весьма непростой и мало изученной проблемы - неявным перезапуском команды update в случае, когда другие сеансы одновременно модифицируют данные обновляемой таблицы. Кроме того, как я заметил, подавляющее большинство разработчиков и администраторов даже и не подозревают о существовании таких перезапусков DML-команд и команды select for update, которые могут очень существенно повлиять на логику и производительность приложений. А если они и знают о них, то не в полной мере. Такие неявные, выполняемые Oracle перезапуски команд, я буду далее называть "мини-откатами". Одним из моих любимых занятий является изучение всякого рода дампов и трассировочных файлов. На мой взгляд, это сильно помогает в понимании принципов и алгоритмов работы любой системы. Кроме того, мне кажется, что только зная как и почему это работает именно так, а не иначе, можно строить эффективно работающие системы и писать высокопроизводительные приложения. При этом абсолютно не важно, относится ли сказанное к СУБД Oracle, операционной системе или вообще, скажем, к работе двигателя внутреннего сгорания. Поэтому я выбрал немного другой способ для выяснения того, что же происходит в обсуждаемом Томом и посетителями его сайта случае - изучение трассировочной информации, которую может предоставить Oracle. Эта информация даст нам возможность более детально разобраться в алгоритмах работы ядра Oracle. Кроме того, такой способ не требует для обнаружения "мини-откатов" использования триггеров, автономных процедур и т.д., что может так или иначе повлиять на чистоту экспериментов. Несмотря на то, что здесь будет представлен весьма большой объем информации, понятной в основном администраторам, надеюсь, что разработчики приложений тоже проявят интерес к данной статье. Если кому-то не интересно вникать во все подробности рассматриваемой проблемы и изучать трассировочные файлы, можно сразу перейти к концу статьи, где описаны условия возникновения, алгоритм выполнения "мини-откатов" и часть возможных способов борьбы с ними. Учитывая то, что, если я приведу здесь все результаты моих исследований, то это займет очень много места и вызовет головную боль у читателей :), я решил ограничиться опубликованием только части результатов тестов. Но в конце статьи я постараюсь кратко описать то, что же все-таки происходит при возникновении "мини-откатов", ну или, по крайней мере, то, что мне удалось узнать касательно этой темы. Кроме того, хочу заметить, что практически все, о чем здесь будет идти речь, не документировано. Поэтому в статье могут быть ошибки, упущенные нюансы и тому подобные недоразумения. Учитывая сложность темы и объем статьи, можно даже сказать, что они наверняка здесь есть :). Кроме этого, кое-где сознательно, а кое-где и нет, я делал некоторые упрощения в объяснениях, которые, на мой взгляд, не меняют общей картины, но существенно упрощают понимание. Объяснение и обсуждение причин такого поведения Oracle, то есть, почему это работает так, а не иначе, в мои планы не входит, так как их уже постарался изложить Том Кайт. Почитать его комментарии можно, пройдя по ссылкам, указанным в [1]-[4]. Поэтому основной целью этой статьи будет показать, как это работает, то есть сам алгоритм "мини-откатов" и альтернативный способ его изучения с использованием некоторых недокументированных возможностей. Кроме того, я постараюсь также дать несколько рекомендаций по возможности уменьшения влияния "мини-откатов" на производительность приложений. Что такое "мини-откат"?Итак, суть проблемы. В нашей многопользовательской системе имеется достаточно большая таблица. Причем, не важно, секционированная ли это таблица, индекс-организованная и т.д., имеет ли она индексы или нет - сути это не меняет. В одном из сеансов мы запустили на выполнение в режиме изолированности READ COMMITED, принятом в Oracle по умолчанию, команду update, указав во фразе WHERE (так называемом предикате) условие, по которому будут выбираться строки, подлежащие обновлению. После того, как началось выполнение команды update, другой сеанс обновил или удалил одну или более строк, которые удовлетворяли предикату команды update первого сеанса на момент начала ее выполнения, но до изменения которых тот еще не успел дойти (разумеется, уже обновленные первым сеансом строки заблокированы и не могут никем модифицироваться). При этом:
В этом случае Oracle произведет "мини-откат": неявно перезапустит команду update и выполнит обновление уже нового набора строк. Если же в момент выполнения команды update происходит вставка новых (даже удовлетворяющих предикату) строк другими сеансами, то перезапуска команды не будет. Происходит это потому, что выполнение команды update состоит из двух фаз: 1) согласованное чтение ( consistent read ) таблицы на момент начала выполнения команды для нахождения строк, подлежащих модификации (в том числе удовлетворяющих предикату, если он задан); Поэтому все новые строки, которые были вставлены в таблицу другими сеансами после начала выполнения команды UPDATE, "не видны" во время первой фазы команды update даже несмотря на фиксацию (COMMIT) таких вставок. Но, как мы увидим позже, это уже не так, если производится "мини-откат" этих команд. Кроме того, "мини-откаты" могут возникать при выполнении команд DELETE, MERGE и SELECT FOR UPDATE. Так как в каждом случае существует достаточно много нюансов, то об этом я постарался рассказать в конце статьи, описав алгоритм "мини-откатов" для каждой из команд в отдельности.
Как будет проводиться тестированиеПеред тем, как мы рассмотрим результаты тестирования, я приведу краткое описание диагностических событий, которые буду использовать. Описание каждого события будет выделено тем же самым цветом, каким выделена соответствующая ему информация в трассировочных файлах, размещенных в этой статье. Event 10219 - "monitor multi-pass row locking" ("отслеживать многопроходные блокировки строк"). Это событие включает запись в трассировочный файл серверного процесса, с которым соединен пользователь, диагностической информации при возникновении исследуемой нами ситуации в случае, если выполняется команда update или фаза update команды merge. Level 2 и выше включает выдачу информации об обновляемых строках. Event 10218 - "dump uba of applied undo" ("выдавать uba применяемых undo -записей"). Включение этого события позволяет получать в трассировочном файле uba -адреса ( uba - Undo Block Address) блоков, просмотренных и примененных при откате транзакции или команды (при явном откате после выдачи команды ROLLBACK или при неявном откате команды, например, при возникновении ошибки ORA-00001: unique constraint и ей подобных). Event 10200 - "consistent read buffer status" ("статус буфера при выполнении согласованного чтения"). При выполнении согласованного чтения блока данных (не заголовков сегментов и прочих "служебных" блоков) выдает в трассировочный файл список и статус всех копий (клонов) этого блока, находящихся в буферном кэше. Event 10228 - "trace application of redo by kcocbk" ("отслеживать применение информации повтора в программе kcocbk"). Записывает в трассировочный файл в пригодном для анализа виде всю redo -информацию, генерируемую серверным процессом. Теперь приступим к тестированию. Большей частью тесты мной проводились на Oracle 9.2.0.7EE. Часть из них повторялась на Oracle 8.1.7.4 и 10.1.0.4 и показала идентичность алгоритмов. Все трассировочные файлы получены при выполнении тестов на Oracle 9.2.0.7EE. При выполнении тестов на других версиях Oracle ваши трассировочные файлы, возможно, будут немного отличаться. Для упрощения понимания я постарался не сильно отклоняться от тестов, которые приводились на сайте Тома Кайта, но ввел еще один сеанс с целью демонстрации дополнительных тонкостей и нюансов "мини-откатов". В начале каждого теста я буду заново создавать таблицу wc_test в табличном пространстве с размером блока 4k, управляемом локально, с одинаковым размером экстента и с ручным (при помощи freelists ) управлением свободным пространством внутри сегмента (это не обязательные требования, просто они упростят нам обработку полученной информации). В каждом тесте будут запускаться несколько сеансов одного и того же пользователя. Идентификация сеансов будет осуществляться при помощи строки приглашения в SQL*Plus. С этой целью была использована утилита SQL*Plus 10.1.0.4.0 от версии Oracle 10g, которая позволяет легче и нагляднее осуществлять модификацию строки приглашения. Тест 1 (с минимально достаточным объемом
|
SQL> --Session "Blocker" SQL> conn scott/tiger@o92 Connected. SQL> set sqlprompt "_USER'-Blocker'> " wrap off linesize 100 SCOTT-Blocker> create table wc_test tablespace uniform_64k_block_4k as 2 select rownum id, rownum+10 y, cast(username as char(1000)) filler 3 from all_users where rownum <= 9; Table created. SCOTT-Blocker> column id format 99 SCOTT-Blocker> column y format 99 SCOTT-Blocker> column hex_dump_id format a18 SCOTT-Blocker> column hex_dump_y format a22 SCOTT-Blocker> column file# format 9999 SCOTT-Blocker> column block# format 99999999 SCOTT-Blocker> column row# format 9999 SCOTT-Blocker> column dba format 99999999 SCOTT-Blocker> column hex_dba format a8 SCOTT-Blocker> select id, y, dump(id,16) hex_dump_id, dump(y,16) hex_dump_y, 2 dbms_rowid.rowid_relative_fno(rowid) file#, 3 dbms_rowid.rowid_block_number(rowid) block#, 4 dbms_rowid.rowid_row_number(rowid) row#, 5 dbms_utility.make_data_block_address(dbms_rowid.rowid_relative_fno(rowid), 6 dbms_rowid.rowid_block_number(rowid)) dba, 7 to_char(dbms_utility.make_data_block_address(dbms_rowid.rowid_relative_fno(rowid), 8 dbms_rowid.rowid_block_number(rowid)),'FMxxxxxxxx') hex_dba 9 from wc_test; ID Y HEX_DUMP_ID HEX_DUMP_Y FILE# BLOCK# ROW# DBA HEX_DBA --- --- ------------------ ---------------------- ----- --------- ----- --------- ------- 1 11 Typ=2 Len=2: c1,2 Typ=2 Len=2: c1,c 10 18 0 41943058 2800012 2 12 Typ=2 Len=2: c1,3 Typ=2 Len=2: c1,d 10 18 1 41943058 2800012 3 13 Typ=2 Len=2: c1,4 Typ=2 Len=2: c1,e 10 18 2 41943058 2800012 4 14 Typ=2 Len=2: c1,5 Typ=2 Len=2: c1,f 10 19 0 41943059 2800013 5 15 Typ=2 Len=2: c1,6 Typ=2 Len=2: c1,10 10 19 1 41943059 2800013 6 16 Typ=2 Len=2: c1,7 Typ=2 Len=2: c1,11 10 19 2 41943059 2800013 7 17 Typ=2 Len=2: c1,8 Typ=2 Len=2: c1,12 10 20 0 41943060 2800014 8 18 Typ=2 Len=2: c1,9 Typ=2 Len=2: c1,13 10 20 1 41943060 2800014 9 19 Typ=2 Len=2: c1,a Typ=2 Len=2: c1,14 10 20 2 41943060 2800014 9 rows selected. SCOTT-Blocker> update wc_test set y=y where id=3; 1 row updated. SCOTT-Blocker> |
SQL> --Session "Long update" SQL> conn scott/tiger@o92 Connected. SQL> set sqlprompt "_USER'-Long update'> " SCOTT-Long update> alter session set max_dump_file_size = unlimited tracefile_identifier='Long_Test_1'; Session altered. SCOTT-Long update> alter session set events '10218 trace name context forever, level 10: 2 10219 trace name context forever, level 10'; Session altered. SCOTT-Long update> --alter session set events '10200 trace name context forever:10228 trace name context forever'; SCOTT-Long update> update wc_test set y=88 where y=15 or id in(2,3,4) --Мы ждем... ; |
Сеанс "Long update" ждет завершения транзакции в сеансе "Blocker".
Теперь в сеансе "First/Last" производим модификации и сразу же фиксируем изменения. После этого, когда "Long update" доберется до строки с id =5, он обнаружит, что на момент старта команды update эта строка удовлетворяла предикату (то есть, должна быть изменена, так как y =15 на момент старта команды), но в данный момент она не удовлетворяет предикату, так как y =99 и транзакция, изменившая это значение, уже зафиксировала изменения:
SQL> --Session "First/Last" SQL> conn scott/tiger@o92 Connected. SQL> set sqlprompt "_USER'-First/Last'> " SCOTT-First/Last> update wc_test set y=15 where id=1; 1 row updated. SCOTT-First/Last> update wc_test set y=99 where id=5; 1 row updated. SCOTT-First/Last> commit; Commit complete. SCOTT-First/Last> |
В сеансе "New rows" вставляем 3 новые строки, удовлетворяющие предикату "Long update" ( y =15), фиксируем транзакцию, после чего модифицируем строку с id =10 так, что она теперь не удовлетворяет предикату. Обновление вызовет блокировку "Long update", когда тот дойдет до строки с id =10. Это обновление пока не фиксируем:
SQL> --Session "New rows" SQL> conn scott/tiger@o92 Connected. SQL> set sqlprompt "_USER'-New rows'> " SCOTT-New rows> insert into wc_test values(10,15,' '); 1 row created. SCOTT-New rows> insert into wc_test values(11,15,' '); 1 row created. SCOTT-New rows> insert into wc_test values(12,15,' '); 1 row created. SCOTT-New rows> commit; Commit complete. SCOTT-New rows> update wc_test set y=99 where id=10; 1 row updated. SCOTT-New rows> |
Возвращаемся в сеанс "Blocker" и фиксируем транзакцию. В результате сеанс "Long update" продолжит выполнение команды update, обнаружит, что строка с id =5 теперь не удовлетворяет предикату и начнет выполнение команды update сначала, произведя "мини-откат". Но, как мы увидим, дойдя до заблокированной строки с id =10, он будет ждать завершения транзакции в сеансе "New rows":
SCOTT-Blocker> commit; Commit complete. SCOTT-Blocker> |
Наконец, в сеансе "New rows": тоже зафиксируем транзакцию:
SCOTT-New rows> commit; Commit complete. SCOTT-New rows> |
Теперь сеансу "Long update" никто не мешает завершить команду update:
6 rows updated. SCOTT-Long update> select id, y from wc_test; ID Y ---------- ---------- 1 88 2 88 3 88 4 88 5 99 6 16 7 17 8 18 9 19 10 99 11 88 12 88 12 rows selected. SCOTT-Long update> |
В результате мы видим, что сеанс "Long update" модифицировал строки с id =2,3,4, которые мы не изменяли во время тестов, строка с id =5 не изменена, хотя на момент начала выполнения update она удовлетворяла предикату. Кроме того, видно, что изменены строки с id =11 и 12, которых вообще не было на момент начала выполнения команды update в сеансе "Long update".
Теперь посмотрим, какую же информацию можно почерпнуть из трассировочного файла. Как я уже говорил, цвет текста трассировочного файла будет соответствовать цвету, которым выделено описание каждого события в начале статьи. Ключевые места будут выделяться желтым фоном, комментарии и пояснения к содержимому ключевых мест трассировочных файлов я буду писать с отступом, черным цветом сразу за комментируемым текстом. Напоминаю, что трассировочные файлы, которые представлены в Листингах 6 и 7, созданы сеансом "Long update", который запускался мной под Oracle 9.2.0.7. Обратите внимание на то, что и объем трассировочной информации, и ее содержимое у вас могут отличаться от полученных мной. Это зависит от многих факторов, в том числе от версии Oracle, установки некоторых параметров инициализации (например, TRANSACTION_AUDITING), установок режима SUPPLEMENTAL LOGGING и т.д.
updaul mode is UPDATE NOT LOCKED snap oldsnap env: (scn: 0x0004.00f32abf xid: 0x0000.000.00000000 uba: 0x00000000.0000.00 statement num=0 parent xid: xid: 0x0000.000.00000000 scn: 0x0000.00000000 0sch: scn: 0x0000.00000000) env: (scn: 0x0000.00000000 xid: 0x0000.000.00000000 uba: 0x00000000.0000.00 statement num=0 parent xid: xid: 0x0000.000.00000000 scn: 0x0000.00000000 0sch: scn: 0x0000.00000000) Видим, что сеанс начал обновление еще не заблокированных строк с SCN=0x0004.00f32abf updrow: kauupd table 0 00018545.02800012.1 code 0 updrow: kauupd table 0 00018545.02800012.2 code 0 updrow: kauupd table 0 00018545.02800013.0 code 0 updrow: kauupd table 0 00018545.02800013.1 code 1 Происходит обновление строк в таблице. 0x00018545=99653 - это OBJECT_ID таблицы wc_test в DBA_OBJECTS. table 0 - это номер таблицы в блоке данных. В нашем случае в блоке одна таблица. 02800012.1 и т.д. - это dba (Data Block Address) и номера изменяемых строк. Сравните это с HEX_DBA и ROW# в Листинге 1. Процесс обновления был заблокирован сеансом ″Blocker″ на строке с id =3 (строка 00018545.02800012.2). После фиксации не изменившей значения столбца y транзакции в сеансе ″Blocker″ сеанс ″Long update″ продолжил работу. При модификации строки с id =5 обнаружилось, что строка перестала удовлетворять предикату команды update ( code 1) и ... uba: 0x00c01256.3c42.54 uba: 0x00c01256.3c42.53 uba: 0x00c01256.3c42.52 ... ″Long update″ произвел откат всех изменений, что нам показало событие 10218. При этом, что интересно и вполне разумно, несмотря на то, что блокировки со всех ранее заблокированных строк сняты, ″Long update″ не ″разбудил″ другие сеансы, которые ждут заблокированных при выполнении фазы ″mode is UPDATE NOT LOCKED″ строк. В этом можно убедиться, например, попытавшись еще в одном сеансе в момент после блокирования ″Long update″ (соответствующий концу Листинга 2) выполнить обновление строки с id =2 - этот сеанс будет заблокирован до окончания транзакции в сеансе ″Long update″. updaul mode is LOCK snap oldsnap env: (scn: 0x0004.00f32ae9 xid: 0x0002.05f.0001658f uba: 0x00000000.0000.00 statement num=0 parent xid: xid: 0x0000.000.00000000 scn: 0x0004.00f32ae9 0sch: scn: 0x0000.00000000) env: (scn: 0x0004.00f32abf xid: 0x0000.000.00000000 uba: 0x00000000.0000.00 statement num=0 parent xid: xid: 0x0000.000.00000000 scn: 0x0000.00000000 0sch: scn: 0x0000.00000000) Теперь первая фаза команды update - фаза согласованного чтения - обнаружит, что строка с id =1 тоже удовлетворяет условию команды update, что новые строки с id =10,11 и 12 - тоже, а строка c id =5 - нет. Но строка с id =10 (00018545.02800015.0) будет заблокирована командой update в ″New rows″, поэтому ″Long update″ продолжит работу только после фиксации изменений в ″New rows″. После этой фиксации ″Long update″ обнаруживает, что строка с id =10 была изменена так, что перестала удовлетворять предикату (об этом свидетельствует code 17 ), но, несмотря на это, продолжает производить поиск и блокировку всех удовлетворяющих предикату строк таблицы. После окончания просмотра всей таблицы проверяется, возникала ли хоть раз ситуация, когда code был >0 при попытке блокировки строк. Если это так, как и случилось в нашем случае, то... updaul mode is LOCK snap oldsnap env: (scn: 0x0004.00f32af1 xid: 0x0002.05f.0001658f uba: 0x00c01256.3c42.5a statement num=0 parent xid: xid: 0x0000.000.00000000 scn: 0x0004.00f32af1 0sch: scn: 0x0000.00000000) env: (scn: 0x0004.00f32ae9 xid: 0x0002.05f.0001658f uba: 0x00000000.0000.00 statement num=0 parent xid: xid: 0x0000.000.00000000 scn: 0x0004.00f32ae9 0sch: scn: 0x0000.00000000) Происходит снова попытка блокировки всех удовлетворяющих предикату строк с новым SCN. При этом блокировки с уже заблокированных на фазе ″mode is LOCK″ строк не снимаются, никаких изменений и никакой redo- информации для таких строк не генерируется. Обратите внимание, что идентификатор (XID) транзакции не изменяется. Такие попытки блокировок будут происходить либо до тех пор, пока все вызовы функции kddlkr не завершатся с code 0 , либо до тех пор, пока число таких попыток не превысит 5000 (подробности см. далее в описании алгоритмов ″мини-откатов″). updrow: kddlkr table 0 00018545.02800012.0 code 0 updrow: kddlkr table 0 00018545.02800012.1 code 0 updrow: kddlkr table 0 00018545.02800012.2 code 0 updrow: kddlkr table 0 00018545.02800013.0 code 0 updrow: kddlkr table 0 00018545.02800015.1 code 0 updrow: kddlkr table 0 00018545.02800015.2 code 0 И только тогда, когда все code при очередной попытке блокировки строк на фазе ″mode is LOCK″ будут равны нулю, происходит обновление всех ранее заблокированных строк на завершающей фазе ″mode is UPDATE ALL LOCKED″. updaul mode is UPDATE ALL LOCKED snap oldsnap env: (scn: 0x0004.00f32af1 xid: 0x0002.05f.0001658f uba: 0x00c01256.3c42.5a statement num=0 parent xid: xid: 0x0000.000.00000000 scn: 0x0004.00f32af1 0sch: scn: 0x0000.00000000) env: (scn: 0x0004.00f32ae9 xid: 0x0002.05f.0001658f uba: 0x00000000.0000.00 statement num=0 parent xid: xid: 0x0000.000.00000000 scn: 0x0004.00f32ae9 0sch: scn: 0x0000.00000000) updrow: kauupd table 0 00018545.02800012.0 code 0 updrow: kauupd table 0 00018545.02800012.1 code 0 updrow: kauupd table 0 00018545.02800012.2 code 0 updrow: kauupd table 0 00018545.02800013.0 code 0 updrow: kauupd table 0 00018545.02800015.1 code 0 updrow: kauupd table 0 00018545.02800015.2 code 0 Теперь это обновление обязательно завершится успешно (разумеется, если не возникнет форс-мажорных обстоятельств в виде SHUTDOWN ABORT, доступа к данным через поврежденный индекс и т.д.), так как: 1) эта фаза выполняется с тем же SCN, что и последняя фаза ″mode is LOCK″. Что в свою очередь означает, что после выполнения согласованного чтения блоков таблицы wc_test на этой завершающей фазе мы получим тот же набор строк, подлежащих изменению, что и на последней фазе ″mode is LOCK″. Кроме того, эти строки уже заблокированы нашей транзакцией на предыдущих шагах, что гарантирует их неизменность с момента этой блокировки. |
Главная страница - Программные продукты - Статьи - СУБД и хранилища данных, Oracle |
Распечатать »
Правила публикации » |
Написать редактору | |||
Рекомендовать » | Дата публикации: 10.10.2008 | |||
|
Новости по теме |
Рассылки Subscribe.ru |
Статьи по теме |
Новинки каталога Download |
Исходники |
Документация |