Lua API: ngx.*, shared dict, cosocket и lua-resty-core

Рейтинг: 70.1% · 9 голосов
Самый подробный курс по Nginx - от первого конфига до production API-gateway. Архитектура и event loop, контексты и location, виртуальные хосты, статика, reverse proxy и upstream-балансировка, кэширование, HTTPS и TLS (post-quantum), HTTP/2 и HTTP/3 QUIC, безопасность и rate limiting, realip, JSON-логи и сквозная трассировка, метрики, тюнинг и траблшутинг. Плюс OpenResty (Lua, фазы, JWT, динамика) и Angie (нативные Prometheus, ACME, dynamic upstream). На основе эталонного gateway и лучших практик. Для новичков и SRE/DevOps. Актуально на 2026 (nginx 1.30/1.31, Angie, ingress-nginx EOL -> Gateway API).
Ответить
Аватара пользователя
Igor_NgINX
Сообщения: 44
Зарегистрирован: 11 май 2026, 05:31

Lua API: ngx.*, shared dict, cosocket и lua-resty-core

Сообщение Igor_NgINX »

Оглавление курса (44)
  1. Что такое Nginx и почему он захватил веб
  2. Установка Nginx, Angie и OpenResty: пакеты, версии, Docker
  3. Первый конфиг с нуля: минимальный сервер за 5 минут
  4. Архитектура процессов: master, worker и event loop
  5. Анатомия конфигурации: контексты, наследование и модульность
  6. Виртуальные хосты: server и server_name
  7. Директива location: матчинг и приоритеты по шагам
  8. Раздача статики: root, alias, try_files, sendfile
  9. Переменные, map и блок if
  10. rewrite, return и канонизация URL
  11. Работа с заголовками: add_header и proxy_set_header
  12. Модель доверия прокси-заголовков: realip и защита от спуфинга
  13. Сжатие: gzip, brotli и zstd
  14. Reverse proxy: proxy_pass и проброс запроса
  15. upstream и балансировка нагрузки
  16. Таймауты, повторы и устойчивость прокси
  17. FastCGI и PHP: nginx + PHP-FPM (и uwsgi/scgi)
  18. WebSocket, gRPC и стриминг через Nginx
  19. Кэширование ответов: proxy_cache и микрокэш
  20. HTTPS и TLS: сертификаты, протоколы, шифры (2026)
  21. Let's Encrypt и ACME: certbot, acme.sh, нативный ACME Angie
  22. HTTP/2 и HTTP/3 (QUIC) в 2026
  23. Безопасность: заголовки, скрытие версии, ограничения
  24. Rate limiting и защита от перегрузки
  25. Контроль доступа: allow/deny, auth_basic, auth_request и mTLS
  26. Логирование: access_log, форматы и структурированный JSON
  27. Сквозная трассировка запросов и OpenTelemetry
  28. Метрики и мониторинг Nginx
  29. Производительность и тюнинг под нагрузку
  30. Траблшутинг: 502, 504, 403, 404 и debug-лог
  31. OpenResty: Nginx как платформа на LuaJIT
  32. Фазы обработки запроса и Lua-хуки
  33. Lua API: ngx.*, shared dict, cosocket и lua-resty-core (вы здесь)
  34. Практика OpenResty: авторизация, JWT, кэш, Redis
  35. Angie: современный форк Nginx 2026
  36. Динамическая конфигурация и service discovery
  37. Nginx как API-gateway и Kubernetes Gateway API
  38. Динамические модули, njs и WAF
  39. Stream-модуль: проксирование TCP и UDP
  40. Деплой, перезагрузка и конфиг как код
  41. Сценарий: статика, SPA и кэширование за CDN
  42. Сценарий: микросервисный gateway по образцу ngx-trace-gateway
  43. Капстоун: собираем production-gateway с нуля
  44. Чек-лист безопасности, CVE 2026 и аудит конфигурации
