Введение в общие API в OpenResty

API7.ai

November 4, 2022

OpenResty (NGINX + Lua)

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

API, связанные с регулярными выражениями

Начнем с рассмотрения наиболее часто используемых и важных регулярных выражений. В OpenResty мы должны использовать набор API, предоставляемый ngx.re.*, для обработки логики, связанной с регулярными выражениями, вместо использования Lua pattern matching. Это связано не только с вопросами производительности, но и с тем, что регулярные выражения Lua являются самодостаточными и не соответствуют спецификации PCRE, что может быть неудобным для большинства разработчиков.

В предыдущих статьях вы уже сталкивались с некоторыми API из ngx.re.*, документация к которым очень подробна. Поэтому я не буду их перечислять. Здесь я отдельно представлю два следующих API.

ngx.re.split

Первым является ngx.re.split. Разделение строк — это очень распространенная функция, и OpenResty также предоставляет соответствующий API, но многие разработчики не могут найти такую функцию и вынуждены реализовывать её самостоятельно.

Почему? API ngx.re.split находится не в lua-nginx-module, а в lua-resty-core; он не в документации на главной странице lua-resty-core, а в документации в третьем уровне каталога lua-resty-core/lib/ngx/re.md. В результате многие разработчики совершенно не знают о существовании этого API.

Аналогично, API, которые трудно обнаружить, включают ngx_resp.add_header, enable_privileged_agent и другие, о которых мы упоминали ранее. Как же быстро решить эту проблему? Помимо чтения документации на главной странице lua-resty-core, вам также нужно прочитать документацию в каталоге lua-resty-core/lib/ngx/ в файлах *.md.

lua_regex_match_limit

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

Мы знаем, что если мы используем регулярный движок, реализованный на основе backtracking NFA, то существует риск катастрофического бэктрекинга (Catastrophic Backtracking), когда регулярное выражение слишком много раз возвращается при сопоставлении, вызывая 100% загрузку CPU и блокировку сервисов.

Как только происходит катастрофический бэктрекинг, нам нужно использовать gdb для анализа дампа или systemtap для анализа онлайн-среды, чтобы локализовать проблему. К сожалению, заранее обнаружить её нелегко, потому что только специальные запросы могут её вызвать. Это позволяет злоумышленникам воспользоваться этим, и ReDoS (RegEx Denial of Service) относится к этому типу атак.

Здесь я в основном расскажу вам, как использовать следующую строку кода в OpenResty, чтобы просто и эффективно избежать вышеуказанных проблем:

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

lua_regex_match_limit 100000;

API, связанные с временем

Наиболее часто используемым API для работы со временем является ngx.now, который выводит текущую временную метку, например, следующая строка кода:

resty -e 'ngx.say(ngx.now())'

Как видно из результатов вывода, ngx.now включает дробную часть, поэтому он более точен. Связанный API ngx.time возвращает только целую часть значения. Другие, такие как ngx.localtime, ngx.utctime, ngx.cookie_time и ngx.http_time, в основном используются для возврата и обработки времени в разных форматах. Если вы хотите их использовать, вы можете проверить документацию, они не сложны для понимания, поэтому мне не нужно о них рассказывать.

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

$ resty -e 'ngx.say(ngx.now()) os.execute("sleep 1") ngx.say(ngx.now())'

Между двумя вызовами ngx.now мы использовали блокирующую функцию Lua для сна на 1 секунду, но временная метка, возвращаемая в обоих случаях, одинакова, как показано в результатах вывода.

Что, если мы заменим её на неблокирующую функцию сна? Например, следующий новый код:

$ resty -e 'ngx.say(ngx.now()) ngx.sleep(1) ngx.say(ngx.now())'

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

Например, если у вас есть фрагмент кода, который выполняет интенсивные вычисления, занимающие много времени, запросы, соответствующие этому фрагменту кода, будут продолжать занимать ресурсы воркера и CPU в течение этого времени, вызывая очередь других запросов и не получая своевременного ответа. В этот момент мы можем вставить ngx.sleep(0), чтобы этот код отдал управление, и другие запросы также могли быть обработаны.

