APC
личная страница

· JMeter vs MSRPC ·

JMeter vs MSRPC

Обсудить статью

Вступление

Почему LoadRunner все же плох

С момента предыдущих экспериментов по нагрузочному тестированию RPC-сервера прошел примерно год. Весь этот год наша компания провела под флагом повышения производительности и стабильности ПО. Приятно отметить, что мои нагрузочные тесты процедуры логина c помощью LoadRunner сыграли в этих работах небольшую, но полезную роль.

Интереснее всего было то, что банк заказал нагрузочное тестирование некоей сторонней компании (о сторонних компаниях либо хорошо, либо не указывать название). Компания эта опытна в нагрузочном тестировании, имеет солидных клиентов и использует для реализации проектов LoadRunner. Я опущу множество язвительных замечаний по общему ощущению от работы этой компании, сосредоточусь на нагрузочных тестах:

  1. Нагрузочная модель была составлена странновато, реально важные для банка сценарии оказались не покрыты, вместо этого были покрыты второстепенные. И банк утвердил эту модель! Наблюдать за такими чудесами некомпетентности просто удивительно.
  2. Для того чтобы удобно связать LoadRunner с сервером был выбран способ модификации библиотек, отвечающих за RPC-вызов. Библиотеки модифицировались так, чтобы запросы к серверу могли вызываться из LoadRunner. Вообще решение достаточно остроумное и сиюминутно задачу решает практически полностью. Однако, количество минусов решения убивает его привлекательность: при модификации кода сервера нужно снова копировать код транспортной библиотеки в сторонку и модифицировать его (нужно знать C++ и понимать, где и что закомментировать/заглушить), чтобы использовать это решение, строя сценарии тестов, также нужно знать C/C++ и прочие проблемы сопровождения тестов.
  3. По признанию сотрудников банка, ни один из прогнозов, сделанных компанией по части нагрузки, не сбылся, выбранное к закупке оборудование было впоследствии со скандалом обменяно у вендора на другое.

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

Самое интересное, что код тестов передан, а вот инструмент тестирования, запускающий этот код, банку пришлось бы покупать у HP! И вот тут становится совсем уж весело, так как порядок стоимости закупки выходит на десятки тысяч долларов (и очень может быть, что выходит за сотню). Нужно отдать должное в целом неплохому инструменту LoadRunner, но я назову финансовый аспект главным его минусом: при такой стоимости готового продукта более приемлемой может оказаться разработка собственного инструмента и работа с продуктами Open Source.

Почему JMeter все же хорош

В прошлый заход на нагрузочное тестирование я писал, что безуспешно искал инструменты тестирования TCP-трафика помимо LR. Тогда я натыкался на JMeter, но не нашел толкового описания, как c его помощью тестировать бинарный TCP-трафик. Возможно, хороший туториал изменил бы расклад, но такового я не встретил.

Однако, благодаря тому, что за год я достаточно много провозился с нагрузочным тестированием веб-приложения на JMeter (в этой области он точно поворотливей LR), я увидел, как можно использовать JMeter для тестов RPC. Плюс к этому, после получения опыта написания плагинов для JMeter я понял, что при необходимости сам смогу дописать нужные мне функции в программу.

При этом ключевые плюсы, которые были видны сразу, это:

  1. Программа бесплатна, при этом достаточно качественна. JMeter не имеет понятия «лицензионное ограничение количества пользователей», можно хоть 100, хоть 10 000 параллельных потоков запускать, лишь бы нагрузочный стенд справлялся с генерацией.
  2. На случай генерации очень больших нагрузок есть простой и надежный механизм распределенного тестирования.
  3. Нет проблем с инсталляцией, так как решение на Java и вообще не имеет понятия «инсталляция». Скачал, распаковал, запустил – заработало.
  4. Не нужно знать языки программирования для разработки тестов. Нагрузочный тест строится визуально на своеобразном макроязыке, в виде дерева, это под силу нетехническому специалисту. Безусловно, знание языка регулярных выражений, JavaScript и Java может существенно расширить возможности тестировщика в JMeter, но достаточно далеко можно зайти и без них.

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

Замечания о тестируемом ПО и окружении

За прошедший год самые существенные изменения произошли как раз в процедуре логина. Теперь второй поток непрерывной связи с сервером отсутствует, и не сервер опрашивает клиентов на предмет живости, а клиенты оповещают сервер запросами keep-alive, что они не отвалились. А еще я узнал, что очень важно проверять коды возврата от методов RPC-сервера, иначе можно сделать жутко некорректный тест и не заметить этого.

В нагрузочной модели четко выделились два сценария, дающие 60% транзакционной нагрузке в банке: это взнос и выдача наличных. Также возросло количество данных (около 80ГБ) и количество пользователей, работающих с сервером, выросло до 600(!).

Нужно помнить, что палочка-выручалочка это Wireshark, без него мне не отладить тесты бинарного RPC-трафика. Я не буду даже пытаться использовать запись в JMeter, так как убедился, что надежней работать с помощью Wireshark. И, наконец, спецификация RPC также должна быть всегда под рукой.

RPC Bind и первый RPC Call с помощью JMeter

Итак, запускаем JMeter, добавляем в тест-план Thread Group, TCP Sampler и View Results Tree.

TCP Sampler настраиваем на работу с бинарным трафиком – указываем TCPClient-класс BinaryTCPClientImpl, адрес и порт сервера, а также таймаут в 5000 миллисекунд, чтобы сократить цикл отладки.

Теперь самое главное – копируем из Wireshark байты пакета RPC Bind командой Copy – Bytes (Hex Stream) и вставляем их в поле Text to send. BinaryTCPClientImpl понимает как раз этот формат представления байт, и, в отличие от варианта работы с LoadRunner, мне не нужно мучиться с переформатированием последовательности. Наш bind-пакет таков:

05000b03100000004800000001000000d016d016
000000000100000000000100a097cec2158bd111
96ab00a0c9103fcf01000000045d888aeb1cc911
9fe808002b10486002000000

Не забываем запустить Wireshark для контроля достоверности трафика, запускаем наш тест и идем смотреть в View Results Tree результаты. Первое наблюдение – сэмпл завершил работу по таймауту, слава Богу, мы поставили его достаточно коротким. Второе наблюдение – Wireshark показывает ответ RPC Bind_ack, это значит, что трафик был эмулирован успешно. Response data показывает нам Hex-байты всего пакета ответа. Не будем отвлекаться на мелкие препятствия, проверим, что там у нас с эмуляцией непосредственно RPC-вызовов. Благо, первый RPC вызов прост как пробка – он спрашивает у сервера, жив ли тот.

Обязательно отмечаем первому TCP Sampler галочку Re-use connection, чтобы сокет не закрывался после Bind и последующие запросы работали по тому же сокету. Эту галочку нужно будет проставлять всем запросам. Переименовываем первый TCP Sampler в RPC Bind и добавляем второй TCP Sampler, сразу с именем Is Server Alive. Настраиваем опции второго сэмплера так же как у первого.

Снова копируем все байты эталонного пакета и вставляем их в поле Text to send:

050000031000000018000000010000000000000000000000

Wireshark перезапущен, очищаем результаты JMeter (Ctrl+E), запускаем тест, смотрим результаты. Запрос RPCCall сработал успешно, мы видим это в Wireshark и View Results Tree. Успешность обнадеживает, вот только есть несколько «но»...

Замечания по работе с TCP Sampler

При появлении второго TCP Sampler приходит мысль, что хорошо бы уже начать использовать TCP Sampler Config, чтобы выделить общие настройки. «Но» в отношении TCP Sampler Config состоит в том, что срабатывают все опции кроме TCPClient classname, и приходится имя класса прописывать (и по необходимости менять) в каждом сэмплере. Долг пользователя открытого и бесплатного ПО побуждает отправить разработчикам багрепорт о найденной проблеме, что я и сделал, записав баг 48709.