Ты уже умеешь вставлять Lua в конфиг через access_by_lua, content_by_lua, rewrite_by_lua. Но кусочек кода в фазе - это только дверь. За дверью лежит целый инструментарий: переменные nginx из Lua, чтение тела запроса и заголовков, разделяемая на всех воркеров память, неблокирующие сокеты к Redis и внешним сервисам, фоновые таймеры. Этот урок - про сам арсенал. Без него ты будешь писать Lua, который либо роняет воркер блокирующим вызовом, либо течёт соединениями к базе, либо хранит данные так, что они исчезают при каждом запросе. Разберём механику ngx.* по слоям: что это вообще такое под капотом, какой API даёт каждый слой, и где разложены грабли, на которые наступают все.

lua-resty-core: FFI-фундамент, на котором стоит весь nginx lua api

Начнём с того, что почти никто не замечает, пока оно просто работает. Когда ты пишешь ngx.var.uri или ngx.shared.cache:get(), ты вызываешь не магию, а конкретную реализацию. Исторически эти функции были написаны на C и торчали в Lua через классический Lua C API (lua_pushcfunction и подобное). Это работает, но каждый вызов через C API - это дорогой переход с пересборкой стека, который LuaJIT не может заинлайнить в свой JIT-компилятор. На горячем пути, где ngx.var.* дёргается миллионы раз в секунду, это заметно.

Библиотека lua-resty-core переписала весь этот API через FFI (Foreign Function Interface) LuaJIT. FFI позволяет звать сишные функции напрямую, и - что важнее - JIT-компилятор LuaJIT умеет такие вызовы инлайнить и оптимизировать как нативный код. Грубо говоря, lua-resty-core - это быстрая FFI-версия всего ngx.*, которая заменяет старую медленную C-API-версию.

Ключевой факт для 2026: начиная с OpenResty 1.15.8.1 библиотека resty.core загружается автоматически. Тебе не нужно ничего делать - ngx.* уже работает через FFI. В актуальной сборке (OpenResty 1.31.1.1 на базе nginx 1.31.1, июнь 2026) это давно дефолт. Но есть важный нюанс: ряд продвинутых API физически живёт ТОЛЬКО в lua-resty-core и без неё просто не существует. Это ngx.balancer (Lua-балансировка апстрима), ngx.ssl (динамические сертификаты по SNI), ngx.semaphore (синхронизация лёгких потоков). Если ты соберёшь голый lua-nginx-module без resty.core или отключишь её, эти модули выдадут ошибку загрузки.

Код: Выделить всё

-- В современном OpenResty это уже сделано за тебя.
-- Но если видишь старый код или работаешь с ручной сборкой -
-- именно так включали FFI-ядро явно:
require "resty.core"

-- Проверить, что ядро на месте и FFI-путь активен:
local ok, core = pcall(require, "resty.core")
if not ok then
    ngx.log(ngx.ERR, "resty.core not loaded: ", core)
end
Вывод простой: не борись с lua-resty-core, не пытайся её выключить ради мнимой экономии. Она и есть причина, по которой Lua в nginx быстрый, а не игрушечный. Если в логах при старте видишь жалобу вида "no support for ngx.balancer ... please rebuild" - почти всегда дело в том, что ядро не подгрузилось.

Изображение

Слой запроса и ответа: openresty ngx.var, ngx.req и вывод

Теперь сам API. Первое, что нужно - доступ к переменным nginx. Через openresty ngx.var ты читаешь и пишешь любые переменные текущего запроса: ngx.var.uri, ngx.var.remote_addr, ngx.var.http_user_agent, ngx.var.arg_id. Чтение - дёшево, но не бесплатно: каждое обращение лезет в механизм переменных nginx, поэтому в цикле кэшируй в локальную переменную. Запись доступна только для тех переменных, что объявлены через set в конфиге или встроены как изменяемые (например ngx.var.args).

Код: Выделить всё

