Nginx + Lua, гибкая балансировка нагрузки с сохранением сессии

от автора

При балансировке нагрузки важный вопрос — сохранение сессии клиента. Особенно, если за балансировщиком стоит какой-то интерактивный backend. И тем более, если захотелось сделать A/B тестирование и гибко регулировать порции клиентов к различному содержанию. "Nginx plus" предлагает такие возможности, но что делать, если хочется дёшево и быстро?

На помощь приходит возможность расширить функционал Nginx с помощью Lua.

Алгоритм прост. При первом запросе клиента, выставляем ему cookie, а при последующих в зависимости от значения, отправляем к конкретному бэкенду. Сами же куки распределяем подходящим алгоритмом с анализом нужных параметров.
В качестве мощного nginx-комбайна можно использовать сборку OpenResty, но для наших нужд это избыточно, потому соберём только нужный функционал на базе nginx 1.10.3 из репозитория.

Подопытным у нас будет:

Debian jessie 4.9.0-0.bpo.1-amd64 Nginx 1.10.3 (nginx.org) libluajit-5.1-2

Необходимые компоненты сборки:

ngx_devel_kit-0.3.0 lua-nginx-module-0.10.8 lua-resty-core-0.1.11 lua-resty-lrucache-0.06

Устанавливаем пакеты для сборки deb-пакета:

# cd /usr/src/ # aptitude install quilt debhelper libluajit-5.1-dev libluajit-5.1-2 # apt-get -t jessie source nginx

Последняя команда скачивает исходные коды nginx-а, из настроенного репозитория. Мы используем nginx: пакеты для Linux.

Скачиваем и распаковываем текущие версии исходных кодов модулей: ngx_devel_kit и lua-nginx-module

# wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz # wget https://github.com/openresty/lua-nginx-module/archive/v0.10.8.tar.gz # tar -xf v0.3.0.tar.gz # tar -xf v0.10.8.tar.gz

Первый модуль необходим для сборки желанного второго.

Правим файл правил сборки deb-пакета по адресу nginx-1.10.3/debian/rules, добавив в список параметров секции config.status.nginx: config.env.nginx:

--add-module=/usr/src/ngx_devel_kit-0.3.0 --add-module=/usr/src/lua-nginx-module-0.10.8

Собираем и устанавливаем получившийся пакет:

# cd nginx-1.10.3 && dpkg-buildpackage -us -uc -b && cd ../ # dpkg -i nginx_1.10.3-1~jessie_amd64.deb # aptitude hold nginx

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

Кроме этого, нам потребуется ещё две lua-библиотеки из проекта OpenResty, предоставляющих Nginx API for Lua: lua-resty-core и lua-resty-lrucache. Они из себя представляют набор *.lua файлов, устанавливаемых (по умолчанию) по пути /usr/local/lib/lua/.

# wget https://github.com/openresty/lua-resty-core/archive/v0.1.11.tar.gz # wget https://github.com/openresty/lua-resty-lrucache/archive/v0.06.tar.gz # tar -xf v0.1.11.tar.gz  # tar -xf v0.06.tar.gz # cd lua-resty-core-0.1.11 && make install && cd ../ # cd lua-resty-lrucache-0.06 && make install && cd ../

Подготовительная часть завершена, приступаем к настройке nginx-а. Приведу упрощённую конфигурацию с комментариями происходящего.

В нашем случае требовалось реализовать только варианты контента, потому и балансировщик и бэкенд будут на одном сервере и upstream будет указывать на локальные адреса с портами 800x.
Но гибкость реализации позволяют построить любые желаемые конфигурации. Итак по порядку.

В блоке http {} инициализируем lua.

код с комментариями:

# путь до локально установленных *.lua библиотек с добавлением системных путей lua_package_path "/usr/local/lib/lua/?.lua;;"; init_by_lua_block {     -- подключение основного модуля     -- в принципе, этот блок можно опустить     require "resty.core"     collectgarbage("collect")  -- just to collect any garbage }

в блоках *_lua_block уже идёт lua-код со своим синтаксисом и функциями.

Основной сервер, который принимает на себя внешние запросы.

код с комментариями:

server {     listen 80;     server_name test.domain.local;    location / {     # проверяем наличие cookie "upid" и если нет — выставляем по желаемому алгоритму     if ($cookie_upid = "") {             # инициализируем пустую переменную nginx-а, в которую запишем выбранный ID бэкенда             set $upstream_id '';             rewrite_by_lua_block {                 -- инициализируем математический генератор для более рандомного рандома используя время nginx-а                 math.randomseed(ngx.time())                 -- также пропускаем первое значение, которое совсем не рандомное (см документацию)                 math.random(100)                 local num = math.random(100)                 -- получив число, бесхитростно и в лоб реализуем веса 20% / 80%                 if num > 20 then                     ngx.var.upstream_id = 1                     ngx.ctx.upid = ngx.var.upstream_id                 else                     ngx.var.upstream_id = 2                     ngx.ctx.upid = ngx.var.upstream_id                 end                 -- ID запоминаем в переменной nginx-а "upstream_id" и в "upid" таблицы ngx.ctx модуля lua, которая используется для хранения значений в рамках одного запроса              }     # отдаём клиенту куку "upid" со значением выбранного ID     # время жизни явно не задаём, потому она будет действительна только на одну сессию (до закрытия браузера), что нас устраивает     add_header Set-Cookie "upid=$upstream_id; Domain=$host; Path=/";     }      # если же кука у клиента уже есть, то запоминаем ID в ngx.ctx.upid текущего запроса     if ($cookie_upid != "") {         rewrite_by_lua_block {             ngx.ctx.upid = ngx.var.cookie_upid         }     }      # передаём обработку запроса на блок upstream-ов     proxy_pass http://ab_test;   } }

Блок upstream, который используя lua заменяет встроенную логику nginx.

код с комментариями:

upstream ab_test {   # заглушка, чтобы nginx не ругался. В алгоритме не участвует   server 127.0.0.1:8001;      balancer_by_lua_block {         local balancer = require "ngx.balancer"          -- инициализируем локальные переменные         -- port выбираем динамически, в зависимости от запомненного ID бэкенда         local host = "127.0.0.1"         local port = 8000 + ngx.ctx.upid          -- задаём выбранный upstream и обрабатываем код возврата         local ok, err = balancer.set_current_peer(host, port)             if not ok then                 ngx.log(ngx.ERR, "failed to set the current peer: ", err)                 return ngx.exit(500)             end         -- в общем случае надо, конечно же, искать доступный бэкенд, но нам не к чему     } }

Ну и простой демонстрационный бэкенд, на который в итоге придут клиенты.

код без комментариев:

server {   listen        127.0.0.1:8001;   server_name   test.domain.local;    location / {     root                /var/www/html;     index               index.html;   } }  server {   listen        127.0.0.1:8002;   server_name   test.domain.local;    location / {     root                /var/www/html;     index               index2.html;   } }

При запуске nginx-a с этой конфигурацией в логи свалится предупреждение:

use of lua-resty-core with LuaJIT 2.0 is not recommended; use LuaJIT 2.1+ instead while connecting to upstream

которое можно убрать собрав и установив требуемую версию. Но и на 2.0 (libluajit-5.1-2) работает.
Теперь, используя браузер с инструментами разработчика, можем проверять работу сервера и выставляемые куки.

Таким образом мы получили необходимую для тестирования и статистики гибкость. И необходимое для правильной работы бэкенда сохранение сессии клиента. Ну и просто интересный опыт.

PS Подобные задачи можно решить и другими методами, например используя haproxy, который позволяет балансировать с учётом сессий. Или для разделения клиентов использовать ngx_http_split_clients_module и с помощью map сопоставлять одни значения в зависимости от других.
Но приведённый вариант распределения клиентов и выбора бэкенда позволяет гибче настраивать систему. И при необходимости, добавлять разнообразную логику в работу. При этом не перестраивая текущую систему.

Спасибо за внимание.

ссылка на оригинал статьи https://habrahabr.ru/post/326486/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *