Русская поговорка
Lua нравится всем, он простой, но не примитивный. Нет, скорее — продуманный, выверенный, оптимальный. Р.Иерузалимски (автор языка), в своей книге «Программирование на языке Lua» пишет: «Lua — это крошечный и простой язык». Это так. И ещё он скриптовый, переносимый, эффективный, расширяемый, склеивающий (glue). Как же такой не попробовать?
Как и многие другие, я поддался искушению заглянуть, что же из себя представляет Lua. Ну а поскольку самый лучший способ изучить язык, это написать на нём программу, я решил набросать простой веб-микрофреймворк под сервер Apache (версии 2.3+). Апач выбран потому, что он есть на каждом хостинге, и вся настройка под Lua заключается во включении модуля mod_lua.so в конфигурации сервера. Это решение конечно, будет медленнее, чем на Nginx, но возможно, нам больше и не надо?
Я с удовольствием прочитал первоапрельскую статью LUA в nginx: лапшакод в стиле inline php о создании сайта на Lua под Nginx в стиле раннего РНР. Как известно, в каждой шутке… До первого апреля я не дотянул, но пятница — тоже неплохо. Из написанной статьи можно сделать по крайней мере два однозначных вывода: на Lua можно разрабатывать сайты (или движки под них), сайты точно будут быстрыми.
Приступая к написанию фреймворка, а он именно микро, я не был уверен, что это в принципе будет работать вообще. Было интересно и любопытно, тем более, что язык Lua мне был не знаком. Пришлось выучить язык и на ходу придумать, как его применить. Полученный за пару недель результат не претендует на большое и великое (и не рассчитан на использование в продакшене), но всё же надеюсь, что смогу внести малую лепту в популяризацию Lua в среде вебпрограммирования.
Архитектура фреймворка
Хотя слово архитектура и слишком громкое, тем не менее структура у фреймворка есть, и я попробую её описать. Со стартом происходит инициализация и запускается DI. С его помощью запускается Front — главный контролёр. Front получает от Request запрос, передаёт его в Router и получает имя контролёра. У контролёра есть несколько презентеров (например презентер статьи, меню и т.д.). Собрав ответы презентеров, контролёр возвращает ответ Front-у, а тот отправляет ответ на вывод. Всё достаточно просто и прозрачно.
Front — самый главный, но сам он ничего не делает (работу делают презентеры). Контролёр (один из них), отвечает за то, какие презентеры будут на странице пользователя. Презентер должен выполнить свою миссию и дать ответ, содержащий название, описание и контент (можно ещё прикрутить дату, чтобы отдавать Last-Modified, так я по крайней мере делал в PHP). Что из ответа презентера использовать, а что нет, решает контролёр. Шаблонизация реализована нативно, поэтому с одной стороны — быстрая, с другой, требует разработки (каталог package/Demo/View/ пока пустой)
Подробности
Настроив файл htaccess так, чтобы все запросы шли на index.lua, в индексном файле мы должны обязательно создать функцию handle(r ), которая аргументом принимает таблицу request_rec, структуру её содержимого можно посмотреть тут: httpd.apache.org/docs/trunk/mod/mod_lua.html Получив запрос, можно дальше писать код по его обработке. (В роутере кстати на POST запросах и на GET запросах, не попавших в какой-нибуть роут пока стоят заглушки.)
Мне показалось интересным использовать возможности Lua по созданию и использованию предкомпилированного кода. У такого подхода очевидный плюс в повышении быстродействия, ведь фаза предкомпиляции пропускается каждый раз при запуске программы. Ну и минус в том, что предкомпилированный файл напрямую не отредактируешь — надо править исходный код и компилировать файл заново. Программу можно заставить каждый раз сравнивать дату последнего изменения файлов, но для «быстрого» сайта это не совсем верное решение. Вариант с использованием мемкэша я пока не рассматриваю, оставлю на будущее.
Для удобства, и наверное по привычке, код я организовал в псевдо-ООП стиле. Реализована инкапсуляция, наверно потому, что именно её я ценю больше всего (субъективно). Полиформизм не потребовался, а наследование сделаю тогда, когда это будет необходимо. Конечно, для маленького фреймворка, написанного для создания небольших сайтов, можно было бы обойтись и вовсе функциями, но как-то неудобно это, хотя и вполне возможно.
Именование классов во фреймворке сделано классически: оно передаёт физическое расположение файла. Например класс CoreKernelRoute обозначает, что файл Route.lua располагается в подкаталоге Kernel пакета Core. Полный путь от корня сайта package/Core/Kernel/Route.lua Таким образом простой (да, тут всё простое и маленькое) иньектор зависимостей легко собирает все зависимые классы для создаваемого объекта и передаёт их ему в конструкторе. Список зависимостей хранится в файле system/dependency.lua Если требуется, чтобы объект был синглтоном, то его нужно прописать файле system/single.lua
Демо
Чтобы знакомство с фреймворком не означало простое прочтение статьи с не очень понятными пояснениями автора, я в фреймворк добавил примитивный модуль Demo, по которому можно понять и увидеть (если поместить дистрибутив на сервер) работу сайта. Все контролёры лежат в каталоге package/Core/Controller/ Все они, кроме Hello.lua используют презентер DemoPresenterArticle для генерации списка статей из демонстрационной базы DemoDataDb, а контроллёр Page также у этого презентера получает и контент конкретной статьи.
Контроллёр Hello.lua — сделан именно как Hello world! — это самый короткий путь, который может проделать скрипт, при этом полностью задействовав своё ядро (если тестировать на максимальную скорость, то это именно эту страничку). Кстати, чтобы правильно работала главная страница, не забудьте в httpd.conf в разделе DirectoryIndex добавить index.lua
Пример
Допустим, вы хотите показывать время на главной странице. Для этого нужно создать презентер DemoPresenterTime с парой методов, возвращающих время на сервере в 24-х или 12-и часовом формате. Можно добавить и метод, возвращающий дату (название презентера это вполне подразумевает). Располагать его нужно по адресу package/Demo/Presenter/Time.lua
--[[
DemoPresenterTime
--]]
function genObj()
local M = {}
local L = {}
--[===================[
Public methods
--]===================]
function M.doJob(route, event)
event = event or 'Clock24'
local exe_event = 'exe' .. event
if L[exe_event] then
local id_page = tostring(route.id_page)
return L[exe_event](id_page)
else
return L.conf.Core.array_error_1
end
end
--[===================[
Private methods
--]===================]
-- Getting time 24
function L.exeClock24()
return {title = "Clock 24",
description = "Time in 24-hour format",
content = os.date("%H:%M:%S"),
status = true}
end
-- Getting time 12
function L.exeClock12()
return {title = "Clock 12",
description = "Time in 12-hour format",
content = os.date("%I:%M:%S"),
status = true}
end
return M
end
Теперь шаблон CoreTemplatePage сохраняем как CoreTemplateIndex — делаем индивидуальный шаблон для главной страницы. В нём добавляем {server_time} где-нибудь под меню. Если добавить {server_time} в CoreTemplatePage, то потребуется подключать презентер к каждому контроллёру, а в наших планах этого нет.
В контролёре CoreControllerIndex добавляем за блоком с сайдбаром
-- time
res = L.obj.server_time.doJob(route, 'Clock24')
tpl = string.gsub (tpl, '{server_time}', res.content, 1)
И в файле system/dependency.lua для этого контроллёра подключаем новый презентер строкой
server_time = 'DemoPresenterTime',
Всё, презентер создан, и вы можете наблюдать на главной странице время сервера, что буден весьма «интересно» посетителю, особенно если ваш хостинг где-нибудь на другой стороне планеты :)
Стиль
Боюсь, что и на архитектуру, и на стиль оформления фреймворка слишком сильно повлиял мой предыдущий «опыт» написания скриптов на РНР. Записывать его в «бэст практик» не берусь. Но писать на Lua мне понравилось. Код получается лаконичный, количество скобочек небольшое, точки с запятыми в конце строк не нужны. Также мне показалось очень удобным использовать многострочные комментарии --[[ --]], достаточно поставить между пар скобок любой знак и блок кода вышел из комментариев, удалил — всё обратно в комментариях.
Функции-классы возвращают объекты, которые по сути — замыкания. Мыслить такими замыканиями оказалось очень легко, хотя раньше я сторонился подобной практики. Но полагаю, глубинное восприятие Lua у меня ещё впереди, и возможно, ждёт вместе с корутинами и C API. Вообще, у Lua всё как-то на своём месте, что оставляет чёткое ощущение продуманности языка, без прикручивания забытой впопыхах детали (сугубо ИМХО, без холиварности).
Lua и PHP
Этот абзац никак не ради вброса топлива в огонь священных войн. Просто я немного писал на PHP и могу провести некую параллель. Хотя мне понравился Lua, это не значит, что он лучше PHP. Возможно, что мне просто хочется чего-то нового. Кроме того, при работе с PHP появилась привычка к подробнейшей высококачественной документации, огромному числу примеров, статей, разжевываний самых разных мелочей, вылизанным библиотекам и модулям, которым несть числа, и которые либо уже есть на самом заштатном хостинге, либо вам их подключат при первой необходимости.
Если бы меня кто-то спросил, на чём писать большой и сложный проект, я бы сказал, что Lua мне нравится, но лучше пиши на PHP. Тем не менее, считаю, что Lua должен занять хоть и небольшую, но достаточно значимую нишу в вебстроительстве. Он быст, лёгок и прост, и если это всё, что нужно, то почему бы и нет? Кроме того, благодаря изначальному проектированию языка под интеграцию с Си, решению на основе Lua (и LuaJIT) вполне показана дорога на хайлоад. Мне кстати очень интересно было прочитать о возможности писать приложения на Lua в Tarantool.
Сферический конь
Любые самостоятельные бенчи лучше рассматривать относительно, поскольку настройка операционной системы, железо, могут сильно повлиять на результаты. Да и вообще, тестировать тоже надо уметь. Но в сравнении много чего становится понятно, во всяком влучае в категориях «быстрее-медленней», т.е. субъективных.
На моём стареньком ХХХ компе показатели:
главной страницы (2 презентера)
- ab -n 1000 -c 10 --> 622 r/s
- ab -n 1000 -c 100 --> 582 r/s
article3.html (2 презентера)
- ab -n 1000 -c 10 --> 660 r/s
- ab -n 1000 -c 100 --> 634 r/s
Hello world! (без презентеров, только контролёр)
- ab -n 1000 -c 10 --> 736 r/s
- ab -n 1000 -c 100 --> 727 r/s
Для сравнения
скорость работы простого r:puts(«Hello World!!!»), на Lua без фреймворка
- ab -n 1000 -c 10 --> 2210 r/s
- ab -n 1000 -c 100 --> 2067 r/s
главная страница сайта, сделанного на PHP фреймворке, похожем по применённым архитектурным принципам, но немного сложнее устроенном (с некоей системой шаблонизации и т.д.) и с четырьмя статическими презентерами (т.е. презентер грузит статический контент — текст) не смотря на схожесть реализации на цифры прошу смотреть только краем глаза
- ab -n 1000 -c 10 --> 207 r/s
- ab -n 1000 -c 100 --> 187 r/s
Общий вывод из поверхностного тестирования такой: фреймворк на инициализацию ядра тратит порядка миллисекунды, что естественно, не мало, но для обычного сайта эта величина весьма небольшая. Также он немного тратит и памяти: без фреймворка 25Кб, с фреймворком 61Кб, итого 36Кб (с ростом фреймворка цифра конечно будет расти). Учитывая, что фреймворк собирает с десяток разных файлов, думаю, результат можно назвать приемлемым.
Эффективность предкомпиляции
Сгенерировав предкомпилированные .ls файлы я смог посмотреть, насколько эффективно такое решение. Я пришёл к выводу, что эффект вырастает на больших файлах. Т.е. если бы код фреймворка был собран в одном файле, то это бы очень сильно повлияло на скорость работы. Предкомпиляция же маленьких файлов эффекта не даёт. В пакете Demo в качестве базы данных используется простая lua-таблица. Увеличив её до мегабайта, я увидел очень сильное влияние использования файла в предкомпилированном виде на скорость (не хочу тут приводить ещё одного сферического коня, но разница была на порядок). Я бы сказал, что для небольшого сайта (в смысле размера БД), вполне можно обойтись хранением данных в lua-таблице, без использования SQL, это конечно не панацея, но вполне дежурное решение.
Ссылка на GutHub: github.com/claygod/Rumba
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (14)
m0sia
04.03.2016 20:13Предкомпиляция в lua — это профанация. Это скорее обфускация. Выигрыш, как было замечено, только на больших файлах, благодаря уменьшенному размеру файла.
Если нужна скорость и хочется именно lua, то надо делать nginx(фронтенд)+uwsgi(апп-сервер)+luajit.
Один из множества плюсов: использование легковесных lua корутин для многопоточности вместо создания нового lua state(по сути запуска отдельной lua машины) в apache/mod_lua.
Кстати при наличии luajit я вообще не вижу повода использовать стандартный lua интерпретатор.Pugnator
04.03.2016 20:25про веб не скажу, но в целом jit сильно отстает от ванильной луа. в некоторых случаях это важно
m0sia
04.03.2016 20:36что именно имеется в виду под отстает? он умеет почти все, что есть в lua-5.2 и немного больше(eg bitwise операторы), но действительно не умеет некоторых вещей из lua-5.3. Приходится выбирать: либо скорость, либо фишки 5.3.
Arrest
05.03.2016 02:37C API у LuaJIT пока что несовместим даже с Lua 5.2, и приходится подтыкать костыли в виде compat-5.2, чтобы собирать свежие модули.
claygod
04.03.2016 22:19Предкомпиляция в lua — это профанация. Это скорее обфускация. Выигрыш, как было замечено, только на больших файлах, благодаря уменьшенному размеру файла.
Позволю себе процитировать автора языка:
"Код в предкомпилированной форме не всегда меньше исходного кода, но он загружается быстрее"
Предкомпиляция маленьких файликов действительно часто давала файлы чуть большего размера, правда, существенной эту разницу назвать трудно.
Один из множества плюсов: использование легковесных lua корутин для многопоточности вместо создания нового lua state(по сути запуска отдельной lua машины) в apache/mod_lua.
Если не сложно, поделитесь опытом работы с Lua-корутинами.
mva
05.03.2016 08:13А я, вот, не согласен с Вами по поводу того, что лучше NgX как фронтенд и uwsgi как апп-сервер.
Лучше NgX+mod_lua (кстати, в последней версии NgX научился в shared-модули) :)
— ну и кроме прочего — потому что у uwsgi каждый раз не угадаешь что поломали в новом релизе
(или в "apache/mod_lua" последнее относилось не к апачевому, а к NgX'овому? Если так, то там что-то такое было про инстанс на воркера. Ну и корутины работают (хотя я их не осилил :())claygod
05.03.2016 15:57Лучше NgX+mod_lua (кстати, в последней версии NgX научился в shared-модули) :)
Да так и попроще, на мой взгляд. Голосую за то, чтобы nginx вобрал в себя этот модуль и он был сразу "из коробки"
Ну и корутины работают (хотя я их не осилил :())
Если говорить о сопрограммах, доступных изнутри Lua, то я до сих пор не знаю, есть ли профит для обычного приложения в них. Например, в представленном фреймворке можно было бы каждый презентер сунуть в отдельную сопрограмму, но смысл…? Пока выполняется сопрограмма, основная программа-то стоит. Конечно, если скачивать файлы, или ещё что-то делать, "общаясь" с другими серверами, то выигрыш будет, а вот в обычном случае?
DenVdmj
10.03.2016 19:37В LuaJIT string.dump или опция -b позволяет получить портабельный байткод, а это как минимум экономия на парсинге и построении ast.
m0sia
04.03.2016 23:15+1Я не настоящий сварщик на самом деле, скорее проходил мимо lua и внимательно посмотрел.
Идея в следующем. Корутины в lua это такой дешевый и легкий механизм многопоточности(со своими граблями). Так вот uwsgi можно настроить так, что будет запускаться один инстанс lua машины(в случае luajit там полноценная машина с промежуточным байткодом и JIT) на процесс (мы запускаем по отдельному процессу на физический CPU). В свою очередь каждая lua-машина запускает единственное lua приложение, которое уже обрабатывает параллельные запросы.
В результате мы получаем очень мало накладных расходов при обработка параллельных запросов, а также всегда прогретый JIT. На синтетических тестах получается огромный выигрыш. Но все это надо, если у вас есть необходимость обслуживать десятки тысяч запросов.
В случае mod_lua есть разные варианты многопоточности, но по сути они все не такие красивые, как описаный выше вариант: нет возможности использовать одну lua-машину per-CPU для обработки параллельных запросов.
Tarantool от ребят из мейл.ру очень любопытный: они скрестили ежа с ужом. Они сделали помесь между апп сервером и in-ram storage engine, который чертовски хорошо параллелится с помощью тех же самых корутин.
claygod
04.03.2016 23:26m0sia, спасибо за развёрнутый ответ!
По поводу:
В случае mod_lua есть разные варианты многопоточности, но по сути они все не такие красивые, как описаный выше вариант: нет возможности использовать одну lua-машину per-CPU для обработки параллельных запросов.
Какие варианты многопоточности считаете возможными и реальными?
Tarantool от ребят из мейл.ру очень любопытный: они скрестили ежа с ужом. Они сделали помесь между апп сервером и in-ram storage engine, который чертовски хорошо параллелится с помощью тех же самых корутин.
Сам не пробовал Тарантул, но мысль запилить что-то внутри него на Lua была.m0sia
04.03.2016 23:54mod_lua я вообще не смотрел(только догадывался о его существовании). Apache для меня это такой вымирающий динозавр, который все никак не может отойти в мир иной.
На сайте modlua(http://www.modlua.org/gs/tweaking) они расписывают какие варианты многопоточность есть. Все вообщем-то достаточно гибко, но по сути всегда будут ограничения в лучших традиция apache: один thread на один request с одной lua-машиной.
PS я наткнулся на заметку "Хабр уже не торт" и решил написать комментарий, первый за много лет. :)claygod
05.03.2016 15:44mod_lua я вообще не смотрел(только догадывался о его существовании). Apache для меня это такой вымирающий динозавр, который все никак не может отойти в мир иной.
Моя идея была в том, что этот динозавр стоит на большинстве обычных хостингов, и если они с популярной нынче версии 2.2 перейдут на 2.4, то подключение mod_lua вообще не должно вызывать никаких затруднений. Мало того, такие хостеры должны были бы популяризовать Lua, т.к. сайт под его управлением не будет давать большую нагрузку, а значит, можно разместить больше сайтов на одном сервере и больше заработать. Но это ИМХО конечно, поскольку я сам не администратор, и нюансы хостеров не знаю.
PS я наткнулся на заметку «Хабр уже не торт» и решил написать комментарий, первый за много лет. :)
Лиха беда начало. А вообще, старые времена всегда "добрые", и трава тогда была зеленее. ))
mva
И всё-таки я за то, что лучше бы вы сконтрибьютили в OpenResty, нежели вот такие велосипеды строить :) Было бы намного полезнее с точки зрения популяризации Lua ;)
claygod
mva, спасибо, тренд к Nginx конечно верный (и интересный) но "я не волшебник, я только учусь"… так что боюсь, рановато мне.