Вторая бяка расположена в TCP Sampler: JMeter проглотит результат запроса, если класс TCPClient породит exception, а это есть штатный способ сообщить о сбое в работе тонких материй бинарного TCP-трафика. Проблему можно будет увидеть только изучая файл jmeter.log, View Results Tree просто промолчит. На эту тему я записал баг 48747, и сразу дал к нему патч, исправляющий ситуацию. Очень приятно, что разработчики быстро приняли патч и включили исправление в готовящуюся к выпуску версию JMeter.

Как уже было замечено выше, самое первое и главное «но» в использовании TCP Sampler – это его неумение определять окончание бинарного ответа, в результате чего он всегда дожидается таймаута. Имеющийся Length-Prefixed класс LengthPrefixedBinaryTCPClientImpl улучшает работу с отправляемыми данными, но в RPC необходимо вовремя остановить их прием.

Счастье состоит в том, что JMeter имеет открытые исходные коды и ряд удачных архитектурных решений, позволяющих писать к нему плагины и всячески расширять имеющуюся функциональность. Я воспользуюсь возможностью указать любой TCP Client класс, и напишу свой собственный.

Собственный TCPClient для RPC

Мне легко переходить к написанию кода, дополняющего JMeter, так как к этому моменту я уже написал плагин Samples vs Active Threads. AbstractTCPClient – достаточно простой класс, реализуя его методы read и write, можно заложить в JMeter требующуюся логику работы с трафиком.

Я не буду утомлять читателя подробностями программирования на Java, опишу в вкратце функциональность первого прототипа DCERPCSampler:

  • Всё управление спецификой RPC идет через значение поля Text to send
  • Первая строка поля определяет опции отправки запроса и получения ответа
    • Если первое слово в первой строке bind, то серверу будет отправлен запрос RPC Bind с указанными UUID интерфейса.
    • Если запрос не типа bind, то первый параметр это CallID, а второй OpNum (см. спецификацию RPC)
  • Остальная часть текста – данные заглушки для отправки на сервер. Заголовочная часть RPC-пакета формируется автоматически по CallID и OpNum. Все символы, не являющиеся HEX-последовательностью, игнорируются, это позволяет удобно форматировать байтовую последовательность запроса переносами строк.
  • Главное, что отличает этот класс от стандартных – он умеет прочесть заголовок RPC-ответа и по нему определить, когда пора прекращать считывать пакет ответа.

NetBeans и юнит-тесты помогли мне отладить этот класс. Теперь можно указать этот класс двум сэмплерам (сожалея о баге в TCP Sampler Config) и изменить тексты запросов в понятный DCERPCSampler вид. Замечу, что указывать собственный класс TCP Client нужно полным идентификатором класса, в моем случае kg.apc.jmeter.dcerpc.DCERPCSampler.

Формат запроса bind теперь выглядят так:

bind 372b57f0-1e3d-11df-8a39-0800200c9a66 40b1b2b0-1e3d-11df-8a39-0800200c9a66

Запрос IsServerAlive без заголовка стал полностью соответствовать своей простой пробковой сути:

1 0
00000000

Забегая вперед, хочу сказать, что со временем функциональность этого класса будет дополняться новыми и новыми удобствами, сохраняя, тем не менее, совместимость с первым простым форматом. Кстати, многие параметры запроса RPC Bind пока сознательно прошиты в коде и не вынесены в параметры, время покажет, нужно ли выделать их.

Кто-то из читателей заметил мне, что вместо написания класса TCPClient я мог бы использовать Java Request или BeanShell Sampler. Действительно, мог бы, но сознательно не стал, поскольку мне пришлось бы заботиться о передаче между запросами открытого сокета и обеспечивать прочую инфраструктуру сетевого сэмплера, которая уже готова к работе в TCP Sampler. С другой стороны, я мог бы реализовать вообще полноценный RPC Sampler, чтобы с заданием параметров запросов все было совсем красиво, но для меня затраты на разработку уже готовой в TCP Sampler функциональности пока не оправдываются потребностями в красивом GUI.

Параметризация RPC-трафика в JMeter

Как и в случае с LoadRunner, наша цель – корректный нагрузочный тест процедуры логина на десятках (а лучше сотнях) параллельных пользователей. Такой тест требует соответствующей параметризации запросов: нужно генерировать UUID клиента и использовать уникальное имя пользователя, так как тестируемый сервер отслеживает эти параметры. Очередной запрос в последовательности логина это Register User, несущий целый ряд текстовых параметров, что видно по отображению пакета в WireShark. Сырые данные заглушки выглядят так:

