Этот случай имел место на заключительном этапе занятий Hotsos Clinic. После двух с половиной дней лекций, которые я читал вместе с одним коллегой, мы предложили слушателям принести в класс файлы трассировки и попробовать открыто продиагностировать их совместными усилиями. Идея была в том, что мы мужественно брались за диагностику файла любого, кто не побоялся бы рассказать на публике о медленной работе своих приложений. Было очень весело. Студенты имели возможность увидеть, действительно ли работает на практике то, чему мы их учили, и могли поделиться с аудиторией своими новыми идеями. Студент, представивший свою программу на рассмотрение, обычно получал готовое решение серьезной проблемы. А мы могли познакомиться с множеством чрезвычайно интересных проблем производительности приложений.
И вот в последний день занятий очаровательная молодая девушка, сидевшая в последнем ряду, принесла нам компакт-диск. Она объяснила, что файл трассировки, записанный на этом диске, относится к купленному приложению, созданному на PowerBuilder, которое всегда работало очень медленно. Более того, компания, офис которой находился на противоположной стороне улицы, применяла то же самое приложение и также имела серьезные претензии к его производительности. История продолжалась бесконечно - на каждой встрече пользователи интересовались друг у друга, не удалось ли кому-то заставить его работать быстрее. Никто не понимал, почему приложение работает так медленно.
Все происходит естественным путем.
Представлялась чудесная возможность демонстрации мощи Метода R -замечательно, что мы можем решить проблему производительности, с которой безуспешно пытались справиться годами! Ситуация полностью отражала описанное в первых абзацах главы 1.
Когда я увидел профиль ресурсов, построенный по файлу трассировки, мое сердце опустилось. Там присутствовало множество событий SQL*Net message from client, и практически ничего больше (пример 12.6). Заговорив, я в первую очередь высказал сожаление - эффективность нашей помощи, наверное, не могла быть такой, как нам бы этого хотелось. Очевидно было, что профиль состоял в основном из времени бездействия пользователя или из времени выполнения неизмеряемого кода на сервере приложений или еще из чего-то подобного.
Пример 12.6. Профиль ресурсов приложения, написанного на PowerBuilder
Response Time Component
|
Duration
|
# Calls
|
Dur/Call
|
|
SQL*Net message from client
|
166.6s
|
91.8°%
|
6,094
|
0.027338s
|
CPU service
|
9.7s
|
5.3%
|
18,750
|
0.000515s
|
unaccounted-for
|
1.9s
|
1.1%
|
||
db file sequential read
|
1.6s
|
0.9%
|
1,740
|
0.000914s
|
log file sync
|
1.1s
|
0.6%
|
681
|
0.001645s
|
SQL*Net more data from client
|
0.3s
|
0.1%
|
71
|
0.003521s
|
SQL*Net more data to client
|
0.1s
|
0.1%
|
108
|
0.001019s
|
free buffer waits
|
0.1s
|
0.0%
|
4
|
0.022500s
|
db file scattered read
|
0.0s
|
0.0%
|
34
|
0.001176s
|
SQL*Net message to client
|
0.0s
|
0.0%
|
6,094
|
0.000007s
|
log file switch completion
|
0.0s
|
0.0%
|
1
|
0.030000s
|
latch free
|
0.0s
|
0.0%
|
1
|
0.010000s
|
log buffer space
|
0.0s
|
0.0%
|
2
|
0.005000s
|
direct path read
|
0.0s
|
0.0%
|
5
|
0.000000s
|
direct path write
|
0.0s
|
0.0%
|
2
|
0.000000s
|
Total
|
181.5s
|
100.0%
|
Но слушательница уверяла нас, что не спала на лекциях и что хорошо знакома с ошибками сбора данных и временем бездействия пользователя, и что ничего подобного в ее файле трассировки нет. Она начала сбор данных расширенной трассировки SQL непосредственно перед тем, как пользователь нажал кнопку «OK» для запуска операции, и завершила сбор данных сразу же, как только заметила, что система вернула результат (пользователь отключился от приложения). Пользователь действительно ждал отключения около трех минут после нажатия кнопки. Приложение было двухзвенным, промежуточного прикладного звена не существовало, следовательно, не было и неизмеряе-мого прикладного кода. Более того, для сбора данных она применяла наше средство - Sparky.
Н-да...
Если честно, то одну улику я пропустил. Выходные данные текущей версии Hotsos Profiler содержат не только информацию о средней длительности вызова, но и минимальное и максимальное значения времени ожидания для каждого события (я не привожу все эти сведения в тексте примера 12.6, т. к. ширина страницы слишком мала). Максимальная продолжительность события SQL*Net message from client составила около нескольких секунд. И если бы я обратил более пристальное внимание на количество вызовов (# Calls в примере 12.6), то понял бы, что речь не идет об ошибке сбора данных или задумчивом пользователе.
В то время у меня еще не было ясного представления о том, как именно бороться с такой проблемой, и мы, под руководством Джеффа, принялись изучать вывод Hotsos Profiler. Руководствуясь принципом опережающего атрибутирования (см. главу 11), мы искали вызовы базы данных, которые следовали за событиями SQL*Net message from client. С тех пор компания Hotsos доработала программу Hotsos Profiler, и теперь подобные проблемы легко решаются всего за пару минут. Здесь описан процесс диагностики, реализованный в современной усовершенствованной версии.
Для того чтобы ничего не упустить, мы чуть внимательнее посмотрели на продолжительности событий SQL*Net message from client. В примере 12.6 этого не видно, т. к. страницы книги недостаточно широки, но на самом деле вывод Hotsos Profiler показывает, что максимальная продолжительность выполнения SQL*Net message from client составила 17,43 секунды. Быстрый поиск строки ela= 1743 (обратите внимание на пробел, который ядро Oracle вставляет между символами = и 1) в файле трассировки показал, что в хвосте файла действительно присутствует небольшая ошибка сбора данных. Между двумя строками XCTEND притаилось событие SQL* Net message from client со значением ela, равным 17,43 секунды. Первая фиксация соответствовала концу пользовательской операции. Вторая фиксация имела место, когда пользователь отключился от приложения. Девушке потребовалось несколько секунд на то, чтобы заметить, что операция завершилась. После исправления этой небольшой ошибки сбора данных профиль ресурсов пользовательской операции стал выглядеть так, как показано в примере 12.7.
Пример 12.7. Профиль ресурсов из примера 12.6 после исправления ошибки сбора данных, составляющей 17,43 секунды
Response Time Component
|
Duration
|
# Calls
|
Dur/Call
|
|
SQL*Net message from client
|
149.2s
|
91.0%%
|
6,093
|
0.024482s
|
CPU service
|
9.7s
|
5.9%
|
18,750
|
0.000515s
|
unaccounted-for
|
1.9s
|
1.2%
|
||
db file sequential read
|
1.6s
|
1.0%
|
1,740
|
0.000914s
|
log file sync
|
1.1s
|
0.7%
|
681
|
0.001645s
|
SQL*Net more data from client
|
0.3s
|
0.2%
|
71
|
0.003521s
|
SQL*Net more data to client
|
0.1s
|
0.1%
|
108
|
0.001019s
|
free buffer waits
|
0.1s
|
0.1%
|
4
|
0.022500s
|
db file scattered read
|
0.0s
|
0.0%
|
34
|
0.001176s
|
SQL*Net message to client
|
0.0s
|
0.0%
|
6,094
|
0.000007s
|
log file switch completion
|
0.0s
|
0.0%
|
1
|
0.030000s
|
latch free
|
0.0s
|
0.0%
|
1
|
0.010000s
|
log buffer space
|
0.0s
|
0.0%
|
2
|
0.005000s
|
direct path read
|
0.0s
|
0.0%
|
5
|
0.000000s
|
direct path write
|
0.0s
|
0.0%
|
2
|
0.000000s
|
Total
|
164.0s
|
100.0%
|
Следующий вопрос, требующий ответа, звучит так: «Какая команда SQL несет ответственность за оставшуюся длительность SQL*Net message from client?*. Выходные данные Hotsos Profiler автоматически предоставляют ответ на этот вопрос (см. пример 12.8).1
Пример 12.8. Hotsos Profiler определяет вклад каждой команды SQL в длительность события SQL*Net message from client
SQL Statement Id 1525010069
|
D
23.1s
|
uration 15.5%
|
2038166283
|
18.7s
|
12.5%
|
1966856986
|
17.6s
|
11.8°%
|
1547563725
|
13.9s
|
9.3%
|
3230460720
|
10.8s
|
7.2%
|
77 others
|
65.1s
|
43.6%
|
Total
|
149.2s
|
100.0%
|
Процентное соотношение, приведенное в примере 12.8, показывает, что не существует какой-то одной команды SQL, отвечающей за непропорционально большой вклад в длительность события SQL*Net message from client. Вот почему нельзя заметно сократить длительность этого события, ограничившись рассмотрением только одной команды SQL. Но начнем все же с первой, текст и статистики производительности которой приведены в примере 12.9.
Пример 12.9. SQL-текст и статистики производительности для команды 1525010069, вносящей основной вклад в длительность события SQL*Net message from client в сеансе
Cursor
|
Action
|
Response Time
|
LIO
|
PIO
|
||
Action
|
Count
|
Rows
|
Elapsed CPU
|
Other
|
Blocks
|
Blocks
|
Parse
|
696
|
0
|
0.9 0.8
|
0.1
|
0
|
0
|
Execute
|
348
|
348
|
1.7 1.6
|
0.0
|
5,251
|
351
|
Fetch
|
0
|
0
|
0.0 0.0
|
0.0
|
0
|
0
|
Total
|
1,044
|
348
|
2.6 2.4
|
0.1
|
5,251
|
351
|
Per Exe
|
1
|
1
|
0.0 0.0
|
0.0
|
15
|
1
|
Per Row
|
1
|
1
|
0.0 0.0
|
0.0
|
15
|
1
|
Я не включил это в показанный здесь вывод, но по данным Hotsos Profiler в файле трассировки имелось 347 «похожих» команд. Profiler считает две команды SQL похожими в том и только в том случае, если они идентичны во всем (с точки зрения ядра Oracle), кроме значений литералов. Утилита Oracle tkprof не производит подобного суммирования неразделяемых команд SQL; вместо этого она вывела бы 348 различных команд SQL, указав, что каждая из них потребляет очень мало ресурсов. Конечно, это несколько усложнило бы анализ, но вся информация, необходимая для того, чтобы определить, что команды должны быть разделяемыми, присутствует в файле трассировки SQL.
Данные примера 12.9 поясняют, почему различных команд SQL так много. Команды место заполнителей (переменных связывания) занимают литералы:
Значение, которое сразу же бросается в глаза как абсолютно препятствующее повторному использованию - это значение даты в последней позиции. Оно соответствует столбцу TMSP_LAST_UPDT - временной метке последнего обновления. Сколько раз приложение могло бы повторно использовать идентичную команду SQL, содержащую жестко запрограммированное значение времени с секундным разрешением?
Конечно, вы наверняка думаете, что правильный ответ - ноль, но на самом деле тут есть небольшая хитрость. Данное конкретное приложение повторно использует команду ровно один раз (т. е. данное приложение в целом делает это ровно два раза). Обратите внимание на количество разборов и выполнений данной команды (и ей подобных): для 348 команд в целом насчитывается 696 вызовов разбора и 348 выполнений. Именно так, количество разборов в два раза больше количества выполнений! В разделе «Оптимизация разбора» главы 11 я советовал выносить вызовы разбора из циклов, чтобы приложение могло повторно использовать курсор, подготовленный одним вызовом разбора, несколько раз. Вывод Hotsos Profiler показывает (хотя этого и не видно в примере 12.9), что 696 вызовам разбора соответствует 348 непопаданий в библиотечный кэш (из статистики mis необработанных данных трассировки). Невероятно, но данное приложение действительно разбирает каждую из этих команд SQL дважды для каждого вызова выполнения, как показано в примере 12.10.
Пример 12.10. Приложение выполняет два разбора для каждого выполнения... это никуда не годится
Первый вызов разбора для каждой команды приводит к непопаданию в библиотечный кэш (полный разбор), а второй - к попаданию в библиотечный кэш (частичный разбор).
Теперь давайте вспомним о том, в чем собственно состоит наша цель. События SQL*Net message from client занимают основную часть пользовательского времени отклика. Пример 12.7 показывает, что данная пользовательская операция выполняет в целом 6093 таких события, общий вклад которых в значение времени отклика составляет 149,2 секунды (каждое потребляет приблизительно 0,024482 секунды за вызов). SQL*Net message from client - это событие ожидания, которое ядро Oracle выполняет между вызовами базы данных. Поэтому избавление от вызовов базы данных приведет к ликвидации некоторой части времени отклика, относящейся к событиям SQL*Net message from client. К счастью, мы только что обнаружили возможность избавления от 348 вызовов разбора. Надо просто найти способ прекратить двойной разбор для каждого вызова выполнения. Ожидаемая экономия при этом составит около 8,5 секунды за сеанс (около 5% общего времени отклика сеанса).
Не очень впечатляющее начало, но мы еще не закончили. В главе 11 я говорил, что при удалении вызова разбора из цикла удается устранить все вызовы разбора, кроме одного. Если мы используем переменные связывания и сделаем код похожим на масштабируемый код приложения, приведенный в примере 11.2, то при этом мы избавимся от 695 ненужных вызовов разбора. Ожидаемая экономия пользовательского времени при этом составит около 17 секунд (приблизительно 10% общего времени отклика сеанса).
Нас заинтересовало, сколько еще команд SQL может страдать от тех же проблем, что и первая исследованная нами. Обратившись к подробной статистике для всех команд в выводе Hotsos Profiler, мы обнаружили, что кандидатами на устранение являются более 3000 вызовов базы данных. Ожидаемое улучшение времени отклика должно было составить около 73 секунд, т. е. порядка 45% общего времени отклика сеанса.
Но и это еще не все. Если помните, пользователь нажимает клавишу «OK» всего один раз, а затем более 180 секунд ждет результата, при этом никакие другие входные данные для приложения не предполагаются. Но что же мы видим в примере 12.9? Команда INSERT, выполняющая вставку одной строки, выполняется 348 раз, обрабатывая в целом 348 строк. Зачем приложению выполнять 348 вызовов базы данных для вставки 348 строк? В Oracle имеется функция вставки массивом, которая может уменьшить количество команд INSERT с 348 до 4 (если приложение может работать с массивами из 100 строк). Если удастся реализовать подобное сокращение количества вызовов базы данных, то общее число вызовов в сеансе уменьшится более чем на 650 вызовов. Дополнительная экономия составит около 16 секунд, т. е. еще 10% общего времени отклика сеанса.
Если удастся осуществить все предложенные исключения вызовов базы данных, то будет удалено около 3650 вызовов, в результате чего время отклика сократится приблизительно на 89 секунд или на 54%. После избавления от этих вызовов базы данных пользователь может рассчитывать на улучшение времени отклика со 164 секунд до примерно 75 секунд. Возможно, что с приложением одновременно работает множество пользователей, тогда ненужные вызовы базы данных поглощают значительную часть пропускной способности сети. Если дело обстоит именно так, то избавление от ненужных вызовов базы данных может снизить и задержки в очереди к сети, так что 0,024-секундная задержка событий SQL*Net message from client может уменьшиться до приблизительно 0,015 секунды. В этом случае оставшиеся после оптимизации примерно 2400 событий SQL*Net message from client будут выполнены всего за 37 секунд, тогда общее уменьшение пользовательского времени отклика составит около 110 секунд.
При анализе файлов трассировки на занятиях у нас не всегда была возможность увидеть конечный результат наших рекомендаций. Это был как раз такой случай - рассматриваемая пользовательская операция осуществлялась в приложении стороннего производителя, поэтому три предложенные нами усовершенствования требовали его вмешательства. Студентка передала наши предложения поставщику программного обеспечения:
Не выполнять каждый разбор дважды, сократить количество вызовов разбора для многих курсоров вдвое.
Использовать переменные связывания и вынести вызовы разбора из циклов для дальнейшего снижения количества вызовов разбора до одного на курсор.
Уменьшить общее количество вызовов базы данных за счет применения обработки массивами вместо построчной обработки.
Когда я пишу эту главу, мы и наша слушательница все еще ждем реакции поставщика. Ее компания планирует переход на новую версию программы вскоре после выхода книги. Мы будем держать вас в курсе происходящего на нашем сайте в Интернете.
Эта история учит нас следующему:
Нельзя просто игнорировать события ожидания SQL*Net message from client. Если они вносят значительный вклад во время отклика корректно выбранной пользовательской операции, то необходимо обратить на них внимание.
Даже если с кодом SQL все в порядке, слишком большое количество вызовов базы данных может привести к ухудшению производительности. В этом случае может обнаружиться, что в командах SQL на самом деле есть изъяны. Но решение этой проблемы до устранения избыточных вызовов базы данных приведет к незначительным и практически незаметным результатам.
< Предыдущая | Следующая > |
---|