Passenger, Sinatra, Nginx, RabbitMQ и SSE (как Passenger запускает процессы и где на рисунке помещаются потоки Ruby)
У меня проблема с текущими настройками, которые не работают должным образом и не позволяют мне перейти на веб-сайт с поддержкой событий, отправленных сервером (SSE). Мой основной вопрос может быть найден ниже жирным шрифтом, но сводится к "Как я могу запустить дополнительный поток из веб-приложения Sinatra в настройках Пассажира?".
Я использую Passenger 5.0.21 и Sinatra 1.4.6. Приложение написано как классическое приложение Sinatra, не модульное, но оно может быть изменено при необходимости.
Я положил директиву passenger_min_instances 3
в конфигурации Nginx, чтобы запустить минимум 3 экземпляра веб-приложения. У меня два puts
в config.ru
файл моего приложения Sinatra, поэтому при запуске потока я получаю обратную связь внутри /var/log/nginx/passenger.log
а также когда поток получает сообщения через свою очередь RabbitMQ:
...
Thread.new {
puts " [* #{Thread.current.inspect}] Waiting for logs. To exit press CTRL+C"
begin
q.subscribe(:block => true) do |delivery_info, properties, body|
puts " [x #{Thread.current.inspect}] #{body}"
end
rescue Interrupt => _
ch.close
conn.close
end
}
run Sinatra::Application
Я ожидал, что этот код будет запущен n раз, а n - это число процессов, запущенных Passenger. Похоже, это не тот случай.
Кроме того, мой app.rb
содержит много вещей, которые можно сократить до:
puts "(CLASS)... Inside thread #{Thread.current.inspect}"
configure do
puts "(CONFIGURE)... Inside thread #{Thread.current.inspect}"
end
get '/debug' do
puts "(DEBUG)... Inside thread #{Thread.current.inspect}"
end
Когда я перезагружаю Nginx и делаю первый HTTP GET доступ к URL /debug
процессы создаются, и один из процессов обслуживает запрос. Что я получу в /var/log/nginx/passenger.log
?
(CLASS)... Inside thread #<Thread:0x007fb29f4ca258 run>
(CONFIGURE)... Inside thread #<Thread:0x007fb29f4ca258 run>
[* #<Thread:0x007fb29f7f8038@config.ru:68 run>] Waiting for logs. To exit press CTRL+C
(DEBUG)... Inside thread #<Thread:0x007fb29f4ca8e8@/usr/lib/ruby/vendor_ruby/phusion_passenger
192.168.0.11 - test [30/Dec/2015:10:09:08 +0100] "GET /debug HTTP/1.1" 200 2184 0.0138
Оба сообщения начинаются с CLASS
а также CONFIGURE
напечатаны внутри той же нити. Я ожидал, что это произойдет во время создания процесса, как это произошло, но это произошло только один раз, заставив меня думать, что Пассажир запускает только один процесс. Однако я могу видеть 3 процесса с passenger-status --verbose
, Другой поток создан (в config.ru
) получать сообщения RabbitMQ.
Как видите, первый процесс обработал 1 запрос (сокращен для ясности):
$ passenger-status --verbose
----------- General information -----------
Max pool size : 6
App groups : 1
Processes : 3
Requests in top-level queue : 0
----------- Application groups -----------
/home/hydro/web2/public:
App root: /home/hydro/web2
Requests in queue: 0
* PID: 1116 Sessions: 0 Processed: 1 Uptime: 2m 19s
CPU: 0% Memory : 18M Last used: 2m 19s ago
* PID: 1123 Sessions: 0 Processed: 0 Uptime: 2m 19s
CPU: 0% Memory : 3M Last used: 2m 19s ago
* PID: 1130 Sessions: 0 Processed: 0 Uptime: 2m 19s
CPU: 0% Memory : 2M Last used: 2m 19s ago
Тестовая программа ruby, которая публикует сообщение RabbitMQ для подписчиков, которые иногда получают, работает, а иногда нет. Может быть, Пассажир отключает запущенный процесс, даже если он не видел запрос в данное время. Ничего не появляется в журнале. Нет обратной связи с подписчиком, нет сообщения от самого Пассажира.
Если я обновлю страницу, я получу DEBUG
сообщение и GET /debug
след. passenger-status --verbose
показывает, что первый процесс уже обработал два запроса.
Во время моих разных тестов я видел, что мне приходится запускать много запросов, чтобы заставить Passenger обслуживать запросы с двумя другими процессами или даже запускать новые процессы максимум до 6. Давайте сделаем это с другой машины в той же локальной сети сroot@backup:~# ab -A test:test -kc 1000 -n 10000 https://192.168.0.10:445/debug
, Пассажир запустил максимум 6 процессов для обработки запросов, но я ничего не вижу в passenger.log
файл кроме DEBUG
сообщения и GET /debug
следы, как будто никакие другие процессы не были начаты.
$ passenger-status --verbose
----------- General information -----------
Max pool size : 6
App groups : 1
Processes : 6
Requests in top-level queue : 0
----------- Application groups -----------
/home/hydro/web2/public:
App root: /home/hydro/web2
Requests in queue: 0
* PID: 1116 Sessions: 0 Processed: 664 Uptime: 16m 29s
CPU: 0% Memory : 28M Last used: 32s ago
* PID: 1123 Sessions: 0 Processed: 625 Uptime: 16m 29s
CPU: 0% Memory : 27M Last used: 32s ago
* PID: 1130 Sessions: 0 Processed: 614 Uptime: 16m 29s
CPU: 0% Memory : 27M Last used: 32s ago
* PID: 2105 Sessions: 0 Processed: 106 Uptime: 33s
CPU: 0% Memory : 23M Last used: 32s ago
* PID: 2112 Sessions: 0 Processed: 103 Uptime: 33s
CPU: 0% Memory : 22M Last used: 32s ago
* PID: 2119 Sessions: 0 Processed: 92 Uptime: 33s
CPU: 0% Memory : 21M Last used: 32s ago
Итак, главный вопрос: как я могу запускать поток (подписчик RabbitMQ) из процесса веб-приложения Sinatra при каждом запуске процесса?
Я хочу иметь возможность отправлять данные в процессы моего веб-приложения, чтобы они могли отправлять их обратно веб-клиенту с помощью SSE. Я хотел бы иметь два потока на процесс веб-приложения: основной поток, используемый Sinatra, и мой дополнительный поток, чтобы сделать некоторые вещи RabbitMQ. Существует также база данных Oracle и серверная часть Erlang, но я не думаю, что они здесь актуальны.
Мне также интересно, как Passenger обрабатывает создание экземпляров процессов в случае веб-приложения Sinatra. Множественная среда Ruby? Как могло случиться, что класс создается только один раз, если запущено несколько процессов? Это файл config.ru
(и даже app.rb
) обрабатывается только один раз даже при запуске нескольких процессов? Я прочитал много вещей в Интернете, но не мог понять это.
В целом, как правильно делать SSE с Ruby, Nginx, Passenger и Sinatra.
Подробности относительно Nginx были приведены ниже для ясности.
Nginx настроен как обратный прокси-сервер, стоящий перед Passenger, а веб-приложение настраивается в server
а также location /
с базовой аутентификацией SSL и HTTP и следующими директивами:
location / {
proxy_buffering off;
proxy_cache off;
proxy_pass_request_headers on;
passenger_set_header Host $http_host;
passenger_set_header X-Real-IP $remote_addr;
passenger_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
passenger_set_header X-Forwarded-Proto $scheme;
passenger_set_header X-Remote-User $remote_user;
passenger_set_header Host $http_host;
passenger_min_instances 3;
proxy_redirect off;
passenger_enabled on;
passenger_ruby /home/hydro/.rbenv/versions/2.3.0/bin/ruby;
passenger_load_shell_envvars on;
passenger_nodejs /usr/bin/nodejs;
passenger_friendly_error_pages on;
}
1 ответ
Я думаю, что ваша нынешняя архитектура не так. Ваше приложение Sinatra не должно смешиваться с дополнительными потоками или поддерживаться в рабочем состоянии просто для того, чтобы оно могло отправлять push-сообщения вашим клиентам - у вас должен быть отдельный push-сервер, предназначенный для выталкивания сообщений, и пусть ваш HTTP API делает то, что работает лучше всего - спать, пока не получит запрос.
Вы упоминаете, что используете nginx, поэтому я очень рекомендую компилировать в этом модуле:
https://github.com/wandenberg/nginx-push-stream-module
Теперь вы можете избавиться от своей очереди RabbitMQ - любой процесс, которому нужно отправить сообщение одному из ваших подписчиков push, просто должен отправить HTTP-запрос в RESTful API этого модуля:
Пример запроса curl:
curl -s -v -X POST 'http://localhost/pub?id=my_channel_1' -d 'Hello World!'
Конечно, по умолчанию этот модуль будет только слушать запрос от localhost по соображениям безопасности.