Опишу я тут историю как на коленках сделал «нагрузочное» тестирование сервиса, и некоторые соображения по поводу Ruby;)

Жила-была себе одна большая система, жила уже не первый год, не первый релиз.
Система представляет из себя центральные сервера, через которые бегает информация и набор desktop приложений которые эту информацию и порождают/потребляют, так сказать gateways и destination.

Однажды с сервером случилась беда, для локализации дефекта нужно было нагрузить сервер — имитировать работу БОЛЬШОГО кол-ва одновременных активных клиентов.



Клиенты общаются с серверами посредством «Windows Communication Foundation» aka WCF.

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

Собственно это затянувшееся вступление, перейдем к деталям.
Надо было сделать нагрузку в 6000 работающих клиентов.
Каждый из них проявляет активность раз в 10 секунд.
у каждого свой логин/пароль.

Один эмулятор оказался в состоянии запустить 250 потоков/клиентов Итого для запуска нужно было 24 копии эмулятора, на куче машин (в зависимости от мощности от 1 до 4 штук на машину).
Однако сказали мужуки, но надо значит надо, запустили, и первый этап на этом закрыли, подтвердили теорию и отложили за редкостью ситуации.

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

Собственно я не девелопер — я тестировщик/QA, но с разными странными бэкграундами.

Решил я дома посмотреть, а можно ли упростить это дело, можно ли создать нагрузку «более другими» методами.

Взял эмулятор, взял Fiddler2 (Fiddler is a Web Debugging Proxy which logs all HTTP (S) traffic between your computer and the Internet), и посмотрел что делается.

Увидел я там XML который бегает в обе стороны, и волшебные слово SOAP.

О, сказал я, хороший повод наконец с ним познакомиться поближе.

Заодно и проверить, можно ли такие вещи делать на Ruby.
Почему Ruby?
ну во первых потому что красиво, это раз,
во вторых Ruby + WATIR после этой связки я и увлекся руби (думаю отдельно про это написать)
в третьих — мультиплатфоменность, это для меня было важным фактором т.к. дома, где и проходила первая фаза исследований — OS X (Hackintosh), на работе Windows, ну и Linux нам тоже пригодился в реальной жизни.

Поискал библиотеку подходящую, нашлись soap4r, Savon, выбрал последнюю, т.к. показалась полегче. Поставил, началис�� игры.

На удивление быстро получился первый работающий код, задача была получить запрос выглядящий как запрос родного эмулятора.
  1. def data_available(client,id,password)
  2.   _xml=%Q|<?xml version="1.0" encoding="utf-8"?>
  3.   <soap:Envelope
  4.         xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
  5.         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  6.         xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  7.         <soap:Body>
  8.                 <DataAvailable xmlns="Company.ESD">
  9.                         <ID>#{id}</deviceID>
  10.                         <Password>#{password}</Password>
  11.                 </DataAvailable>
  12.         </soap:Body>
  13.   </soap:Envelope>
  14.   |  
  15.   _xml.gsub!(/\r|\n/,"")
  16.  
  17.   response = client.request "Company.ESD/Data/DataAvailable","xmlns" => "Company.ESD" do
  18.     soap.xml=_xml
  19.   end
  20.   response  
  21. end
  22.  
  23. def do_test(id,pwd)
  24.   client = Savon::Client.new do
  25.     wsdl.endpoint = "https://superserver.super.com/DDService/Data.svc"
  26.     wsdl.namespace = "https://superserver.super.com"
  27.     http.read_timeout = 90
  28.     http.open_timeout = 90
  29.   end
  30.  
  31.   r=data_available(client,id,pwd).to_hash
  32. end


т.е. есть один клиент, ну что, пора сделать МНОГО клиентов открываем документацию находим пример, применяем
run_client — код одного «клиента»

  1. def run_test(clnt_id)
  2.         pwd=get_pwd(clnt_id)
  3.         id=get_id(clnt_id)
  4.         while true
  5.                 do_test(id,pwd)
  6.                 sleep 9
  7.         end
  8. end
  9.  
  10. clients=Array.new
  11. 1.upto(10) do |client_id|
  12.   client << Thread.new do
  13.     puts "Start: #{client_id}"
  14.     run_client(t_cnt)
  15.   end
  16. end
  17. clients.map {|t| t.join}


