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 , якщо хто упустив або почав з кінця, але хоче гримнути подробиці.
 
 Всім бажаю не піддаватися на провокації :)
  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.