Статьи

Понимание параллельного программирования с Руби Голиафом

PostRank недавно выпустил новый веб-сервер Ruby: Goliath . Он использует цикл обработки событий таким же образом, как node.js и nginx для достижения высокого уровня параллелизма, но добавляет некоторый специальный соус, который позволяет традиционно сложному асинхронному коду быть написанным в синхронном стиле.

Например, асинхронный код на Ruby обычно выглядит следующим образом (с использованием библиотеки событий машины):

require 'eventmachine' require 'em-http' EM.run { EM::HttpRequest.new('http://www.sitepoint.com/').get.callback {|http| puts http.response } } 

Это удобно тем, что позволяет приложению выполнять другие действия, пока HTTP-запрос завершается (это «неблокирующий»), но для последовательного извлечения двух сайтов вам необходимо вложить обратные вызовы:

 EM::HttpRequest.new('http://www.sitepoint.com/').get.callback {|http| # extract_next_url is a fake method, you get the idea url = extract_next_url(http.response) EM::HttpRequest.new(url).get.callback {|http2| puts http2.response } } 

Как вы можете себе представить, эта модель быстро становится грязной. Голиаф позволяет нам писать вышеуказанный код простым синхронным способом, с которым мы знакомы:

 http = EM::HttpRequest.new("http://www.sitepoint.com").get # extract_next_url is a fake method, you get the idea url = extract_next_url(http.response) http2 = EM::HttpRequest.new(url).get 

… все же за кадром это все еще выполняется асинхронно! Другой код может быть запущен во время выполнения HTTP-запросов.

Это поражает меня. Как это работает? Давай выясним.

Волокна

Исходя из документации , Голиаф утверждает, что творит свое волшебство, «используя волокна Ruby, представленные в Ruby 1.9+». Этот первый намек отправляет нас в ruby rdocs, чтобы найти:

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

Ух, слишком много громких слов. Давайте просто погрузимся и начнем изучать код Голиафа. Документация Goliath содержит полный пример прокси сайта:

 require 'goliath' require 'em-synchrony' require 'em-synchrony/em-http' class HelloWorld < Goliath::API def response(env) req = EM::HttpRequest.new("http://www.google.com/").get resp = req.response [200, {}, resp] end end # to play along at home: # $ gem install goliath # $ gem install em-http-request --pre # $ ruby hello_world.rb -sv 

Мы знаем, что для того, чтобы это происходило асинхронно, в этом вызове #get должно происходить какое-то забавное дело, поэтому давайте попробуем найти это. Мое чувство паука говорит мне, что это будет где-то в em-synchrony/em-http

 $ gem unpack em-synchrony Unpacked gem: '/Users/xavier/Code/tmp/em-synchrony-0.3.0.beta.1' $ cd em-synchrony-0.3.0.beta.1 # I used tab completion on the next line to find the exact path $ cat lib/em-synchrony/em-http.rb 

Это показывает:

 # em-synchrony/lib/em-synchrony/em-http.rb begin require "em-http" rescue LoadError =< error raise "Missing EM-Synchrony dependency: gem install em-http-request" end module EventMachine module HTTPMethods %w[get head post delete put].each do |type| class_eval %[ alias :a#{type} :#{type} def #{type}(options = {}, &amp;blk) f = Fiber.current conn = setup_request(:#{type}, options, &amp;blk) conn.callback { f.resume(conn) } conn.errback { f.resume(conn) } Fiber.yield end ] end end end 

Джек-пот! Волокна! Похоже, что это патча для существующей библиотеки em-http , поэтому прежде чем мы зайдем слишком далеко, давайте выясним, как выглядит обычный код em-http без волокон. В вики em-http-request есть удобный пример:

 EventMachine.run { http = EventMachine::HttpRequest.new('http://google.com/').get :query =< {'keyname' =< 'value'} http.errback { p 'Uh oh'; EM.stop } http.callback { p http.response_header.status p http.response_header p http.response EventMachine.stop } } 

Это выглядит почти так же, как и в приведенном выше коде, который является многообещающим, и когда мы углубимся в это, это станет еще более очевидным.

 $ gem unpack em-http ERROR: While executing gem ... (Gem::RemoteFetcher::FetchError) SocketError: getaddrinfo: nodename nor servname provided, or not known (http://rubygems.org/latest_specs.4.8.gz) # Oh noes it doesn't work! # Search for em gems $ gem list em- *** LOCAL GEMS *** em-http-request (1.0.0.beta.2, 0.3.0) em-socksify (0.1.0) em-synchrony (0.3.0.beta.1) $ gem unpack em-http-request # Ah that is probably it $ cd em-http-request-1.0.0.beta.2 $ ack "get" lib/ lib/em-http/http_connection.rb 4: def get options = {}, &amp;blk; setup_request(:get, options, &amp;blk); end 

Обратите внимание на последнюю строку, которая get откладывает прямо на setup_request , который является тем же самым вызовом, который сделан в примере волокна выше. Да, почти так же. Теперь мы можем вернуться к оптоволоконному коду.

 f = Fiber.current conn = setup_request(:#{type}, options, &amp;blk) conn.callback { f.resume(conn) } conn.errback { f.resume(conn) } Fiber.yield 

Похоже, что происходит, вместо того, чтобы немедленно выполнять какую-либо работу, когда вызывается обратный вызов, resume вызывается на текущем волокне, предположительно запускается резервное копирование этого потока в точке вызова yield . Проверка документации для Fiber.yield подтверждает это, а также объясняет, как переменная conn возвращается из этого метода в последнем предложении:

Выход возвращает контроль к контексту, который возобновил волокно, передавая любые аргументы, которые были переданы ему. Волокно возобновит обработку в этот момент, когда возобновится вызов следующего. Любые аргументы, переданные следующему резюме, будут значением, которое вычисляет это выражение Fiber.yield.

Используй это

Теперь у нас есть представление о том, как Голиаф использует магию, хотя она все еще может быть нечеткой. Давайте посмотрим, правильно ли мы это сделаем, попытавшись написать код, имитирующий его.

Помните, что этот трюк с волокном – это просто способ упростить код, заполненный обратным вызовом, поэтому мы должны иметь возможность сначала написать метод без учета волокна, а затем очистить его. Мне нравится начинать с простого примера, поэтому мы собираемся написать базовый класс Голиафа, который блокируется на одну секунду, а затем визуализирует некоторый текст.

 class Surprise < Goliath::API def response(env) sleep 1 [200, {}, "Surprise!"] end end 

Хит это в вашем веб-браузере и бинго, он ждет секунду. Не так быстро, как тигр, что происходит, когда мы выдаем несколько одновременных запросов:

 $ ab -n 3 -c 3 127.0.0.1:9000/ | grep "Time taken" Time taken for tests: 3.011 seconds 

Увы, наш веб-сервер обслуживал только один запрос за раз. Это не веб-масштаб. sleep вызов блокирует не только наш ответ, но и весь сервер . Вот почему мы перешли к вечернему программированию. Давайте попробуем классический таймер EventMachine вместо этого:

 class Surprise < Goliath::API def response(env) EventMachine.add_timer 1, proc { [200, {}, "Surprise!"] } end end 

Конечно, это не работает, потому что метод #response должен выглядеть синхронно. В этом случае происходит то, что #add_timer возвращает nil и Голиаф немедленно пытается отрендерить это, взорвавшись в процессе. Таймер срабатывает через некоторое время, и нет никакого кода, чтобы заботиться. Мы не можем отправить результат нашего таймера в качестве возвращаемого значения для метода.

Нам нужно объединить синхронную природу первого примера с асинхронными элементами второго; красивый Франкенштейн. Надеюсь, вы поняли, что мы можем использовать волокна для сшивания.

 class Surprise < Goliath::API def response(env) f = Fiber.current EventMachine.add_timer 1, proc { f.resume } Fiber.yield [200, {}, "Surprise!"] end end 

Мы крадем шаблон, который мы видели в em-synchronicity/em-http выше, захватывая текущее волокно и устанавливая вызов resume в асинхронном Fiber.yield который возобновляет выполнение в Fiber.yield . Тестируя это с помощью ab, мы видим, что это действительно решает нашу проблему параллелизма:

 $ ab -n 3 -c 3 127.0.0.1:9000/ | grep "Time taken" Time taken for tests: 1.009 seconds 

Эти волокна довольно крутые.

Завершение

Изучая исходный код Голиафа и связанные с ним библиотеки, мы обнаружили, как он выполняет свой асинхронный трюк, маскирующийся под синхронный, и смогли применить эти знания на практике на простом примере.

Чтобы попрактиковаться в чтении кода, вот несколько других исследовательских задач, которые вы можете попробовать:

  • Найдите, где Голиаф вызывает метод #response и посмотрите, есть ли еще какие-нибудь скрытые трюки с волокнами.
  • Изучите одну из других библиотек, для которых em-synchrony предоставляет API, например em-mongo .
  • Rack-fiber_pool использует волокна в аналогичном контексте, проверьте их и посмотрите, к чему они стремятся .

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