Привет, Хабр. В этом посте мы хотим рассказать о том, как и почему мы в IPONWEB используем язык программирования с красивым названием Lua.

Lua — скриптовый встраиваемый язык программирования со свободно распространяемым интерпретатором и открытыми исходными текстами на C. Он был разработан в 1993 году в Бразилии, в подразделении Tecgraf Католического университета Рио-де-Жанейро, а его прародителями были DEL (Data-Entry Language) и SOL (Simple Object Language), разработанные там же ранее. Один из прародителей, язык SOL, косвенно поучаствовал и в «крещении» новорожденного — «Sol» переводится с португальского как «солнце», а новый язык получил имя «Lua», «луна».

Легкость встраивания Lua в написанные на “системных” языках движки сделала его популярным скриптовым языком видеоигр. На Lua написаны, к примеру, скрипты в Grim Fandango и Baldur's Gate. Те, кто играет в World of Warcraft, тоже наверняка слышали о Lua не раз и не два — именно на нем пишут аддоны к игре, облегчающие жизнь хардкорщикам, казуалам, любителям помериться эффективностью и прочим обитателям игрового мира. Вне геймдева Lua используется как скриптовый язык встроенных систем (телевизоров, принтеров, автомобильных панелей), а также приложений, например, медиаплеера VLC Media Player. Lua используют в качестве встроенного языка такие инструменты, как Tarantool, Redis и OpenResty. А еще Lua был использован как язык расширения для расчетных кодов на языке Фортран, моделирующих термомеханическое поведение ядерного топлива.

Почему Lua?


IPONWEB — разработчик высоконагруженных платформ для компаний, работающих в сфере онлайн-рекламы: DSP, SSP, рекламных агентств и рекламодателей. О нашей работе мы подробно рассказывали в этой статье. Сначала мы разрабатывали бизнес-логику наших платформ на C++, но быстро поняли, что это не лучший выбор. Для минимизации издержек важно быстродействие платформы, а также скорость разработки, и разработка на C++ оказалась для нас слишком медленной, сказалась и сложность добавления функциональности. Мы решили изолировать интерпретацию бизнес-логики от низкоуровневого серверного кода, стали искать подходящий для этого язык и остановились на Lua. Это было в 2008 году, подходящих нам реализаций JavaScript еще не существовало, Perl, Python и Ruby были слишком медленными и недостаточно легко встраивались. И был язык Lua, не слишком известный, но популярный в геймдеве, а то, чего хотели мы, оказалось схоже с потребностями разработчиков игр — нам нужен был быстрый движок для низкоуровневых операций и легко встраиваемый быстрый язык для бизнес-логики.

Lua действительно очень быстрый язык. Дополнительный прирост скорости может дать использование LuaJIT, среды исполнения для Lua 5.1, включающей в себя трассирующий JIT-компилятор (мы используем собственный форк, о котором уже писали). Поскольку мы пишем бизнес-логику для RTB-систем, скорость для нас критически важна: в RTB на обработку каждого входящего запроса есть в среднем 120 миллисекунд. При этом на исполнение кода отводится всего 10-15 миллисекунд, а остальное время занимает ожидание ответа от других сервисов. Если, например, задержку в полсекунды при загрузке сайта в сети пользователь даже не заметит, то для RTB эти 500 миллисекунд — огромный отрезок времени. Ответ на вопрос, насколько быстр язык Lua, таков: он достаточно быстр для того, чтобы мы могли много лет писать на нем бизнес-логику и оставаться в RTB-бизнесе. Будь выбранный нами язык недостаточно быстрым, писать платформу нам было бы не для кого. Означает ли это, что RTB нельзя писать на других языках? Не означает. Но мы пишем RTB на Lua и успешно справляемся со своими и клиентскими бизнес-задачами. Наглядным примером быстродействия Lua на сервере может служить и этот бенчмарк OpenResty.

Lua как встраиваемый язык имеет массу преимуществ: он минималистичный, компактный, с очень маленькой стандартной библиотекой. Его функциональность полностью дублируется на C, что обеспечивает легкое и «бесшовное» взаимодействие Lua и C. У Lua довольно низкий порог входа по сравнению со многими другими языками: большинство программистов, приходящих работать в IPONWEB, никогда раньше не писали на Lua, но им хватает нескольких дней, чтобы полноценно включиться в работу.

Вот простой пример таргетирования рекламной аудитории.

