- PVSM.RU - https://www.pvsm.ru -

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

lapisopenresty, Lua, Nginx

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

Вступление

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

LuaJIT
LuaJIT [2] — это самая быстрая реализация Lua (JIT-компилятор), настоящее произведение искусства. По некоторым оценкам [3], имеет шестикратное преимущество перед стандартным интерпретатором Lua и во многих [4] тестах [5] побивает [6] 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 [7] — один из самых эффективных веб-серверов, разработанный Игорем Сысоевым. Многие крупные сайты используют Nginx. Начиная с версии 0.8 появилась возможность [8] напрямую встраивать язык Lua. Lua исполняется в самом процессе Nginx, а не выносится в отдельный процесс, как это происходит в случае с другими языками. Код на Lua в контексте Nginx выполняется в неблокирующем режиме, включая запросы к БД и внешние HTTP-запросы, порожденные веб-приложением (например, запрос к API другого сайта).

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

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

lapis
Lapis [11] — это веб-фрейморк для написания веб-приложений на 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 [12], green threads [13], а в терминологии Lua просто threads). Весь код, обрабатывающий запрос от пользователя, исполняется в отдельной сопрограмме, а сопрограммы могут останавливаться и продолжаться в определенных местах. В нашем примере такое место было внутри вызова функции http.simple [14].

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

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

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

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

lapis

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

lapis2

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

Lapis

1 апреля 2014 года на Хабре была опубликована первоапрельская статья «LUA в nginx: лапшакод в стиле inline php» [18]. В статье рассматривался шуточный код, реализующий PHP-подобные шаблоны на Lua. В комментариях к той же статье упомянули [19] о 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 ""; Подробнее об этом написано в документации [14].

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

$ lapis server

web-proxy

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

web-proxy result

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

Заключение

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

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

Ссылки

Автор: starius

Источник [34]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/nginx/71851

Ссылки в тексте:

[1] Lua: http://www.lua.org

[2] LuaJIT: http://luajit.org/luajit.html

[3] некоторым оценкам: https://github.com/LewisJEllis/awesome-lua#implementations-and-interpreters

[4] многих: https://attractivechaos.github.io/plb/

[5] тестах: https://gist.github.com/spion/3049314

[6] побивает: https://github.com/starius/lang-bench

[7] Nginx: http://nginx.org

[8] возможность: http://wiki.nginx.org/HttpLuaModule

[9] OpenResty: http://openresty.org/

[10] MoonScript: http://moonscript.org/

[11] Lapis: http://leafo.net/lapis/

[12] coroutines: http://www.lua.org/pil/9.html

[13] green threads: https://en.wikipedia.org/wiki/Green_threads

[14] http.simple: http://leafo.net/lapis/reference/utilities.html#making-http-requests

[15] вот-вот: http://wingolog.org/archives/2013/05/08/generators-in-v8

[16] замечательную статью: http://habrahabr.ru/post/201826/

[17] бенчмарке: http://www.techempower.com/benchmarks/

[18] статья «LUA в nginx: лапшакод в стиле inline php»: http://habrahabr.ru/post/217773/

[19] упомянули: http://habrahabr.ru/post/217773/#comment_7454247

[20] отчёт: https://github.com/leafo/lapis/issues/172

[21] Postgres: http://leafo.net/lapis/reference/database.html

[22] классы-обертки: http://leafo.net/lapis/reference/database.html#models

[23] генерация HTML: http://leafo.net/lapis/reference/html_generation.html

[24] etlua: http://leafo.net/lapis/reference/etlua_templates.html

[25] кеширование: http://leafo.net/lapis/reference/utilities.html#caching

[26] защита от CSRF: http://leafo.net/lapis/reference/utilities.html#csrf-protection

[27] два инструмента для тестирования: http://leafo.net/lapis/reference/testing.html

[28] интерактивная Lua-консоль: http://leafo.net/lapis/reference/lapis_console.html

[29] документация по Lapis: http://leafo.net/lapis/reference.html

[30] подборка хороших ссылок по Lua: https://github.com/LewisJEllis/awesome-lua

[31] пессимистичное: http://calculist.org/blog/2011/12/14/why-coroutines-wont-work-on-the-web/

[32] оптимистичное: https://medium.com/code-adventures/callbacks-vs-coroutines-174f1fe66127

[33] доступны на GitHub: https://github.com/starius/harb-lapis/

[34] Источник: http://habrahabr.ru/post/240217/