25000000000000002500000032663739666462302d326463322d343561
632d386461382d38643735393935623236306600005c000a0000000000
00000a00000046494e4f46464943450065000800000000000000080000
0046494e2d415043000200000000000000020000004300730009000000
0000000009000000696272616576616e00

Где тут UUID, где тут логин можно подсмотреть в Wireshark, который по возможности визуализирует данные заглушки. Я добавляю еще один TCP Sampler и начинаю обнажать закономерности.

Визуализация текста

Здравый смысл подсказывает, что работать с HEX-последовательностью – не самый эффективный путь, по возможности нужно визуализировать получаемую и отправляемую информацию, уходя от уровня TCP-трафика на уровень человекопонятных строк и чисел. Класс DCERPCSampler пришлось сделать чуть умнее, снабдив его двумя ключевыми умениями:

  • Умением текст между фигурных скобок ({text}) превратить в соответствующие 4 байта HEX-последовательности
  • Умением перевести в ответе последовательности текста длиннее 4 байт в визуальные последовательности ({text}). "Больше 4 байт" потому что 4 байта используется для кодирования int-чисел, коих множество в трафике, переводить числа в текст нам не нужно. Конечно, встречается полезный текст и меньше 4 байт в длину, но оставим решение этой проблемы на будущее.

Запрос логина (без параметров CallID и OpNum) стал выглядеть так (я не забываю проверять его корректность запуском теста JMeter под присмотром Wireshark):

25000000 00000000 25000000 {a9ee5b20-1e33-11df-8a39-0800200c9a66}
00005c00
0a000000 00000000 0a000000 {MY_DOMAIN}
006500
08000000 00000000 08000000 {MY_HOST}
00
02000000 00000000 02000000 {C}
007300
09000000 00000000 09000000 {my_login}
00

Внимательный глаз сразу видит паттерн: префикс длины, нулевой int, снова префикс длины, строка текста с нулевым байтом в конце. Это явно штатный ход маршалинга, и он обнажает проблему эмуляции трафика: если данные будут переменной длины, то нужно для них обеспечивать корректные префиксы длины. Пробы показали, что если не соблюсти это правило, сервер будет отвечать пакетом RPC Fault, и о корректном тесте без корректных префиксов не может быть речи.

Дополнительные параметры маршалинга

Я снова дорабатываю DCERPCSampler, добавляя опцию «режим маршалинга» к блокам текста. Мой первый режим маршалинга – «D», двойной префикс длины и нулевой байт в конце. Отлично! Запрос обретает все более гуманные черты:

{D:a9ee5b20-1e33-11df-8a39-0800200c9a66}
005c00
{D:MY_DOMAIN}
6500
{D:MY_HOST}
{D:C}
7300
{D:my_login}

Оставшиеся HEX-байты так и остаются непонятыми, но корректности теста это не вредит. Пробы показывают, что теперь можно менять логин на любой и RPC-пакет будет прочитан сервером корректно.

Генерация UUID, набор логинов и авто-инкремент CallID

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

Для генерации UUID в JMeter я воспользовался примерно таким же путем как в LoadRunner – добавил в Test Plan элемент RandomVariable и указал ей формат FFFFFFFF-FFFF-FFFF-FFFF-000000000000 (остальные параметры очевидны).

Для набора логинов я использовал CSV Data Set Config, подготовив для него файлик с парой сотен тестовых логинов.

Еще одна вещь, о которой пора позаботиться, это CallID. Спецификация RPC требует, чтобы в рамках соединения этот параметр был уникальным и возрастающим у каждого запроса. Попытка использовать элемент JMeter Config -> Counter показала, что этот счетчик считает итерации теста, а не обращения к переменной. Зато у JMeter есть функция intSum, дающая необходимую логику инкремента. В корне Test Plan я завел переменную callID=0, а во всех TCP-запросах поставил использование функции intSum.

