Фазы обработки запроса и Lua-хуки

Рейтинг: 68.5% · 14 голосов
Самый подробный курс по 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-хуки

Сообщение 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 и аудит конфигурации
Ты уже умеешь писать конфиг nginx руками: location, proxy_pass, upstream, rewrite, map. И в какой-то момент упираешься в стену. Тебе нужно сходить в Redis перед тем как пустить запрос дальше. Или выбрать апстрим не по статической таблице, а по содержимому JWT. Или отдать разный TLS-сертификат разным доменам, которых тысячи и они появляются на лету. Голый nginx это не умеет - его язык конфигурации декларативный, у него нет if с настоящей логикой, нет циклов, нет похода в базу в момент запроса. И вот тут на сцену выходит OpenResty. Но чтобы Lua реально работал, а не падал с loud невнятными ошибками, надо понять одну вещь: Lua не выполняется "где-то в конфиге". Он встраивается в конкретные ФАЗЫ обработки запроса. Это и есть ключ ко всему OpenResty. Не выучив фазы, ты будешь дёргать cosocket там где его нет, ставить авторизацию там где ответ уже ушёл клиенту, и удивляться почему ngx.exit ведёт себя по-разному. Разберём механику до конца.

Зачем nginx вообще делит обработку на фазы

Когда запрос приходит в nginx, он не обрабатывается одним куском кода. Внутри есть конвейер - последовательность строго упорядоченных фаз, через которые проходит каждый HTTP-запрос. Это не выдумка OpenResty, это архитектура самого nginx: модули регистрируют свои обработчики в нужной фазе, и ядро прогоняет запрос по ним по очереди. Сначала разбор и переписывание URI, потом контроль доступа, потом генерация контента, потом фильтрация ответа, в самом конце - логирование. Каждая фаза решает свою задачу и не лезет в чужую.

Почему это важно для тебя. Директивы вроде limit_req, auth_request, try_files, proxy_pass - все они живут в определённых фазах. Когда ты пишешь access_by_lua, ты буквально подключаешься в ту же access-фазу, где работает штатный модуль авторизации. Поэтому твой Lua-код выполнится в правильный момент: после того как nginx уже определил location и переписал URI, но до того как начал генерировать ответ. Если бы Lua-хук не привязывался к фазе, ты бы не знал, что уже посчитано, а что ещё нет. Фаза - это контракт о том, какие данные доступны и что можно делать.

Полный порядок Lua-директив OpenResty по жизненному циклу:
  • init_by_lua - один раз при старте/reload мастер-процесса, ДО форка воркеров. Прогрев: загрузить модули, прочитать конфиг-файлы, заполнить разделяемые словари константами. Cosocket НЕЛЬЗЯ (сети ещё нет).
  • init_worker_by_lua - один раз на КАЖДЫЙ воркер после форка. Тут стартуют фоновые таймеры (ngx.timer.at / ngx.timer.every) - опрос health, обновление кэша конфигурации.
  • set_by_lua - вычислить значение переменной nginx. Синхронно и быстро, БЛОКИРУЕТ воркер. Cosocket и ngx.sleep НЕЛЬЗЯ.
  • ssl_client_hello_by_lua / ssl_certificate_by_lua - на TLS-хендшейке, до выбора сертификата. Динамические сертификаты по SNI.
  • rewrite_by_lua - раннее вмешательство: переписать URI, аргументы, заголовки, сделать редирект.
  • access_by_lua - контроль доступа: пустить или нет. Здесь авторизация, проверка токенов, поход в Redis/БД.
  • balancer_by_lua - выбор конкретного бэкенда в момент проксирования. Сердце Lua-балансировки.
  • content_by_lua - сгенерировать тело ответа целиком средствами Lua (вместо proxy_pass).
  • header_filter_by_lua - модификация заголовков ответа перед отправкой.
  • body_filter_by_lua - модификация тела ответа кусками по мере прохождения.
  • log_by_lua - после того как ответ ушёл клиенту: метрики, аналитика, отправка в очередь. Клиента НЕ задерживает.
Изображение

Что можно и нельзя в каждой фазе - openresty фазы без иллюзий

Главная грабля новичка - думать, что Lua везде одинаковый. Нет. Возможности зависят от фазы, потому что в разный момент жизни запроса доступны разные ресурсы ядра nginx.

