[Из песочницы] Почему мы пишем бизнес-логику на Lua

Привет, Хабр. В этом посте мы хотим рассказать о том, как и почему мы в 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 миллисекунд.

czskd3si2n2k2mott6inf7f9qtk.png

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

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


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

Что дальше?


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

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

© Habrahabr.ru