-- deal: Объект, задающий условия, на которых продавец готов показать 
-- рекламу на своём ресурсе (на сайте, в приложении и т.п.)
-- imp: Рекламный объект (impression opportunity), который 
-- покупатель хочет разместить на ресурсе продавца
local function can_be_shown(deal, imp)
    local targeting = deal.targeting
 
    -- Никаких ограничений нет
    if not targeting then
        return true
    end
 
    -- Со стороны продавца есть ограничения на тип рекламы
    -- (например, можно показывать простые баннеры, а видео нельзя):
    if targeting.media_type then
        if not passes_targeting(targeting.media_type, imp.details.media_type) then
            return false
        end
    end
 
    -- Со стороны продавца есть ограничения на размер рекламы:
    if targeting.size then
        if not passes_targeting(targeting.size, imp.details.sizes) then
            return false
        end
    end
 
    return true
end

А так выглядит несложный хендлер (обработчик событий).

local adm_cache = require 'modules.adm_cache' -- adm = "ad markup"
local config = require 'modules.config'
local util = require 'modules.util'
 
local AbstractHandler = require 'handlers.abstract'
local ImpRenderHandler = AbstractHandler:new({is_server_request = false})
local IMP_RENDER_TEMPLATE = config.get('imp_render_template')
 
local IMP_RENDER_BILLING = {name = 'free', type = 'in'}
 
-- Показать рекламное объявление, выигравшее аукцион.
-- Все показы (как успешные, так и неуспешные) логируются.
-- В случае успешного показа стоимость показа учитывается в бюджетной подсистеме.
function ImpRenderHandler:handle(params)
    local user_id = self.uuid
 
    local cache_id = get_param('id')
    if not cache_id or cache_id == '' then
        return self:process_bad_request({reason = '"id" parameter is expected', user_id = user_id})
    end
 
    local adm = adm_cache.get(cache_id)
    if not adm then
        return self:process_bad_request({reason = 'No adm in cache', user_id = user_id})
    end
 
    update_billing(IMP_RENDER_BILLING)
    self:log_request('imp_render', {adm = adm, user_id = user_id, cache_id = cache_id})
 
    local content = util.expand_macro(IMP_RENDER_TEMPLATE, {ADM = adm})
 
    return {200, content = content}
end
 
return ImpRenderHandler

Простота Lua не только обеспечивает быструю разработку, но и позволяет делать большую работу малыми силами. Платформа IPONWEB — это общее решение, подстраиваемое под нужды того или иного клиента, при этом проект могут вести один разработчик и один менеджер. Читать код на Lua могут не только сами разработчики, но и менеджеры проектов, а иногда и клиенты. Вместе мы быстрее обнаруживаем проблему, находим ее причину и решение. Зачастую менеджер проекта сообщает разработчику о проблеме и тут же подсказывает путь ее решения.

При этом простота Lua может быть обманчивой, а у минимализма и компактности есть обратная сторона. Если код пишется, например, на Perl или Python, в распоряжении разработчика есть огромные хранилища готовых модулей, у Ruby есть RubyGems, богатыми хранилищами располагают и многие другие языки. А у Lua есть LuaRocks и три тысячи модулей, которые там лежат. Кроме того, даже если на LuaRocks есть нужный модуль, велика вероятность того, что придется еще поработать, чтобы им можно было пользоваться в условиях той или иной компании. Lua дает хорошие средства для создания защищенной среды исполнения кода (песочниц), а при работе в песочницах некоторые функции могут быть отключены. Это означает, что модули LuaRocks могут оказаться нерабочими в случае использования ими функций, заблокированных безопасной средой компании. Такова цена компактности и встраиваемости, но эту цену стоит заплатить — языки “с батарейками”, такие, как, например, Python, встраиваются не в пример сложнее, чем Lua.

Как это работает?


Основа нашей платформы — кастомизируемый HTTP-сервер с удобным и расширяемым API, предоставляющий Lua-разработчику набор функций и заточенный под задачи рекламного рынка. Этот сервер обрабатывает сотни миллионов запросов и пишет терабайты логов в день. Входящие запросы равномерно распределяются по системным тредам, а внутри системных тредов находятся песочницы.

image

Когда на сервер приходит запрос, внутри песочницы, в которую этот запрос попал, создается корутина, которая осуществляет обработку запроса. Корутины работают независимо друг от друга, каждая создающаяся корутина ставится в очередь на исполнение. Время жизни каждой корутины (полное время обработки запроса с учетом ожидания ответа вовлеченных служб: базы данных, метрик, бюджетного сервера) не должно превышать 120 миллисекунд.



