Что такое LuaJIT? Почему APISIX выбирает LuaJIT?
Tao Yang
April 14, 2023
Готовы ли вы вывести свою игру с API-шлюзами на новый уровень? Тогда вам стоит обратить внимание на Apache APISIX, облачный API-шлюз, который вызывает волну интереса в сообществе разработчиков. Что еще более интересно, Apache APISIX в основном построен на LuaJIT, легковесном и эффективном языке, который не так известен, как некоторые его аналоги.
В этой статье мы подробнее рассмотрим, почему Apache APISIX выбрал LuaJIT вместо более популярных языков и технологий, и как уникальные особенности и преимущества LuaJIT могут помочь вам создавать сверхбыстрые API-шлюзы, способные справляться даже с самыми требовательными рабочими нагрузками. Так что, если вы хотите улучшить свой API-шлюз, продолжайте читать!
Что такое LuaJIT
Определение
Проще говоря, LuaJIT — это реализация JIT-компилятора (Just-In-Time) для языка программирования Lua. Для лучшего понимания читатели, не знакомые с LuaJIT, могут разбить его на две части: Lua и JIT.
Lua
Lua — это элегантный и простой в изучении язык программирования, который включает автоматическое управление памятью, полную лексическую область видимости, замыкания, итераторы, корутины, правильные хвостовые вызовы и практическую обработку данных с использованием ассоциативных массивов. Для более подробного ознакомления с синтаксисом Lua, вы можете прочитать статью Getting Started With Lua.
Lua разработан для легкой интеграции с C или другими широко используемыми языками программирования, что позволяет разработчикам использовать преимущества этих языков. Он предоставляет функции, которые не являются сильными сторонами таких языков, как C, например, высокоуровневые абстракции относительно аппаратного обеспечения, динамические структуры данных и простое тестирование. Его небольшой языковой ядро и зависимость от стандарта ANSI C делают его высокопортативным на различных платформах. В результате Lua является не только языком сценариев, который может работать как самостоятельная программа, но и встраиваемым языком, который может быть интегрирован в другие приложения.

Однако на данный момент Lua все еще имел две общие проблемы, характерные для традиционных языков сценариев: низкая эффективность и открытый код. Технология JIT, представленная LuaJIT, может эффективно решить эти две проблемы.
JIT
JIT (Just-In-Time Compilation) — это форма динамической компиляции. Динамическая компиляция не является единственной формой компиляции в компьютерных науках. Например, широко используемый язык C использует другую форму компиляции, известную как статическая компиляция.
Важно отметить, что хотя мы часто используем термин Ahead-of-Time Compilation (AOT) для описания противоположности динамической компиляции, используемой в C, эти два понятия не полностью эквивалентны. AOT описывает только поведение компиляции "высокоуровневого" языка в "низкоуровневый" язык перед выполнением программы. Целевой язык компиляции не обязательно должен быть машинным кодом, специфичным для машины, на которой выполняется программа, но может быть произвольно определен. Например, компиляция Java в C или компиляция JavaScript в V8 также будет считаться AOT. Поскольку вся статическая компиляция технически выполняется заранее, в данном контексте AOT можно рассматривать как статическую компиляцию, противоположную JIT.
Отложив в сторону эти сложные термины, при рассмотрении результата статической компиляции вы можете обнаружить, что проблемы, с которыми сталкивается язык Lua, также могут быть решены с помощью статической компиляции. Однако это приведет к потере преимущества, которое Lua предоставляет как язык сценариев: гибкость горячих обновлений и хорошая совместимость с платформами. Поэтому в настоящее время большинство языков сценариев, за исключением тех, которые имеют особые требования, используют JIT для повышения производительности языка, например, JavaScript на платформе Chromium с использованием V8 и Ruby с использованием YJIT.
JIT пытается объединить преимущества и недостатки динамической интерпретации Lua и статической компиляции C. Во время выполнения языка сценариев JIT непрерывно анализирует выполняемые фрагменты кода и компилирует или перекомпилирует их для повышения эффективности выполнения. В этот момент предположение, лежащее в основе JIT, заключается в том, что выигрыш в производительности, полученный в результате этого процесса, перевесит затраты на компиляцию или перекомпиляцию кода. В теории, поскольку JIT может динамически перекомпилировать код, он может оптимизировать и ускорять выполнение для конкретной архитектуры платформы, на которой работает программа, и в некоторых случаях достигать более высокой скорости выполнения, чем статическая компиляция.
JIT делится на два типа: традиционный Method JIT и Trace JIT, который в настоящее время используется LuaJIT. Method JIT переводит каждый метод в машинный код, в то время как Trace JIT, который является более продвинутым, предполагает, что "Для кода, который выполняется только один или два раза, интерпретируемое выполнение быстрее, чем выполнение, скомпилированное с помощью JIT". На основе этого Trace JIT оптимизирует традиционный JIT, идентифицируя часто выполняемые фрагменты кода (т.е. код на горячем пути) как код, который нужно отслеживать, и компилируя эту часть кода в машинный код для выполнения, как показано на диаграмме ниже.

