s2k3-soft-ice

поиск бутылочного горлышка вместе с soft-ice

крис касперски, ака мыщъх, a.k.a. nezumi, a.k.a. souriz, a.k.a. elraton, no-email

производительность сервера зависит не только от мощности аппаратной части, но и программной оснастки. один «кривой» драйвер (приложение) способен сожрать все ресурсы, затормозив систему до уровня асфальтового катка и системный монитор не покажет ровным счетом ничего! как найти виновника?! из множества программ самым доступным, точным и надежным инструментом как ни странно оказывается отладчик soft-ice

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

Soft-Ice, будучи отладчиком уровня ядра, позволяет остановить систему в любой момент и посмотреть какими интересными делами она сейчас занимается. С Soft-Ice в руках поиск «бутылочного горлышка» занимает буквально считанные минуты, причем, результат абсолютно надежен. От него не скрыться ни «кривым» драйверам, ни некорректно работающему аппаратному обеспечению. Кстати, знания ассемблера для этого не потребуется. Невероятно, но факт! Достаточно освоить несколько несложных команд, а все остальное Soft-Ice сделает за нас!

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

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

Почему же ошибаются счетчики производительности и, в частности, индикатор загрузки процессора? Дело в том, что под «загрузкой процессора» создатели NT понимают отнюдь не загрузку процессора, как таковую, а готовность потока поделиться остатками кванта отпущенного ему процессорного времени. На хакерских конференциях было продемонстрировано большое количество exploit'ов, отдающих ~3%-5% от полного кванта времени и заставляющих счетчик производительности отображать загрузку близкую к нулевой, чем с успехом пользуются многие зловредные программы, использующие распределенные сети для взлома паролей например.

Другой источник ошибок — асинхронные операции ввода/вывода, поддерживаемые ядром. При чтении данных с диска или файла подкачки, поток «замораживается» системой, передающей управление потокам этого же или другого процесса, в результате чего поток, инициирующий процесс чтения, показывает намного более низкое потребление процессорного времени, чем происходит в действительности. Строго говоря, индикатор загрузки не лжет. Поток, действительно, «спит» во время чтения с диска, но ведь нас интересует отнюдь не формальная сторона проблемы, а полное время, включающее в себя длительность всех операций ввода/вывода, поскольку, интенсивный ввод/вывод — отличное средство торможения!

Наконец, счетчик загрузки ЦП не учитывает время, потребляемое драйверами. «Загрузка ядра» более или менее корректно вычисляется только для некоторых системных вызовов, да и то с кучей оговорок и ограничений.

Рисунок 1 счетчики производительности — популярный, но крайне ненадежный способ измерений

Как узнать: на что уходит львиная доля системного времени? Очень просто — установить Soft-Ice, вызывать его комбинацией <CTRL-D>, посмотреть чем занимается сервер, выйти из Soft-Ice по <CTRL-D> или команде «x;<ENTER>», затем тут же вызывать его вновь: чем занимается система на этот раз? Повторяя данную операцию в цикле, мы достаточно быстро соберем богатый статистический материал. Совершенно очевидно: истинное процессорное время, потребляемое каждым компонентом, прямо пропорционально частоте его появления при вызове отладчика.

Когда сервер вообще ничем не нагружен, свыше 90% «всплытий» приходится на превдопроцесс «Idle», означающий, что текущие операции выполняются так быстро, что мы их просто не засекаем, т.к. они занимают ничтожные доли процессорного времени, при этом сервер может обслуживать достаточно большое количество пользователей, дефрагментировать жесткий диск в фоновом режиме и заниматься прочими делами. Вот так парадокс! Сервер работает, а процессор — отдыхает, что означает, что конфигурация сервера имеет определенный избыток по мощности и узких мест в ней нет.

