За пределами веб-сервера: Привилегированные процессы и задачи по таймеру

API7.ai

November 3, 2022

OpenResty (NGINX + Lua)

В предыдущей статье мы представили API OpenResty, shared dict и cosocket, которые реализуют функциональность в рамках NGINX и веб-серверов, предоставляя программируемый веб-сервер с более низкой стоимостью и более простым в обслуживании реализацией.

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

Задачи по таймеру

В OpenResty иногда нам нужно регулярно выполнять определенные задачи в фоновом режиме, такие как синхронизация данных, очистка логов и т.д. Если бы вы проектировали это, как бы вы это сделали? Самый простой способ, который приходит на ум, — это предоставить внешний API-интерфейс для выполнения этих задач, затем использовать системный crontab для вызова curl через определенные промежутки времени для доступа к этому интерфейсу и таким образом косвенно реализовать это требование.

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

Задачи по таймеру в OpenResty можно разделить на следующие два типа.

  • ngx.timer.at используется для выполнения одноразовых задач по таймеру.
  • ngx.timer.every используется для выполнения задач по таймеру с фиксированным периодом.

Помните вопрос, который я оставил в конце предыдущей статьи? Вопрос был о том, как обойти ограничение, что cosocket нельзя использовать в init_worker_by_lua, и ответ — это ngx.timer.

Следующий код запускает задачу по таймеру с задержкой 0. Он запускает функцию обратного вызова handler, и в этой функции используется cosocket для доступа к веб-сайту.