Вкратце процесс обработки запроса можно описать так:

  1. Каждый полученный запрос парсится и проходит стандартную проверку на корректность.
  2. Запускается корутина, отвечающая за обработку этого запроса. Внутри каждой песочницы работает множество корутин, находящихся в разном статусе.
  3. Запускается обработка запроса, у которой может быть два результата:
    • Обработка успешно завершается.
    • Корутина передает управление серверу. Обычно это происходит, когда корутина ждет ответ от других сервисов. В таких случаях работа корутины приостанавливается до тех пор, пока не приходит ответ или не истекает время ожидания. При передаче управления сервер запускает обработку следующего запроса. Это может быть как новый запрос, так и запрос, получивший ответ от всех вовлеченных сервисов и готовый продолжить исполнение кода. Очередность обработки запросов определяется требованиями бизнес-логики.


Использование корутин — отдельная интересная тема, заслуживающая подробного разговора. Например, в этой статье подробно рассказано о том, как можно использовать корутины при создании катсцен в видеоиграх. А корутинам на сервере приложений стоит посвятить отдельную статью, и, возможно, в будущем мы это сделаем.

Что дальше?


А дальше, возможно, применение Lua в IPONWEB будет расширено. У нас есть идеи о том, как еще можно использовать Lua в нашем бизнесе, и когда эти идеи будут воплощены в жизнь, мы обязательно поделимся новым опытом. Удобство и возможности Lua как встраиваемого скриптового языка могут помочь нам, в частности, ускорить обработку клиентских данных. Но это пока из области планов и перспектив.

Подводя итог, можно сказать, что наш выбор языка бизнес-логики, сделанный 11 лет назад, продолжает оправдывать себя, позволяя нам успешно справляться с собственными бизнес-задачами и помогать нам в решении задач наших клиентов. Легко читаемый, легко встраиваемый, быстрый и несложный в освоении, Lua был и остается одним из лучших скриптовых языков, сфера применения которого отнюдь не ограничивается разработкой игр. Написанная на нем бизнес-логика IPONWEB — только один из подтверждающих это примеров.