При дальнейшем росте загрузки, мы получаем более или менее «гладкое» распределение, попадая то в интерпретатор PHP, то в процесс, обслуживающий SQL, то в другой «демон», на языке Microsoft, называемый сервисом. Это вполне нормальное явление, однако, доминирование одного процесса над другим — плохой признак, указывающий на дефекты проектирования программы. В нормальной ситуации свыше 90% машинного времени уходит на ввод/вывод, в течении которого процессы, инициировавшие запрос ввода/вывода, спят как младенцы независимо от того, справляется дисковая подсистема/сетевая карта со своей работой или нет.

Повышения активность процессов свидетельствует о «тяжелых» операциях типа криптографии, упаковке/распаковке потока данных и прочих «наукоемких» задачах, большинство из которых, кстати говоря, сугубо опционально. В частности, шифрование можно либо вообще отключить, либо выбрать «легкие» алгоритмы. Аналогичным образом обстоит и с упаковкой. Естественно, ситуацию с вещанием цифрового аудио/видео мы в расчет не берем. Тут, понятное дело, требуется мощный процессор с емкой кэш-памятью. А как узнать, что кэш памяти достаточно для решения поставленной задачи? Очень просто! Если Soft-Ice останавливается на произвольных машинных инструкциях — все ОК, а вот если доминируют инструкции чтения/записи в память (MOV плюс что-то в квадратных скобках, а так же MOVSx, CMPSx и STOSx), то установка процессора с более емком кэш-памятью существенно поднимет производительность!

Но это все голимая теория, переходим к практике и вещам далеко не очевидным даже для тех, кто давно использует Soft-Ice. Имя процесса, исполняемого в данный момент, отображается в правом нижнем углу, а текущий исполняемый код — в окне CODE, расположенным посередине экрана. В правильно сконструированной программе, процесс останавливается каждый раз в случайном месте. То есть колонка адресов будет каждый раз разная. Можно даже сказать очень сильно разная. Это означает, что в исследуемом процессе нет горячих точек или они выражены крайне слабо. Напротив, если какие-то диапазоны адресов встречаются чаще остальных — мы поймали горячую точку, которую разработчики программы должны были устранить еще на стадии проектирования, ну или на худой конец в процессе оптимизации своего продукта перед выбросом его на рынок. Впрочем, если они его не устранили, это не повод для паники, ибо, как мы уже говорили, ресурсоемкие операции типа криптографии занимают намного более времени, чем, например, отдача пользователю файла в том виде, в котором он записан на диске.

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

В качестве наглядного примера рассмотрим простую программу, мотающую холостой цикл, исходный код который укладывается в несколько строк на Си:

main()

{

while(1);

}

Листинг 1 исходный текст программы hCPUl.c

Откомпилируем ее без опций оптимизации (в случае MS VC это: cl.exe hCPUl.c), чтобы оптимизатор не выбросил ненужный с его точки зрения цикл while и запустим ее на выполнение. На однопроцессорных машинах мы получим 100% загрузку по индикатору, на двухпроцессорных (или однопроцессорных с двумя ядрами) — 50%, соответственно, четыре процессора/ядра дадут всего лишь 25%, хотя Soft-Ice при каждом вызове будет всплывать в одном и том же процессе — hCPUl, исполняющемся в одном и том же месте — в цикле while, то есть, согласно, Soft-Ice, загрузка процессора близка к 100%. Это одновременно верно и нет. С одной стороны, поток, в котором исполняется цикл while, нагружает всего лишь один процессор/ядро из всех имеющихся, совсем не препятствуя исполнению потоков других процессов на оставшихся процессорах (ядрах), однако, реальное торможение намного больше, чем это следует из простых арифметических расчетов. Системный планировщик распределяет очереди потоков равномерно между всеми процессорами (ядрами) и если один из процессоров (ядер) захватывается «неправильным» потоком, планировщик начинает оптимизировать очередь потоков, но эффективность такой оптимизации невелика, в чем нетрудно убедиться, измерив реакционноспособность сервера с запущенной программой hCPUl и без нее.

