Why Does lua-resty-core Perform Better?
API7.ai
September 30, 2022
Как мы говорили в предыдущих двух уроках, Lua — это встраиваемый язык разработки, который сохраняет ядро коротким и компактным. Вы можете встроить Lua в Redis и NGINX, чтобы гибко реализовывать бизнес-логику. Lua также позволяет вызывать существующие функции и структуры данных на языке C, чтобы избежать изобретения велосипеда.
В Lua вы можете использовать Lua C API для вызова функций на языке C, а в LuaJIT — FFI. Для OpenResty:
- В ядре
lua-nginx-moduleAPI для вызова функций на языке C реализовано с использованием Lua C API. - В
lua-resty-coreнекоторые API, уже присутствующие вlua-nginx-module, реализованы с использованием модели FFI.
Вы, вероятно, задаетесь вопросом: зачем нам нужно реализовывать это с помощью FFI?
Не беспокойтесь. Давайте возьмем ngx.base64_decode, простой API, в качестве примера и посмотрим, чем отличается реализация с использованием Lua C API от реализации с использованием FFI. Вы также сможете интуитивно понять их производительность.
Lua CFunction
Давайте посмотрим, как это реализовано в lua-nginx-module с использованием Lua C API. Мы ищем decode_base64 в коде проекта и находим его реализацию в файле ngx_http_lua_string.c.
lua_pushcfunction(L, ngx_http_lua_ngx_decode_base64); lua_setfield(L, -2, "decode_base64");
Вышеприведенный код выглядит сложным, но, к счастью, нам не нужно углубляться в две функции, начинающиеся с lua_, и в конкретную роль их аргументов; нам нужно знать только одно — здесь зарегистрирована CFunction: ngx_http_lua_ngx_decode_base64, и она соответствует ngx.base64_decode, который является API, доступным для публичного использования.
Давайте продолжим "следовать по карте" и поищем ngx_http_lua_ngx_decode_base64 в этом C-файле, она определена в начале файла:
static int ngx_http_lua_ngx_decode_base64(lua_State *L);
Для тех функций на языке C, которые могут вызываться из Lua, их интерфейс должен соответствовать форме, требуемой Lua, то есть typedef int (*lua_CFunction)(lua_State* L). Он содержит указатель L типа lua_State в качестве аргумента; его возвращаемое значение — целое число, которое указывает количество возвращаемых значений, а не само возвращаемое значение.
Он реализован следующим образом (здесь я удалил код обработки ошибок).
static int ngx_http_lua_ngx_decode_base64(lua_State *L) { ngx_str_t p, src; src.data = (u_char *) luaL_checklstring(L, 1, &src.len); p.len = ngx_base64_decoded_length(src.len); p.data = lua_newuserdata(L, p.len); if (ngx_decode_base64(&p, &src) == NGX_OK) { lua_pushlstring(L, (char *) p.data, p.len); } else { lua_pushnil(L); } return 1; }
Главное в этом коде — это ngx_base64_decoded_length и ngx_decode_base64, обе из которых являются функциями на языке C, предоставляемыми NGINX.
Мы знаем, что функции, написанные на языке C, не могут передавать возвращаемое значение в код Lua, но должны передавать параметры вызова и возвращать значение между Lua и C через стек. Именно поэтому здесь так много кода, который мы не можем понять с первого взгляда. Кроме того, этот код не может быть отслежен JIT, поэтому для LuaJIT эти операции находятся в черном ящике и не могут быть оптимизированы.
LuaJIT FFI
В отличие от FFI, интерактивная часть FFI реализована в Lua, что может быть отслежено JIT и оптимизировано; конечно, код также более лаконичен и легче для понимания.
Давайте возьмем пример base64_decode, чья реализация FFI распределена между двумя репозиториями: lua-resty-core и lua-nginx-module, и посмотрим на код, реализованный в первом.
ngx.decode_base64 = function (s) local slen = #s local dlen = base64_decoded_length(slen) local dst = get_string_buf(dlen) local pdlen = get_size_ptr() local ok = C.ngx_http_lua_ffi_decode_base64(s, slen, dst, pdlen) if ok == 0 then return nil end return ffi_string(dst, pdlen[0]) end
Вы заметите, что по сравнению с CFunction, код реализации FFI выглядит намного свежее, его конкретная реализация — это ngx_http_lua_ffi_decode_base64 в репозитории lua-nginx-module. Если вам интересно, вы можете самостоятельно проверить производительность этой функции. Она очень проста, я не буду публиковать код здесь.
Однако, если вы внимательны, заметили ли вы некоторые правила именования функций в приведенном выше фрагменте кода?
Да, все функции в OpenResty имеют соглашения об именовании, и вы можете понять их использование по их именам. Например:
ngx_http_lua_ffi_, Lua-функция, использующая FFI для обработки HTTP-запросов NGINX.ngx_http_lua_ngx_, Lua-функция, использующая функции на языке C для обработки HTTP-запросов NGINX.- Другие функции, начинающиеся с
ngx_иlua_, являются встроенными функциями для NGINX и Lua соответственно.
Кроме того, код на языке C в OpenResty имеет строгие стандарты кодирования, и я рекомендую прочитать официальное руководство по стилю кода на языке C. Это обязательный документ для разработчиков, которые хотят изучить код на языке C в OpenResty и отправлять PR. В противном случае, даже если ваш PR хорошо написан, вам будут постоянно комментировать и просить изменить его из-за проблем со стилем кода.
Для получения дополнительной информации о FFI и API, мы рекомендуем вам прочитать официальные руководства LuaJIT и документацию. Технические колонки не заменяют официальную документацию; я могу только помочь вам указать путь обучения в ограниченное время, с меньшим количеством ошибок; сложные проблемы все еще нужно решать вам.
LuaJIT FFI GC
При использовании FFI мы можем быть смущены: кто будет управлять памятью, запрошенной в FFI? Должны ли мы освобождать ее вручную на языке C, или LuaJIT должен автоматически освобождать ее?
Вот простой принцип: LuaJIT отвечает только за ресурсы, выделенные им самим; ffi.
Например, если вы запрашиваете блок памяти с помощью ffi.C.malloc, вам нужно будет освободить его с помощью парного ffi.C.free. В официальной документации LuaJIT есть пример эквивалента.
local p = ffi.gc(ffi.C.malloc(n), ffi.C.free) ... p = nil -- Последняя ссылка на p исчезла. -- GC в конечном итоге запустит финализатор: ffi.C.free(p)
В этом коде ffi.C.malloc(n) запрашивает участок памяти, а ffi.gc регистрирует функцию обратного вызова для разрушения ffi.C.free, которая затем будет автоматически вызвана, когда cdata p будет собран сборщиком мусора LuaJIT, чтобы освободить память на уровне C. И cdata собирается сборщиком мусора LuaJIT. LuaJIT автоматически освободит p в приведенном выше коде.
Обратите внимание, что если вы хотите запросить большой блок памяти в OpenResty, я рекомендую использовать ffi.C.malloc вместо ffi.new. Причины также очевидны.
ffi.newвозвращаетcdata, который является частью памяти, управляемой LuaJIT.- У сборщика мусора LuaJIT есть верхний предел управления памятью, и LuaJIT в OpenResty не имеет включенной опции GC64. Следовательно, верхний предел памяти для одного воркера составляет всего 2G. Как только верхний предел управления памятью LuaJIT будет превышен, это вызовет ошибку.
При использовании FFI нам также нужно особенно внимательно относиться к утечкам памяти. Однако все совершают ошибки, и пока код пишут люди, всегда будут баги.
Именно здесь пригодится мощная экосистема тестирования и отладки OpenResty.
Давайте сначала поговорим о тестировании. В системе OpenResty мы используем Valgrind для обнаружения утечек памяти.
Тестовый фреймворк, о котором мы говорили в предыдущем курсе, test::nginx, имеет специальный режим обнаружения утечек памяти для запуска наборов тестовых случаев; вам нужно установить переменную окружения TEST_NGINX_USE_VALGRIND=1. Официальный проект OpenResty будет полностью протестирован в этом режиме перед выпуском версии, и мы подробнее расскажем об этом в разделе тестирования позже.
CLI OpenResty resty также имеет опцию --valgrind, которая позволяет вам запускать код на Lua отдельно, даже если вы не написали тестовый случай.
Давайте посмотрим на инструменты отладки.
OpenResty предоставляет расширения на основе systemtap для выполнения динамического анализа программ OpenResty в реальном времени. Вы можете найти ключевое слово gc в наборе инструментов этого проекта, и вы увидите два инструмента, lj-gc и lj-gc-objs.
Для оффлайн-анализа, такого как core dump, OpenResty предоставляет набор инструментов GDB, и вы также можете найти gc в нем и найти три инструмента: lgc, lgcstat и lgcpath.
Конкретное использование этих инструментов отладки будет подробно рассмотрено в разделе отладки позже, так что вы можете получить представление сейчас. В конце концов, OpenResty имеет целый набор инструментов, чтобы помочь вам найти и решить эти проблемы.
lua-resty-core
Из вышеприведенного сравнения мы видим, что подход FFI не только более лаконичен в коде, но также может быть оптимизирован LuaJIT, что делает его лучшим выбором. OpenResty устарел от реализации CFunction, и производительность была удалена из кодовой базы. Новые API теперь реализованы в репозитории lua-resty-core через FFI.
До выпуска OpenResty 1.15.8.1 в мае 2019 года lua-resty-core не был включен по умолчанию, что приводило к потерям производительности и потенциальным ошибкам, поэтому я настоятельно рекомендую всем, кто все еще использует историческую версию, вручную включить lua-resty-core. Вам нужно добавить всего одну строку кода в фазу init_by_lua.
require "resty.core"
Конечно, в выпуске 1.15.8.1 была добавлена директива lua_load_resty_core, и lua-resty-core включен по умолчанию.
Я лично считаю, что OpenResty все еще слишком осторожен в отношении включения lua-resty-core, и открытые проекты должны включать подобные функции по умолчанию как можно скорее.
lua-resty-core не только перереализовал некоторые API из проекта lua-nginx-module, такие как ngx.re.match, ngx.md5 и т.д., но также реализовал несколько новых API, таких как ngx.ssl, ngx.base64, ngx.errlog, ngx.process, ngx.re.split, ngx.resp.add_header, ngx.balancer, ngx.semaphore и т.д., которые мы рассмотрим позже в главе API OpenResty.
Заключение
Сказав все это, я хотел бы заключить, что FFI, хотя и хорош, не является серебряной пулей для производительности. Основная причина его эффективности заключается в том, что он может быть отслежен и оптимизирован JIT. Если вы пишете код на Lua, который не может быть JIT-компилирован и должен выполняться в интерпретируемом режиме, то FFI будет менее эффективным.
Итак, какие операции могут быть JIT-компилированы, а какие нет? Как мы можем избежать написания кода, который не может быть JIT-компилирован? Я расскажу об этом в следующем разделе.
Наконец, практическое домашнее задание: можете ли вы найти один или два API как в lua-nginx-module, так и в lua-resty-core, а затем сравнить различия в тестах производительности? Вы сможете увидеть, насколько значительным является улучшение производительности FFI.
Добро пожаловать оставить комментарий, и я поделюсь вашими мыслями и достижениями, а также приглашаю вас поделиться этой статьей с вашими коллегами и друзьями, чтобы вместе обмениваться опытом и прогрессировать.