Самое жёсткое ограничение - cosocket (неблокирующие сокеты ngx.socket.tcp, основа всех lua-resty-* клиентов к Redis, MySQL, HTTP). Cosocket требует, чтобы у текущей фазы был "yield-механизм" - возможность приостановить корутину и вернуть управление в event loop. Не во всех фазах он есть.
Cosocket и ngx.sleep доступны в: rewrite, access, content, ssl_certificate, ssl_client_hello, init_worker (внутри таймеров), а также в фоновых таймерах. НЕДОСТУПНЫ в: set_by_lua, header_filter_by_lua, body_filter_by_lua, balancer_by_lua, log_by_lua, init_by_lua.
Разбор по фазам, где чаще всего ломаются:
  • set_by_lua - синхронный, без сети и без sleep. Если положишь сюда запрос в Redis, получишь ошибку API disabled. Используй только для быстрых чистых вычислений.
  • header_filter / body_filter - cosocket нет: ответ уже формируется, момент для походов по сети упущен. Тут только трансформация того что есть. В body_filter помни про ngx.arg[1] (кусок тела) и ngx.arg[2] (флаг eof) - тело приходит чанками, а не целиком.
  • balancer_by_lua - cosocket НЕТ напрямую. Это критично. Нельзя в момент балансировки сходить в БД за адресом бэкенда. Решение каноническое: данные о пирах достаёшь раньше - в access_by_lua через cosocket - и передаёшь в balancer через таблицу ngx.ctx.
  • log_by_lua - cosocket НЕТ. Отправить метрику по сети прямо тут нельзя без блокировки. Правильно: складываешь в shared dict или ставишь ngx.timer.at(0, ...), который улетит уже после фазы и спокойно сходит по сети из таймера.
Запомни принцип: сеть - в rewrite/access/content/таймерах; вычисления - в set; трансформация ответа - в фильтрах; учёт - в log через буфер или таймер.

Практика 1: авторизация в access_by_lua и метрика в log_by_lua

Классический сценарий gateway: проверить ключ доступа до того как запрос уйдёт на бэкенд, а после ответа - записать факт в метрику. Это ровно то место, где паттерн nginx lua access_by_lua раскрывается полностью.

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

http {
    lua_shared_dict req_counters 10m;

    server {
        listen 80;

        location /api/ {
            access_by_lua_block {
                local key = ngx.var.http_x_api_key
                if not key or key == "" then
                    return ngx.exit(ngx.HTTP_UNAUTHORIZED)
                end
                -- неблокирующий поход в Redis: тут cosocket доступен
                local redis = require "resty.redis"
                local red = redis:new()
                red:set_timeout(200)  -- мс
                local ok, err = red:connect("127.0.0.1", 6379)
                if not ok then
                    ngx.log(ngx.ERR, "redis connect: ", err)
                    return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
                end
                local consumer = red:get("apikey:" .. key)
                if consumer == ngx.null then
                    red:set_keepalive(10000, 100)  -- вернуть в пул, НЕ close
                    return ngx.exit(ngx.HTTP_FORBIDDEN)
                end
                red:set_keepalive(10000, 100)
                -- кладём consumer в ctx, чтобы log-фаза его увидела
                ngx.ctx.consumer = consumer
            }

            log_by_lua_block {
                -- cosocket тут НЕЛЬЗЯ, пишем в shared dict
                local who = ngx.ctx.consumer or "anon"
                local dict = ngx.shared.req_counters
                dict:incr(who .. ":" .. ngx.var.status, 1, 0)
            }

            proxy_pass http://backend;
        }
    }
}
Разбор. В access_by_lua мы на ты с сетью: достаём ключ, идём в Redis, решаем судьбу запроса через ngx.exit. Обрати внимание на set_keepalive вместо close - cosocket-соединение надо вернуть в пул воркера, иначе на каждый запрос будет новый коннект и весь смысл неблокирующего клиента теряется. Результат кладём в ngx.ctx - это таблица, живущая ровно один запрос и видимая во всех его фазах. В log_by_lua сети уже нет, поэтому метрику инкрементим в lua_shared_dict (он общий для всех воркеров), а наружу её утащит отдельный таймер или /metrics-эндпоинт. Клиент при этом ответ уже получил - log-фаза его не тормозит.

Практика 2: balancer_by_lua - механизм Lua-балансировки

Вот фаза, ради которой многие и приходят в OpenResty. Штатный upstream в nginx статичен: список серверов задан в конфиге, чтобы поменять - нужен reload. balancer_by_lua даёт выбирать бэкенд кодом в момент запроса: по содержимому заголовка, по результату consistent hashing, по данным из shared dict, который обновляет фоновый таймер. Это и есть основа динамических апстримов (так устроены крупные gateway, включая то, как раньше работал ingress).

Ключевая функция - ngx.balancer.set_current_peer(host, port) из lua-resty-core. Она говорит nginx: "проксируй вот на этот адрес". Важно: домены НЕ поддерживаются, только IP - резолвить надо заранее (через lua-resty-dns в access-фазе).

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

http {
    upstream backend {
        server 0.0.0.1;          # плейсхолдер, реальный адрес задаст Lua
        balancer_by_lua_block {
            local balancer = require "ngx.balancer"
            -- адрес мы посчитали раньше, в access-фазе, и положили в ctx
            local peer = ngx.ctx.peer or { host = "10.0.0.10", port = 9000 }
            local ok, err = balancer.set_current_peer(peer.host, peer.port)
            if not ok then
                ngx.log(ngx.ERR, "set_current_peer: ", err)
                return ngx.exit(500)
            end
            -- ретраи: до 2 попыток, таймауты на этот пир
            balancer.set_more_tries(1)
            balancer.set_timeouts(1, 2, 2)  -- connect, send, read (сек)
        }
        keepalive 32;
    }

    server {
        listen 80;
        location / {
            access_by_lua_block {
                -- ТУТ можно cosocket: выбрать пир, например через shared dict
                local pool = { {host="10.0.0.10", port=9000},
                               {host="10.0.0.11", port=9000} }
                local idx = (tonumber(ngx.var.connection) or 0) % #pool + 1
                ngx.ctx.peer = pool[idx]
            }
            proxy_pass http://backend;
        }
    }
}
Разбор и грабли. Сервер 0.0.0.1 в upstream - заглушка: реальный адрес всё равно перезапишет Lua, но nginx требует хотя бы один server в блоке. Выбор пира мы делаем НЕ в balancer_by_lua, а раньше - в access_by_lua, потому что в balancer-фазе cosocket недоступен, а выбор по живой БД/health требует сети. В balancer остаётся только дешёвая операция set_current_peer плюс настройка ретраев: set_more_tries(1) разрешает одну дополнительную попытку, и при ретрае balancer_by_lua вызовется ПОВТОРНО - значит твой код должен уметь выдать следующий пир, а не зациклиться на упавшем. keepalive 32 держит пул соединений к выбранным бэкендам.

Практика 3: динамические сертификаты в ssl_certificate_by_lua

Мульти-тенант: тысячи доменов, на каждый - свой TLS-сертификат, и они появляются динамически (привязка домена клиентом, выпуск через ACME). Прописывать ssl_certificate статикой на каждый - безумие и reload на каждый чих. Решение - ssl_certificate_by_lua: код вызывается на хендшейке, видит SNI, грузит нужный сертификат на лету (из файла, из БД, из кэша). Так работают платформы вроде Cloudflare/хостингов под капотом.

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

server {
    listen 443 ssl;
    server_name _;
    # заглушки - реальный серт подставит Lua
    ssl_certificate     /etc/nginx/fallback.crt;
    ssl_certificate_key /etc/nginx/fallback.key;

    ssl_certificate_by_lua_block {
        local ssl = require "ngx.ssl"
        local name = ssl.server_name()        -- SNI клиента
        if not name then return end
        -- тут cosocket доступен: можно сходить за сертом в кэш/БД
        local cert_pem, key_pem = load_cert_for(name)  -- твоя функция
        if not cert_pem then
            ngx.log(ngx.ERR, "no cert for ", name)
            return
        end
        ssl.clear_certs()                     -- убрать заглушку
        ssl.set_der_cert(ssl.cert_pem_to_der(cert_pem))
        ssl.set_der_priv_key(ssl.priv_key_pem_to_der(key_pem))
    }
}
В ssl_certificate_by_lua cosocket ЕСТЬ - можно неблокирующе сходить за сертификатом. Связка с ACME очевидна: фоновый таймер выпускает/обновляет сертификаты и кладёт в кэш, а этот хук их раздаёт по SNI. Если нужен ещё более ранний доступ к данным ClientHello (ALPN, расширения) - есть ssl_client_hello_by_lua, который срабатывает до выбора сертификата вообще.

