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 по соображениям безопасности.

Другие вопросы по тегам