Skip to main content
Все материалы

Знакомство с 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 проектов.

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

 

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