Продолжаем серию статей, в которой мы знакомим читателей с различными веб фреймворками. И сегодня позвольте представить Goliath (Голиаф, http://postrank-labs.github.com/goliath/) — асинхронный веб фреймворк на Ruby, созданный компанией PostRank (http://postrank.com/), ныне купленной Google.
Главной особенностью Голиафа является применение модели событий для ввода-вывода, посредством библиотеки EventMachine, а также механизма волокон (fibers), появившегося в Ruby 1.9. Его можно считать аналогом столь модного сегодня Node.js, только на Ruby.
В статье мы рассмотрим такие вопросы:
Волокно (fiber) — своеобразный контекст выполнения, логически подобный потоку, но это не поток. Потоки используются для распараллеливания задач, в то время как волокна больше подойдут для асинхронных операций ввода-вывода. Фактически волокно – это такой продвинутый goto, обернутый в абстракцию, похожую на поток, только волокна реально выполняются в одном физическом потоке. Если в настоящем потоке выполнение прерывается само и в произвольном месте кода по воле операционной системы, то в случае волокон разработчик сам решает, когда и где передать управление в другой участок кода программы.
Волокна в Ruby реализованы через класс Fiber и его методы new, yield и resume. Волокно создается как блок кода, который можно выполнять, аналогично потоку, но не запускается сразу. Затем вызов resume для некоторого волокна передаст управление внутрь блока. Блок кода в волокне будет выполняться до тех пор, пока не закончится либо не будет встречен вызов yield. Вызов yield значит – приостановить выполнение кода в этом месте, запомнить состояние и перейти к выполнению основного кода, вызвавшего resume. Обычно существует общий цикл событий для всех волокон, куда управление передается каждый раз, когда одно из волокон закончило или приостановило свою работу – Event loop. В цикле программа дожидается наступления каких-либо событий и вызывает resume соответственно для тех волокон, которые эти события ожидали.
У такого подхода есть ряд преимуществ перед обычными потоками:
Однако стоит помнить и об ограничениях:
Вот небольшой пример демонстрирующий работу волокон:
Волокна поддерживаются в Ruby начиная с версии 1.9. Более детально о волокнах и библиотеке EventMachine вы можете прочесть в блоге Ильи Григорика — автора Голиафа и других интересных библиотек.
Я не буду повторно описывать как устанавливать Helicon Zoo, это достаточно подробно описано на домашней странице продукта http://www.helicontech.com/zoo/
Чтобы установить Goliath запустите Web Platform Installer, выберете Zoo -> Engines -> Goliath. При установке Goliath автоматически будет установлен Ruby 1.9.3, если его еще нет в системе. На данный момент это наиболее подходящая версия. Голиаф поддерживает JRuby, но поскольку волокна там реализованы через потоки, скорость оказывается значительно ниже чем в случае с Ruby 1.9. Отметим, что команда JRuby планирует улучшить поддержку волокон в ближайшем будущем.
В предыдущей статье я показывал, как создать новое приложение с использованием WebMatrix и IIS Express. В этот раз я покажу, как сделать то же самое напрямую из менеджера IIS без установки WebMatrix и IIS Express в систему. Перейдите по этой ссылке и скачайте zip-файл для проекта Goliath. Теперь запустите IIS Manager, создайте новый веб сайт и выберете на вкладке Depoly -> Import Application. Затем найдите скачанный файл и следуйте указаниям мастера.

Ну и чтобы происходящее не казалось магией, стоит добавить, что проект на Goliath очень прост. В нем нет никаких папок и прав, нет deploy-скриптов. Просто в папке приложения создаются два файла – app.rb и web.config. Вот содержимое web.config с комментариями. Можно просто создать такой файл в любом IIS приложении и получить там рабочее приложение Goliath.
Чтобы продемонстрировать возможность реализации long-polling с использованием Голиаф и IIS, напишем простой чат. Он будет состоять из двух частей: серверной (Ruby, Голиаф) и клиентской (JavaScript). Для правки кода вам потребуется редактор или среда разработки. Мы использовали Aptana (http://aptana.org):

Серверная часть – файл app.rb:
Клиентская часть, index.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 при разработке приложений.
Главной особенностью Голиафа является применение модели событий для ввода-вывода, посредством библиотеки 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 соответственно для тех волокон, которые эти события ожидали.
У такого подхода есть ряд преимуществ перед обычными потоками:
- Накладные расходы на создание волокна минимальны – системный планировщик тут не участвует.
- Нет нужды заботиться о синхронизации т.к. все волокна выполняются по очереди в одном потоке и разработчик сам решает, когда можно передать управление.
- Производительность на одном ядре потенциально выше, т.к. переключения между контекстами выполнения можно расставить в наиболее удобных местах.
Однако стоит помнить и об ограничениях:
- Волокна не помогут задействовать несколько ядер процессора.
- Если в коде одного волокна произойдет зависание или вечный цикл – все волокна в этом потоке будут заблокированы.
Вот небольшой пример демонстрирующий работу волокон:
require 'fiber' # Fiber.new получает бло�� в качестве аргумента, # но он не выполняется сразу же, а только после resume. my_fiber = Fiber.new do puts 'fiber> started' Fiber.yield # Отдаем управление контексту, который запустил волокно. puts 'fiber> resumed' end puts 'main> let\'s start our fiber:' my_fiber.resume puts 'main> we\'re back in the main flow. Let\'s resume the fiber again:' my_fiber.resume puts 'main> end.'
Волокна поддерживаются в Ruby начиная с версии 1.9. Более детально о волокнах и библиотеке EventMachine вы можете прочесть в блоге Ильи Григорика — автора Голиафа и других интересных библиотек.
Установка рабочего окружения
Я не буду повторно описывать как устанавливать Helicon Zoo, это достаточно подробно описано на домашней странице продукта http://www.helicontech.com/zoo/
Чтобы установить Goliath запустите Web Platform Installer, выберете Zoo -> Engines -> Goliath. При установке Goliath автоматически будет установлен Ruby 1.9.3, если его еще нет в системе. На данный момент это наиболее подходящая версия. Голиаф поддерживает JRuby, но поскольку волокна там реализованы через потоки, скорость оказывается значительно ниже чем в случае с Ruby 1.9. Отметим, что команда JRuby планирует улучшить поддержку волокон в ближайшем будущем.
В предыдущей статье я показывал, как создать новое приложение с использованием WebMatrix и IIS Express. В этот раз я покажу, как сделать то же самое напрямую из менеджера IIS без установки WebMatrix и IIS Express в систему. Перейдите по этой ссылке и скачайте zip-файл для проекта Goliath. Теперь запустите IIS Manager, создайте новый веб сайт и выберете на вкладке Depoly -> Import Application. Затем найдите скачанный файл и следуйте указаниям мастера.

Ну и чтобы происходящее не казалось магией, стоит добавить, что проект на 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 при разработке приложений.