Tarantool это не просто база данных, а сервер приложений. Внутри него живёт полноценный интерпретатор Lua, и весь код вашего приложения, и сама конфигурация, и хранимые процедуры это Lua. Поэтому понимать, как устроен стек Lua/LuaJIT/box, важнее, чем знать конкретные функции: от этого зависит и производительность, и то, почему ваш require иногда не находит модуль.
В этом уроке разберём три слоя: язык Lua и его JIT-компилятор LuaJIT, пространство имён box, через которое Lua дотягивается до движка базы, и систему модулей с менеджером rocks.
Механика: три слоя стека
1. LuaJIT как сердце
Tarantool использует не эталонный Lua (PUC-Rio), а LuaJIT 2.1 и притом собственный форк (репозиторий tarantool/luajit), который команда чинит и развивает сама. Это критично: версия языка зафиксирована на Lua 5.1 с отдельными заимствованиями из 5.2/5.3 (например goto, доступными благодаря LuaJIT). Не ждите синтаксиса 5.4, integer-типов из ванильного Lua или bitwise-операторов из 5.3 как языковых конструкций.
LuaJIT это трассирующий JIT. Он не компилирует функции целиком, а наблюдает за горячими циклами и вызовами, записывает реальный путь исполнения (trace) и генерирует машинный код именно под него. Пока код холодный, работает быстрый интерпретатор на ассемблере. Отсюда практический вывод: первые проходы по коду медленнее, разогретый цикл может ускоряться в десятки раз.
2. GC64 и память
В современных сборках включён режим GC64: указатели сборщика мусора 64-битные, и снимается старое ограничение на 1-2 ГБ Lua-памяти. На Linux GC64 включён по умолчанию, исторически на macOS он был выключен. Важно: это про Lua-память (таблицы, строки, замыкания), а не про арену таплов базы, которая управляется отдельным slab-аллокатором и настраивается через memtx_memory.
3. FFI: мост в C без обёрток
LuaJIT несёт FFI (ffi.cdef, ffi.C), позволяющий звать C-функции напрямую, описав их сигнатуру. Многие встроенные модули Tarantool под капотом используют FFI, что даёт почти нулевой оверхед на границе Lua/C. Но FFI-вызовы накладывают ограничения на JIT: некоторые конструкции и реентрантные ситуации (так называемый FFI-сэндвич) приводят к отмене трассы или прерыванию.
4. box как пространство имён движка
box это глобальная таблица, через которую Lua управляет базой: box.cfg{} конфигурирует инстанс, box.space.* даёт доступ к спейсам, box.schema.* меняет схему, box.begin/box.commit управляют транзакциями. Ключевой момент жизненного цикла: до вызова box.cfg{} база ещё не инициализирована, и box.space пуст. Многие функции box становятся доступны только после конфигурации. В Tarantool 3.x с декларативным config.yaml box.cfg вызывается за вас, но семантика та же: сначала конфигурация, потом доступ к данным.

Стек Lua/LuaJIT/box и загрузка модулей
Ключевые команды и код
Минимальный модуль
Модуль (он же rock в терминах Lua) это файл, который возвращает таблицу со своим API:
Код: Выделить всё
-- mymodule.lua
local export = {}
function export.greet(name)
return 'Hello, ' .. name
end
return export
Код: Выделить всё
local m = require('mymodule')
print(m.greet('Tarantool')) -- Hello, Tarantool
-- повторный require не перечитывает файл:
print(require('mymodule') == m) -- true
require('a.b.c') не работает с точечной нотацией box. Точки в имени превращаются в разделители каталогов, а затем имя подставляется вместо вопросительного знака в шаблонах package.path (для Lua) и package.cpath (для C-библиотек .so):
Код: Выделить всё
-- посмотреть текущие шаблоны поиска
print(package.path)
print(package.cpath)
-- добавить свой каталог ПЕРЕД остальными (важен порядок)
package.path = 'scripts/?.lua;scripts/?/init.lua;' .. package.path
Часть функциональности живёт не в box, а в отдельных встроенных модулях, которые тоже подключаются через require, но не требуют установки:
Код: Выделить всё
local fiber = require('fiber') -- кооперативные потоки
local fio = require('fio') -- файловый ввод-вывод
local json = require('json') -- сериализация
local msgpack = require('msgpack') -- бинарный формат Tarantool
local log = require('log') -- логирование
local clock = require('clock') -- точное время
Код: Выделить всё
tt rocks install http -- HTTP-сервер
tt rocks install metrics -- метрики мониторинга
tt rocks list -- что установлено
tt rocks install vshard 0.1.27 -- конкретная версия
Частые заблуждения и грабли
Главная боль новичка: запуск приложения не из корня каталога. Модули из .rocks резолвятся относительно текущего рабочего каталога, и при старте из другого места require их не находит. tt и расширенный загрузчик умеют рекурсивно искать .rocks в родительских каталогах, но если вы зовёте tarantool вручную из чужого каталога, готовьтесь чинить package.path руками.
- box.cfg меняет рабочий каталог. Если в конфиге задан work_dir, после box.cfg текущий каталог сменится. Все относительные пути в package.path, заданные до этого, могут перестать работать. Добавляйте абсолютные пути или настраивайте package.path после box.cfg.
- package.path это НЕ системный путь Lua. У Tarantool свой LuaJIT и свои дефолтные пути. Модули, установленные системным luarocks, Tarantool по умолчанию не видит. Ставьте через tt rocks.
- Это Lua 5.1, а не 5.4. Нет встроенных целых чисел (все number это double, кроме типов Tarantool вроде uint64), нет оператора // и битовых операторов из 5.3 как синтаксиса. Для битов есть модуль bit от LuaJIT.
- Индексация с 1. Таблицы Lua, поля тапла, аргументы select индексируются с единицы, а не с нуля.
- require кэширует. Поправили файл модуля, а изменения не видны? Старый объект уже в package.loaded. Для горячей перезагрузки чистят package.loaded['mymodule'].
- Глобальные переменные опасны. Забытый local создаёт глобал в _G, который живёт вечно и мешает сборщику мусора. Линтер luacheck (тоже rock) ловит это.
Создайте файл geo.lua со следующим содержимым и проверьте механику require и кэширования:
Код: Выделить всё
-- geo.lua
local geo = {}
geo.loaded_at = require('clock').time()
function geo.distance(x1, y1, x2, y2)
return math.sqrt((x2-x1)^2 + (y2-y1)^2)
end
return geo
Код: Выделить всё
local g1 = require('geo')
print(g1.distance(0, 0, 3, 4)) -- ожидаем 5
local g2 = require('geo')
print(g1.loaded_at == g2.loaded_at) -- true, файл прочитан один раз
package.loaded['geo'] = nil -- сбрасываем кэш
local g3 = require('geo')
print(g1.loaded_at == g3.loaded_at) -- false, перезагрузка
Контрольные вопросы
- Чем трассирующий JIT в LuaJIT отличается от пофункциональной компиляции и почему первый проход по горячему циклу медленнее последующих?
- Что произойдёт с box.space, если обратиться к нему до вызова box.cfg{}, и почему?
- Из каких двух переменных require берёт пути для Lua-модулей и для C-библиотек, и как в шаблоне подставляется имя модуля?
- Почему модуль, установленный системным luarocks, не виден из Tarantool, и каким инструментом ставить rocks правильно?