Lua в Nginx: динамическая маршрутизация запросов

Привет, Хабр!
Сегодня рассмотрим то, как использовать Lua в Nginx: динамическую маршрутизацию, балансировку трафика, подмену заголовков и трансформацию тела запроса в реальном времени. OpenResty и lua‑nginx‑module позволяют перенести часть логики на уровень веб‑сервера, сокращая задержки и повышая гибкость.
Динамическое изменение upstream через lua-resty-balancer
Одной из самых крутых фич, за которую я люблю OpenResty, является возможность изменять upstream в рантайме. Вместо того чтобы перезагружать сервер при смене конфигурации, просто обновляем значение в lua_shared_dict — и всё работает.
Пример конфигурации:
http {
lua_shared_dict balancer_cache 10m;
upstream dynamic_upstream {
server 127.0.0.1:8080; # Резервный сервер по умолчанию
}
server {
listen 80;
server_name example.com;
location / {
access_by_lua_block {
local balancer_cache = ngx.shared.balancer_cache
local new_upstream = balancer_cache:get("current_upstream")
if new_upstream then
ngx.var.upstream = new_upstream
else
ngx.var.upstream = "dynamic_upstream"
end
ngx.log(ngx.INFO, "Используем upstream: ", ngx.var.upstream)
}
proxy_pass http://$upstream;
}
}
}
Храним текущий upstream в lua_shared_dict и можем динамически менять его, например, через REST API или cron‑задачу. Так даже можно реализовывать failover‑механизмы, A/B‑тесты и какие‑нибудь схемы балансировки. Но, как говорится, с большой силой приходит большая ответственность: всегда добавляйте обработку ошибок и логирование.
Подмена заголовков и изменение тела запроса/ответа на ходу
Еще есть возможность изменять заголовки и тело запросов/ответов в реальном времени.
Пример изменения заголовков:
http {
lua_shared_dict balancer_cache 10m;
upstream dynamic_upstream {
server 127.0.0.1:8080; # Резервный сервер по умолчанию
}
server {
listen 80;
server_name example.com;
location / {
access_by_lua_block {
local balancer_cache = ngx.shared.balancer_cache
local new_upstream = balancer_cache:get("current_upstream")
if new_upstream then
ngx.var.upstream = new_upstream
else
ngx.var.upstream = "dynamic_upstream"
end
ngx.log(ngx.INFO, "Используем upstream: ", ngx.var.upstream)
}
proxy_pass http://$upstream;
}
}
}
Подменяем заголовок User‑Agent. Но на этом возможности не заканчиваются: можно динамически добавлять, удалять или модифицировать любые заголовки, в зависимости от бизнес‑логики. А теперь пример изменения тела ответа:
server {
listen 80;
server_name example.com;
location /modify_body {
proxy_pass http://backend_service;
header_filter_by_lua_block {
ngx.header["Content-Type"] = "application/json"
}
body_filter_by_lua_block {
local chunk = ngx.arg[1]
if chunk then
-- Пример: добавляем поле "extra": "value" в JSON-ответ.
chunk = string.gsub(chunk, "}", ', "extra": "value"}')
end
ngx.arg[1] = chunk
}
}
}
Приправляем JSON‑ответ, добавляя новое поле. Конечно, тут еще потребуется более аккуратное парсирование JSON и проверка корректности данных. Но главное здесь — понять принцип: можно менять данные на ходу, без вмешательства в бек.
Nginx как API Gateway с динамической маршрутизацией
Используем lua_shared_dict
для хранения динамических правил маршрутизации, что позволяет адаптироваться к изменяющимся условиям — будь то A/B‑тестирование, экстренное переключение на резервный сервер или плавное обновление конфигурации.
http {
lua_shared_dict route_config 1m;
server {
listen 80;
server_name api.example.com;
location / {
access_by_lua_block {
local route_config = ngx.shared.route_config
local uri = ngx.var.uri
local route = route_config:get(uri)
if route then
ngx.var.target = route
else
ngx.var.target = "default_backend"
ngx.log(ngx.WARN, "Маршрут для URI ", uri, " не найден, используем default_backend")
end
ngx.log(ngx.INFO, "Маршрутизируем URI: ", uri, " к ", ngx.var.target)
}
proxy_pass http://$target;
}
}
}
lua_shared_dict route_config
позволяет хранить и изменять правила маршрутизации без перезагрузки Nginx, что по сути база для обновлений через API или по расписанию. В access_by_lua_block
определяем маршрут на основе URI, а при его отсутствии автоматом используем default_backend
, тем самым делая некую отказоустойчивость.
Redis+Lua: управление балансировкой трафик
Когда Nginx должен быстро адаптироваться к изменяющимся условиям, логично вынести логику маршрутизации в хранилище с мгновенным доступом. Redis + Lua — мощный дуэт, позволяющий динамически управлять трафиком без перезагрузки сервера.
Пример интеграции Redis с Nginx:
http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_shared_dict redis_cache 10m;
server {
listen 80;
server_name api.example.com;
location /traffic {
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000) -- Таймаут 1 секунда
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Ошибка подключения к Redis: ", err)
return ngx.exit(500)
end
local route, err = red:get("route:" .. ngx.var.uri)
ngx.var.backend = (route and route ~= ngx.null) and route or "default_backend"
ngx.log(ngx.INFO, "Выбран backend: ", ngx.var.backend)
-- Возвращаем соединение в пул
local ok, err = red:set_keepalive(10000, 100)
if not ok then
ngx.log(ngx.ERR, "Ошибка возврата соединения в пул: ", err)
end
}
proxy_pass http://$backend;
}
}
}
Nginx перед каждым запросом обращается к Redis по ключу route:/traffic
для выбора целевого backend, что позволяет централизованно управлять балансировкой и оперативно перенаправлять нагрузку; при этом высокая производительность достигается за счёт минимальных задержек Redis и использования lua_shared_dict
для кеширования, а пул соединений, настроенный через set_keepalive
, экономит ресурсы, возвращая неиспользованные TCP‑соединения (до 100 соединений с таймаутом 10 секунд) вместо создания нового для каждого запроса.
Если у вас возникнут вопросы или захотите поделиться своими фишками — пишите в комментариях.
Больше актуальных навыков по IT‑инфраструктуре вы можете получить в рамках практических онлайн‑курсов от экспертов отрасли. В каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.