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 = {}, &blk) f = Fiber.current conn = setup_request(:#{type}, options, &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 = {}, &blk; setup_request(:get, options, &blk); end
Обратите внимание на последнюю строку, которая get
откладывает прямо на setup_request
, который является тем же самым вызовом, который сделан в примере волокна выше. Да, почти так же. Теперь мы можем вернуться к оптоволоконному коду.
f = Fiber.current conn = setup_request(:#{type}, options, &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 использует волокна в аналогичном контексте, проверьте их и посмотрите, к чему они стремятся .
Дайте нам знать, как вы идете в комментариях. Настройтесь на следующую неделю для более захватывающих приключений в джунглях кода.