|
APC личная страница |
· LoadRunner vs MSRPC · |
|
LoadRunner 9.1 vs MS RPC: Первая кровьЯ успешно завершил первые опыты нагрузочного тестирования с использованием LoadRunner 9.1 и спешу поделиться опытом. В статье описан путь от самой первой пробы к успешному стрессовому тесту одной бизнес-процедуры на 100 пользователей, в обход ряда проблем LoadRunner’a и тестируемого ПО. В рассказе сознательно опущены многие мелкие попутные проблемы, особенно специфические для тестируемого ПО, так как они не представят интереса для большинства читателей. Тестируемый софтПО, которое разрабатывается в нашей компании – огромная АБС, жутко старая (более 10 лет самому старому коду), очень большой груз legacy ощущается повсюду. Код где-то на C, где-то на C++, где-то из C++ вызывается C#. И это только один сервер из кучи технологий, но он основной, и нагрузочно тестировать необходимо именно его. Цепляется этот сервер к БД MS SQL Server 2005 и грызет базу более 50ГБ размером. В качестве протокола взаимодействия толстого клиента с сервером у нас используется MSRPC. Сервер является stateful, каждый подключающийся пользователь постоянно висит на сервере и имеет контекст в виде как минимум двух потоков, которые в свою очередь содержат по одному соединению с БД. Проблемой софта давно уже являются сбои, не проявляющиеся при спокойном функциональном тестировании в четыре сотрудника, но вылезающие на продукционном сервере в банке, где с софтом одновременно работают 50-70 пользователей. В связи с этим руководство повысило приоритет разработки нагрузочных тестов. Инструмент тестированияВ качестве инструмента был выбран HP LoadRunner (далее в тексте LR), просто потому что мне понравилась ключевая идея его работы, звучит масштабируемо. LR работает методом записи сетевого трафика от клиента к серверу и воспроизведения затем этого трафика, как будто LR и есть клиент. Ну а дальше потоки трафика множатся и распараллеливаются, эмулируя толпу клиентов. Для очистки совести я пошарил в сети в поиске чего-то еще, умеющего эффективно нагружать сервера на базе Windows, не нашел ничего вразумительного. Качаем триал LR версии 9.1 и ставим по умолчанию. Толковых доков на русском языке по инструменту мне найти не удалось, английские доки в подавляющем большинстве тестируют веб-системы, а это пока не наш случай (у нас есть технологии, работающие по веб-протоколам, но они второстепенны по отношению к "real-time" АБС). Кстати, заработал LR только на двух рабочих станциях из 5 у нас в отделе. На остальных запись почему-то ничего не записывает. Первая пробаКак любой здравый тестировщик, я начал с пробы "в лоб", чтобы хоть что-то пощупать. Я решил отвлечься от первой реальной цели нагрузочного тестирования «научиться моделировать реальный день в банке», и ограничиться тестом входа клиента на сервер и сразу же выхода. Проба записи показала, что протокола MS RPC у LR'a нету, так что работать приходится по чистым сокетам Windows Sockets. В результате записи я получил два файла с кодом такого вида: /********************************************************************* ;WSRData 2 1 Очень хорошо, что LR отделяет данные от кода, но если в сценарии процедур что-то понятно, то в бинарных буферах на первый взгляд царит какой-то ужас. Тем не менее, некоторые вещи можно разглядеть: какие-то UUID, имя пользователя, машина с которой я входил. Есть шансы разобраться. Попытка воспроизвести скрипт после записи показала что LR все же дурак и путает сетевые адреса, на которые открывались сокеты (не всегда, но чаще всего подставляет другой существующий адрес из локалки). Но даже после исправления адресов в скрипте ручками воспроизведение не проходит дальше третьего-четвертого запросов. Ну хорошо, хотя бы какие-то запросы проходят, но все равно в логе идут записи о несовпадении длины возвращаемых сервером данных с ожидаемой. Теперь я знаю отправную точку и могу последовательно двигаться вперед, к конкретным проблемам и решениям. Беседа с программистамиЗадушевная беседа с программистами обнажила следующие предположительные проблемы на уровне отдельных запросов RPC в нашем ПО:
Первую проблему пока решать не буду, неясно, чем она может помешать. Вторую проблему программисты согласились помочь обойти, сделав опцию компиляции ПО, отключающую сессионную шифрацию. На третью проблему программисты возопили "но тесты же будут нереальными при несжатом трафике, к тому же этот код раскидан по системе и править его будет сложно и рисковано", после чего я поковырял сорцы и нашел, что функции компресии zlib обернуты в пару наших процедур. Внутри этих процедур я поставил обход компрессии и все стало в шоколаде (это было со второй попытки, с первой я пошел неверным путем и софт перестал работать). Возможно, что-то там по системе и раскидано (уверен, что раскиданы там баги), но моему логину-логауту это пока не мешает. А что до нереальности теста, так мне бы сначала хоть нереально заставить это все работать, а потом уже думать, как прикрутить zlib на место. Четвертая проблема будет реально мешать, нужно будет что-то с этим придумывать. На всякий случай в софте есть опция отключения проверки клиентов на живость, но ее отключение нежелательно, так как такая проверка является необходимым аспектом работы ПО. Но, поскольку я тестирую пока логин и сразу логаут, на эту проблему можно временно не обращать внимания. Пятую проблему также пока оставляю в стороне, первый этап – повторить единичный логин, UUID и так будет уникальным на сервере. А дальше подыщу решение, так как UUID в записанных LR буферах хорошо виден и его можно будет заменять. С шестой проблемой просто придется научиться работать, разбираясь в череде запросов к серверу. По части процедуры логина-логаута программисты сказали, что она идет так:
Выглядит логично, попробую поверить на слово. Компилирую ПО без шифрации и сжатия трафика, ставлю на тестовый стенд. Изучаем DCERPCВсе же лучше бы мне знать, как выглядит этот протокол RPC в байтах и понимать, что за пакеты гуляют между клиентом и сервером. У меня установлен замечательный сниффер Wireshark, который тут же определяет трафик как DCERPC и расшифровывает его заголовки до уровня данных заглушки. Ага, значит данные заглушки это и есть то, что скормил наш софт сервису MSRPC, остальное является служебным заголовком. Сопоставление данных сниффера и записаных пакетов в LR показывают, что последний видит весь пакет DCE RPC целиком как бинарный буфер. Первой нашей задачей будет научиться отделять заголовок RPC от данных заглушки, чтобы отделить проблемы обеспечения корректной эмуляции слоя RPC от проблем посылания серверу данных через RPC. Снова записываем в LR трафик логина-логаута, но при этом еще и записываем с помощью Wireshark реально происходящий обмен пакетами и сохраняем его как файл эталона общения между клиентом и сервером. Гуглим доки по MSRPC. У самого Майкрософта толкового описания протокола и структуры сетевых пакетов я не нашел (фигня у них какая-то на эту тему), но серф по сети подсказал что Майкрософт основывает свой протокол на DCE/RPC. Я нашел доку по DCE RPC 1.1, после получаса ковыряния в которой нашел-таки имеющую отношение ко мне главу про структуру пакетов протокола, где описаны типы запросов и структура сетевых пакетов. Есть лишь небольшое отличие с тем, что я наблюдаю в WireShark: у меня в сетевых пакетах отсутствует поле packed_drep, но спишем это на то, что дока наверное новее чем протокол, который используется нашим ПО. Далее внимательно изучаем эталонные пакеты и с помощью доки учимся понимать, как работает RPC с точки зрения сетевого трафика. Резюмирую мои наблюдения:
Итогом изучения протокола и документации по RPC является ощущение, что работать с этим всем можно, по крайней мере, бинарные потоки, записанные LR'ом стали понятней. Можно структурировать бинарные буферы в файле data.ws, отделив заголовок от данных заглушки и приведя запросы в некоторый приличный вид подобный такому: send IsAliveSend 24"\x05\x00\x00\x03\x10\x00\x00\x00" // const1 Но я все же причесывал запросы чуть позже, продвигаясь вперед по одному. Отладка скрипта и первая проблемаВооруженный знаниями, я снова пробую повторить воспроизведение скрипта, но на этот раз еще и сниффер запускаю, чтобы можно было видеть трафик, генерируемый LR’ом, и сравнивать его с эталонным. Скрипт валится на получении ответа от запроса номер четыре, перед этим при получении ответа на третий запрос он сообщает, что ответ сервера по длине отличался от ожидаемого. По словам программистов, третий запрос – это регистрация пользователя на сервере, а четвертый - проверка пароля (аутентификация). Тем временем Wireshark показывает, что именно после третьего запроса (по порядку, а не по call ID) я получаю ответ RPC fault со статусом “nca_proto_error (0x1c01000b)”. Выходит у меня что-то не так на третьем запросе. Ставлю перед четвертым запросом “return 0;” и сосредотачиваюсь на отладке последовательности до третьего запроса. Очищаю сниффер и еще раз запускаю воспроизведение. Теперь в LR осталось лишь предупреждение о длине запроса, заглядываю в сниффер. А там оказывается, что третий запрос получился каким-то сдвоенным, а в эталонном наборе ничего подобного нет. Но самое главное – третий запрос имеет call ID = 3, что совсем не укладывается в правила обмена RPC, которые я выучил по доке. Да и эталонный набор показывает, что должен идти обычный запрос. Выглядит все странновато, поскольку по длине запросы вроде совпадают (162 байта). Начинаю сравнивать собственно данные, которые идут в эталонном пакете и посылаются LR’ом. WireShark показывает, что он собрал сдвоенный RPC -пакет из двух запросов, это значит что LR пихает в сокет что-то совсем непотребное. Смотрю на бинарный буфер, отправляемый на третьем шаге и вижу, что длина отправляемого буфера – 258 байт. Оказывается он дурак также и тут, и не смог правильно записать трафик. Что делать? Wireshark позволяет копировать данные сетевых пакетов в Hex-виде, и я воспользуюсь тем, что эталонная сессия байтово та самая что я отлаживаю в скрипте. Просто скопирую данные пакета уровня RPC как Hex-строку (Copy -> Bytes (Hex Stream)), получаю строчку вида «0500000310000000a2000000020000008a00000000000900…». Для того, чтобы сделать из этой строки буфер LR, просто перед каждой парой символов ставлю “\x” и получаю бинарный буфер «\x05\x00\x00\x03\x10\x00\x00\x00\xa2…». Заменяю этими данными неправильный буфер в файле data.ws и подправляю длину буфера. Согласен, что решение пахнет дурно, но мне важнее понять, в чем проблема, и будет ли работать механизм, если все же слать пакеты как в эталонном трафике. Размяв пальцы копипейстом 162 раза втыкаюсь в еще одну проблему – если длина строчки в файле data.ws слишком большая (какая – не знаю), то он начинает ругаться на невозможность прочитать буфер. Ну что ж, еще раз дурак. Слава Богу, HP предусмотрели, что если я нажимаю Enter в посреди строчки, он ее переносит и сам кавычки добавляет. Главное не резать строку посреди байтов. Доразмяв пальцы Enter’ом, пробую теперь запустить скрипт, не забыв очистить и перезапустить сниффер. Теперь скрипт LR завершается без ошибки, есть лишь сообщение о несовпадении размера полученного буфера. Ну еще бы, если он перекосил буферы запроса, страшно подумать что он там насобирал в буферы ответов. Ладно, попробую подправить длину буфера ответа, сниффер говорит что сервер отвечает 48 байтами. Вот тут полезное наблюдение – LR автоматически не проверяет содержимое буфера ответа в файле data.ws, он просто проверяет по длине. То есть можно совсем стереть содержимое буфера, оставив только имя и длину. Еще один запуск воспроизведения скрипта – и все проходит чисто, как в LR’e, так и в сниффере. На распутьеТеперь я стою перед важным выбором: либо и дальше забить на записанный LR’ом трафик и брать образцы пакетов из сниффера, либо попытаться дальше опираться на ненадежные возможности LR. Я принял первый путь, написал скриптик на PHP, преобразующий буфер сниффера в экранированный вариант для LR, но в момент написания статьи для интереса сделаю несколько шагов по второму пути, чтобы не отринуть его безосновательно. В итоге шаг по второму пути показал что дальше все так же плохо, записанный трафик не соответствует реальности, а меня еще ждут проблемы с открытием параллельно второго сокета и посылкой в него корректных данных. К тому же LR поддерживает запись только если сам запускает приложение, и мне не улыбается в будущем каждый тест записывать от начала логина, а потом ковыряться, разбираясь, где он чего напутал. Лучше я буду отдельные запросы к серверу сниффером записывать и выработаю надежную процедуру переноса данных из сниффера в LR, зато глюки последнего останутся при нем. Работа параллельно с двумя сокетамиДалее я двигаюсь уже по данным снифера и идентифицирую запросы по Call ID, Wireshark хорошо визуализирует это поле в списке пакетов. Четвертый запрос – это аутентификация, без особых проблем организую запрос к серверу и получение данных от него. Пробую запуск до четвертого запроса включительно – все шоколадно. Аналогично поступаю с пятым запросом, попутно отмечая, что пятый запрос программистами не описан, в журнале сервера он фигурирует как «Send message». Ну что ж, я знал, что программистам нельзя верить. Никогда. С шестым запросом интересней: судя по эталону, клиент отправляет серверу шестой запрос, потом открывает второй сокет, шлет по нему запрос, потом снова шлет шестой запрос. Странно конечно, но попробую поверить снифферу и слать такие запросы. В итоге посылка второй раз шестого запроса на сервер получает в ответ RPC Failed, значит, я тут что-то не доглядел. А ответ таков: второй шестой запрос – это запрос уже от сервера клиенту, нужно лишь быть внимательней, рассматривая в сниффере пакеты, и смотреть IP-адреса src и dest. Пересматриваю всю эталонную сессию до конца и все становится понятно: в некоторый момент клиент и сервер меняются местами и сервер начинает вызывать клиента, а не наоборот. Да это же наш поллинг! Оказывается, программисты еще и соврали про то, что поллингом занимается второй сокет. Судя по трафику, второй сокет просто загружает в клиента крупные блоки данных (в сниффере видно что это информация о версиях ПО, список прав пользователя и опции), а поллингом занимается первый сокет, тот который аутентифицировался на сервере. Немного странновато то, что все запросы поллинга идут под одним call ID, но я пока не буду в этом разбираться, удовлетворюсь фактом знания, к чему они относятся. Дальше я просто методично переношу в LR бинарные буферы из эталона, но меня поджидает еще один сюрприз: протокол DCE RPC позволяет при необходимости разбивать длинные пакеты на части, так что мне нужно дожидаться получения всех этих пакетов. Но процедура получения данных из сокета по умолчанию имеет достаточно короткий таймаут, и сообщает что недополучила данных из сокета. Это приводит к тому, что следующая процедура получения данных из сокета получит не свои данные, а оставшиеся от предыдущего запроса, в общем, бардак. Надо с этим что-то делать. Надежная работа с сокетомДля того чтобы надежно дожидаться от сокета заранее определенного количества байт, я решил написать собственную процедуру-обертку получения ответа, которая стала бы дожидаться всех данных. При этом информацию, сколько байт дожидаться в ответ от сервера, я решил брать из файла data.ws. Однако среди встроенных функций LR я не нашел функции получения размера буфера из файла data.ws, есть лишь функция получения самого буфера. Ну что ж, примем еще одно дурацкое решение: поскольку LR игнорирует соответствие длины буфера его содержимому в файле data.ws, я запишу в содержимое буфера его длину, но в строковом виде, а в своей функции превращу строку в число и буду сравнивать. Тут нужно сделать лирическое отступление: у меня шевелятся волосы от того что тестировщику приходится работать на языке C, со всеми указателями, аллокациями памяти и прочей лабудой. Хуже не придумаешь. Разум тестировщика должен быть чист от подобного мусора. Он должен работать в managed code, не думая о выделениях памяти и типах переменных. Его язык – скрипты, а не компилируемые сорцы, чуть ли не на ассемблере. Беда в том, что буферы LR не являются null-terminated, так что мне придется еще подобавлять нулевые байты каждому буферу, чтобы сишная процедура atoi смогла воспринять строку буфера. Вот так теперь выглядит мой буфер: recv IsAliveRecv 28 А так выглядит процедура гарантированного получения заданного количества байт: my_receive(char * socket, char * bufname) Алгоритм прост: процедуру просят получить по сокету socket буфер bufname, она лезет в data.ws, узнает там ожидаемый размер буфера и получает из сокета данные. Если она получила не то, что ожидала, она ждет секунду и пробует еще раз. Если опять не хватило – пробует еще, и так 4 попытки. Если в итоге все же не получила сколько задали – возвращает false, иначе возвращает true. Если в результате какой-то попытки она получит 0 байт, то она прекратит попытки. Для того чтобы гарантировать прохождение скрипта логина корректно, я создам еще одну процедуру: my_receive_strict(char * socket, char * bufname) Она завершит выполнение текущей транзакции LR с сообщением об ошибке, если не удалось получить буфер ожидаемого размера. Теперь заменяю все lrs_receive на процедуру my_receive_strict и опробуем работу. После некоторой шлифовки мой скрипт заработал на отлично, заваливаясь в нужной ситуации (к примеру, логиним несуществующего юзера). Пробую поставить в свойствах Vuser’а десяток итераций и убеждаюсь, что скрипт работает стабильно. Общую схему событий при логине, реально выясненную в процессе эмуляции,
можно увидеть на диаграмме. Кстати, в процессе всего этого программизма я быстро понял, что надо скорее начинать хранить все эти файлы в SVN, так как слишком много проб и ошибок, после которых хочется вернуться назад. Параметризация запросовКак говорится, гуртом и батьку легче бить. Это я к тому, что уже хочется запулить сотню пользователей в параллельный логин, но в данный момент они точно пересекутся по UUID клиента на сервере и не смогут прологиниться. Также они врубятся в дублирование имени пользователя, так как сервер запрещает одновременный вход нескольких юзеров под одним логином. Нужно параметризовать запросы к серверу так, чтобы имя юзера было уникальным в пределах множества параллельных юзеров, и чтобы UUID был так же уникален. Для этого в LR есть механизм параметров, которые подставляются в момент выполнения скрипта и могут менять свои значения весьма разнообразным правилам. Для начала возьмусь за UUID, он очень хорошо светится во всех пакетах в сниффере и его нетрудно выделить среди сконвертированных буферов. Исходный UUID у меня «9c1e8353-df81-4a8e-8fa8-e7dec6ddd064». Завожу у виртуального юзера новый параметр – SessionUUID, тип ему даю Random Number от 1 до 1000000, формат «00000000-0000-0000-0000-%012lu», частоту обновления ставлю Once , этого будет достаточно для того, чтобы каждый поток тестирования имел уникальный UUID в процессе параллельного теста. Теперь во всех буферах заменяю «9c1e8353-df81-4a8e-8fa8-e7dec6ddd064» на «<SessionUUID>» и опробую 10 итераций. Шоколад. Теперь очередь имени юзера. Это имя содержится в буфере регистрации входа нового пользователя на сервере, в окружении кучи пока непонятных байтиков: "\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00" Однако если обратить внимание на выделенные зеленым байты перед словом «administrator», то первый из 4 имеет значение в десятичной системе 14. В слове «administrator» букв 13, видимо, следующий за этим словом нулевой байт – терминирующий, 14 – длина параметра, а обратный порядок байт при маршалинге нам уже знаком по рассмотрению формата DCE RPC и его поля frag_length. Создаю параметр UserName, тип ему задаю Unique number, минимальное значение 1 и, внимание, Block size per Vuser = 1. Это я выяснил путем некоторых боданий уже с многопоточным тестовым сценарием, и это необходимо, чтобы каждому тестирующему потоку был выделен один номер, но меньше 100. Вообще, LR мог бы подобную мою потребность в нумерации отработать как-нибудь поизящней, потому как параметры типа Vuser ID очень быстро выходят за пределы сотни, а другие способы какие-то мутные на мой вкус. Формат параметру ставлю %03d, чтобы длина логина была фиксированной Теперь я пишу скриптик для SQL Server’а, который заведет нам сотню пользователей с именами user001-user100. Потом изменяю буфер, поменяв байт длины имени пользователя на значение 8 (помним про терминирующий байт), а имя пользователя заменю на параметр «<UserName>». Теперь нужно не забыть, что длина пакета RPC изменилась и стала меньше на 6 байт. Для того чтобы пакет DCE RPC остался корректным, нужно значение байтов frag_length так же уменьшить на 6, равно как и размер буфера LR. В конце этих сложных манипуляций буферы входа пользователя на сервер имеют вид: send RegisterUserSend 156"\x05\x00\x00\x03\x10\x00\x00\x00" // headerrecv RegisterUserRecv 48 "48\x00" Многопоточный тестовый сценарийПришло время колпашить сервер! Жму в Virtual User Generator кнопку «Create controller scenario» и перехожу к работе с контроллером многопоточного теста. Делаю Manual Scenario, ставлю сотню пользователей. Открывается монстрский экран управления, и я вижу справа внизу график планируемой нагрузки. Пока что он прямоугольный, но я хочу сделать стрессовый тест с постепенным ростом нагрузки, чтобы понаблюдать динамику работы сервера с увеличением количества пользователей. Для этого я отыскал в группе Global Schedule параметр Start Vusers и настроил его так, чтобы каждые 5 секунд стартовал новый юзер. В Stop Vusers я поставил остановку всех одновременно, график стал прямоугольной трапецией. Нажал старт сценария и с замиранием сердца смотрел на свой первый стрессовый тест. Первая кровьРазумеется, у меня не сразу все было гладко, я прошел несколько раундов разборок с ошибками в скрипте, поставил на процедуру логина и логаута скобки транзакции, предусмотрел корректное завершение итераций при неудачном логине, чтобы виртуальные юзеры не валились без веских причин. В процессе отладки уже многопоточного теста я вышел на стабильную картину, что в момент, когда у меня нагрузка уже 25-30 параллельно логинящихся юзеров, сервер вдруг перестает отвечать на запросы и постепенно все юзеры вываливаются. Попытки пощупать сервис АБС показывали, что он и вправду висит. Это был уже третий день катания многопоточного сценария, причины винить в зависании сервера кривой скрипт или кривые условия эксперимента уже кончились. Я в очередной раз повесил сервер, поставил и настроил WinDBG, приаттачился к серверу, пригласил главного архитектора и ведущего программиста взглянуть на ситуацию. Сюрпризом было то, что они нашли банальный баг в коде, который как раз менялся в тестируемом релизе. Баг был прост: сервер входил в критическую секцию, а выход из нее стоял внутри if, так что если if не случался, сервер был обречен повиснуть. На этот момент с начала экспериментов с LR’ом прошло где-то две недели, все это время релиз тестировался функционально и повесить сервер никому не удавалось. Вот так я сразу получил вескую отдачу от небольшого экспериментального теста производительности. ЭпилогЯ еще покатал тесты, один раз наткнулся на другое зависание сервера, тоже показал его разработчикам, они наметили, как будут ловить этот уже непростой баг, но он оказался трудно воспроизводимым. Также при тестах я иногда вижу, что какой-то из серверных потоков валится с неизвестным эксепшном. Все это в новинку и жутко интересно, но в целом стрессовый тест на 100 пользователей выходит на свой предел и утюжит слабый виртуальный серверок без посторонних сбоев. Отмечу основные результаты проделанной работы:
Следующими этапами я планирую:
Общий итог – результат пробы инструмента удовлетворителен и вполне соответствует ожиданиям. Главное – у пользователей нашей системы появилась надежда, что через какое-то время поставляемый им софт не будет содержать багов, проявляющихся лишь в нагруженном многопользовательском режиме.
Netscape unfriendly HTML (дата последней модификации этого файла: 23.12.2009 г.) © Сделал сам R G B |
||