API для воркеров и процессов

OpenResty предоставляет API ngx.worker.* и ngx.process.* для получения информации о воркерах и процессах. Первый относится к рабочим процессам Nginx, а второй относится ко всем процессам Nginx в целом, включая не только рабочие процессы, но и мастер-процесс, привилегированный процесс и т.д.

Проблема значений true и null

Наконец, рассмотрим проблему значений true и null. В OpenResty определение значений true и null всегда было очень сложным и запутанным моментом.

Давайте посмотрим на определение значения true в Lua: за исключением nil и false, все остальные значения являются true.

Таким образом, значения true также включают 0, пустую строку, пустую таблицу и т.д.

Теперь рассмотрим nil в Lua, что означает неопределенное. Например, если вы объявляете переменную, но не инициализируете её, её значение будет nil.

$ resty -e 'local a ngx.say(type(a))'

И nil также является типом данных в Lua. Поняв эти два момента, давайте теперь рассмотрим другие проблемы, вытекающие из этих определений.

ngx.null

Первая проблема — это ngx.null. Поскольку nil в Lua не может быть значением таблицы, OpenResty вводит ngx.null как значение null в таблице.

$ resty -e 'print(ngx.null)' null
$ resty -e 'print(type(ngx.null))' userdata

Как видно из двух приведенных выше фрагментов кода, ngx.null выводится как null, и его тип — userdata, так можно ли его рассматривать как значение false? Конечно, нет. Логическое значение ngx.nulltrue.

$ resty -e 'if ngx.null then ngx.say("true") end'

Поэтому помните, что только nil и false являются значениями false. Если вы упустите этот момент, легко попасть в ловушку, например, когда вы используете lua-resty-redis и делаете следующее условие:

local res, err = red:get("dog") if not res then res = res + "test" end

Если возвращаемое значение resnil, вызов функции завершился неудачей; если resngx.null, ключ dog не существует в redis, тогда код завершится с ошибкой, если ключ dog не существует.

cdata:NULL

Вторая проблема — это cdata:NULL. Когда вы вызываете C-функцию через интерфейс LuaJIT FFI, и функция возвращает указатель NULL, то вы столкнетесь с другим типом значения null, cdata:NULL.

$ resty -e 'local ffi = require "ffi" local cdata_null = ffi.new("void*", nil) if cdata_null then ngx.say("true") end'

Как и ngx.null, cdata:NULL также является true. Но что еще более запутывает, так это то, что следующий код, который выводит true, означает, что cdata:NULL эквивалентен nil.

$ resty -e 'local ffi = require "ffi" local cdata_null = ffi.new("void*", nil) ngx.say(cdata_null == nil)'

Так как же нам обрабатывать ngx.null и cdata:NULL? Не самое лучшее решение заставлять уровень приложения заботиться об этих проблемах. Лучше сделать вторичную обертку и не давать вызывающей стороне знать об этих деталях.

cjson.null

Наконец, рассмотрим значения null, которые появляются в cjson. Библиотека cjson берет NULL в json, декодирует его в Lua lightuserdata и использует cjson.null для представления.

$ resty -e 'local cjson = require "cjson" local data = cjson.encode(nil) local decode_null = cjson.decode(data) ngx.say(decode_null == cjson.null)'

Lua nil становится cjson.null после кодирования и декодирования JSON. Как вы можете догадаться, он введен по той же причине, что и ngx.null, потому что nil не может быть значением в таблице.

До сих пор вы были сбиты с толку таким количеством типов значений null в OpenResty? Не беспокойтесь. Прочитайте эту часть несколько раз и разберитесь самостоятельно, тогда вы не будете путаться. Конечно, в будущем нам нужно больше думать о том, работает ли что-то вроде if not foo then.

Итог

Сегодняшняя статья познакомила вас с Lua API, часто используемыми в OpenResty.

Наконец, я оставлю вам вопрос: в примере с ngx.now, почему значение ngx.now не изменяется, когда нет операции yield? Поделитесь своим мнением в комментариях, а также поделитесь этой статьей, чтобы мы могли общаться и улучшать её вместе.