Подтвердить что ты не робот

Параллельные запросы с помощью MRI Ruby

Я собрал простой пример, пытающийся доказать параллельные запросы в Rails, используя базовый пример. Обратите внимание, что я использую MRI Ruby2 и Rails 4.2.

  def api_call
    sleep(10)
    render :json => "done"
  end

Затем я перехожу к 4 различным вкладкам в Chrome на моем Mac (ядро I7/4) и вижу, запускаются ли они последовательно или параллельно (действительно параллельное, но близкое, но не одно и то же). то есть http://localhost:3000/api_call

Я не могу заставить это работать с помощью Puma, Thin или Unicorn. Запросы каждый поступают последовательно. Первая вкладка через 10 секунд, вторая после 20 (поскольку она должна была дождаться завершения первой), третий после этого....

Из того, что я прочитал, я считаю, что справедливо следующее (пожалуйста, поправьте меня) и мои результаты:

  • Unicorn - многопроцессор, и мой пример должен был работать (после определения числа рабочих в файле конфигурации unicorn.rb), но это не так. Я могу видеть 4 рабочих, но все работает последовательно. Я использую драгоценный камень unicorn-rails, начиная рельсы с unicorn -c config/unicorn.rb, а в моем unicorn.rb у меня есть:

- unicorn.rb

worker_processes 4
preload_app true
timeout 30
listen 3000
after_fork do |server, worker|
  ActiveRecord::Base.establish_connection