запустили — УРА, работает.
попробуем 1000, опс, на экране запуск ~130 потоков и тормоза;(
похоже Ruby не очень хочет пускать много потоков,
ну в общем то не вопрос, нам важен результат.
пустим в одном файле 100 штук, а файлов таких будет 10, вот и имеем 1000 клиентов.
пробуем — ура работает!!!, но проц грузим мягко скажем не подески, начинаем думать что делать, нам то надо еще больше!

тут вспоминаем что клиент проявляет активность раз в 10 секунд
т.е. мы можем пустить один поток который спокойно может обработать 10 последовательных клиентов, на каждого по секунде, в результати и получим активность каждого клиента раз в 10 секунд.
ура, удалось уменьшить кол-во потоков в 10 раз, уже радует, т.е. на 1000 клиентов надо всего 100 потоков, а это уже реально…

получили такой запускатель
  1. def do_for_time(time_in_sec,&block)
  2.  time_end=Time.now+time_in_sec-0.01
  3.  yield
  4.  time_rest=time_end-Time.now
  5.  sleep time_rest if time_rest>=0.1
  6. end
  7.  
  8. def run_10(start_id)
  9.     clinets=Array.new
  10.     1.upto(10) do |nn|
  11.       id=get_id[start_id+nn]
  12.       pwd=get_pwd[start_id+nn]
  13.       clinets << [id,pwd]
  14.     end
  15.    
  16.     clnt = HTTPClient.new
  17.     while true do
  18.       clinets.each do |client|
  19.         id=client[0]
  20.         pwd=client[1]
  21.         do_for_time(1) do
  22.            data_available(clnt,id,pwd)
  23.            update_status(clnt,id,pwd)
  24.         end
  25.       end
  26.     end
  27. end


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

Посмотрел я еще раз на оригинальные запросы и понял, так зачем нам нужен SOAP?
это же в конце концов не более чем POST запрос c зараннее известным XML!

сказано — сделано, переписали наши функции, выкинули SOAP, получили 
  1. def check_data(http_client,id,pwd)
  2.     uri='https://superserver.super.com/DDService/Data.svc'
  3.     soap=%Q|<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><DataAvailable xmlns="company.ESD"><ID>#{id}</ID><Password>#{pwd}</Password></DataAvailable></soap:Body></soap:Envelope>
  4.   |
  5.   headers = {  
  6.     'Content-Type' => 'text/xml;charset=UTF-8',
  7.     'SOAPAction' => 'company.EDC/IData/ ',
  8.   }
  9.  
  10.   http_client.post uri, soap, headers
  11. end


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

3000 клиентов живут достаточно легко…

тут домашние игры я прекратил, т.к. уперся в несеметричный канал ADSL, да и отдыхать то надо;)

в понедельник продолжил на работе, на быстром канале.
, а тут начинаются чудеса, грустные
виндовый Ruby 1.8.7 жрет 100% 2х ядерного cpu при 1000 клиентов, опаньки…

для проверки в виртуалке (на этой же машине) ставлю ubuntu, даю ей два ядра, и пускаю тоже самое, и…
в общем чудеса, в ВИРТУАЛКЕ под линухом имеем 5000 клиентов.

далнейие эксперементы:
ставми jruby, у него по описанию правильные треды, пускаем 1000 — 20% загрузки — УРА
пускаем второй файл с 1000 и опаньки, загрузка 2х ядер 80%, терпимо, но не очень понятно;(

в итоге, 2000 на машине подошли для нашей задачи (надо было пускать на удаленной тачке, виртуалку там поставить было нельзя)
Задачу решили, сервер нагрузили.
Поигрался с SOAP, забавно, не очень сложно.
Времени потратил, сумарно часов 8.

выводы:
1. «Keep it simple, Stupid!» — в нашем случае упрощение помогло решить задачу, уход от сложного SOAP к просто http requests спасло положение.
2. RUBY можно использовать для таких задач, мне не пришлось воевать с языком, он просто позволил мне сделать то что я хотел;) (почти не создавая проблем)
3. использование scripting language — сильно ускоряет время разбора с проблеммой, т.к. дает легко эксперементировать, это быстро и просто.
4. мультиплатформенность — рулит
5. теперь можно при необходимости переписать это все на C#, может и на одной машине будет много клиентов, но оно пока не надо, задача получилась разовая.
6. i love ruby!
______________________
Текст подготовлен в Редакторе Блогов от © SoftCoder.ru