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

Веб-сервер 08.09.2017 20:37
Документ без названия
Вступление


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 <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">request</span> = require(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"request"</span>); <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">request</span>.<span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">get</span>(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"http://www.whatever.com/my.csv"</span>, <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">function</span> (<span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">error</span>, <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">response</span>, body) { <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">if</span> (!<span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">error</span> &amp;&amp; <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">response</span>.statusCode == <span class="hljs-number" style="box-sizing: inherit; color: #986801;">200</span>) { console.<span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">log</span>(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"Got body: "</span> + body); } }); 

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

<span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">local</span> <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">http</span> = <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">require</span>(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"lapis.nginx.http"</span>) <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">local</span> body, status_code, headers = <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">http</span>.simple(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"http://www.whatever.com/my.csv"</span>) <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">if</span> status_code == <span class="hljs-number" style="box-sizing: inherit; color: #986801;">200</span> <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">then</span> print(body) <span class="hljs-function" style="box-sizing: inherit;"><span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">end</span> </span>

Код для Lapis писать удобно, как будто он синхронный, но за кулисами он исполняется полностью асинхронно. Это возможно благодаря активному использованию сопрограмм (coroutinesgreen 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).
<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">$ </span>wget http://openresty.org/download/ngx_openresty<span class="hljs-number" style="box-sizing: inherit; color: #986801;">-1.7</span><span class="hljs-number" style="box-sizing: inherit; color: #986801;">.4</span><span class="hljs-number" style="box-sizing: inherit; color: #986801;">.1</span>.tar.gz <span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">$ </span>tar xzvf ngx_openresty<span class="hljs-number" style="box-sizing: inherit; color: #986801;">-1.7</span><span class="hljs-number" style="box-sizing: inherit; color: #986801;">.4</span><span class="hljs-number" style="box-sizing: inherit; color: #986801;">.1</span>.tar.gz <span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">$ </span>cd ngx_openresty<span class="hljs-number" style="box-sizing: inherit; color: #986801;">-1.7</span><span class="hljs-number" style="box-sizing: inherit; color: #986801;">.4</span><span class="hljs-number" style="box-sizing: inherit; color: #986801;">.1</span>/ <span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">$ </span>./configure <span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">$ </span>make # make install 


Установка Lapis

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

<span class="hljs-meta" style="box-sizing: inherit; color: #4078f2;"># luarocks install lapis</span> 


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

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

$ lapis new --lua wrote nginx<span class="hljs-selector-class" style="box-sizing: inherit; color: #986801;">.conf</span> wrote mime<span class="hljs-selector-class" style="box-sizing: inherit; color: #986801;">.types</span> wrote app<span class="hljs-selector-class" style="box-sizing: inherit; color: #986801;">.lua</span> 

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

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

<span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">local</span> lapis = <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">require</span>(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"lapis"</span>) <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">local</span> app = lapis.Application() <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">local</span> <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">http</span> = <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">require</span>(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"lapis.nginx.http"</span>) app:<span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">get</span>(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"/"</span>, <span class="hljs-function" style="box-sizing: inherit;"><span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">function</span>(<span class="hljs-title" style="box-sizing: inherit; color: #4078f2;">self</span>)</span> <span class="hljs-literal" style="box-sizing: inherit; color: #0184bb;">return</span> [[ &lt;form method=<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"POST"</span> action=<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"/geturl"</span>&gt; &lt;input type=<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"text"</span> <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">value</span>=<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"http://ip4.me/"</span> name=<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"url"</span> /&gt; &lt;input type=<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"submit"</span> <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">value</span>=<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"Get"</span> /&gt; &lt;/form&gt; ]] <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">end</span>) app:<span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">post</span>(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"/geturl"</span>, <span class="hljs-function" style="box-sizing: inherit;"><span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">function</span>(<span class="hljs-title" style="box-sizing: inherit; color: #4078f2;">self</span>)</span> <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">local</span> url = self.req.params_post.url <span class="hljs-built_in" style="box-sizing: inherit; color: #c18401;">local</span> body, status_code, headers = <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">http</span>.simple(url) <span class="hljs-literal" style="box-sizing: inherit; color: #0184bb;">return</span> body <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">end</span>) <span class="hljs-literal" style="box-sizing: inherit; color: #0184bb;">return</span> app 

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

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

 location / { set $_url <span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">""</span>; default_type text/html; content_by_lua ' require(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"lapis"</span>).serve(<span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"app"</span>) '; } location /proxy { <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">internal</span>; rewrite_by_lua ' local req = ngx.req <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">for</span> k,v <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">in</span> pairs(req.get_headers()) <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">do</span> <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">if</span> k ~= <span class="hljs-string" style="box-sizing: inherit; color: #50a14f;">"content-length"</span> <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">then</span> req.clear_header(k) <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">end</span> <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">end</span> <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">if</span> ngx.ctx.headers <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">then</span> <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">for</span> k,v <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">in</span> pairs(ngx.ctx.headers) <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">do</span> req.set_header(k, v) <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">end</span> <span class="hljs-keyword" style="box-sizing: inherit; color: #a626a4;">end</span> '; resolver <span class="hljs-number" style="box-sizing: inherit; color: #986801;">8.8</span><span class="hljs-number" style="box-sizing: inherit; color: #986801;">.8</span><span class="hljs-number" style="box-sizing: inherit; color: #986801;">.8</span>; proxy_http_version <span class="hljs-number" style="box-sizing: inherit; color: #986801;">1.1</span>; 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 давно перерос уровень первоапрельской шутки и стремительно набирает позиции в сообществе веб-разработчиков. Желаю приятного изучения этих перспективных технологий!

Ссылки



Источник: https://habrahabr.ru/post/240217/ (253 перехода)

Файлы статьи:

Похожие статьи

  • Коллекция конфигураций Nginx для самых популярных CMS
    Список CMS конфигураций для nginx:

    Asgard CMS
    Bolt CMS
    CMS Made Simple
    Codeigniter
    Concerte5
    CraftCMS 2
    Data Life Engine
    Drupal 7, 8
    FuelPHP
    HostCMS
    ImpressPages
    InstantCMS
    Invision Power Board 3
    Joomla 2, 3
    KodiCMS
    Kohana
    Laravel
    LiveStreet
    MaxSite CMS
    MediaWiki
    MODx Revolution
    Octobercms
    OpenCart 1.5
    OsTicket
    Phalcon
    phpBB3
    ProcessWire 2
    Symfony
    UMI.CMS
    WebAsyst
    Wordpress 4
    Wordpress 4 + SuperCache
    Yii Advanced
    Yii Basic
    ZenCart 1.5
    Zend Framework
    1C Bitrix


  • Защита для NGINX: Как заблокировать, SQL Injections, File Injections, Spam, User Agents, Etc.
    Измените свой Nginx Vhosts

    Настройки, которые мы используем здесь, должен быть вставлены в каждый nginx vhost (в server {} контейнер), где Вы хотите использовать его. К сожалению, его нельзя использоваться в глобальном конфиге nginx, потому что директива набора не позволена в http {} контейнер.
  • Настройка сети в Debian 8
    Настройка сети в дистрибутиве Debian вещь довольно простая, для этого есть файл interfaces в котором содержаться все необходимые для этого настройки. Итак, открываем сам файл(не забудьте сделать это под root или sudo, иначе не будет прав на запись, я использую редактор “nano” ([root@vpsserver ~]#nano /etc/network/interfaces ), хоть и говорят, что он не тру для админов, но мне он нравиться):
0 комментариев
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.