Работая над несколькими open-source проектами, в один прекрасный день я решил упростить себе жизнь и разработал Upstream-модуль для nginx, который помог мне убрать громоздкие слои многослойной архитектуры. Это был забавный опыт, которым я хочу поделиться в этой статье. Мой код лежит в открытом доступе тут: github.com/tarantool/nginx_upstream_module. Его можно поднять с нуля или скачать Docker-образ по этой ссылке: hub.docker.com/r/tarantool/tarantool-nginx.
На повестке дня:
- Введение и теория.
- Как использовать эти технологии.
- Оценка производительности.
- Полезные ссылки.
Введение и теория
Вот так выглядит стандартная архитектура микросервисов. Запросы пользователей поступают через nginx на сервер приложений. На сервере есть бизнес-логика, с которой взаимодействуют пользователи.
Сервер приложений не хранит состояния объектов, поэтому их нужно хранить где-то еще. Для этого можно использовать базы данных. И не забывайте про кэш, который уменьшит задержку и обеспечит более быструю доставку контента.
Разобьем это на слои:
1-й слой — nginx.
2-й слой — сервер приложений.
3-й слой — кэш.
4-й слой — прокси базы данных. Этот прокси необходим для обеспечения отказоустойчивости и поддержания постоянного подключения к базе данных.
5-й слой — сервер базы данных.
Раздумывая об этих слоях, я придумал, как исключить некоторые из них. Зачем? Много причин. Мне нравятся простые понятные вещи; я не люблю поддерживать большое количество разных систем в prodution; и последнее, но не по значимости — чем меньше слоев, тем меньше точек отказа. В результате я сделал модуль Tarantool Upstream под nginx, который помог сократить количество слоёв до двух.
Как Tarantool помогает уменьшить количество слоёв? Первый слой — nginx, второй, третий и пятый слои заменяет Tarantool. Четвертый слой — прокси базы данных — теперь в nginx. Фишка в том, что Tarantool — это база данных, кэш и сервер приложений, три в одном. Мой upstream-модуль связывает nginx и Tarantool друг с другом и позволяет им слаженно работать без других трёх слоёв.
Вот так выглядит новый микросервис. Пользователь отправляет запрос к REST или JSON RPC в nginx с модулем Tarantool Upstream. Модуль может быть подключен напрямую к Tarantool, или нагрузка может быть сбалансирована на несколько серверов Tarantool. Между nginx и Tarantool мы используем эффективный протокол, в основе которого лежит MSGPack. Дополнительную информацию вы найдете в этой статье.
Еще можете перейти по этим ссылкам, чтобы загрузить Tarantool и модуль nginx. Но я бы посоветовал установить их через пакетный менеджер вашего дистрибутива или воспользоваться Docker-образом (
docker pull tarantool/tarantool-nginx
). Docker-образы: hub.docker.com/r/tarantool/tarantool
Tarantool NginX upstream module
Бинарные пакеты: Tarantool — Download
Исходный код: Tarantool
tarantool/nginx_upstream_module
Как использовать эти технологии
Вот пример файла nginx.conf. Как видите, это обычный upstream nginx. Тут у нас есть
tnt_pass
, прямо указывающий nginx, по какому пути расположить upstream tarantool.nginx-tnt.conf
http {
# upstream
upstream tnt {
server 127.0.0.1:3301;
keepalive 1000;
}
server {
listen 8081;
# gateway
location /api/do {
tnt_pass_http_request parse_args;
tnt_pass tnt;
}
}
}
Вот ссылки на документацию:
nginx.org/en/docs/http/ngx_http_upstream_module.html
github.com/tarantool/nginx_upstream_module/blob/master/README.md
Сконфигурировали связку nginx и Tarantool, что потом? Теперь нужно прописать функцию обработчика для нашего сервиса и разместить ее в файле. Я положил её в файл “app.lua”.
Вот ссылка на документацию Tarantool: tarantool.io/ru/doc/1.9/book/box/data_model/#index
-- Bootstrap Tarantool
box.cfg { listen='*:3301' }
-- Grants
box.once('grants', function()
box.schema.user.grant('guest', 'read,write,execute', 'universe')
end)
-- Global variable
hello_str = 'Hello'
-- function
function api(http_request)
local str = hello_str
if http_request.method == 'GET' then
str = 'Goodbye'
end
return 'first', 2, { str .. 'world!' }, http_request.args
end
Теперь рассмотрим код Lua.
Наш
Box.cfg {}
говорит Tarantool начать слушать порт 3301, но он может принимать и другие параметры.Box.once
говорит Tarantool вызывать какую-то функцию один раз.function api ()
— это функция, которую я скоро буду вызывать. Она принимает HTTP-запрос в качестве первого аргумента и возвращает массив значений.Я сохранил этот код в файл и назвал его “app.lua”. Выполнить его можно, просто запустив Tarantool-приложение.
$> tarantool app.lua
Вызовем нашу функцию с помощью GET-запроса. Я для этого использую “wget”. По умолчанию, “wget” сохраняет ответ в файл. И для чтения данных из файла я использую “cat”.
$ wget '0.0.0.0:8081/api/do?arg_1=1&arg_2=2'
$ cat do*
{ “id”:0, # — unique identifier of the request
“result”: [ # — is what our Tarantool function returns
[“first”], [2], [{
“request”:{“arg_2”:”2",”arg_1":”1"}
“1”:”Goodbye world!”
}]
]}
Оценка производительности
Оценка проводилась на данных из production. Входные данные — это большой JSON-объект. Средний размер такого объекта 2 Кб. Один сервер, 4-ядерный CPU, 90 Гб RAM, OS Ubuntu 14.04.1 LTS.
Для этого теста мы используем только один nginx worker. Этот worker — балансировщик с простым алгоритмом ROUND-ROBIN. Он балансирует нагрузку между двумя узлами Tarantool. Нагрузка масштабируется с помощью шардинга.
Эти графики показывают количество операций чтения в секунду. Верхний график показывает задержки (в миллисекундах).
А эти графики показывают количество операций записи в секунду. Верхний график показывает задержки (в миллисекундах)
Впечатляюще!
В следующей статье я подробно расскажу про REST и JSON RPC.
Англоязычная версия статьи: hackernoon.com/shrink-the-number-of-tiers-in-a-multitier-architecture-from-5-to-2-c59b7bf46c86