-- читаем переменные nginx
local uri  = ngx.var.uri
local ip   = ngx.var.remote_addr
local ua   = ngx.var.http_user_agent  -- заголовок User-Agent
local id   = ngx.var.arg_id           -- параметр ?id=...

-- запись: переменная должна быть объявлена в конфиге через set $my_flag "";
ngx.var.my_flag = "blocked"
Группа openresty ngx.req - это всё про входящий запрос. Тут нужно понимать порядок: тело запроса по умолчанию nginx не читает в память, его надо явно затребовать.

Код: Выделить всё

local method  = ngx.req.get_method()            -- "GET", "POST", ...
local headers = ngx.req.get_headers()           -- таблица заголовков
local args    = ngx.req.get_uri_args()          -- ?a=1&b=2 -> { a="1", b="2" }

-- тело надо сначала прочитать, иначе get_body_data вернёт nil
ngx.req.read_body()
local body    = ngx.req.get_body_data()         -- сырое тело
local post    = ngx.req.get_post_args()         -- разбор form-urlencoded
Грабля номер один тут: ngx.req.get_body_data() вернёт nil, если ты не вызвал ngx.req.read_body(), ИЛИ если тело оказалось настолько большим, что nginx сбросил его во временный файл (по client_body_buffer_size). В последнем случае данные есть, но не в памяти - бери ngx.req.get_body_file() и читай файл сам. Не забудь, что get_uri_args и get_headers по умолчанию режут до 100 элементов - это защита от флуда, но если у тебя легитимно больше, передавай лимит явным аргументом.

Вывод ответа: ngx.say печатает с переводом строки, ngx.print без него, ngx.status задаёт код, ngx.header.NAME пишет заголовок ответа, ngx.exit завершает обработку с указанным кодом. ngx.log пишет в error_log с уровнем (ngx.ERR, ngx.WARN, ngx.INFO).

Код: Выделить всё

ngx.status = 403
ngx.header["Content-Type"] = "application/json"
ngx.header["X-Gate"] = "lua"
ngx.say('{"error":"forbidden"}')
ngx.log(ngx.WARN, "blocked ip=", ngx.var.remote_addr)
return ngx.exit(403)
Отдельно стоит ngx.location.capture - внутренний субзапрос к другой location того же сервера. Он неблокирующий и удобен, когда нужно дёрнуть внутренний эндпоинт (например, проверить токен у бэкенда через internal-локацию) и получить status, header, body. Важно: capture работает только с внутренними location, для внешних HTTP ходи через cosocket. И помни, что ngx.location.capture не работает в фазах вроде set_by_lua и в таймерах - там нет контекста запроса.

nginx lua_shared_dict: общая память на всех воркеров

Главная боль новичка в OpenResty: переменные в Lua живут в пределах одного запроса (или одного воркера для модульных upvalue), а воркеров у тебя несколько. Глобальная переменная в одном воркере не видна другому. Счётчик запросов, кэш, флаги rate-limit - всё это нужно разделять между всеми процессами. Для этого есть nginx lua_shared_dict - кусок разделяемой памяти, доступный всем воркерам одновременно, с блокировками под капотом.

Объявляется он в http-блоке, размер задаётся сразу и фиксированно:

Код: Выделить всё

http {
    lua_shared_dict counters 10m;
    lua_shared_dict mycache  50m;
}
Дальше из Lua работаешь через ngx.shared.ИМЯ. API богатый, и важно знать его весь, потому что половина багов - от незнания нужного метода:
  • get(key) - значение или nil. Возвращает ещё и flags.
  • set(key, val, exp?, flags?) - записать, опционально с TTL в секундах. Возвращает success, err, forcible.
  • safe_set(key, val, ...) - как set, но НЕ вытесняет чужие данные при нехватке памяти, а честно вернёт "no memory".
  • add / safe_add - записать, только если ключа ещё нет (атомарно).
  • replace - записать, только если ключ уже есть.
  • incr(key, n, init?, init_ttl?) - атомарный инкремент. Параметр init - вот что критично: без него incr по несуществующему ключу вернёт ошибку, а с init он создаст ключ со значением init и прибавит n. Это идиома для счётчиков.
  • get_stale(key) - вернёт значение, ДАЖЕ если у него истёк TTL (и третьим значением stale=true). Незаменимо для паттерна "отдать протухшее, пока обновляем в фоне".
  • delete, expire, ttl, flush_all, flush_expired - управление.
  • get_keys(max?) - список ключей (осторожно, дорого, не для горячего пути).