Комментарии (20)


  1. maxzhurkin
    26.09.2019 19:34

    TL;DR: мазохисты?


  1. cy-ernado
    26.09.2019 22:03
    +1

    А как вы пишете тесты на эту логику?


    1. NeonMercury
      26.09.2019 23:24

      Писать юнит-тесты для lua не сложнее, чем для cpp. Как по мне, даже проще.
      lua-users.org/wiki/UnitTesting


      1. eliasdaler
        27.09.2019 11:46

        Из этого списка я бы особенно выделил busted, как один из лучших и до сих пор актуальных фреймворков для тестирования.


  1. VBKesha
    27.09.2019 00:17

    Насколько сложно перейти на вашу версию LuaJIT с обычного Lua?
    Просто перекомпилировать будет достаточно?


    1. eliasdaler
      27.09.2019 11:44

      Наша версия Lua очень близка по поведению к Lua 5.1, поэтому если ваш код не слишком завязан на специфичном поведении LuaJIT, то всё должно работать после подмены LuaJIT на наш форк.


  1. jetcar
    27.09.2019 09:51

    хм, не понял, а нафига LUA? есть же другие более удобные языки, а по быстродействию было ли сравнение с другими?


    1. 16tomatotonns
      27.09.2019 15:52
      +4

      Удобство — понятие относительное. Луа изначально придумывалась как встраиваемый язык, поэтому делать любые сишные/плюсовые биндинги оказывается проще чем (почти) в чём угодно. Интеграция с уже существующей системой производится малой кровью, вплоть до выгрузки плюсовых объектов и использование их со скриптовой стороны практически аналогично плюсам. Такой глубины интеграции нет практически нигде. А инструментарий для бизнес-логики — или уже существует, или пишется за обозримые сроки. Ну и очень лёгкие корутины, приятный синтаксис (спорно, да) и всё такое. Скорость работы, для динамического языка (да ещё и с ffi/luajit) — очень хорошая, и не так много абстракций, чтобы они сильно протекали на разных платформах. Тут совокупность плюшек, а не какая-то конкретная киллер-фича.


    1. zeuswept Автор
      27.09.2019 18:45

      Собственно, на вопрос уже ответили, но добавлю: мы используем Lua потому, что нас устраивает его производительность, скорость разработки на нем, скорость освоения технологии новичками. Иначе говоря, нас устраивает соотношение cost/benefit.


  1. cudu
    27.09.2019 16:49

    Подскажите, а как вы сопровождаете\поддерживаете все эти скрипты? У вас какая-то библиотека готовых «модулей»? Как происходит «миграция» бизнес логики, при изменении скриптов(к примеру, один из скриптов зависит от данных другого)?
    Вы склонны писать большие бизнес сценарии и внутри их разбивать на ф-ии или же вы пишете больше отдельные скрипты?


    1. zeuswept Автор
      27.09.2019 18:48

      В нашей команде более 40 разработчиков. Разумеется, есть библиотеки с готовыми модулями. Также в компании есть гайды по написанию и структурированию кода, позволяющие обеспечить легкую читаемость и дальнейшую поддержку кода.


    1. 16tomatotonns
      27.09.2019 19:00

      Я работаю не в IPONWEB, но тоже пишу на луа в команде довольно приличные штуки, и в случае коллектива до 50 человек, вполне хватает гита. Есть протокол общения между подсистемами, есть модули верификации, мол «входные данные и выходные соответствуют стандарту», поэтому если кто-то от кого-то зависит — пока всё соответствует протоколу, оно будет нормально работать. А если зависимость именно от данных, то тут уже, как правило, договариваемся с человеком, занимающимся разработкой и поддержкой соседней подсистемы, мол «данные изменились, адаптируем», далее стыкуем обе части (опционально в git-flow фиче) и тестируем. Если обе части под твоей юрисдикцией, то всё проще: сам стыкуешь и тестируешь.
      Отдельные модули сервисов или систем, как правило, являются наследниками абстрактного «класса сервиса», которые запускаются в таск-менеджере, который оперирует именно такими классами, соответственно для миграции модуля/сервиса обычно достаточно перенести класс и всю его вспомогательную требуху на другой кластер, и запустить, опционально указав в глобальной системной конфигурации, мол «а у нас вот тут появился новый сервис, и к нему можно обращаться по таким-то вопросам».

      В общем-то ничем не отличается от разработки произвольной распределённой системы, всё примерно то же самое.


  1. Vplusplus
    27.09.2019 18:02
    +1

    Возможна ли реализация на Lua back-end? Есть ли какие библиотеки для этого, типа Flask или Jinja2?


    1. 16tomatotonns
      27.09.2019 18:38
      +1

      Да, возможна, OpenResty в помощь. Фреймворков не очень много, но есть, например Lapis (он на мунскрипте, это как тайпскрипт для жаваскрипта, только транслируется в луа перед исполнением), можно писать веб-часть на луа или мунскрипте на выбор, шаблонизаторы с драйверами до бд присутствуют.

      Лично я предпочитаю катать полностью самописные велосипеды на OpenResty, но это уже мой персональный выбор для пет-проектов.


    1. zeuswept Автор
      27.09.2019 18:38
      +1

      Да, есть много web-фреймворков, можно использовать Lapis + OpenResty (работает через Nginx). Вот небольшой обзор: lua.space/webdev/the-best-lua-web-frameworks


    1. playermet
      27.09.2019 22:17

      Есть годный Luvit. Это как Node.js, только для Lua. Под капотом тот же libuv.


  1. DeuterideLitium6
    28.09.2019 03:04

    Действительно Lua классный ЯП. Например, игра сталкер, на нём значительная часть функционала реализовано. Конечно, это было сделано чтобы привлечь не слишком компетентных программистов, а значит дешёвых. Но как бы там ни было, Lua гораздо легче того же питона.


    1. playermet
      28.09.2019 09:38

      Встраиваемые языки встраивают не для того чтобы привлекать дешевых программистов, а потому что скриптовать игровую логику на тех же плюсах/си — это адок. Неудобно, громоздко, недружественно к непрограммистам (например геймдизайнерам, моддерам, и т.д.), и требует перекомпиляции на каждый чих. Скриптеры изначально есть в любой крупной геймдев команде, их не нужно привлекать на пол пути, и тем более не нужно называть их «не слишком компетентными». Кроме того, хоть какого-нибудь соискателя на Lua найти сложнее, чем дешевого на плюсах, просто потому что их намного, намного меньше. Из-за чего зачастую скриптерами выступают те же самые люди что пилят движковую часть.


  1. gre
    28.09.2019 13:18

    Заметка хороша тем, что публично пишет то, что пишется в договорах — в случае факапа вы максимум получите стоимость услуг хостинга (а скорее меньше)

    Поэтому, если владельца продукта волнует его SLA — он не должен полагаться на внешние SLA, а организовывать отказоустойчивость самостоятельно.

    То есть договор не гарантирует SLA, но соблюдение SLA возможно, если выстроены процессы.


    1. gre
      28.09.2019 23:33

      moderator коммент явно не к этой статье — удалите, пожалуйста.