Простенький нагрузочный тест

Tuesday, 11 May 2010

Тестирование

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

Поиск инструмента

Если бы речь не шла о процессинге, то тут и говорить было бы не о чем – товарищ msyu довольно подробно расписал как из access.log-а сделать нагрузочный тест. Да и по интернету валяется просто таки туча утилит, которые позволяют нагрузить сервер так, что мало не покажется.

Но у нас ведь особый случай – речь идет о POST запросах!!! И вот тут я впал в ступор на неприлично долгий период гугленья. Некоторые утилиты таки поддерживают пост-запросы, но как-то кривовато.

Например тот же apache bench позволяет брать post-данные из файла, но при этом сами данные для всех запросов одинаковые, как, впрочем, и урлы. Был еще pylot, написанный на питоне, который умел корректно досить пост запросами, но с ним как-то не сложилось. Да и весьма простенькая утилита, не в пример JMeter.

А все время пока гуглил и выспрашивал у знакомых, моя русская натура не давала мне покоя. Внутренний голос постоянно свербил “ну что ты паришься, неужели так сложно написать простенький скрипт отсылки пост запросов с данными из файла”.Но вдоволь нахлебавшись самописных утилит после 30-ти лет понимаешь что лучше потратить 10 часов на освоение чужого инструмента, чем полчаса на написание своей утилиты. Потому и гнал прочь мысли о создании самописной утилиты.

Наконец, когда мое терпение лопнуло, я в очередной раз набрел на статью Андрея Жукова и наконец-то прозрел. Я давно обнаружил в JMeter возможность создания одиночного POST запроса, но мне и в голову не приходило что этот инструмент настолько мощный, что позволяет вместо непосредственных значений указывать имена переменных, значения которых будут читаться из csv файла. За это – огромнейшее спасибо Андрею.

Генерация тестовых данных

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

import random, datetime
 
terminal_count = 1000
terminal_transaction_count = 100
card_count = 300000
fuel_count = 7
amount_limit = 70
 
file = open("requests.csv","w")
 
for transaction_id in xrange(1,terminal_count*transaction_count+1):
  terminal_id = random.randint( 1,  terminal_count )
  card_id     = random.randint( 1,  card_count )
  fuel_id     = random.randint( 1,  fuel_count )
  amount      = random.randint( 1,  amount_limit )
  file.write("%d,%d,%d,%d" % ( terminal_id, card_id, fuel_id, amount ) )
 
file.close()

Немного коментариев к написанному коду. Константы terminal_count и terminal_transaction_count олицетворяют соответствующее требование ака “не менее 100 транзакций в сутки с каждого из 1000 терминалов”. Ну и чтобы Тут все банально, а вот далее начинается ловкость рук.

Но сгенерировав такой набор данных, мы получили банальный ддос, который мало соответствует реальности. А не соответствие реальности грозит бесполезностью тестирования, потому давайте подумаем чего тут не хватает:
1. Должны быть возвраты, скажем каждая 100-я операция.
2. Должны быть проблемы с нехваткой средств, скажем каждая 30-я операция. Пришел заправиться, а на карте всего 2 литра осталось. Естественно предоплата на 10 литров завершится неудачей.
3. Должны быть проблемы с кошелями. Хочу заправиться 92-ым, а на карточке только 95-й. Процессинг должен отвергнуть операцию сообщив о причине. Таких операций – каждая 50-я.
4. Должны быть карточки, которых вообще нет в базе. Каждая 200-я операция.

Теперь давайте думать над реализацией. Ну с пунктом 1 все просто – добавили проверку остатка от деления на 100 и можем писать возвраты. А вот как сделать так, чтобы мы знали что в базе гарантированно нет средств… Ну не залазить же в базу, чтобы узнать какой там текущий остаток с учетом всех нагенеренных транзакций.

Инженерная мысль подсказала решение в обход – можно списывать по литру-двум для тех транзакций, которые гарантированно должны пройти, и по 10000 – для тех, которые должны завершиться отказом. А чтобы первые прошли – добавим клиентам на каждый счет по 100 литров. Таким образом перед тестами базу придется соответствующим образом обновлять.

С третьим пунктом тоже своеобразная засада – гарантировать отсутствие кошеля тривиально, достаточно задать номер больше 8-ми, таких в базе ни у кого нет. А как гарантировать? Не хотелось бы лезть в базу чтобы выяснить у кого какие кошели – это усложнит генерацию теста. Но и добавлять всем какой-то кошель – тоже не выход, ибо это переводит тест из разряда реальных в синтетические.

А как все хорошо начиналось – простой тест, для простого интерфейса. Вот оно, отличие теории от практики.

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

Первый вариант мне не понравился. С одной стороны тестовые запросы и тестовые данные логично было бы засунуть в один скрипт, а вот с другой – хотелось бы меньше связности, даже если учесть что генерация данных выполняется за 5 минут.