Что можно хранить - только примитивы: строки, числа, булевы значения. Нельзя положить таблицу, функцию, и уж тем более коннект к Redis или cosocket. Хочешь сложную структуру - сериализуй в строку (cjson.encode) и клади строку, а на чтении декодируй. Память фиксирована, и когда она кончается, set по умолчанию начинает ВЫТЕСНЯТЬ старые записи по LRU - и тот самый третий возвращаемый флаг forcible станет true, сигналя "я выкинул чужие данные, чтобы влезть". Сначала идёт первый принудительно удаляемый элемент независимо от срока, потом докидываются протухшие. Если forcible часто true - значит зона мала, увеличивай размер. Когда вытеснять нельзя (например, это критичный лимитер), используй safe_set/safe_add: они вернут "no memory" вместо порчи чужих ключей.

Код: Выделить всё

local counters = ngx.shared.counters

-- атомарный счётчик запросов на IP с авто-инициализацией
local key = "req:" .. ngx.var.remote_addr
local newval, err = counters:incr(key, 1, 0)   -- init=0, если ключа нет
if not newval then
    ngx.log(ngx.ERR, "incr failed: ", err)
end

-- запись с TTL и контролем вытеснения
local ok, err, forcible = counters:safe_set("flag:" .. key, "1", 60)
if forcible then
    ngx.log(ngx.WARN, "shared dict full, evicting; bump zone size")
end

-- паттерн "stale-while-revalidate" на shared dict
local val, flags, stale = ngx.shared.mycache:get_stale("k1")
if stale then
    -- отдаём протухшее немедленно, а обновление запускаем в фоне (см. ngx.timer)
end
openresty cosocket и грабли пула соединений

Когда из Lua нужно сходить наружу - в Redis, в HTTP-сервис, в MySQL - ты НЕ берёшь синхронную библиотеку. Любой блокирующий сетевой вызов заморозит весь воркер nginx целиком, а с ним сотни-тысячи параллельных запросов. Правильный путь - openresty cosocket: ngx.socket.tcp(). Это неблокирующий сокет, привязанный к событийному циклу nginx. Пока сокет ждёт ответа, воркер обслуживает другие запросы. На cosocket построены lua-resty-redis, lua-resty-http, lua-resty-mysql - все официальные клиенты.

Код: Выделить всё

local sock = ngx.socket.tcp()
sock:settimeouts(1000, 1000, 1000)   -- connect, send, read - в мс, всегда ставь!
local ok, err = sock:connect("10.0.0.5", 6379)
if not ok then
    ngx.log(ngx.ERR, "connect failed: ", err)
    return ngx.exit(502)
end
sock:send("PING\r\n")
local line, err = sock:receive("*l")
А теперь главная грабля всего урока, на которой горят даже опытные. Сам по себе connect открывает новое TCP-соединение каждый раз - это дорого (TCP-handshake, у Redis ещё и возможный AUTH). Чтобы переиспользовать соединение, после работы ты ОБЯЗАН вызвать set_keepalive вместо close. set_keepalive не закрывает сокет, а кладёт его в пул живых соединений для последующего connect.

Код: Выделить всё

-- max_idle_timeout=10с, pool_size=100 соединений НА КАЖДЫЙ воркер
local ok, err = sock:setkeepalive(10000, 100)
if not ok then
    ngx.log(ngx.ERR, "setkeepalive failed: ", err)
