Знакомство с Goliath

Продолжаем серию статей, в которой мы знакомим читателей с различными веб фреймворками. И сегодня позвольте представить Goliath (Голиаф, http://postrank-labs.github.com/goliath/) — асинхронный веб фреймворк на Ruby, созданный компанией PostRank (http://postrank.com/), ныне купленной Google.

Главной особенностью Голиафа является применение модели событий для ввода-вывода, посредством библиотеки EventMachine, а также механизма волокон (fibers), появившегося в Ruby 1.9. Его можно считать аналогом столь модного сегодня Node.js, только на Ruby.

В статье мы рассмотрим такие вопросы:

  • волокна и события;
  • установка Goliath;
  • написание простого чата с применением механизма long-polling;

В заключении вы найдете традиционные тесты производительности.

Волокна и события

Волокно (fiber) — своеобразный контекст выполнения, логически подобный потоку, но это не поток. Потоки используются для распараллеливания задач, в то время как волокна больше подойдут для асинхронных операций ввода-вывода. Фактически волокно – это такой продвинутый goto, обернутый в абстракцию, похожую на поток, только волокна реально выполняются в одном физическом потоке. Если в настоящем потоке выполнение прерывается само и в произвольном месте кода по воле операционной системы, то в случае волокон разработчик сам решает, когда и где передать управление в другой участок кода программы.

Волокна в Ruby реализованы через класс Fiber и его методы new, yield и resume. Волокно создается как блок кода, который можно выполнять, аналогично потоку, но не запускается сразу. Затем вызов resume для некоторого волокна передаст управление внутрь блока. Блок кода в волокне будет выполняться до тех пор, пока не закончится либо не будет встречен вызов yield. Вызов yield значит – приостановить выполнение кода в этом месте, запомнить состояние и перейти к выполнению основного кода, вызвавшего resume. Обычно существует общий цикл событий для всех волокон, куда управление передается каждый раз, когда одно из волокон закончило или приостановило свою работу – Event loop. В цикле программа дожидается наступления каких-либо событий и вызывает resume соответственно для тех волокон, которые эти события ожидали. 

У такого подхода есть ряд преимуществ перед обычными потоками:

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

Однако стоит помнить и об ограничениях:

  • Волокна не помогут задействовать несколько ядер процессора.
  • Если в коде одного волокна произойдет зависание или вечный цикл – все волокна в этом потоке будут заблокированы.

Вот небольшой пример демонстрирующий работу волокон:

Ну и чтобы происходящее не казалось магией, стоит добавить, что проект на Goliath очень прост. В нем нет никаких папок и прав, нет deploy-скриптов. Просто в папке приложения создаются два файла – app.rb и web.config. Вот содержимое web.config с комментариями. Можно просто создать такой файл в любом IIS приложении и получить там рабочее приложение Goliath.

 

<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <heliconZoo> <!-- Настройки приложения и переменный среды --> <application name="goliath.project" > <environmentVariables> <!-- Скрипт точки входа в приложение --> <add name="APP_WORKER" value="app.rb" /> <!-- Deploy-скрипт, где можно делать миграции и т.п. --> <add name="DEPLOY_FILE" value="deploy.rb" /> <!-- Туда будет сохранен вывод deploy-скрипта --> <add name="DEPLOY_LOG" value="log\zoo-deploy.log" /> <!-- Включаем режим разработки --> <add name="RACK_ENV" value="development" /> </environmentVariables> </application> </heliconZoo> <handlers> <add name="goliath.project#x86" scriptProcessor="goliath.http" path="*" verb="*" modules="HeliconZoo_x86" preCondition="bitness32" resourceType="Unspecified" requireAccess="Script" /> <add name="goliath.project#x64" scriptProcessor="goliath.http" path="*" verb="*" modules="HeliconZoo_x64" preCondition="bitness64" resourceType="Unspecified" requireAccess="Script" /> </handlers> <!-- Rewrite правило для ускорения обработки статических файлов --> <!-- Если запрошенный файл найден в директории /public/ то обработать --> <!-- его в IIS как статический --> <rewrite> <rules> <rule name="Avoid Static Files" stopProcessing="true"> <match url="^(?!public)(.*)$" ignoreCase="false" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="true"> <add input="{APPL_PHYSICAL_PATH}" pattern="(.*)" ignoreCase="false" /> <add input="{C:1}public\{R:1}" matchType="IsFile" /> </conditions> <action type="Rewrite" url="public/{R:1}" /> </rule> </rules> </rewrite> </system.webServer> </configuration>

Пишем первое приложение

Чтобы продемонстрировать возможность реализации long-polling с использованием Голиаф и IIS, напишем простой чат. Он будет состоять из двух частей: серверной (Ruby, Голиаф) и клиентской (JavaScript). Для правки кода вам потребуется редактор или среда разработки. Мы использовали Aptana (http://aptana.org):

Серверная часть – файл app.rb:

 

require 'rubygems' require 'goliath' require 'cgi' class Chat < Goliath::API use Goliath::Rack::Params # Включить поддержку json use Goliath::Rack::Render, 'json' # Возвращает массив callbacks def callbacks @@callbacks ||= [] end # Точка входа для приложения def response( env ) case env[ 'PATH_INFO' ] when '/' # Вернуть index.html [200, {'Content-Type' => 'text/html; charset=utf-8'}, File.read( 'index.html' ) ] when '/send' on_send( env.params ) when '/recv' on_recv end end # Получает сообщение от одного клиента и рассылает его другим. def on_send( params ) # Рассылка сообщения всем клиентам. Технически сдесь просто по очереди освобождаются волокна, # приостановленые в on_recv. until callbacks.empty? do callbacks.shift.call({ nickname: CGI.escapeHTML( params[ 'nickname' ] || 'Anonymous' ), text: CGI.escapeHTML( params[ 'text' ] || '' ), color: CGI.escapeHTML( params[ 'color' ] || '' ) }) end [200, {}, {status: 'ok'}] end # Ожидание сообщений long-polling. Браузер делает запрос и ожидает ответ, а получает его тогда, когда есть данные. def on_recv # Запоминаем текущее волокно и добавляем в массив процедуру, при вызове которой # управление вернется в это волокно и сообщение как аргумент. # Переменная req_fiber будет доступна внутри процедуры. req_fiber = Fiber.current callbacks.push(proc {|message| req_fiber.resume( message ) }) # Приостанавливаем выполнение волокна. # При вызове resume волокно продолжит выполнение с этого места и вернется ответ. response = Fiber.yield( nil ) [200, {}, response] end end

Клиентская часть, index.html:

<!DOCTYPE html> <html> <head> <title>Goliath + Helicon Zoo chat</title> <style type="text/css"> body { font-family: Sans-Serif; font-size: 13pt; padding: 0 6px; } h1 { font-family: "Trebuchet MS", Sans-Serif; font-size: 1.5em; color: #FF9933; } #messages { list-style: none; margin-top: 20px; } </style> </head> <body> <h1>Goliath + Helicon Zoo chat</h1> <form action="/send" method="post" id="send"> <label for="nickname">Nickname:</label> <input name="nickname" size="10" id="nickname" /> <label for="text">Message:</label> <input name="text" size="40" id="text" /> <input type="submit" value="Send" /> </form> <li id="messages"></li> </body> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6/jquery.min.js" type="text/javascript"></script> <script type="text/javascript"> // По нажатию Submit отсылает сообщение на сервер function on_send( evt ) { evt.preventDefault(); var arr = $(this).serializeArray(); var message = { nickname : arr[ 0 ].value, text : arr[ 1 ].value, color: window.ClientColor }; $.post( '/send', message, function( data ) { $('#text').val( '' ).focus(); }, 'json' ); } // Вызывается при поступлении новых сообщений function long_polling( message ) { if ( message ) { var $li = $( '<li><b style="color: ' + message.color + ';">' + message.nickname + ':</b> <span>' + message.text + '</span>' ); $li.hide().appendTo('#messages').slideDown(); } // сообщение обработано, ждем следующих $.ajax({ cache: false, type: 'GET', url: '/recv', success: long_polling }); } // Инициализируем после загрузки страницы $(document).ready(function(){ window.ClientColor = '#' + Math.floor( Math.random() * 16777215 ).toString( 16 ); $('form#send').submit( on_send ); long_polling(); $('#nickname').focus(); }); </script> </html>

Обратите внимание на метод on_recv. Мы получаем текущее волокно и добавляем его в массив ожидающих обработчиков. Точнее мы помещаем туда руби процедуру, в которой вызывается метод resume, передающий управление волокну. Переменная req_fiber, хоть и локальная, как бы «замыкается» в контексте процедуры. Далее мы сразу останавливаем волокно. Когда придет сообщение, все процедуры будут последовательно вызваны и удалены из массива.

Попробуем запустить то что получилось:

Тесты производительности

Тестовая машина в качестве сервера — Core 2 Quad 2.4 Ghz, 8 Gb RAM, гигабитная сеть. Для генерации нагрузки использовался более мощный компьютер и Apache Benchmark командой «ab.exe -n 100000 -c 100 –k». Операционные системы — Ubuntu 11.04 Server x64 и Windows Server 2008 R2. Никаких виртуалок — честное железо.

Было проведено три теста. В первом Goliath приложение должно был просто выводить на страничке текущее время с высоким разрешением. Время нужно чтобы гарантировать, что ответы не идут из кеша. Во втором тесте производилось чтение из базы данных MySQL, в третьем запись в базу данных. 

Для тестов использовали Ruby 1.9.3, Goliath 0.9.4 и MySQL 5.1.54. Во всех конфигурациях IIS, Apache и Nginx использовалось HTTP проксирование, т.к. Goliath сам по себе является HTTP сервером.

Вот результаты (величина на графиках — запросы в секунду):

И более подробные графики ab по первому тесту:

Выводы

Goliath — легкий простой и удобный фреймворк. Особенно он хорош при написании различных API и асинхронного кода. Решение многократно проверено в промышленной среде и показывает неплохую скорость работы. И главное – он позволяет использовать обширную экосистему Ruby при разработке приложений.

... <heliconZoo> <application name="django.project.x86" > <environmentVariables> <add name="PYTHONPATH" value="%APPL_PHYSICAL_PATH%" /> <add name="DJANGO_SETTINGS_MODULE" value="site.settings" /> </environmentVariables>

Производительность

И напоследок самое интересное – тестирование производительности. Развернутое тестирование будет позже, сейчас пока «на скорую руку», но и по этим результатам вполне можно судить. Тестовая машина в качестве сервера — Core 2 Quad 2.4 Ghz, 8 Gb RAM, гигабитная сеть. Для генерации нагрузки использовался более мощный компьютер с Apache Benchmark. Для тестирования Apache и Nginx использовалась Ubuntu 11.04 Server x64. IIS 7 тесты работали на Windows Server 2008 R2. Никаких виртуалок — честное железо. В качестве транспорта на Nginx использовали самый продвинутый uwsgi, а также wsgi и fast_cgi для сравнения. Еще на IIS 7 сравнили с PyISAPIе.

В качестве тестовых страниц написали два небольших скрипта. Первый выводит текущее время с высоким разрешением – это сделано для того, чтобы была уверенность что, страницы не берутся из кеша. Второй делает то же самое, но предварительно сохраняет результат в базе данных. Все это через шаблон, чтобы задействовать реальную инфраструктуру Django, база данных – MySQL. Настройки везде по умолчанию, т.к. цель протестировать именно наиболее типичные конфигурации. Итак, результаты (в запросах в секунду):

Тут все более менее понятно и стабильно. Uwsgi впереди, видимо за счет более тесной интеграции с кодом Django — нужно будет его покурить… PyISAPIe сильно отстает, что и понятно.

А вот с базой данных результат не столь стабилен. Почему Nginx + fcgi + MySQL показал столь странные 175 запросов в секунду – не понятно. Результат MySQL на Windows тоже удручает, хотя на shared хостинге проблема возможно и не будет такой критичной. Дело в том что производительность падает в основном из-за каких-то внутренних блокировок в MySQL, сам же сервер практически не нагружен, пока генерирует эти 104 запроса. Можно предположить, что с ростом количества сайтов на сервере, и соответственно ростом количества баз данных, если они не будут блокироваться между собой, суммарная мощность сервера будет достаточной. 

Так что мы решили добавить MS SQL Express к тестам. Его результат тоже понятен – все упирается в сам Python и драйвера базы данных к нему, однако в целом выглядит вполне прилично. PyISAPIe с MS SQL Express не заработал.

Хочется отдельно отметить способность IIS 7 держать большое число подключений. Так IIS 7 + Helicon Zoo с легкостью выдерживал тысячи одновременных подключений (больше нам просто нечем было генерировать), в то время как Ubuntu, с настройками по умолчанию, с ростом числа подключений быстро пасовала. А Apache еще и оказался весьма прожорливым на память. Так с ростом числа подключений Apache потребил около 3-х Гб памяти за 20 секунд теста — дальше не проверяли.

Выводы

Представленное решение показывает стабильную работу и приличную производительность. Оно вполне готово для использования как в качестве среды разработки так и в продакшене. Особенно важной является возможность использования Helicon Zoo различными Windows хостингами, с целью предоставления сервисов Django своим клиентам. Хочется надеяться, что с ростом числа Django серверов на Windows, ее разработчики станут уделять больше внимания отладке и оптимизации своего кода под Windows платформу. Да и армия существующих Windows-разработчиков может стать неплохим подспорьем для нынешних open-source проектов.

Автор статьи:Ярослав Говорунов