Не стоит воспринимать данный пост всерьез. Несмотря на то, что это работает, в текущем виде данное решение является исключительно забавным концептом и не более того. Так же пост ни в коей мере не является усмешкой над php, который является одним из основных моих рабочих инструментов.
Думаю, что все разработчики на PHP (включая меня) так или иначе проходили через период, когда код представляет из себя жуткую смесь html и php, напиханных в одном файле. И речь не о шаблонах, а вообще о всей логике в лапше/спагетти-коде.
И в качестве концепта я решил к первому апреля набросать реализацию чего-то подобного, но на lua под nginx. Прямо как на картинке.
Скрипты можно писать примерно такие (ссылка, по которой отзывается данный код):
<?lml tmpl:include('sugar') ?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Сейчас <?lml print(ngx.utctime()) ?></title>
</head>
<body>
<?lml local alc = require('lib.alc') ?>
Привет, <?lml print(esc(req:get('name', 'traveler')), '/', ngx.var.remote_addr) ?>.
Это уже <?lml print(alc:inc('cnt')) ?> запрос с последнего перезапуска сервера.
<?lml
local hdrs = {}
for k,v in pairs(ngx.req.get_headers()) do
table.insert(hdrs, '<tr><td style="font-weight:bold;">'..esc(k)..'</td><td>'..esc(v)..'</td></tr>')
end
?>
<h3>Заголовки <?lml print(ngx.req.get_method()) ?> запроса к <?lml print(esc(ngx.var.request_uri)) ?></h3>
<table><?lml print(hdrs) ?></table>
<?lml include('footer') ?>
Т.е. полноценный lua в лапшастиле. Для проверки работы были реализованы:
- непосредственно сам «шаблонизатор»;
- близкий аналог APC: всякие store/fetch/cas и т.п. + compile_string/compile_file для кеширования байткода скомпилированных шаблонов;
- ob_* функции без поддержки вложенности (нет необходимости);
- всякая мелочь для замены htmlspecialchars, $_GET[name] и т.п.
Возможно, кому-то будет интересно почитать о реализации. Кому же интересен только код — выложил на github, хоть там кода и кот наплакал.
Вся работа основана на следующем:
- LUA позволяет в runtime скомпилировать исходный код, представленный строкой, в функцию (на вход строка, на выходе function (callable в терминах php/java)). За это отвечает функция loadstring;
- Для имеющейся function можно в runtime получить ее байткод через вызов string.dump;
- Получить function обратно из байткода можно через все ту же loadstring;
- Для кеширования в оперативке используется ngx.shared.DICT, работу с которым я уже описывал ранее;
- Немного кручу-верчу для соединения этого всего воедино.
Для начала конфигурируем сам nginx:
http {
lua_shared_dict lml_shared 10m;
lua_package_path '/path/to/lml/?.lua;;';
}
# имя location и пути могут быть, само собой, произвольными
location /lml {
# грузим шаблонизатор и выводим шаблон index (по умолчанию, это файл /path/to/lml/tmpl/index.lml)
content_by_lua '
local tmpl = require "lib.tmpl"
tmpl:set_root("/path/to/lml/tmpl/")
tmpl:include("index")
';
}
Обработка шаблонов простейшая: весь текст вне тегов <?lml ?> заворачивается в stdout:print(ТЕКСТ), а содержимое тегов оставляется как есть, выкидывая только сами границы тегов. HTML текст в print заворачивается в многострочные литералы, чтобы не пришлось экранировать символы внутри:
stdout:print([[Hello
world
]])
Но, т.к. возможна ситуация использования границ литерала внутри шаблона(Hello [[<?lml ?>]] World), то шаблонизатор ищет «свободный» вариант границ многострочного литерала, итерационно наращивая его длину:
print([[...]])
print([=[...]=])
print([==[...]==])
...
Компиляция в байткод по аналогии с php вынесена из шаблонизатора в опкод кешер, бесхитростно названный ALC (Alternative Lua Cache).
В самом минимальном исполнении кеширование байткода выглядит так (это крайне урезанная версия! не стоит рассматривать ее как минимальный, но рабочий пример):
function M:compile_string(str, filename)
local cache_key = 'tmpl_bytecode:' .. filename
local bytecode, created_at = cache:get(cache_key)
local lua_func = nil
if not bytecode then
locked = cache:add(key_lock, 1, key_lock_ttl)
bytecode, created_at = cache:get(cache_key)
if not bytecode then
if type(str) == 'function' then
str = str(filename)
end
lua_func = assert(loadstring(str, filename))
bytecode = assert(string.dump(lua_func))
end
if locked then
if lua_func and bytecode then
cache:set(cache_key, bytecode, 0, ngx.now())
end
cache:delete(key_lock)
end
end
if (not lua_func) and bytecode then
lua_func = loadstring(bytecode, filename)
end
return lua_func
end
Передав строку с lua кодом, на выходе получаем function, готовую для выполнения, а в оперативке у нас теперь лежит байткод.
Соотвественно, в шаблонизаторе достаточно вызвать соответствуйщий метод, подсунув ему нужные данные:
local function _include_string(str, filename)
local lua_func = alc:compile_string(str, filename)
if lua_func then
lua_func()
end
end
function M:include_string(str, filename)
local succ, err = pcall(_include_string, str, filename)
if not succ then
ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
local errstr = 'Error (' .. filename .. '): ' .. err
ngx.log(ngx.ERR, errstr)
ngx.say(errstr)
return ngx.exit(ngx.HTTP_OK)
end
return succ
end
-- Для загрузки из файла на диске (как раз тот случай, который используется в самих шаблонах и location nginx'а):
function M:include(name)
local path = root_path .. name .. file_ext
M:include_string(
function(filename)
local str = assert(file:read_all(filename))
return assert(parse_tmpl(str, filename))
end,
path
)
end
Передача в alc:compile_string анонимной функции вместо содержимого файла позволяет не обращаться к диску без необходимости в случае, если байткод уже есть в кеше. Получаем ленивую отложенную загрузку содержимого шаблонов только при необходимости.
Вся функциональность распределена по небольшим модулям: шаблонизатор в lib.tmpl, кешер в lib.alc, вывод и буферизация вывода в lib.stdout и т.д. В шаблонах для работы с модулями в общем случае требуется явная их загрузка и обращение к функциям по полным именам:
-- некий шаблон example.lml
<?lml
local stdout = require('lib.stdout')
local html = require('lib.html')
local tmpl = require('lib.tmpl')
tmpl:include('header')
stdout:print(html:escape(ngx.var.request_uri))
?>
Это явно и понятно, но в качестве «сахара» часть модулей сделаны обязательными и подключаются автоматически через генерацию в коде префикса с подгрузкой этих модулей:
local required_libs = {'stdout', 'html', 'req', 'tmpl'}
-- tmpl_chunks содержит куски lua кода, полученного из lml шаблона
-- добавляем в начало кода подгрузку всех обязательных модулей
for _,l in ipairs(required_libs) do
table.insert(tmpl_chunks, 1, 'local '..l..' = require("lib.'..l..'");')
end
Теперь эти модули можно сразу использовать в шаблоне:
-- некий шаблон example.lml
<?lml
tmpl:include('header')
stdout:print(html:escape(ngx.var.request_uri))
?>
В дополнение к этому были подслащены еще и наиболее часто используемые функции, такие как stdout:print, tmpl:include, html:escape. Сделано это было для примера уже на уровне lml шаблонов:
-- sugar.lml
<?lml
function include(...)
tmpl:include(...)
end
function print(...)
stdout:print(...)
end
function esc(...)
return html:escape(...)
end
?>
-- некий шаблон example.lml
<?lml
tmpl:include('sugar')
include('header')
print(esc(ngx.var.request_uri))
?>
Данное решение является палкой о двух концах и сделано для приведения кода шаблонов ближе к стилистике php.
В заключение сферический тест производительности данного велосипеда в сравнении с php-fpm+apc на простейшем «домашнем сервачке» с Athlon II, ссылка на который приведена в начале поста.
Сравнение происходило со столь же примитивным php кодом из 3х файлов с максимальной адаптацией.
Пока что тестировал через siege по 100Мбит локалке, так что кое где производительность упиралась в сетку.
Запуск через siege -cX -t300S -b URL показал следующие trans/sec:
-c10 | -c100 | -c200 | -c500 | |
---|---|---|---|---|
php-fpm | 3350 | 3150 уперся в cpu | http 502 * | http 502 * |
lml без опкешера | не тестил | 6950 | не тестил | не тестил |
lml с опкешером | 7000 | 8100 уперся в if | 8200 уперся в if | 7500 уперся в if |
* массовые connect() to unix:/var/run/php-fpm-*.sock failed (11: Resource temporarily unavailable)
Вроде не так и ужасно.
Еще раз ссылка на github, если кто упустил или начал с конца, но хочет грянуть подробности.
Всем желаю не поддаваться на провокации :)
Автор: AterCattus