end
-- ВАЖНО: после setkeepalive сокет нельзя использовать - он отдан в пул!
Три железобетонных правила пула cosocket, которые надо вытатуировать:
  • set_keepalive ОБЯЗАТЕЛЕН. Забыл - и каждый запрос открывает новый коннект, база захлёбывается в TIME_WAIT, latency растёт. Это самая частая причина "почему мой gateway медленный".
  • Пул - per-worker, не глобальный. Размер pool_size применяется к КАЖДОМУ воркеру отдельно. Если у Redis потолок 1000 соединений, а воркеров 10 - ставь pool_size около 100 (1000/10), иначе под нагрузкой суммарно откроешь 10000 и положишь базу. Это арифметика, которую все забывают.
  • Коннект НЕЛЬЗЯ хранить на уровне модуля. Соблазн велик: завести local red в верхней части модуля и переиспользовать. Нельзя. Lua-объект сокета привязан к конкретному запросу/потоку; шарить его между запросами - это гонка (race), один поток получит ответ, предназначенный другому. Создавай сокет ВНУТРИ обработчика запроса, отдавай в пул через set_keepalive, на следующем запросе connect сам достанет готовое соединение из пула. Переиспользуется именно TCP-соединение в пуле, а не Lua-объект.
Ещё параметр connect - backlog (в опциях): он ограничивает очередь ожидающих, когда пул исчерпан. Превысили очередь - получишь "too many waiting connect operations" вместо бесконтрольного роста соединений. Полезно как предохранитель.

Фоновые задачи и смертельный грех блокирующего вызова

ngx.timer.at(delay, callback, ...) запускает функцию в фоне, вне контекста запроса. Это способ делать то, что нельзя делать в горячем пути ответа: дописать лог в БД, обновить протухший кэш (тот самый stale-while-revalidate), отправить метрику. Колбэк таймера получает первым аргументом premature (true, если воркер завершается - тогда сворачивайся быстро). В таймере НЕТ ngx.var и ngx.req запроса, но cosocket и shared dict доступны.

Код: Выделить всё

local function refresh(premature, key)
    if premature then return end
    local sock = ngx.socket.tcp()
    -- ... сходить в источник, обновить ngx.shared.mycache ...
end
local ok, err = ngx.timer.at(0, refresh, "k1")   -- 0 = запустить почти сразу
if not ok then ngx.log(ngx.ERR, "timer failed: ", err) end
И финальное предупреждение. Весь неблокирующий мир OpenResty держится на одном условии: ты НЕ зовёшь блокирующих функций. os.execute, io.read/io.open на сетевой/медленный путь, синхронные luasocket-библиотеки из обычного Lua, тяжёлый CPU-цикл без уступки - любой из них замораживает воркер целиком. Один блокирующий вызов в одном запросе останавливает обработку ВСЕХ запросов этого воркера, пока не вернётся. Симптом: периодические скачки latency, таймауты на ровном месте, "иногда подвисает". Правило: для сети - только cosocket и resty-клиенты; для файлов на горячем пути - лучше через ngx.location.capture к внутренней location или не делать вовсе; для тяжёлых вычислений - выносить или дробить.

Мини-лаба: счётчик в shared dict и запрос к Redis с пулом

Соберём руками маленький, но боевой кусок: считаем хиты в разделяемой памяти и подтягиваем значение из Redis через пул.
  • Шаг 1. В http-блок добавь зону: lua_shared_dict hits 5m;
  • Шаг 2. Подними локальный Redis (docker run -p 6379:6379 redis) - он будет апстримом для cosocket.
  • Шаг 3. Создай location /stat с content_by_lua_block (см. код ниже).
  • Шаг 4. Проверь: nginx -t, затем перезапусти и сделай несколько curl на /stat. Счётчик в ответе должен расти, а соединения к Redis - переиспользоваться (следи через redis-cli INFO clients: число connected_clients не должно расти линейно с запросами).
  • Шаг 5. Эксперимент: закомментируй setkeepalive и под нагрузкой (ab или wrk) посмотри, как растёт connected_clients и latency. Верни обратно - и сравни. Это нагляднее любой теории.

