Lapis: сайт на Lua в конфигах Nginx

    lapisopenresty, Lua, Nginx

    Tl;dr Lapis(Lua) = RoR(Ruby) = Django(Python)


    Вступление



    image
    Lua — мощный и быстрый скриптовый язык, который очень легко встраивается в C. Разработан в PUC-Rio (Бразилия).

    LuaJIT
    LuaJIT — это самая быстрая реализация Lua (JIT-компилятор), настоящее произведение искусства. По некоторым оценкам, имеет шестикратное преимущество перед стандартным интерпретатором Lua и во многих тестах побивает V8. Разработчик Mike Pall (Германия).

    А ещё LuaJIT может привязать функции и структуры C на стороне Lua (без написания привязок на C):
    local ffi = require("ffi")
    ffi.cdef[[int printf(const char *fmt, ...);]]
    ffi.C.printf("Hello %s!\n", "wiki")
    


    Nginx
    Nginx — один из самых эффективных веб-серверов, разработанный Игорем Сысоевым. Многие крупные сайты используют Nginx. Начиная с версии 0.8 появилась возможность напрямую встраивать язык Lua. Lua исполняется в самом процессе Nginx, а не выносится в отдельный процесс, как это происходит в случае с другими языками. Код на Lua в контексте Nginx выполняется в неблокирующем режиме, включая запросы к БД и внешние HTTP-запросы, порожденные веб-приложением (например, запрос к API другого сайта).

    OpenResty
    OpenResty — это сборка Nginx с множеством сторонних модулей, в том числе для неблокирующего доступа к популярным БД. Последние версии используют LuaJIT для исполнения Lua. Разработчик Yichun Zhang (США, место работы: CloudFlare, основной разработчик lua-nginx-module).

    MoonScript
    Sailor MoonScript — это скриптовый язык, который транслируется в Lua. Добавляет синтаксический сахар, упрощает написание некоторых вещей, например списковых выражений; реализует классы с наследованием. Можно сказать, что MoonScript для Lua — это CoffeeScript для JavaScript. Разработчик Leaf Corcoran (США).

    lapis
    Lapis — это веб-фрейморк для написания веб-приложений на Lua и MoonScript, который живёт внутри OpenResty. Разработчик Leaf Corcoran (США).

    Какое же преимущество дает Lua в Nginx?


    Tl;dr Все возможности языка высокого уровня и эффективное использование ресурсов при больших нагрузках

    Для ответа вернёмся в далёкое прошлое, когда все сайты обслуживались веб-сервером Apache.
    Apache
    Задержки вносят красные узлы и ребра графа. Желтым закрашены компоненты, расположенные на одной машине.

    Аpache выделял отдельный поток операционной системы, который читал запрос, выполнял обработку и отправлял результат пользователю. (Современный Apache можно научить так не делать.) Получается, сколько активных запросов, столько и потоков ОС, а они стоят дорого. Бóльшая часть времени жизни потока при такой схеме расходуется не на обработку запроса, а на передачу данных по сети, лимитированную скоростью интернета у пользователя.

    Nginx
    Как с этим бороться? Надо поручить операционной системе следить за передачей данных, чтобы нашему веб-серверу работать только тогда, когда сеть выполнила очередную задачу. Такой подход называется неблокирующим вводом-выводом и реализуется в большинстве современных ОС. Веб-сервер Nginx использует эту возможность, за счёт чего может обслуживать десятки тысяч одновременных запросов, используя всего один поток ОС.

    Nginx, PHP
    Таким образом мы оптимизировали передачу данных между браузером и веб-сервером, но есть ещё одно узкое место, на котором простаивают потоки ОС: работа с базой данных и внешними ресурсами (например, HTTP-API другого сайта). Важно понять, что дело не столько в неизбежных задержках самой базы данных или внешнего API, а в том, что наше веб-приложение бездарно простаивает, пока не получит от них ответ.

    Обычное решение: уже в самом веб-приложении наплодить потоков, от которых мы так успешно избавились в веб-сервере. Эффективное решение: сделать так, чтобы веб-приложение и база данных общались неблокирующим способом. Веб-приложение направляет запрос в базу данных и сразу же переходит к следующему запросу от пользователя. База данных считает, возвращает результат, а веб-приложение, когда освободится, возвращается к обработке запроса от пользователя, породившего данный запрос к базе данных. Такой подход используется, например, в node.js:
    node.js
    БД и внешние API по-прежнему закрашены красным, так как они могут вносить задержку. Преимущество подхода в том, что веб-приложение не просто так их ждёт, а обрабатывает в это время другие запросы.

    Замечательно! Теперь посмотрим, как происходит программирование внешних HTTP-запросов в node.js:

    var request = require("request");
    request.get("http://www.whatever.com/my.csv", function (error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log("Got body: " + body);
        }
    });
    

    Допустим, мы хотим скачать файл по URL и что-то с ним сделать. Результат приходится обрабатывать в лямбда-функции. Неудобно? Это неизбежная плата за асинхронность? К счастью, это не так; посмотрим аналогичный код в Lapis:

    local http = require("lapis.nginx.http")
    local body, status_code, headers = http.simple("http://www.whatever.com/my.csv")
    if status_code == 200 then
        print(body)
    end
    

    Код для Lapis писать удобно, как будто он синхронный, но за кулисами он исполняется полностью асинхронно. Это возможно благодаря активному использованию сопрограмм (coroutines, green threads, а в терминологии Lua просто threads). Весь код, обрабатывающий запрос от пользователя, исполняется в отдельной сопрограмме, а сопрограммы могут останавливаться и продолжаться в определенных местах. В нашем примере такое место было внутри вызова функции http.simple.

    Почему же сопрограммы эффективнее потоков ОС? Не перетащили ли мы все накладные расходы в приложение? На самом деле, ключевым отличием сопрограмм от потоков ОС является свобода программиста, в каком именно месте сопрограмма засыпает и просыпается. (В случае потоков ОС решение принимает ОС.) Начали запрос к БД — усыпили сопрограмму, породившую запрос. Пришёл ответ от БД — будим сопрограмму и продолжаем её исполнение. Выполняем одновременно много дел и всё в одном потоке ОС!

    Примечание. Похожий механизм вот-вот появится в node.js.

    Примечание. Советую прочитать замечательную статью про сопрограммы в контексте C++. В конце статьи получился асинхронный код, записываемый как синхронный, и всё благодаря сопрограммам. Жалко, что в C++ сопрограммы являются скорее хаком, чем общепринятым приёмом.

    Помимо этого, Lapis исполняется непосредственно в Nginx, что исключает накладные расходы на передачу информации между Nginx и веб-приложением. Конечно, node.js можно использовать как основной веб-сервер, без Nginx, но тогда пропадает возможность использовать разные возможности Nginx.

    lapis

    С другой стороны, не каждый решится пустить код на Lua прямо в основной Nginx. В таком случае запускаем отдельный Nginx с Lua от имени отдельного пользователя с урезанными правами, а в основном Nginx прописываем прокси.

    lapis2

    Эффективность Lapis подтверждается в 10-гигабитном бенчмарке. Lapis занимает лидирующие места на уровне языков C++ и Java.

    Lapis


    1 апреля 2014 года на Хабре была опубликована первоапрельская статья «LUA в nginx: лапшакод в стиле inline php». В статье рассматривался шуточный код, реализующий PHP-подобные шаблоны на Lua. В комментариях к той же статье упомянули о Lapis. Других упоминаний о Lapis на Хабре я не нашел, поэтому решил написать сам.

    Писать Hello World скучно. Давайте вместо этого напишем простенький веб-прокси на Lapis.

    Установка OpenResty


    Установите perl 5.6.1+, libreadline, libpcre и libssl и убедитесь, что доступна команда ldconfig (её родительская папка может отсутствовать в PATH).
    $ wget http://openresty.org/download/ngx_openresty-1.7.4.1.tar.gz
    $ tar xzvf ngx_openresty-1.7.4.1.tar.gz
    $ cd ngx_openresty-1.7.4.1/
    $ ./configure
    $ make
    # make install
    


    Установка Lapis


    Сначала надо установить LuaRocks (есть в основных дистрибутивах).

    # luarocks install lapis
    


    Создаем веб-приложение


    Создаем костяк сайта:

    $ lapis new --lua
    wrote	nginx.conf
    wrote	mime.types
    wrote	app.lua
    

    Если бы мы не передали опцию --lua, то был бы создан костяк на языке MoonScript.

    Теперь реализуем в app.lua логику нашего приложения: на главной странице сайта отображается форма для ввода URL. Форма отправляется на /geturl, где происходит загрузка страницы по указанному URL и передача содержимого в браузер пользователя.

    local lapis = require("lapis")
    local app = lapis.Application()
    local http = require("lapis.nginx.http")
    
    app:get("/", function(self)
      return [[
        <form method="POST" action="/geturl">
          <input type="text" value="http://ip4.me/" name="url" />
          <input type="submit" value="Get" />
        </form>
        ]]
    end)
    
    app:post("/geturl", function(self)
      local url = self.req.params_post.url
      local body, status_code, headers = http.simple(url)
      return body
    end)
    
    return app
    

    Главная страница просто выдает HTML-код с формой. Двойные квадратные скобки — ещё одно обозначения для строк в Lua. Страница /geturl получает POST-запрос от формы, достает из него URL, вписанный пользователем в форму, скачивает содержимое по этому URL при помощи функции http.simple (поток ОС при этом не блокируется, см. выше) и показывает результат пользователю.

    Для работы http.simple нужно изменить nginx.conf:

        location / {
          set $_url "";
          default_type text/html;
          content_by_lua '
            require("lapis").serve("app")
          ';
        }
    
        location /proxy {
          internal;
          rewrite_by_lua '
            local req = ngx.req
    
            for k,v in pairs(req.get_headers()) do
              if k ~= "content-length" then
                req.clear_header(k)
              end
            end
    
            if ngx.ctx.headers then
              for k,v in pairs(ngx.ctx.headers) do
                req.set_header(k, v)
              end
            end
          ';
    
          resolver 8.8.8.8;
          proxy_http_version 1.1;
          proxy_pass $_url;
        }
    

    Этот код создает в Nginx location /proxy, через который Lua совершает внешние запросы. В главный location нужно добавить set $_url ""; Подробнее об этом написано в документации.

    Запустим наш веб-прокси:

    $ lapis server
    


    web-proxy

    Нажимаем на кнопку «Get». Сайт ip4.me показывает IP-адрес сервера, на котором запущен Lapis.

    web-proxy result

    Если в URL отсутствует path, то в качестве path используется /proxy. Видимо, это баг Lapis'а, по которому уже составлен отчёт.

    Заключение


    В Lapis, Lua и Nginx есть ещё много интересного, например, асинхронная работа с БД Postgres, классы-обертки для объектов БД, генерация HTML, мощный язык шаблонов etlua, кеширование переменных между разными процессами-рабочими Nginx, защита от CSRF, два инструмента для тестирования и интерактивная Lua-консоль прямо в браузере. Если статья найдёт читателя, я продолжу рассказ о Lapis в других статьях.

    Без сомнения, Lapis давно перерос уровень первоапрельской шутки и стремительно набирает позиции в сообществе веб-разработчиков. Желаю приятного изучения этих перспективных технологий!

    Ссылки


    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 21
      +2
      > А ещё LuaJIT может привязать функции и структуры C на стороне Lua (без написания привязок на C):
      При условии, что C функция использует неблокирующие операции. В противном случае код будет работать в блокирующем режиме.
        0
        Совершенно верно! Хороший иллюстрация применительно к веб-прокси: можно было импортировать блокирующие функции из libcurl и делать HTTP-запрос с их помощью. Без сомнения, это привело бы к блокировке потока. В первую очередь в C имеет смысл выносить вычисления, а не работу с сетью.
          +1
          Это вообще основная проблема модуля lua-nginx — выстрелить себе в ногу, не представляя тонкости реализации, очень просто.
            0
            Применительно к nginx-lua сеть вообще не имеет смысла выносить в ffi, протсо потому, то все уже сделано до нас в виде ко-сокетов nginx.
          0
          del
            +2
            Мне MoonScript не понравился своим синтаксисом: он как-то крайне «переслащен». А исходники Lapis написаны как раз именно на нем, *.lua код генерится уже из них, и он нифига не хороший и понятный.
            Еще генерация nginx.conf шаблона видится излишней — ведь было бы достаточно проставить в нужных местах content_by_lua_file и подобные.

            offtopic. Я в мае-июле экспериментировал с исходно lua-минифреймворком, ориентирующимся на скорость, много основополагающего наваял, но потом поменял работу и пока как-то не до него)
              +1
              У меня был опыт переписывания небольшого сайта с Lua на MoonScript и я тоже не восторге от MoonScript, в нём бы убрать половину возможностей. Поначалу мне казалось, что MoonScript — это «питон без двоеточий», но обилие собак, восклицательных знаков и обратных слешей в коде убручает. Совершенно верно, что он переслащён. У Lapis и MoonScript один и тот же автор.

              Однако Lua в составе исходников Lapis, кажется, допиливается напильником после генерации из MoonScript.

              К примеру, привязка к GET и POST происходит неэквивалентным способом на Lua и MoonScript:

              Lua:
              app:post("/geturl", function(self)
                -- code
              end)
              

              MoonScript (часть класса):
              ["/geturl"]: respond_to {
                POST: =>
                  -- code
              

              В документации есть отдельные главы Введение для MoonScript, и Введение для Lua. Самое главное, что Lapis можно полноценно использовать вообще без MoonScript.
                +1
                В neovim одно время использовался moonscript. Если забыть про прочие проблемы (малое число разработчиков, синтаксис, который многим не нравится, отсутствие в ряде дистрибутивов), то мы напоролись на довольно странную проблему: тесты с moonscript уходят в бесконечный цикл, со сгенерированным lua — нет. В связи с тем, что проблема была не единственная и фактически не приводимая к простому примеру, то особо разбираться никто не стал: просто сказали, что все тесты теперь на lua (раньше сначала говорили, что все тесты на moonscript, но из-за сопротивления разработчиков разрешили lua и moonscript; теперь только lua).
              0
              А я, вот, пишу тут код ну Resty-template с полноценными темплейтами с возможностью инлайнить lua (а-ля SSI). И как-то, если честно, понравилось намного больше мунскриптов-ляписов :)
                0
                Эта затея выглядит весьма небезынтересно.

                Некоторое сомнение, правда, вызывает вот эта картинка:

                [Node.js позади nginx]

                Так как движок Node.js сам по себе способен реализовывать неблокирующий ввод и вывод (evented I/O), то ставить nginx между Node и клиентом не очень, наверное, рационально. (Проще говоря, ну что такое может nginx, с чем Node не справится при помощи пары-тройки-другой модулей? Вопрос не очень риторический.)

                Предвижу ещё, что тот подход, при котором скрипты работают непосредственно в рамках цикла событий (event loop) на сервере, действительно обретёт популярность, да вот только наиболее популярным будет сочинение таких скриптов не на Lua для nginx, а на JavaScript для Node. Это предвидение опирается на необыкновенную популярность джаваскрипта (основного языка клиентской части Всемирной Паутины) и, кроме того, на наличие множества готовых модулей для Node — на наличие таких пакетов с открытым исходным кодом, которые npm установить может.

                (В роли сервера, например, будет выступать, скорее всего, не голый движок Node, а что-то вроде Express поверх него — и это ещё по меньшей мере.)
                  0
                  >>что такое может nginx, с чем Node не справится
                  работать быстрее?
                    0
                    >>что такое может nginx, с чем Node не справится
                    эм, таким образом можно сказать и про php, он же умеет в сервер. и зачем обвешивать nodejs кучей модулей, когда, ИМХО, лучше повесить старый, надежный nginx как фронтенд перед nodejs сервером. тот же load balance, кеш, статику, логи лучше отдать nginx.
                    0
                    Справедливости ради надо сказать что ветка nodejs 0.11.xx поддерживает генераторы, которые умеют приостанавливать выполнение кода с помощью оператора yield.

                    С помощью библиотеки co ваш код будет выглядеть так

                    var co = require('co'), 
                        request = require('co-request');
                    
                    co(function* () {
                      var result = yield request('http://www.whatever.com/my.csv'); 
                      console.log("Got body: " + result.body);
                    })();
                    


                    Для обработки ошибок можно использовать try/catch
                      0
                      Сколько памяти ест? Хочется что-то динамическое и чтоб кушал 0-5 метров. Самое то для простеньких штук. Голый html хорош, но главная проблема в отсутствие шаблонов (как include и extends в jinja).
                        0
                        Памяти ест примерно как просто nginx. На моем несложном сайте при тестировании ботами было 1-2 мегабайта памяти и почти нулевая загрузка процессора.

                        В Lapis предусмотрено использование шаблонов etlua, которые позволяют исполнять любой код на Lua и предусматривают экранирование переменных. Ещё можно подключать подшаблоны (аналог include). Аналога extends, кажется, нет. Шаблоны etlua похожи на PHP, что имеет свои плюсы и минусы. Удобно, что для чего-то особого не нужно ваять свой тег, как в django, но опасно, что шаблон может в принципе что угодно. В небольших проектах преимущества такого подхода перевешивают недостатки, без сомнения.

                      0
                      Смотрим вкладку «Multiple queries» теста и что видим — lapis нижней части списка и с кучей ошибок. Что же это? Неправильно приготовленная корутина с тяжелым запросом или это неисправимо?
                      Зато вперед выходит дарт с монгой )
                        0
                        На том же тесте с другим оборудованием Lapis уже в середине списка и без ошибок. В чем дело, сказать не могу. Есть только догадка, что в том тесте могла плохо работать связка nginx-postgres. На той же странице, где Lapis-postgres в конце списка, openresty-mysql занимает неплохое место.
                        Дарт звучит интересно, надо посмотреть.
                        0
                        Для тех кто любит Lua, и Node: есть luvit (Lua + libUV)
                        github.com/luvit/luvit
                          0
                          Видео стриминг сервер с конфигами на LUA:
                          taqlim.blogspot.de/2014/11/video-and-audio-streaming-from-a20som.html

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое