LUA в nginx: лапшакод в стиле inline php

от автора


Не стоит воспринимать данный пост всерьез. Несмотря на то, что это работает, в текущем виде данное решение является исключительно забавным концептом и не более того. Так же пост ни в коей мере не является усмешкой над 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, если кто упустил или начал с конца, но хочет грянуть подробности.

Всем желаю не поддаваться на провокации 🙂

ссылка на оригинал статьи http://habrahabr.ru/post/217773/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *