Знания о NGINX, используемом в OpenResty

API7.ai

September 17, 2022

OpenResty (NGINX + Lua)

Через предыдущий пост вы получили общие знания о OpenResty. В следующих нескольких статьях я расскажу вам о двух основах OpenResty: NGINX и LuaJIT, и вы сможете лучше изучить OpenResty, освоив эти основы.

Сегодня я начну с NGINX, и здесь я расскажу только о некоторых основах NGINX, которые могут использоваться в OpenResty, что является лишь небольшой частью NGINX.

Что касается конфигурации, в разработке OpenResty нам нужно обратить внимание на следующие моменты.

  • Минимизировать настройку nginx.conf.
  • Избегать использования комбинаций нескольких директив, таких как if, set, rewrite и т.д.
  • Не использовать конфигурацию, переменные и модули NGINX, если проблему можно решить с помощью кода на Lua.

Эти методы максимизируют читаемость, поддерживаемость и расширяемость. Следующая конфигурация NGINX является типичным плохим примером использования конфигурации как кода.

location ~ ^/mobile/(web/app.htm) { set $type $1; set $orig_args $args; if ( $http_user_Agent ~ "(iPhone|iPad|Android)" ) { rewrite ^/mobile/(.*) http://touch.foo.com/mobile/$1 last; } proxy_pass http://foo.com/$type?$orig_args; }

Это то, чего нам нужно избегать при разработке с использованием OpenResty.

Конфигурация NGINX

NGINX управляет своим поведением через конфигурационные файлы, которые можно рассматривать как простой DSL. NGINX читает конфигурацию при запуске процесса и загружает её в память. Если вы измените конфигурационный файл, вам нужно будет перезапустить или перезагрузить NGINX и дождаться, пока NGINX снова прочитает конфигурационный файл, чтобы новые настройки вступили в силу. Только коммерческая версия NGINX предоставляет некоторые динамические возможности во время выполнения в виде API.

Давайте начнем со следующей конфигурации, которая очень проста.

worker_processes auto; pid logs/nginx.pid; error_log logs/error.log notice; worker_rlimit_nofile 65535; events { worker_connections 16384; } http { server { listen 80; listen 443 ssl; location / { proxy_pass https://foo.com; } } } stream { server { listen 53 udp; } }

Однако даже эти простые конфигурации затрагивают некоторые фундаментальные концепции.

Во-первых, каждая директива имеет свой контекст, который определяет её область действия в конфигурационном файле NGINX.

Верхний уровень — это main, который содержит некоторые инструкции, не связанные с конкретным бизнесом, такие как worker_processes, pid и error_log, которые все относятся к контексту main. Кроме того, между контекстами существует иерархическая связь. Например, контекст location — это server, контекст server — это http, а контекст http — это main.

Директивы не могут выполняться в неправильном контексте. NGINX проверяет, является ли nginx.conf корректным при запуске. Например, если мы переместим listen 80; из контекста server в контекст main и попытаемся запустить службу NGINX, мы увидим ошибку следующего вида:

"listen" directive is not allowed here ......

Во-вторых, NGINX может обрабатывать не только HTTP-запросы и HTTPS-трафик, но также UDP и TCP-трафик. Уровень L7 находится в HTTP, а уровень L4 — в Stream. В OpenResty lua-nginx-module и stream-lua-nginx-module соответствуют этим двум уровням соответственно.

Здесь стоит отметить, что OpenResty не поддерживает все функции NGINX, и вам нужно учитывать версию OpenResty. Версия OpenResty соответствует версии NGINX, что упрощает идентификацию.

Конфигурационные директивы, задействованные в nginx.conf выше, находятся в основных модулях NGINX ngx_core_module, ngx_http_core_module и ngx_stream_core_module, на которые вы можете перейти, чтобы увидеть конкретную документацию.

Режим MASTER-WORKER

После понимания конфигурационного файла давайте рассмотрим многопроцессный режим NGINX (как показано на рисунке ниже). Как видите, при запуске NGINX будет один процесс Master и несколько процессов Worker (или только один процесс Worker, в зависимости от вашей конфигурации).

Режим Worker NGINX

Прежде всего, процесс Master, как следует из названия, играет роль "менеджера" и не отвечает за обработку запросов от клиентов. Он управляет процессами Worker, включая получение сигналов от администратора и мониторинг состояния процессов Worker. Если процесс Worker завершается аномально, процесс Master перезапустит новый процесс Worker.

Процессы Worker — это "настоящие рабочие сотрудники", которые обрабатывают запросы от клиентов. Они создаются из процесса Master и независимы друг от друга. Эта многопроцессная модель намного более продвинута, чем многопоточная модель Apache, без блокировок между потоками и с легкостью отладки. Даже если процесс завершится аварийно, это обычно не повлияет на работу других процессов Worker.

OpenResty добавляет уникальный привилегированный агент к модели Master-Worker NGINX. Этот процесс не прослушивает никакие порты и имеет те же привилегии, что и процесс Master NGINX, поэтому он может выполнять задачи, требующие высоких привилегий, такие как некоторые операции записи в локальные файлы на диске.

Если привилегированный процесс работает с механизмом горячего обновления бинарных файлов NGINX, OpenResty может реализовать полное самообновление бинарных файлов на лету, не полагаясь на внешние программы.

Снижение зависимости от внешних программ и попытка решить проблемы внутри процесса OpenResty облегчает развертывание, снижает затраты на эксплуатацию и уменьшает вероятность ошибок программы. Привилегированный процесс и ngx.pipe в OpenResty предназначены именно для этого.

Фазы выполнения

Фазы выполнения также являются важной особенностью NGINX и тесно связаны с конкретной реализацией OpenResty. NGINX имеет 11 фаз выполнения, которые мы можем увидеть в исходном коде ngx_http_core_module.h:

typedef enum { NGX_HTTP_POST_READ_PHASE = 0, NGX_HTTP_SERVER_REWRITE_PHASE, NGX_HTTP_FIND_CONFIG_PHASE, NGX_HTTP_REWRITE_PHASE, NGX_HTTP_POST_REWRITE_PHASE, NGX_HTTP_PREACCESS_PHASE, NGX_HTTP_ACCESS_PHASE, NGX_HTTP_POST_ACCESS_PHASE, NGX_HTTP_PRECONTENT_PHASE, NGX_HTTP_CONTENT_PHASE, NGX_HTTP_LOG_PHASE } ngx_http_phases;

Если вы хотите узнать больше о роли этих 11 фаз, вы можете прочитать документацию NGINX, поэтому я не буду здесь углубляться.

Совпадение, OpenResty также имеет 11 директив *_by_lua, связанных с фазами NGINX, как показано на рисунке ниже (из документации lua-nginx-module).

Порядок директив Lua NGINX Module

init_by_lua выполняется только при создании процесса Master, а init_worker_by_lua выполняется только при создании каждого процесса Worker. Остальные команды *_by_lua запускаются запросами клиентов и выполняются повторно.

Таким образом, на этапе init_by_lua мы можем предварительно загрузить модули Lua и общедоступные данные только для чтения, чтобы воспользоваться функцией COW (copy on write) операционной системы для экономии памяти.

Большинство операций можно выполнить внутри content_by_lua, но я рекомендую разделить их в соответствии с различными функциями, как показано ниже.

  • set_by_lua: установка переменных.
  • rewrite_by_lua: перенаправление, редирект и т.д.
  • access_by_lua: доступ, права и т.д.
  • content_by_lua: генерация возвращаемого содержимого.
  • header_filter_by_lua: обработка фильтрации заголовков ответа.
  • body_filter_by_lua: обработка фильтрации тела ответа.
  • log_by_lua: логирование.

Приведу пример, чтобы показать преимущества такого разделения. Предположим, что множество API предоставляются внешне в виде открытого текста, и теперь нам нужно добавить пользовательскую логику шифрования и дешифрования. Нужно ли нам изменять код всех API?

location /mixed { content_by_lua '...'; }

Конечно, нет. Используя функцию фаз, мы можем дешифровать на этапе access и шифровать на этапе body filter, не внося изменений в код в исходной фазе content.

location /mixed { access_by_lua '...'; content_by_lua '...'; body_filter_by_lua '...'; }

Обновление бинарного файла NGINX на лету

Наконец, позвольте мне кратко объяснить обновление бинарного файла NGINX на лету. Мы знаем, что после изменения конфигурационного файла NGINX его нужно перезагрузить, чтобы изменения вступили в силу. Но когда NGINX обновляет себя, он может сделать это на лету. Это может показаться странным, но это понятно, учитывая, что NGINX начинал с традиционного статического балансировщика нагрузки, обратного проксирования и кэширования файлов.

Горячее обновление выполняется путем отправки сигналов USR2 и WINCH старому процессу Master. Для этих двух шагов первый запускает новый процесс Master; второй постепенно завершает процессы Worker.

После этих двух шагов запускаются новый Master и новые Worker. На этом этапе старый Master не завершается. Причина, по которой он не завершается, проста: если вам нужно откатиться, вы можете отправить сигнал HUP старому Master. Конечно, если вы уверены, что откат не нужен, вы можете отправить сигнал KILL старому Master, чтобы завершить его.

Вот и всё, обновление бинарного файла NGINX на лету завершено.

Если вы хотите узнать более подробную информацию об этом, вы можете проверить официальную документацию для дальнейшего изучения.

Итог

В целом, в OpenResty используются основы NGINX, в основном связанные с конфигурацией, мастер-слейв процессами, фазами выполнения и т.д. Остальные вещи, которые можно решить с помощью кода на Lua, решаются с помощью кода, а не с использованием модулей и конфигураций NGINX, что является изменением мышления при изучении OpenResty.

Наконец, я оставляю вам открытый вопрос: Nginx официально поддерживает NJS, что означает, что вы можете писать JS для управления некоторой логикой NGINX, аналогично OpenResty. Что вы думаете об этом? Добро пожаловать поделиться этой статьей.