init_worker_by_lua_block { local function handler() local sock = ngx.socket.tcp() local ok, err = sock:connect(“api7.ai", 80) end local ok, err = ngx.timer.at(0, handler) }

Таким образом, мы обходим ограничение, что cosocket нельзя использовать на этом этапе.

Возвращаясь к пользовательскому требованию, которое мы упомянули в начале этого раздела, ngx.timer.at не решает задачу периодического выполнения; в приведенном выше примере кода это одноразовая задача.

Итак, как мы можем сделать это периодически? У вас, кажется, есть два варианта на основе API ngx.timer.at.

  • Вы можете реализовать периодическую задачу самостоятельно, используя бесконечный цикл while true в функции обратного вызова, который будет sleep на некоторое время после выполнения задачи.
  • Вы также можете создать новый таймер в конце функции обратного вызова.

Однако, прежде чем сделать выбор, нам нужно уточнить одну вещь: таймер по сути является запросом, хотя запрос был инициирован не клиентом. Для запроса он должен завершиться после выполнения своей задачи и не может оставаться резидентным. В противном случае это легко приведет к различным утечкам ресурсов.

Поэтому первый вариант использования while true для реализации периодических задач ненадежен. Второй вариант возможен, но рекурсивное создание таймеров нелегко понять.

Итак, есть ли лучшее решение? Новый API ngx.timer.every в OpenResty специально разработан для решения этой проблемы, и это решение ближе к crontab.

Недостаток в том, что у вас никогда не будет возможности отменить задачу по таймеру после ее запуска. В конце концов, ngx.timer.cancel все еще является функцией, которую нужно реализовать.

На этом этапе вы столкнетесь с проблемой: таймер работает в фоновом режиме и не может быть отменен; если таймеров много, легко исчерпать системные ресурсы.

Поэтому OpenResty предоставляет две директивы, lua_max_pending_timers и lua_max_running_timers, чтобы ограничить их. Первая представляет максимальное количество таймеров, ожидающих выполнения, а вторая — максимальное количество текущих выполняющихся таймеров.

Вы также можете использовать Lua API для получения значений текущих ожидающих и выполняющихся задач по таймеру, как показано в следующих двух примерах.

content_by_lua_block { ngx.timer.at(3, function() end) ngx.say(ngx.timer.pending_count()) }

Этот код выведет 1, что означает, что есть одна запланированная задача, ожидающая выполнения.

content_by_lua_block { ngx.timer.at(0.1, function() ngx.sleep(0.3) end) ngx.sleep(0.2) ngx.say(ngx.timer.running_count()) }

Этот код выведет 1, что означает, что есть одна запланированная задача, выполняющаяся.

Привилегированный процесс

Далее рассмотрим привилегированный процесс. Как мы все знаем, NGINX разделен на процесс Master и процессы Worker, где процессы worker обрабатывают пользовательские запросы. Мы можем получить тип процесса через API process.type, предоставленный в lua-resty-core. Например, вы можете использовать resty для выполнения следующей функции.

$ resty -e 'local process = require "ngx.process" ngx.say("process type:", process.type())'

Вы увидите, что он возвращает результат single вместо worker, что означает, что resty запускает NGINX с процессом Worker, а не с процессом Master. Это правда. В реализации resty можно увидеть, что процесс Master отключен строкой, подобной этой.

master_process off;

OpenResty расширяет NGINX, добавляя privileged agent, Привилегированный процесс имеет следующие особенности.

  • Он не мониторит никакие порты, что означает, что он не предоставляет услуги внешнему миру.

  • Он имеет те же привилегии, что и процесс Master, что обычно означает привилегии пользователя root, позволяя ему выполнять множество задач, которые невозможны для процесса Worker.

  • Привилегированный процесс может быть открыт только в контексте init_by_lua.

  • Кроме того, привилегированный процесс имеет смысл только в контексте init_worker_by_lua, потому что никакие запросы не инициируются, и они не переходят в контексты content, access и т.д.

Давайте рассмотрим пример включенного привилегированного процесса.

init_by_lua_block { local process = require "ngx.process" local ok, err = process.enable_privileged_agent() if not ok then ngx.log(ngx.ERR, "enables privileged agent failed error:", err) end }

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

nginx: master process nginx: worker process nginx: privileged agent process

Однако, если привилегии выполняются только один раз во время фазы init_worker_by_lua, что не очень хорошая идея, как мы можем инициировать привилегированный процесс?

Да, ответ скрыт в только что изученных знаниях. Поскольку он не слушает порты, т.е. не может быть инициирован терминальными запросами, единственный способ инициировать его периодически — использовать ngx.timer, который мы только что представили:

init_worker_by_lua_block { local process = require "ngx.process" local function reload(premature) local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r") if not f then return end local pid = f:read() f:close() os.execute("kill -HUP " .. pid) end if process.type() == "privileged agent" then local ok, err = ngx.timer.every(5, reload) if not ok then ngx.log(ngx.ERR, err) end end }

Приведенный выше код реализует возможность отправки сигналов HUP мастер-процессу каждые 5 секунд. Естественно, вы можете развить это, чтобы делать более интересные вещи, такие как опрос базы данных для проверки наличия задач для привилегированного процесса и их выполнение. Поскольку привилегированный процесс имеет привилегии root, это, очевидно, немного "бэкдорная" программа.

Неблокирующий ngx.pipe

Наконец, рассмотрим неблокирующий ngx.pipe, который использует стандартную библиотеку Lua для выполнения внешней командной строки, отправляющей сигнал мастер-процессу в примере кода, который мы только что описали.

os.execute("kill -HUP " .. pid)

Естественно, эта операция будет блокирующей. Итак, есть ли неблокирующий способ вызова внешних программ в OpenResty? В конце концов, вы знаете, что если вы используете OpenResty как полноценную платформу разработки, а не как веб-сервер, это то, что вам нужно. Для этой цели была создана библиотека lua-resty-shell, и использование ее для вызова командной строки является неблокирующим:

$ resty -e 'local shell = require "resty.shell" local ok, stdout, stderr, reason, status = shell.run([[echo "hello, world"]]) ngx.say(stdout)

Этот код представляет собой другой способ написания hello world, вызывая системную команду echo для завершения вывода. Аналогично, вы можете использовать resty.shell как альтернативу вызову os.execute в Lua.

Мы знаем, что базовая реализация lua-resty-shell опирается на API ngx.pipe в lua-resty-core, поэтому этот пример использует lua-resty-shell для вывода hello world, используя ngx.pipe вместо этого, это будет выглядеть так.

$ resty -e 'local ngx_pipe = require "ngx.pipe" local proc = ngx_pipe.spawn({"echo", "hello world"}) local data, err = proc:stdout_read_line() ngx.say(data)'

Выше приведен базовый код реализации lua-resty-shell. Вы можете ознакомиться с документацией ngx.pipe и тестовыми примерами для получения дополнительной информации о том, как его использовать. Поэтому я не буду углубляться в это здесь.

Итог

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

Наконец, я оставлю вам вопрос для размышления. Поскольку может быть несколько процессов Worker в NGINX, timer будет запускаться один раз для каждого Worker, что неприемлемо в большинстве сценариев. Как мы можем гарантировать, что timer будет запущен только один раз?

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