Как стать автором
Обновить

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

Время на прочтение 8 мин
Количество просмотров 5.8K
Продолжаем серию статей, в которой мы знакомим читателей с различными веб фреймворками. И сегодня позвольте представить 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 соответственно для тех волокон, которые эти события ожидали.

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

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

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

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 при разработке приложений.
Теги:
Хабы:
+24
Комментарии 20
Комментарии Комментарии 20

Публикации

Истории

Работа

Ruby on Rails
17 вакансий
Программист Ruby
15 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн