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

ee6c9c4eebad4d54541a9e78c681a6d8.jpg

Привет, Хабр!

Сегодня рассмотрим то, как использовать 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‑инфраструктуре вы можете получить в рамках практических онлайн‑курсов от экспертов отрасли. В каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.

© Habrahabr.ru