SELECT cf.card_id, c.customer_id, cf.amount
  FROM cardfuel cf INNER JOIN card c
 ORDER BY rand()
 LIMIT 100000;

Такой запрос первым делом мне рассказал о следующей ошибке:

ERROR 126 (HY000): Incorrect key file for table 'C:\Windows\TEMP\#sql7bc_1_3.MYI
'; try to repair it

Причина оказалась банальной более некуда – order by rand требовал создания временной таблички, и на диске C у меня тупо не хватило места. Я освободил еще 6 гигабайт, но и их не хватило. Чтение мануалов не добавило понимания, потому я перекинул temp каталог на диск D, и с ужасом увидел что мускул таки сожрал 31GB свободного места, правда на этот раз он не ругнулся и уверенно продолжал что-то считать.

Оставим этот баг на совести алгоритмистов мускула, ибо выполнения запроса я так и не дождался. Вернемся к протоколу, в котором customer_id не нужен, а стало быть достаточно запроса без джойна.

SELECT card_id, fuel_id, amount
  FROM cardfuel
 ORDER BY rand()
 LIMIT 100000;

Результат был получен в мгновение ока. Ну значит на этом запросе и остановимся. Получается теперь прийдется связать выборку из базы с питон скриптом и всеми условиями, которые расписаны выше.

import random, datetime, MySQLdb
 
terminal_count = 1000
terminal_transaction_count = 100
card_count = 300000
fuel_count = 7
amount_limit = 70
 
file = open("requests.csv","w")
db = MySQLdb.connect(host="localhost", user=options.user, passwd=options.password, db=options.database)
cards = db.cursor()
cards.execute("select card_id, fuel_id, amount  from cardfuel order by  rand() limit %d" % ( self.terminal_count*self.transaction_count ) )
 
counter = 0
 
for transaction_id in xrange(1,terminal_count*transaction_count+1):
  counter = counter + 1
  terminal_id = random.randint( 1,  self.terminal_count )
  card = cards.fetchone()
  card_id = card[0]
  fuel_id = card[1]
  amount = card[2]
 
  if counter % 200 == 0:
    # Отсутствие карточки в базе - каждый 200-й запрос
    file.write("withdraw,%d,%d,%d,%d" % ( terminal_id, card_id + 1000000, fuel_id, 1 ) )
  elif counter % 100 == 0:
    # Возврат средств - каждый 100-й запрос
    file.write("payback,%d,%d,%d,%d" % ( terminal_id, card_id + 1000000, fuel_id, 1 ) )
  elif counter % 50 == 0:
    # Отсутствие нужного кошеля - каждый 50-й запрос
    file.write("withdraw,%d,%d,%d,%d" % (  terminal_id, card_id, fuel_id + 100, 1 )
  elif counter % 30 == 0:
    # Недостача средств - каждый 30-й запрос
    file.write("withdraw,%d,%d,%d,%d" % (   terminal_id, card_id, fuel_id, amount + 10000 )
  else:
    # Нормальные запросы
    file.write("withdraw,%d,%d,%d,%d" % (  terminal_id, card_id, fuel_id, random.randint(1,2) )
 
db.close()
file.close()

Ну вот, в весьма упрощенном варианте код для генерации данных для запросов готов. Можно заняться созданием теста.

Создание теста

Чтобы получить требуемое, нам нужно создать поток, создать ридер данных из csv и создать запрос. Все делается как нельзя проще:

  • Создаем CSV Data Set Config
  • Создаем поток
  • Добавлем в поток следующие listener-ы: “View Resuls in Table”, “Graph Results” и “Aggregate report”.
  • Добавляем в поток “Logic Controller \ Loop Controller”
  • Добавляем в loop controller “Samplers \ Http Request”

Как делается каждый из шагов скриншотить не буду – в тех двух статьях, на которые я ссылаюсь, картинок предостаточно. А вот настройки ридера и семплера – это пожалуйста.

Вот так выглядит ридер из csv:

А вот так выглядит сам HTTP Post запрос:

Ничего сложного, разработчики хорошо потрудились над JMeter. Теперь заходим в поток, ставим там 10 потоков, и loop count = forever, после чего жмем run и … и выкатываем глаза из орбит. Дык а что вы хотели, тюнинг еще никто не проводил.

Но!!! Теперь у нас есть генератор запросов и мы можем воспользоваться различным инструментарием, от мониторилок серверов и до самописного профайлера, который поможет нам найти узкие места в нашей программе. Кроме этого, мы наглядно будем видеть к чему приводять наши изменения, запуская тест после каждой доработки.

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

На этом пожалуй все. Заметки по нюансам профилирования и тюнинга как нибудь в следующий раз.

Похожие заметки:

  1. Готовим данные для тестов
  2. Пишем интерфейсные тесты

,

Ваш отзыв

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 1,463 bad guys.