Код: Выделить всё

location /stat {
    content_by_lua_block {
        -- 1) счётчик в разделяемой памяти
        local hits = ngx.shared.hits
        local n = hits:incr("total", 1, 0)   -- init=0 - ключ создастся сам

        -- 2) поход в Redis через cosocket с пулом
        local sock = ngx.socket.tcp()
        sock:settimeouts(500, 500, 500)
        local ok, err = sock:connect("127.0.0.1", 6379)
        if not ok then
            ngx.status = 502
            ngx.say("redis connect error: ", err)
            return
        end
        sock:send("INCR page:views\r\n")
        local reply, rerr = sock:receive("*l")   -- ответ вида ":42"

        -- 3) ОБЯЗАТЕЛЬНО вернуть соединение в пул, не close!
        local kok, kerr = sock:setkeepalive(10000, 100)
        if not kok then
            ngx.log(ngx.WARN, "keepalive: ", kerr)
        end

        ngx.header["Content-Type"] = "text/plain"
        ngx.say("worker hits=", n, " redis=", reply or rerr)
    }
}
Контрольные вопросы
  • Зачем нужна lua-resty-core и какие три модуля без неё вообще не заработают? Что значит "FFI-фундамент"?
  • Что вернёт ngx.req.get_body_data() без вызова ngx.req.read_body() и что произойдёт, если тело ушло во временный файл?
  • Чем incr(key, 1, 0) отличается от incr(key, 1), и зачем нужен safe_set вместо обычного set? Что означает третий возвращаемый флаг forcible?
  • Почему нельзя сохранить объект Redis-соединения в upvalue модуля, и что именно переиспользуется при set_keepalive? Как посчитать pool_size, если у Redis лимит соединений и N воркеров?
Итог

ngx.* - это четыре слоя инструментов. lua-resty-core даёт FFI-скорость и продвинутые модули (balancer/ssl/semaphore). ngx.var/ngx.req/ngx.header/ngx.say дают полный доступ к запросу и ответу. lua_shared_dict хранит примитивы в общей памяти всех воркеров - с богатым API incr+init, get_stale, safe_set и вытеснением по LRU. cosocket даёт неблокирующий выход в сеть, но требует трёх дисциплин: set_keepalive обязателен, пул per-worker, коннект не хранить на модуле. И сквозное правило: ни одного блокирующего вызова на пути воркера. Освоишь этот арсенал - и Lua в nginx из игрушки превращается в инструмент построения настоящего API-gateway.
👍2 ❤️1 🔥1 😄 🤔
Аватара пользователя
Madeleine
Сообщения: 1
Зарегистрирован: 16 май 2026, 20:02

Re: Lua API: ngx.*, shared dict, cosocket и lua-resty-core

Сообщение Madeleine »

Вот про коннект на уровне модуля - реально наступал на эти грабли. Завел local red вверху файла, оно работало в тестах а под нагрузкой начало мешать ответы между запросами. Час ловил, пока не дошло что это та самая гонка. Спасибо что прямым текстом написали.
👍1 ❤️1 🔥 😄 🤔1
Аватара пользователя
solidity13
Сообщения: 1
Зарегистрирован: 18 май 2026, 16:13

Re: Lua API: ngx.*, shared dict, cosocket и lua-resty-core

Сообщение solidity13 »

А подскажите по pool_size: если воркеров auto и их число равно числу ядер, то делить лимит Redis надо на фактическое количество воркеров после старта? И как быть если запускаю в k8s и реплик несколько - там же лимит делится еще и на поды?
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Фазы обработки запроса и Lua-хуки
Следующая глава →
Практика OpenResty: авторизация, JWT, кэш, Redis

Все главы курса «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Поделиться темой: ✈ Telegram VK
  • Похожие темы

Вернуться в «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя