OpenResty и Lua: логика прямо в Nginx
OpenResty позволяет вынести проверку запросов, простой rate limit и маршрутизацию в Lua прямо внутри Nginx. Разбираем, где это быстрее отдельного сервиса, какие фазы использовать и почему shared dict не заменяет Redis.

Когда Nginx должен проверить заголовок, ограничить частоту запросов или выбрать ветку обработки, отдельный сервис ради нескольких условий добавляет задержку и точку отказа. OpenResty выполняет такую логику внутри Nginx worker-процесса: в примере ниже запросы без идентификатора получают 401, а слишком активные клиенты — 429. Конфигурация рассчитана на OpenResty 1.25.3.2 и занимает один location.
Что именно даёт OpenResty
OpenResty — это сборка Nginx с LuaJIT и модулем lua-nginx-module. Lua-код подключается к фазам обработки запроса: проверке доступа, формированию ответа, фильтрации заголовков или записи лога.
Ключевое отличие от обычного CGI-скрипта или отдельного приложения — Lua не запускается как новый процесс на каждый запрос. Код работает внутри worker-процесса Nginx, поэтому простые проверки не требуют сетевого перехода к backend-сервису.
1 Фаза запроса — этап обработки Nginx, например проверка доступа до проксирования или формирование тела ответа. Директива Lua должна находиться в подходящем контексте: код в access_by_lua_block не предназначен для генерации полноценной страницы.
Lua в OpenResty хорош не как замена приложению, а как короткий слой принятия решений рядом с входящим HTTP-запросом.
Три места для логики
| Место | Что делать | Цена решения |
|---|---|---|
| Конфигурация Nginx | Простые условия, проксирование, лимиты, кеширование | Меньше кода и накладных расходов, но сложная логика быстро становится нечитаемой |
access_by_lua_block | Проверка заголовков, ACL, rate limit, подготовка переменных | Удобная логика до upstream, но ошибки Lua блокируют запрос |
content_by_lua_block | Небольшой HTTP-ответ без отдельного приложения | Не подходит для бизнес-логики с базой данных и длинными операциями |
| Отдельный сервис | Сложные правила, транзакции, работа с несколькими хранилищами | Сетевые задержки, деплой и дополнительная точка отказа |
Практическое правило простое: если решение использует только запрос, несколько переменных Nginx и быстрый локальный словарь, его можно рассмотреть для OpenResty. Если нужны транзакции, очередь, сложная авторизация или долгий внешний вызов, оставляйте задачу приложению.
Пример: проверка клиента и фиксированное ограничение
Ниже — минимальный вариант для внутреннего endpoint. Клиент передаёт заголовок X-Client-ID. Для каждого значения OpenResty считает запросы в текущем минутном окне. Лимит намеренно фиксированный, а не скользящий: это проще проверить, но на границе минуты клиент может сделать два всплеска подряд.
Сначала объявите общее хранилище в блоке http конфигурации Nginx:
lua_shared_dict request_limits 10m;
Затем добавьте обработчик в нужный server или location:
location = /internal/check {
access_by_lua_block {
local headers = ngx.req.get_headers()
local client_id = headers["x-client-id"]
if not client_id or client_id == "" then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say("missing X-Client-ID")
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
local minute = math.floor(ngx.now() / 60)
local key = client_id .. ":" .. minute
local limits = ngx.shared.request_limits
local count, err = limits:incr(key, 1, 0, 120)
if not count then
ngx.log(ngx.ERR, "rate counter failed: ", err or "unknown error")
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
if count > 60 then
ngx.header["Retry-After"] = "60"
return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
end
}
content_by_lua_block {
ngx.header.content_type = "application/json"
ngx.say('{"status":"ok"}')
}
}
После изменения конфигурации проверьте её штатной командой nginx -t, затем перечитайте конфигурацию способом, принятым в вашей установке OpenResty. В запросе с заголовком X-Client-ID: demo-client должен вернуться JSON со статусом 200. Без заголовка будет 401, после 60 запросов в одном минутном окне — 429.
Значение lua_shared_dict доступно всем worker-процессам одного экземпляра Nginx. Это важно: обычная Lua-таблица была бы отдельной у каждого worker и не дала бы общего счётчика.
Почему очевидные решения дают сбои
Счётчик в обычной Lua-таблице
Глобальная таблица Lua живёт только в памяти конкретного worker-процесса. Запросы распределяются между workers, поэтому пользователь увидит непредсказуемый лимит. Для обмена данными между workers нужен ngx.shared.DICT, а для долговечного состояния — внешнее хранилище.
Синхронный запрос к Redis или HTTP-сервису
Сетевой вызов внутри обработки запроса увеличивает задержку и связывает доступность входного слоя с доступностью внешней системы. Для редких административных проверок это может быть оправдано, но для каждого запроса нужны таймаут, ограничение размера ответа и понятное поведение при ошибке.
Длинная бизнес-логика в content-фазе
Lua-код в OpenResty не превращает Nginx в универсальный backend. Большие модули, SQL-транзакции и сложные правила труднее тестировать и обновлять рядом с конфигурацией прокси.
Неправильная модель лимита
Пример использует фиксированное минутное окно. Оно подходит для грубой защиты endpoint, но не гарантирует равномерную скорость: 60 запросов в конце одной минуты и ещё 60 в начале следующей пройдут почти подряд. Для точного token bucket или распределённого лимита нужна отдельная реализация и измерения, а не простая замена числа в примере.
Когда выбирать какой подход
- Нативный Nginx — когда правило описывается несколькими директивами: проксирование, базовый лимит, кеш или проверка маршрута.
- Lua в access-фазе — когда решение зависит от нескольких заголовков, URI и небольшого общего счётчика.
- Lua в content-фазе — когда нужен короткий ответ вроде health-check, служебного JSON или заглушки без отдельного upstream.
- Отдельное приложение — когда появились доменные правила, база данных, роли, транзакции или необходимость масштабировать компонент независимо от Nginx.
Считайте не только среднюю задержку. Для продакшена важнее p95 и p99, доля ответов 4xx/5xx, заполнение shared dict и число срабатываний лимита. Замеры делайте на вашей конфигурации, с реальным размером заголовков и тем же числом worker-процессов. Универсальных цифр ускорения здесь нет.
Где подход ломается
- Переполнение shared dict. При большом числе уникальных ключей новые записи могут не помещаться. Следите за ошибками операций со словарём и не используйте необрезанный пользовательский ввод как ключ.
- Разные экземпляры OpenResty. У двух машин свои shared dict. Лимит будет локальным, если перед ними нет общего механизма координации.
- Перезапуск worker или Nginx. Память shared dict не является постоянным хранилищем. Счётчики и временные решения исчезнут после перезапуска.
- Доверие к заголовку. Клиент может сам прислать любой
X-Client-ID. Такой заголовок годится для внутреннего доверенного контура или предварительной идентификации, но не заменяет аутентификацию. - Отладка в проде. Ошибки Lua попадают в error log, а не обязательно в тело ответа. Логируйте причину отказа без токенов, персональных данных и полного содержимого заголовков.
Что сделать дальше
Начните с одного endpoint и включите наблюдение за четырьмя показателями: p95 задержки, доля 401 и 429, ошибки операций с shared dict и число ключей в словаре. Затем нагрузочным тестом проверьте границу окна и поведение после перезапуска. Если правило стало зависеть от внешнего состояния или нескольких сервисов, перенесите его из Lua в приложение, оставив OpenResty для маршрутизации и быстрых проверок.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: lua-nginx-module: официальная документация OpenResty, Официальный сайт OpenResty
Частые вопросы
Какая версия OpenResty нужна для этого примера?
Можно ли хранить счётчик в обычной переменной Lua?
Сохранится ли shared dict после перезапуска Nginx?
Можно ли считать такой rate limit защитой от злоупотреблений?
Что делать, если запрос всегда получает 500?
Можно ли обращаться из Lua к Redis или базе данных?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.