LuaJIT
LuaJIT (версия 2.x) значительно улучшает производительность JIT, интегрируя высокоскоростной интерпретатор, написанный на языке ассемблера, и оптимизированный генератор кода на основе Static Single Assignment (SSA). В результате LuaJIT стал одной из самых быстрых реализаций динамических языков.
Кроме того, по сравнению с громоздким связыванием Lua и C в нативном Lua для взаимодействия с C, LuaJIT также реализует FFI (Foreign Function Interface). Эта технология позволяет нам вызывать внешние функции C и использовать структуры данных C непосредственно из кода Lua, не зная количество и тип параметров. С помощью этой функции мы можем напрямую использовать FFI для реализации необходимых структур данных вместо нативного типа Lua Table, что дополнительно повышает производительность программы в сценариях, чувствительных к производительности. Техники использования FFI для повышения производительности выходят за рамки этой статьи, и более подробную информацию можно найти в статье Why Does lua-resty-core Perform Better?.
В итоге, LuaJIT реализовал один из самых быстрых Trace JIT в языках сценариев на сегодняшний день, используя синтаксис Lua. Кроме того, он предоставляет функции, такие как FFI, для решения проблем низкой эффективности и открытого кода Lua, делая Lua гибким, высокопроизводительным и сверхлегким языком сценариев и встраиваемым языком.
Сравнение с WASM и другими языками
По сравнению с Lua и LuaJIT, мы можем быть более знакомы с другими языками, такими как JavaScript (Node.js), Python, Golang, Java и т.д. Сравнивая Lua/LuaJIT с этими популярными языками, мы можем лучше понять уникальные особенности и преимущества LuaJIT. Ниже приведены некоторые краткие сравнения:
- Синтаксис Lua разработан для не-инженеров программного обеспечения. Подобно языку R, Lua также имеет индекс массива, начинающийся с 1, что подходит для обычных людей.
- Lua очень подходит как встраиваемый язык. Lua сам по себе имеет легковесную виртуальную машину, и LuaJIT остается легковесным даже после добавления различных функций и оптимизаций. В результате размер LuaJIT не увеличивается значительно при интеграции непосредственно в программы на C, в отличие от крупных сред выполнения, таких как Node.js и Python. Таким образом, Lua фактически является наиболее широко используемым и основным выбором среди всех встраиваемых языков.
- Lua также очень подходит как "клеевой" язык. Как и JavaScript (Node.js) и Python, Lua также может хорошо связывать различные библиотеки и коды. Однако, немного отличаясь от других языков, Lua имеет более высокую связность с базовой экосистемой, поэтому экосистема Lua может быть не универсальной в разных областях.
WASM (Web Assembly) — это новая кросс-платформенная технология. Эта технология, изначально разработанная для дополнения, а не замены JavaScript, может компилировать другие языки в байткод WASM и выполнять код в безопасной песочнице, что заставляет все больше программ рассматривать WASM как встраиваемую или "клеевую" платформу. Тем не менее, Lua/LuaJIT все еще имеет множество преимуществ по сравнению с новой технологией WASM:
- Производительность WASM ограничена и не может достичь уровня ассемблера. В общих сценариях WASM, безусловно, лучше Lua по производительности, но между WASM и LuaJIT все еще есть разрыв.
- Эффективность передачи данных между WASM и основной программой относительно низкая. С другой стороны, LuaJIT может выполнять эффективную передачу данных через FFI.
Почему Apache APISIX выбрал LuaJIT?
Хотя выше было описано множество преимуществ LuaJIT, Lua не является популярным языком и не является популярным выбором для большинства разработчиков. Так почему же Apache APISIX, облачный API-шлюз под эгидой Apache Foundation, выбрал LuaJIT?
Как облачный API-шлюз, Apache APISIX обладает характеристиками динамичности, реального времени и высокой производительности, предоставляя богатые функции управления трафиком, такие как балансировка нагрузки, динамический апстрим, canary release, деградация сервисов, аутентификация, наблюдаемость и т.д. Мы можем использовать Apache APISIX для обработки традиционного север-южного трафика, а также восточно-западного трафика между сервисами, и он также может служить Ingress-контроллером для k8s.
Все это построено на стеке технологий NGINX и LuaJIT, выбранном Apache APISIX.
Преимущества сочетания LuaJIT с NGINX
NGINX — это известный высокопроизводительный веб-сервер, который служит HTTP, TCP/UDP прокси и обратным прокси.
Однако на практике мы сталкиваемся с тем, что каждый раз, когда мы изменяем конфигурационный файл NGINX, нам нужно использовать команду nginx -s reload для перезагрузки конфигурации NGINX.
Более того, частое использование этой команды для перезагрузки конфигурации может вызвать нестабильность соединений и увеличить вероятность потери бизнеса. В некоторых случаях механизм перезагрузки конфигурации NGINX также может привести к тому, что старые процессы будут слишком долго освобождаться, что повлияет на нормальную работу бизнеса. Для более подробного анализа этой темы мы рекомендуем прочитать статью Why NGINX's reload is not a hot reload?. Мы не будем углубляться в эту тему здесь.
Одна из целей Apache APISIX — решить проблему динамической конфигурации NGINX. Высокая гибкость, производительность и сверхнизкое использование памяти LuaJIT делают это возможным. В качестве примера, Apache APISIX настраивает только один location в качестве основного входа в конфигурационном файле NGINX, а последующее распределение маршрутов выполняется модулем распределения маршрутов APISIX, что позволяет достичь динамической конфигурации маршрутов.
Для достижения высокой производительности Apache APISIX использует алгоритм сопоставления маршрутов на основе префиксного дерева, написанный на C, и на этой основе предоставляет интерфейс для Lua с использованием FFI, предоставляемого LuaJIT. Гибкость Lua также позволяет модулю распределения маршрутов Apache APISIX легко поддерживать сопоставление подчиненных маршрутов одного и того же префикса через специфические выражения и другие методы. В конечном итоге, заменив нативную функцию распределения маршрутов NGINX, он достигает функциональности динамической конфигурации с высокой производительностью и гибкостью. Для подробной реализации этой функции вы можете обратиться к lua-resty-radixtree и route.lua.
Помимо маршрутизации, APISIX также может перезагружать функции, такие как балансировка, проверка здоровья, конфигурация узлов апстрима, сертификаты серверов и плагины, расширяющие возможности APISIX, без перезапуска сервера.
Кроме того, помимо разработки плагинов и других функций с использованием LuaJIT, Apache APISIX также поддерживает разработку плагинов на различных языках, таких как Java, Go, Node, Python и WASM. Это значительно снижает порог входа для кастомной разработки Apache APISIX, что приводит к богатой экосистеме плагинов и активному сообществу с открытым исходным кодом.

Заключение
LuaJIT — это реализация Lua, JIT-компилятора.
Как динамический, высокопроизводительный API-шлюз с открытым исходным кодом, Apache APISIX предоставляет богатые функции управления трафиком, такие как балансировка нагрузки, динамический апстрим, canary release, circuit breaker, аутентификация и наблюдаемость, основываясь на высокой производительности и гибкости, предоставляемых NGINX и LuaJIT.
В настоящее время Apache APISIX выпустил новую версию 3.x, которая включает больше интеграций с открытыми проектами и облачными провайдерами, нативную поддержку gRPC, дополнительные опции для разработки плагинов и поддержку service mesh. Присоединяйтесь к сообществу Apache APISIX, чтобы узнать больше о применении LuaJIT в облачных API-шлюзах.