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 уперся в сеть 8200 уперся в сеть 7500 уперся в сеть

    * массовые connect() to unix:/var/run/php-fpm-*.sock failed (11: Resource temporarily unavailable)

    Вроде не так и ужасно.

    Еще раз ссылка на github, если кто упустил или начал с конца, но хочет грянуть подробности.

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

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 21

      +3
      Весьма забавно. А ведь действительно на Lua можно делать сайты не хуже PHP. Отсутсвуют только всякого рода фреймворки.
      Я бы даже был бы рад, если бы исторически эту нишу захватил бы Lua вместо PHP.
        0
        >А ведь действительно на Lua можно делать сайты
        народ уже делает, но только небольшие но нагруженные задачки
        >Отсутсвуют только всякого рода фреймворки
        во времена РНР3 они тоже отсутствовали, так что наши мастера скоро напишут
          –1
          Если бы он ещё захватил нишу JS, было бы совсем отлично
          +1
          На луа есть абсолютно все что нужно для создания полноценных хайлоад сайтов. Если вдруг чего-то не хватает, то есть множество хороших библиотек для луа. Если и их не хватает, то есть модуль FFI — через него можно подключить любую библиотеку на си.

          Всю внешнюю часть я пишу на луа, а админки на пхп — там нету большой нагрузки.
            0
            А с luajit тестировали?
              0
              Это и работает через luajit.
                0
                В статье об этом ни слова.
                  0
                  Я, учтя рекомендации авторов lua-nginx-module, сразу везде использую jit версию. И для меня это стало уже как-то ествественно и очевидно :)
              +3
              Думаю, что все разработчики на PHP (включая меня) так или иначе проходили через период, когда код представляет из себя жуткую смесь html и php, напиханных в одном файле.

              Разработчики WordPress так и остановились на этом этапе, видимо.
                0
                что такое
                уперся в if

                ? сетка?
                  0
                  Да, стандартное обозначение сетевого интерфейса. lua отдавал быстрее, чем исходящий канал успевал через себя прокачать.
                    +1
                    О, может быть нужно пояснить? Я первым делом подумал о конструкции if (если) в nginx, т.к. о ней кучу раз говорили, что она не особо производительная.
                      0
                      Ок, поменял.
                        +1
                        подумал о конструкции if (если) в nginx, т.к. о ней кучу раз говорили, что она не особо производительная
                        Но уж раз в 100 будет производительнее чем lua.
                    0
                    может быть стоит попробовать lapis?
                      0
                      На данный момент нет цели использовать что-либо подобное на реальных задачах. Это лишь первоапрельский концепт, написанный за несколько часов.
                        0
                        сорри, я как обычно думал что это всерьёз)
                          +1
                          Но концепт можно допилить, и найти применение ему в хайлоаде :)
                            0
                            В нашей команде на всяких lua, python, java и так только я один пишу, что нифига не хорошо для проекта в случае чего. Делать на lua бизнес логику явно на данный момент не стоит, ограничиваясь небольшими задачами. Вот как хобби — возможно, но пока я пилю другую прикольную штуку :)
                        0
                        Кстати, в неком игровом проекте Multi Theft Auto подобная реализация используется уже на протяжении долгих лет (html + inline Lua), только в купе с Embedded HTTP Server.
                          0
                          В конфигурации «LAMP» они заменили «AMP» :)

                        Only users with full accounts can post comments. Log in, please.