Итак, Text to send в запросе Register User теперь выглядит так:

${__intSum(${callID},1,callID)} 9
{D:${CLIENT_UUID}}
005c00
{D:MY_DOMAIN}
6500
{D:MY_HOST}
{D:C}
7300
{D:${LOGIN}}

Теперь мне по глазам бьет цифра 9, так как совсем неочевидно, что 9 это OpNum запроса Register User.

Номера методов OpNum

Решение для проблемы визуализации OpNum я искал значительное время. Прежде всего, я убедился, что в JMeter поживиться на эту тему нечем, кроме BeanShell, который ввиду своей универсальности может все, но уж очень неудобен в отладке. Я понял, что придется писать какой-то собственный Config-плагин к JMeter, реализующий то, что мне нужно.

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

Правильным ответом было написать универсальный плагин Variables from CSV File, который просто берет из файла строки и превращает их в переменные JMeter. Далее написал на PHP элементарный парсер IDL и получил csv-файл с содержимым «имя метода = номер OpNum». Этот файл скармливается плагину и я получаю в свое распоряжение переменные JMeter вида ${OpNum_RegisterUser}. Теперь я могу смело программировать нагрузочный тест без опаски, что при очередном изменении IDL все номера методов поедут вкривь и вкось.

Работа с Multi-PDU запросами и ответами

Успешно пройдя формирование следующего запроса Auth User, я столкнулся с очередной проблемой корректной эмуляции RPC: Multi-PDU ответы от сервера. В ответ на запрос GetProgVersions я получаю не один RPC-пакет, а последовательно два пакета, каждый со своим заголовком, имеющим соответствующие флаги FirstPacket и LastPacket. А следующий запрос к серверу GetUserRights возвращает даже четыре пакета, а еще следующий запрос GetXMLOptions вообще шесть пакетов возвращает.

Кстати говоря, по спецификации, понятие Multi-PDU распространяется и на запросы и на ответы, данные заглушки разбиваются на порции по лимитам MaxXmitFrag и MaxRecvFrag (передаются в запросе Bind), в заголовке пакетов проставляются соответствующие флаги.

Я снова дорабатываю свой класс, добавляя в него поддержку Multi-PDU ответов (поддержку запросов я добавлю значительно позже, в рамках процедуры логина она мне не понадобилась). Юнит-тесты в NetBeans снова служат мне хорошую службу. Надо заметить, что тема обработки Multi-PDU оказалась очень интересной для меня c точки зрения программирования на Java.

Финал

Ну вот, когда инфраструктура разработки нагрузочного теста готова, можно переходить к созданию сложного тест-плана и на этом рассказ подходит к завершению. Финальные действия это: добавление сэмплеров для оставшихся вызовов логина и логаута, использование Assertion, чтобы убеждаться в корректности ответов сервера и настройка Thread Group на работу сотен пользователей.


Запуск тестов показал устойчивую работу JMeter и плотную загрузку сервера приложений и баз данных. Через пару месяцев работы я получил тест-план, покрывающий два самых частых сценария и молотящий транзакции. За это время тестовый класс оброс дополнительными форматами визуализации входящего/исходящего трафика и работой с Multi-PDU запросами. Страшно представить, во что бы мне обошлось то же самое при использовании LoadRunner. Стоимость двух месяцев моей работы пока что менее $10 000, а качество результата намного выше по сравнению с неназванной компанией, так что экономическая эффективность применения JMeter также налицо.

Мои итоговые замечания к JMeter:

  1. Нагрузочно тестировать RPC-трафик можно, с JMeter такие тесты более легковесны, чем с LoadRunner
  2. Ощущение что с TCP Sampler в мире работали мало, про RPC я вообще молчу
  3. Разобраться в грамотном построении структуры тест-плана JMeter новичку не очень просто, возможно, потому что непривычно строить план в виде визуального дерева
  4. В JMeter не очень просто разбить тест-план на повторно используемые параметризованные модули (см. мой плагин Parameterized Controller)


PWE Visitors Counter
Netscape unfriendly HTML
(дата последней модификации этого файла: 15.03.2010 г.)
© Сделал сам
 R   G   B