NGINX Unit: новый application-сервер для Ruby

Кратко говоря: NGINX Inc. выпустил поддержку Ruby для нового многоязычного сервера приложений NGINX Unit. Что это дает веб-приложениям Ruby? Обратить ли внимание на NGINX Unit? (1653 слова/10 минут чтения)

Для Ruby появился новый сервер приложений — NGINX Unit. Как вы догадались по названию, это проект NGINX Inc., опенсорсной некоммерческой компании, которой принадлежит веб-сервер NGINX. Осенью 2017 года они объявили, что винегрет серверов приложений (для Ruby — Puma, Unicorn и Passenger) будет заменен единым сервером NGINX Unit. NGINX Unit также работает с Python, Go, PHP и Perl.

Главная идея — упростить администрирование микросервисов. Один процесс NGINX Unit запускает любое количество приложений с любым количеством языков — например, единственный сервер NGNX Unit запускает штук 5 приложений Ruby и для каждого создает свою среду. Или можно запустить приложения Ruby и Python параллельно. Сочетания ограничены только системными ресурсами.

Увы, инфопространство «микросервиса» заспамлено маркетологами. (*Мне не нравится, когда программные проекты позиционируют себя как «современные». Это похоже на «подтасовку» всех существовавших программных проектов в этом проблемном пространстве, говоря, что все они старые и сломанные, а этот способ — новый. Почему этот способ лучше никогда не объясняется. Такой маркетинг заставляет разработчиков программного обеспечения бояться устаревания их навыков, вместо того, чтобы делать что-то существенное.) Они запросто смешивают в одном предложении «динамический», «модульный», «ресурсоемкий» — и «хаотичный», «цельный», «изящный». Мы попробуем прорваться через маркетинг и добраться до сути NGINX Unit.

Прежде чем говорить об архитектуре и особенностях NGINX Unit, убедимся, что все понимают разницу между сервером приложений и веб-сервером. Веб-сервер подключается к клиентам через HTTP и отдает статические файлы или прокси на другие HTTP-серверы, т.е. действует как посредник. Сервер приложений запускает и исполняет языковую среду. В Ruby эти явления не до конца разделены. Основные серверы приложений служат веб-серверами, а вот Nginx и Apache немогут служить серверами приложений. Nginx UNIT — одновременно веб-сервер и сервер приложений.

NGINX Unit запускает четыре процесса: основной (main), маршрутизация (router), контроллер (controller) и обработка (application). Что такое обработка, очевидно из названия: это среда выполнения Ruby, запускающая Rails приложение. Специфику NGINX Unit определяет реализация и взаимодействие маршрутизации, контроллера и обработки.

Основной процесс создает процессы маршрутизации и обработки. Собственно только этим он и занимается. Однако процессы обработки в NGINX Unit динамичны: на работающем сервере можно менять количество запущенных процессов, версию Ruby, и даже запускать новые приложения. Какие приложения запустить, говорит основному процессу контроллер.

Контроллер (он единственный, как и основной процесс) выполняет две задачи: предоставить API конфигурации JSON по HTTP и настроить основной процесс и процесс маршрутизации. Пожалуй, это самый интересный аспект NGNIX Unit для Ruby. Вместо возни с файлами конфигурации вы передаете объекты JSON в процесс управления. Вот такой файл json:

{
"listeners": {
"*:3000": {
"application": "rails"
}
},
    "applications": {
"rails": {
"type": "ruby",
"processes": 5,
"script": "/www/railsapp/config.ru"
}
}
}

… мы можем передать контроллеру NGINX Unit (при условии, что наш сервер NGINX Unit слушает порт 8443):

curl -d “myappconfig.json” -X PUT ‘127.0.0.1:8443’

… и создать новое приложение Ruby.

Объект конфигурации JSON NGINX Unit разделен на получателя запросов и приложение. Приложение исполняется, а получатель запросов слушает внешний мир (т.е. порт, на котором он висит).

Изменять конфигурацию приложения и получателя запросов нужно без пауз. Например, для «горячего развертывания» новой версии приложения добавим новое приложение в конфигурацию:

{
"rails-new": {
"type": "ruby",
"processes": 5,
"script": "/www/rails-new-app/config.ru"
}
}
curl -d "mynewappconfig.json" -X PUT

А затем переключим получателя запросов на новое приложение:

curl -X PUT -d '"rails-new"' '127.0.0.1:8443/listeners/*:3000/application`

Переход (предположительно) прошел гладко, и клиенты ничего не заметили. Это похоже на «поэтапный перезапуск» Puma. При поэтапном перезапуске в Puma рабочие процессы перезапускаются по одному, пока оставшиеся рабочие процессы принимают запросы. Puma использует управляющий сервер (под контролем pumactl). Однако, в отличие от Puma, при «горячей перезагрузке» NGINX Unit не будет двух версий приложения, одновременно принимающих запросы.

В поэтапном перезапуске Puma приложение имеет шесть рабочих процессов. В процессе поэтапного перезапуска три рабочих процесса запускают старый код, остальные три — новый код. Это может вызвать проблемы, например, с изменениями схемы базы данных. Перезапуски NGINX Unit происходят «одновременно», поэтому работают две версии кода, но в любой момент времени принимает запросы только одна.

Этот функционал полезен тем, кто использует собственное приложение Ruby на сервисе AWS, где нужно контролировать развертывание. Пользователям Heroku это не нужно, в предзагрузочной среде Heroku уже предусмотрено «горячее развертывание». Однако, эти средства работают по-разному. Heroku создает новый виртуальный сервер и переключается на него, а NGINX Unit меняет процессы на той же машине неразличимо для клиента.