end
  • Thin и Puma многопоточные (хотя Puma по крайней мере имеет режим clustered ', где вы можете начинать работу с -w параметр) и не должен работать в любом режиме (в многопоточном режиме) с помощью MRI Ruby2.0, потому что "существует глобальная блокировка перехватчика (GIL), которая обеспечивает одновременный запуск только одного потока".

Итак,

  • Есть ли у меня допустимый пример (или используется сбой просто неправильно)?
  • Правильны ли мои утверждения выше о многопроцессорности и многопоточности (по отношению к MRI Rails 2)?
  • Любые идеи о том, почему я не могу заставить его работать с Unicorn (или любым сервером в этом отношении)?

Есть очень похожий вопрос для моего, но я не могу заставить его работать как ответ, и он не отвечает на все мои вопросы о параллельных запросах с использованием МРТ Рубин.

Проект Github: https://github.com/afrankel/limitedBandwidth (примечание: проект рассматривает больше, чем этот вопрос о многопроцессорной/потоковой передаче на сервере)

4b9b3361

Ответ 1

Я приглашаю вас прочитать серию Jesse Storimer Никто не понимает GIL Это может помочь вам лучше понять некоторые внутренние МРТ.

Я также нашел Pragmatic Concurrency с Ruby, который читает интересно. Он имеет несколько примеров тестирования одновременно.

EDIT: Кроме того, я могу порекомендовать статью Удаление config.threadsafe! Возможно, это не относится к Rails 4, но в нем объясняются параметры конфигурации, один из которых можно использовать для разрешения concurrency.


Давайте обсудим ответ на ваш вопрос.

У вас может быть несколько потоков (с использованием MRI), даже с Puma. GIL гарантирует, что за один раз активен только один поток, то есть ограничение, которое разработчики дублируют как ограничивающие (из-за отсутствия реального параллельного выполнения). Имейте в виду, что GIL не гарантирует безопасность резьбы. Это не означает, что другие потоки не работают, они ждут своей очереди. Они могут чередовать (статьи могут помочь лучше понять).

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

Что касается конфигурации вашего вопроса и репо GitHub, которую вы поделили, я думаю, что подходящая конфигурация (я использовал Puma) - это настроить 4 рабочих и от 1 до 40 потоков. Идея состоит в том, что один рабочий обслуживает одну вкладку. Каждая вкладка отправляет до 10 запросов.

Итак, давайте начнем:

Я работаю над Ubuntu на виртуальной машине. Поэтому сначала я включил 4 ядра в настройках виртуальной машины (и некоторые другие настройки, о которых я думал, что это может помочь). Я мог проверить это на своей машине. Поэтому я пошел с этим.

Linux command --> lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                4
On-line CPU(s) list:   0-3
Thread(s) per core:    1
Core(s) per socket:    4
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 69
Stepping:              1
CPU MHz:               2306.141
BogoMIPS:              4612.28
L1d cache:             32K
L1d cache:             32K
L2d cache:             6144K
NUMA node0 CPU(s):     0-3

Я использовал ваш общий проект GitHub и немного изменил его. Я создал файл конфигурации Puma с именем puma.rb (поместите его в каталог config) со следующим содержимым:

workers Integer(ENV['WEB_CONCURRENCY'] || 1)
threads_count = Integer(ENV['MAX_THREADS'] || 1)
threads 1, threads_count

preload_app!

rackup      DefaultRackup
port        ENV['PORT']     || 3000
environment ENV['RACK_ENV'] || 'development'

on_worker_boot do
  # Worker specific setup for Rails 4.1+
  # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
  #ActiveRecord::Base.establish_connection
end

По умолчанию Puma запускается с 1 рабочим и 1 потоком. Вы можете использовать переменные среды для изменения этих параметров. Я сделал это:

export MAX_THREADS=40
export WEB_CONCURRENCY=4

Чтобы запустить Puma с этой конфигурацией, я набрал

bundle exec puma -C config/puma.rb

в каталоге приложений Rails.

Я открыл браузер с четырьмя вкладками, чтобы вызвать URL приложения.

Первый запрос начался около 15:45:05, и последний запрос был около 15: 49: 44. Это истекшее время 4 минуты и 39 секунд. Также вы можете увидеть идентификатор запроса в неупорядоченном порядке в файле журнала. (См. Ниже)

Каждый вызов API в проекте GitHub спит в течение 15 секунд. У нас есть четыре 4 вкладки, каждая из которых имеет 10 вызовов API. Это составляет максимальное время, равное 600 секундам, т.е. 10 минут (в строго последовательном режиме).

Идеальный результат в теории был бы параллельным, и прошедшее время не было бы за пределами 15 секунд, но я этого не ожидал. Я не был уверен, чего ожидать в результате точно, но я все еще был удивлен (учитывая, что я побежал на виртуальной машине, а МРТ сдерживается GIL и некоторыми другими факторами). Истекшее время этого теста было меньше половины максимального прошедшего времени (в строго последовательном режиме), мы сокращаем результат менее чем наполовину.

EDIT. Я читал далее о Rack:: Lock, который обертывает мьютекс вокруг каждого запроса (третья статья выше). Я нашел вариант config.allow_concurrency = true для сохранения времени. Небольшое предостережение было увеличение пула соединений (хотя запрос не делает запрос база данных должна была быть установлена ​​соответственно); число максимальных потоков хороший дефолт. 40 в этом случае.

Я тестировал приложение с помощью jRuby, и фактическое прошедшее время составляло 2 минуты, с allow_concurrency = true.

Я тестировал приложение с помощью МРТ, и фактическое прошедшее время составляло 1 мин47, с allow_concurrency = true. Это было большим сюрпризом для меня. Это меня действительно удивило, потому что я ожидал, что МРТ будет медленнее, чем JRuby. Не было. Это заставляет меня расспрашивать о широко распространенной дискуссии о различиях в скорости между МРТ и JRuby.

Наблюдение ответов на разных вкладках теперь "более случайное". Бывает, что вкладка 3 или 4 завершается перед первой закладкой, которую я запросил.

Я думаю, потому что у вас нет условий гонки, тест, похоже, ОК. Однако я не уверен в широких последствиях приложения, если вы устанавливаете config.allow_concurrency = true в приложении реального мира.

Не стесняйтесь проверить это и сообщить мне любую обратную связь, которую вы, возможно, читатели. У меня все еще есть клон на моей машине. Сообщите мне, если вы заинтересованы.

Чтобы ответить на ваши вопросы в порядке:

  • Я думаю, что ваш пример действителен по результату. Однако для Concurrency лучше протестировать с использованием общих ресурсов (как, например, во второй статье).
  • Что касается ваших заявлений, как упоминалось в начале этого ответ, MRI является многопоточным, но ограниченным GIL одним активным поток за раз. Это ставит вопрос: с МРТ не лучше тестировать с большим количеством процессов и меньше потоков? Я действительно не знаю, первая догадка была бы скорее неважной или не очень важной. Может быть, кто-то может пролить свет на это.
  • Твой пример просто прекрасен, я думаю. Просто нужно было немного модификаций.

Приложение

Файл журнала Rails:

**config.allow_concurrency = false (by default)**
-> Ideally 1 worker per core, each worker servers up to 10 threads.

[3045] Puma starting in cluster mode...
[3045] * Version 2.11.2 (ruby 2.1.5-p273), codename: Intrepid Squirrel
[3045] * Min threads: 1, max threads: 40
[3045] * Environment: development
[3045] * Process workers: 4
[3045] * Preloading application
[3045] * Listening on tcp://0.0.0.0:3000
[3045] Use Ctrl-C to stop
[3045] - Worker 0 (pid: 3075) booted, phase: 0
[3045] - Worker 1 (pid: 3080) booted, phase: 0
[3045] - Worker 2 (pid: 3087) booted, phase: 0
[3045] - Worker 3 (pid: 3098) booted, phase: 0
Started GET "/assets/angular-ui-router/release/angular-ui-router.js?body=1" for 127.0.0.1 at 2015-05-11 15:45:05 +0800
...
...
...
Processing by ApplicationController#api_call as JSON
  Parameters: {"t"=>"15?id=9"}
Completed 200 OK in 15002ms (Views: 0.2ms | ActiveRecord: 0.0ms)
[3075] 127.0.0.1 - - [11/May/2015:15:49:44 +0800] "GET /api_call.json?t=15?id=9 HTTP/1.1" 304 - 60.0230

**config.allow_concurrency = true**
-> Ideally 1 worker per core, each worker servers up to 10 threads.

[22802] Puma starting in cluster mode...
[22802] * Version 2.11.2 (ruby 2.2.0-p0), codename: Intrepid Squirrel
[22802] * Min threads: 1, max threads: 40
[22802] * Environment: development
[22802] * Process workers: 4
[22802] * Preloading application
[22802] * Listening on tcp://0.0.0.0:3000
[22802] Use Ctrl-C to stop
[22802] - Worker 0 (pid: 22832) booted, phase: 0
[22802] - Worker 1 (pid: 22835) booted, phase: 0
[22802] - Worker 3 (pid: 22852) booted, phase: 0
[22802] - Worker 2 (pid: 22843) booted, phase: 0
Started GET "/" for 127.0.0.1 at 2015-05-13 17:58:20 +0800
Processing by ApplicationController#index as HTML
  Rendered application/index.html.erb within layouts/application (3.6ms)
Completed 200 OK in 216ms (Views: 200.0ms | ActiveRecord: 0.0ms)
[22832] 127.0.0.1 - - [13/May/2015:17:58:20 +0800] "GET / HTTP/1.1" 200 - 0.8190
...
...
...
Completed 200 OK in 15003ms (Views: 0.1ms | ActiveRecord: 0.0ms)
[22852] 127.0.0.1 - - [13/May/2015:18:00:07 +0800] "GET /api_call.json?t=15?id=10 HTTP/1.1" 304 - 15.0103

**config.allow_concurrency = true (by default)**
-> Ideally each thread serves a request.

Puma starting in single mode...
* Version 2.11.2 (jruby 2.2.2), codename: Intrepid Squirrel
* Min threads: 1, max threads: 40
* Environment: development
NOTE: ActiveRecord 4.2 is not (yet) fully supported by AR-JDBC, please help us finish 4.2 support - check http://bit.ly/jruby-42 for starters
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
Started GET "/" for 127.0.0.1 at 2015-05-13 18:23:04 +0800
Processing by ApplicationController#index as HTML
  Rendered application/index.html.erb within layouts/application (35.0ms)
...
...
...
Completed 200 OK in 15020ms (Views: 0.7ms | ActiveRecord: 0.0ms)
127.0.0.1 - - [13/May/2015:18:25:19 +0800] "GET /api_call.json?t=15?id=9 HTTP/1.1" 304 - 15.0640

Ответ 2

Как для @Elyasin, так и для @Arthur Frankel, я создал этот repo для тестирования Puma, работающего в MRI и JRuby. В этом маленьком проекте я не выполнял sleep, чтобы эмулировать длинный запрос. Поскольку я обнаружил, что в MRI, GIL, похоже, относится к этому иначе, чем к обычной обработке, более аналогично внешнему запросу ввода-вывода.

Я установил вычисление последовательности фибоначчи в контроллере. На моей машине fib(39) занял 6.x секунд в JRuby и 11 секунд в MRI, что достаточно, чтобы показать различия.

Я открыл 2 окна браузера. Вместо того, чтобы открывать вкладки в том же браузере, я сделал это, чтобы предотвратить определенные ограничения параллельного запроса, отправляемого браузером в тот же домен. Я теперь уверен, подробности, но 2 разных браузера достаточно, чтобы это не происходило.

Я тестировал тонкий + МРТ и Puma + MRI, затем Puma + JRuby. Результаты:

  • тонкий + МРТ: не удивлен, когда я быстро перезагрузил 2 браузера, первый из них завершился через 11 секунд. Затем второй запрос начался, потребовалось еще 11 секунд.

  • Сначала поговорим о Puma + JRuby. Поскольку я быстро перезагрузил два браузера, они, похоже, начали почти в то же самое время, и закончили с той же секунды. Оба заняли около 6,9 секунды. Puma - многопоточный сервер, а JRuby поддерживает многопоточность.

  • Наконец, Puma + MRI. Потребовалось 22 секунды, чтобы закончить оба браузера после того, как я быстро перезагрузил 2 браузера. Они начали почти в то же самое время, закончили почти в то же самое время. Но для обоих потребовалось два раза. Именно это делает GIL: переключение между потоками для concurrency, но сама блокировка предотвращает parallelism.

О моей настройке:

  • Серверы были запущены в режиме производства Rails. В режиме производства config.cache_classes устанавливается на true, что подразумевает config.allow_concurrency = true
  • Пума была запущена с 8 нитями мин и 8 потоками.