Теперь перейдем к более сложным вопросам, зарывшись в недра драйверов. Soft-Ice _всегда_ отображает имя процесса в правом нижнем углу, вне зависимости от того, находится ли система на прикладном (user-land) или ядерном (kernel-space) уровне. А что у нас в ядре? Ну, например, драйвера, которые могут вообще не иметь _никакого_ отношения к отображаемому процессу. Просто драйвер получил управление (например, по прерыванию, поступившему извне) именно в том момент, когда система переключила контекст на данный процесс. А может быть, совсем другой процесс прямо или косвенно инициировал вызов драйвера и тут же заснул, система переключила контекст на следующий процесс, а драйвер… как бы выразиться поделикатнее… если не завис, то конкретно застрял. Соответственно, при вызове отладчика, мы будет оказываться в нем чаще, чем в остальных, но имя процесса, отображаемое в нижнем углу, тут не причем. Оно может быть как одним и тем же, так и меняется в произвольном порядке, но даже если процесс каждый раз один и тот же, он, скорее всего тут не причем.

Определить, что система находится в kernel-space очень просто. В конфигурации по умолчанию, нижняя половина адресного пространства принадлежит прикладным программам, а верхняя — ядру. Соответственно, если мы наблюдаем колонку адресов < 80000000h, отладчик находится внутри прикладных программ, в противном случае — это ядро или один из его драйверов. Селектор (то, что записано в регистре CS и то, что находится перед адресом, отдельное от него знаком двоеточия) в прикладном режиме равен 1Bh и 08h в режиме ядра.

(Примечание: при указании ключа 3GB в boot.ini, ядро с драйверами «ужимается» до 1 Гбайта, соответственно, граница ядерных земель отодвигается на 40000000h, во всяком случае, в 32-битном режиме все происходит именно так, а в 64-битном все совсем не так, но поскольку, Soft-Ice работает только в 32-битном режиме, оставим проблемы 64-битных серверов за рамками данной статьи).

Рисунок 2 Soft-Ice считает, что он остановился внутри процесса VMwareService, но колонка адресов, выходящая за пределы user-land, указывает, что мы находится в одном из драйверов

Допустим, Soft-Ice остановится в некотором процессе, скажем, VMwareService, но столбец адресов выходит за пределы 80000000h и текущая исполняемая команда, выделенная инверсной строкой, расположена по BFF0ACE2h (см. рис. 02). Как узнать какому из драйверов она принадлежит?! Нет ничего проще! Даем команду «DRIVER» или «MOD» и смотрим на базовые адреса загрузки драйверов. К сожалению, Soft-Ice не сортирует их по списку возрастания, но этот недостаток легко исправить! Достаточно выйти из Soft-Ice, запустить поставляемый вместе с ним Symbol Loader и сохранить историю команд в текстовой файл, а затем посредством MS Word или MS Excel преобразовать ее в таблицу.

Драйвер, в чьих границах находится обозначенный адрес, и есть «виновник». В данном случае (см. рис. 03) это NDIS – низкоуровневый сетевой драйвер. При интенсивной работе сетевой карте его появление в Soft-Ice вполне объяснимо, приемлемо и понятно, однако, это должна быть действительно _очень_ высокая нагрузка, в противном случае, это кривой драйвер или неправильная сетевая карта, смена которой (вместе с драйвером) ощутимо повысит общую производительность сервера, причем (внимание!) факт подобной «кривизны» не определяет ни системный монитор, ни легион программ, предназначенных для тюнинга сервера, поскольку все они работают на более высоком уровне.

Рисунок 3 ловля драйвера на живца

Другая широко распространенная ситуация — Soft-Ice начинает часто всплывает на команде чтения (реже — записи) в/из порт ввода-вывода, обычно представленный машинными командами IN Ax DX/OUT DX,Ax соответственно (см. рис. 04). Это плохой признак!!! Нормальное железо, управляемое правильными драйверами, работает по прерыванию, обмениваясь данными через DMA. Обращение к портам лишь инициируют ту или иную операцию, а потому занимают ничтожное время.

Интенсивная работа с портами ввода/вывода нагружает системную шину, снижая производительность всех подсистем компьютера в целом, а потому, такой ситуации необходимо избегать любой ценой. Легко сказать — избегать! А для этого, как минимум, необходимо определить к какому именно оборудованию обращается система. No problem, как говорят в Одессе, ща определим!

Рисунок 4 знакомство с портами ввода/вывода

Смотрим на значение, содержащееся в регистре DX (младшие 16-бит регистра EDX или, выражаясь человеческим языком, крайние четыре цифры справа), выходим из отладчика, запускаем «Диспетчер устройств» в меню «Вид» выбираем сортировку устройств по ресурсам и в портах ввода/вывода тут же находим наш порт, равный в данном случае 379h и принадлежащий LPT1 – порту принтера, который вообще не установлен на обозначенной системе. Хм, странно, принтера нет, а обращение к порту происходят с чрезвычайно высокой степенью интенсивности, причем, не по прерыванию, а по опросу (готовности устройства).

Лезем в очередь печати и видим, что в ней каким-то чудом оказался документ, отправленный на печать, и какой-то левый принтер (в смысле его драйвер) «мистическим» образом повешен на LPT1, хотя физическим принтера нет. Очищаем очередь печати — не очищается!!! Сносим к черту драйвер — обращение к порту исчезает, а машина летит вперед с такой производительностью, что буквально отрывается от асфальта. Причем, другим путем обнаружить «бутылочное горлышко» не удавалось — системный монитор упорно твердил, что все нормально, никаких косяков.

Рисунок 5 определение оборудования, к которому происходит обращение через «Диспетчер устройств»

Наконец, Soft-Ice может останавливаться не только внутри драйверов, но и ядерных функций, лежащих в области адресов 8xxxxxxxh, где находится ntoskrnl.exe. Само ядро тут, понятное дело, не причем. Это просто какой-то противный драйвер напрягает его вызовом тех или иных функций. Что же это за драйвер такой?

Даем команду «STACK» и смотрим на левую колонку адресов (см. рис. 06), по которой и определяем драйвер описанным выше способом. В данным случае им оказался драйвер DVD-привода, автоматически установленный системой. Тщательное расследование выяснило, что к драйверу никаких претензий нет, а вот дефект проектирования DVD-привода привел к тому, что драйвер начинал обмениваться с ними каким-то командами (так и не найдены в стандарте на ATAPI-команды) даже если в лотке отсутствовал диск. Смена DVD-привода «утихомирила» драйвер, «мистический» обмен прекратился, а общая производительность системы опять-таки возросла.

Рисунок 6 просмотр стека позволяет определить какой именно драйвер вызывает функции ядра

Мы убедились, что поиск «узких» мест сервера при помощи Soft-Ice – это простой, но чрезвычайно точный и надежный способ, охватывающий абсолютно все уровни: аппаратный, ядерный и программный. Конечно, не стоит ждать от Soft-Ice точных «телеметрических» показаний, выраженных в численном виде. Да этого, в общем-то, и не нужно. Если в системе имеется «бутылочное горлышко», то Soft-Ice обнаружит его практически сразу после пяти-шести вызовов.

К недостаткам описанного способа следует в первую очередь отнести, что Soft-Ice «мертв». На Server 2003 в 32-разрядном режиме он еще работает (да и то не со всеми видео-картами и чипсетами), но более новые системы ему не по зубам. К тому же, Soft-Ice способен обрушивать систему, вызывая голубые экраны смерти, зависания (от которых порой не спасает и RESET, а только «передерживание» питания) и перезагрузки.

Последние два случая очень опасны — сброса дисковых буферов не происходит и раздел, с которым ведется активная работа на чтение/запись, рискует отправиться к праотцам. Но даже если «полет нормальный», в момент вызова Soft-Ice,сервер останавливает системные часы (которые потом нам придется корректировать) и прекращает весь сетевой обмен, что при продолжительном пребывании в отладчике ведет к обрыву TCP/IP соединений, вызывая естественное недовольство пользователей.