Процесс маршрутизации делает то, что заложено в названии: превращает HTTP-подключения клиентов в запросы к процессам веб-приложений. NGINX утверждает, что один маршрутизатор Unit может обрабатывать тысячи одновременных подключений. Маршрутизатор работает так же, как веб-сервер NGINX, и имеет ряд рабочих потоков для приема, буферизации и парсинга входящих подключений.

Это одна из самых интересных частей NGINX Unit. Серверам приложений Ruby сложно работать с HTTP-соединениями без какого-нибудь обратного прокси-сервера перед сервером приложения. Например, Unicorn рекомендуется использовать только за обратным прокси-сервером, поскольку он не может буферизовать запросы, т.е. если клиент отправляет один байт запроса, а затем останавливается (из-за сетевых условий или плохого соединения сотовой связи), то процесс Unicorn прекращает работу и не продолжает до тех пор, пока запрос не завершит буферизацию. Использование NGINX перед Unicorn позволяет ему буферизовать запрос до того, как он достигнет Unicorn. Поскольку NGINX написан в высоко оптимизированном C и не ограничен GVL Ruby, он может буферизовать сотни соединений для Unicorn. Passenger решает эту проблему, будучи дополнением для NGINX или Apache(mod_ruby!) и выгружая работы, связанные с подключением на веб-сервер.

Конфигурация приложения имеет ключ процесса. В этом ключе указывают минимальное и максимальное количество процессов.

{
"rails-new": {
"type": "ruby",
"processes": {
"spare": 5
"max": 10
},
"script": "/www/rails-new-app/config.ru"
}
}

По неизвестной причине «минимальное» количество процессов называется spare. В приведенном конфиге сразу запускается 5 процессов масштабируется до 10, если этого требует загрузка.
Пока ничего неизвестно о доступности таких настроек как Puma’s preload_app! и аналогов в Passenger и Unicorn, так что можете запускать процессы заранее и пользоваться преимуществом копирования при записи (cow memory).

Нам осталось обсудить процессы приложений. Что здесь есть интересного и нового? Маршрутизатор не взаимодействует с процессами приложений через HTTP — он использует сокеты Unix и разделяемую память. Эта оптимизация нацелена на микросервисную архитектуру, поскольку связь между сервисами на одной машине будет значительно быстрее без HTTP-прослойки. Однако я еще не видел примеры кода Ruby, где это реализовано.

Неясно, предназначено ли оно для запуска NGINX перед NGINX Unit и может ли NGINX Unit работать самостоятельно. Сейчас (1 квартал 2018 года) вы, вероятно, запускаете NGINX перед NGINX Unit в качестве обратного прокси-сервера, потому что NGINX Unit нет предоставления статических файлов, HTTPS (TLS) и HTTP / 2. Очевидно, это бесшовная интеграция.

NGINX Unit приближается к стабильной версии 1.0. Пока его рано запускать на продакшн для приложений Ruby. На момент написания статьи модулю Ruby исполнилось 5 дней. Он все еще на стадии активной разработки: новые версии выходят каждые несколько недель. На очереди крупные нововведения, связанные с TLS и HTTP, а там дело дойдет и до предоставления статических файлов. Идет дискуссия о поддержке Java, и как вариант — поддержке JRuby и TruffleRuby.

Поддержки Windows нет, и не то, чтобы я ждал ее, затаив дыхание. NGINX Unit поддерживает только Ruby 2.0 и выше.

Я не буду тестировать производительность NGINX Unit в этом посте. Модуль Ruby только появился, замерять производительность рано. Однако реальная причина, по которой я не буду сравнивать NGINX Unit с Puma, Unicorn или Passenger, заключается в том, что выбор сервера приложений в Ruby — это вопрос не скорости (технически — латентности), а пропускной способности. Мы различаем серверы приложений по количеству обслуживаемых запросов, а не по скорости обработки. Серверы приложений имеют латентность в считанные миллисекунды.

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

В настоящее время единственным бесплатным сервером приложений, который запускает веб-приложения Ruby в нескольких потоках, является Puma. В Passenger Enterprise нужно платить за лицензию.

NGINX Unit планирует поддержку нескольких потоков в приложениях Python, поэтому не исключено, что он будет поддерживать приложения Ruby в нескольких потоках в будущем.

Итак, где NGINX Unit сейчас уступает Unicorn, Passenger и Puma? Я думаю, при традиционной настройке Rails-приложений: одно монолитное приложение, работающее на провайдере платформа-как-сервис (например, Heroku), вообще не получит никакой пользы от текущих возможностей и запланированных нововведений NGNX Unit. Puma и так хороша для подобных задач.

NGINX Unit может быть интересен пользователям Unicorn, которые хотят отказаться от обратного прокси. Когда NGINX Unit будет отполирован, он заменит связку Unicorn / NGINX единственным сервером Unit NGNX.

NGINX Unit больше всего похож на Phusion Passenger, который перешел на «микросервисы», поддерживая Javascript, Python и приложения Ruby. NGINX Unit сейчас поддерживает больше языков и в будущем их число еще увеличится, поэтому на него переходят те, кому это важно. Зато Phusion сосредоточены на Ruby, поэтому Passenger всегда будет поддерживать Ruby лучше, полноценнее, чем NGINX Unit. Как я уже упоминал, Phusion Passenger Enterprise уже поддерживает многопоточность.

Где же NGINX Unit идеален? Если вы используете собственное облако (собственное, а не на сервисе, управляющем маршрутизацией, например Heroku), и у вас есть приложения Ruby, работающие на разных версиях языка, или много сервисов на разных языках, и эти сервисы/приложения нуждаются в быстром сообщении друг с другом, то NGINX Unit разработан для вас. В противном случае лучше придерживаться трех лучших вариантов (Puma, Passenger и Unicorn).

Оригинал: A New Ruby Application Server: NGINX Unit