Типичные грабли
  • ngx.exit ведёт себя по-разному. В rewrite/access ngx.exit(ngx.HTTP_FORBIDDEN) прерывает запрос и отдаёт статус. В content фазе ngx.exit(ngx.OK) завершает отдачу. Не путай ngx.HTTP_OK (200, реально завершить с кодом) и ngx.OK (0, "продолжить как обычно"). В access вернуть ngx.exit(ngx.OK) - значит "пропустить дальше", а ngx.exit(ngx.HTTP_OK) - "ответить 200 прямо сейчас".
  • Порядок постпонится. rewrite_by_lua по умолчанию выполняется в КОНЦЕ rewrite-фазы, после штатных rewrite-директив. Если нужно строго до них - есть rewrite_by_lua_no_postpone, но почти всегда он не нужен и только путает.
  • Cosocket не в той фазе. Самая частая ошибка - запрос по сети в set/header/body/log/balancer. Получишь "API disabled in the context of ...". Лечится переносом сети в access и передачей результата через ngx.ctx.
  • ngx.ctx не переживает internal redirect. При rewrite/try_files в новый location создаётся новый ngx.ctx. Если данные нужны после редиректа - не полагайся слепо на ctx.
  • Тяжёлая логика в set_by_lua. Он блокирующий и зовётся ровно когда переменная нужна (лениво). Цикл на миллион итераций тут заморозит воркер. Для тяжёлого - rewrite/access.
  • content_by_lua и proxy_pass в одном location несовместимы - оба претендуют на content-фазу. Выбирай одно.
Мини-лаба: пройти фазы руками

Соберём минимальный стенд и увидим порядок фаз своими глазами.
  • 1. Запусти OpenResty в Docker: docker run -d -p 8080:80 -v $PWD/conf:/usr/local/openresty/nginx/conf/conf.d openresty/openresty:latest (актуальный образ 2026 - OpenResty 1.31.1.1).
  • 2. В conf.d создай server c location /trace и навешай по одному lua-блоку на каждую фазу, в каждом - ngx.log(ngx.ERR, "PHASE: rewrite") и так далее для rewrite/access/content/header_filter/body_filter/log.
  • 3. В content_by_lua сделай ngx.say("hello from content").
  • 4. Дёрни curl -s localhost:8080/trace и смотри error.log. Убедись, что строки идут строго в порядке: rewrite -> access -> content -> header_filter -> body_filter -> log.
  • 5. Теперь добавь в set_by_lua_block попытку ngx.sleep(0.1). Перезапусти, проверь nginx -t и логи - поймай ошибку "API disabled" и осознай, почему set синхронный.
  • 6. Перенеси любую "сетевую" задачу (хоть resolver через socket) в access_by_lua - ошибка уйдёт.
  • 7. Бонус: добавь log_by_lua с ngx.timer.at(0, function() ... end) и убедись, что код таймера выполняется уже ПОСЛЕ того как клиент получил ответ.
Контрольные вопросы
  • 1. В каких фазах cosocket НЕДОСТУПЕН и куда переносить сетевые вызовы из balancer_by_lua и log_by_lua?
  • 2. Чем отличается ngx.exit(ngx.OK) от ngx.exit(ngx.HTTP_OK) внутри access_by_lua?
  • 3. Почему выбор бэкенда обычно делают в access_by_lua, а в balancer_by_lua остаётся только set_current_peer?
  • 4. Зачем после работы с resty.redis звать set_keepalive вместо close, и где ngx.ctx теряет свои данные?
Итог

Фазы - это не теория, а карта, по которой ты размещаешь Lua. Сеть и решения - в rewrite/access; динамический бэкенд - в balancer (а данные для него готовь раньше); генерация - в content; правка ответа - в фильтрах; учёт - в log без блокировки. Помни про cosocket-ограничения и про разницу ngx.OK/HTTP_OK - и openresty content_by_lua, nginx lua rewrite_by_lua, balancer_by_lua перестанут быть магией и станут инструментом. Дальше мы соберём из этих кубиков настоящий API-gateway.
👍4 ❤️4 🔥1 😄 🤔1
Аватара пользователя
pandas_main
Сообщения: 1
Зарегистрирован: 14 май 2026, 17:19

Re: Фазы обработки запроса и Lua-хуки

Сообщение pandas_main »

А я-то думал почему мой redis:get в log_by_lua падал с API disabled. Оказывается фаза не та, переписал на shared dict + timer.at и всё, спасибо за разбор по фазам.
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
reactninja
Сообщения: 1
Зарегистрирован: 20 май 2026, 04:22

Re: Фазы обработки запроса и Lua-хуки

Сообщение reactninja »

Заглушка 0.0.0.1 в upstream это конечно жесть, но логично - реальный адрес же все равно из balancer_by_lua прилетает. Вопрос: а set_more_tries реально вызовет блок повторно с другим пиром или надо самому индекс крутить в ctx?
👍 ❤️ 🔥 😄 🤔1
Ответить
← Предыдущая глава
OpenResty: Nginx как платформа на LuaJIT
Следующая глава →
Lua API: ngx.*, shared dict, cosocket и lua-resty-core

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

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

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

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

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