Tarantool - это и база данных, и сервер приложений, который слушает порт по бинарному протоколу IPROTO. Модуль net.box - это встроенный Lua-клиент к этому порту. Он позволяет из одного инстанса Tarantool (или из tt run -i, из теста, из соседнего сервиса) подключиться к другому и выполнять там запросы так, будто они локальные: select/insert/update по спейсам, вызов хранимых функций через call, выполнение произвольного Lua через eval.
Главная идея урока: net.box - это не просто обёртка над сокетом, а полноценная асинхронная машина с воркер-фибером, конвейером запросов (pipelining) и объектами-фьючерсами для неблокирующей работы. Понимание этой механики отделяет рабочий код от кода, который тормозит или ловит гонки.
Механика: что происходит внутри
Подключение ленивое. Вызов net_box.connect(URI) (синоним net_box.new) сразу возвращает объект conn, но физическое соединение устанавливается на первом запросе либо в фоне. Под капотом connect порождает отдельный воркер-фибер, который ведёт конечный автомат соединения: initial -> auth -> fetch_schema -> active. На этапе fetch_schema клиент скачивает с сервера описание спейсов и индексов - именно поэтому после connect доступны conn.space.bands и conn.space.bands.index.
Один сокет, много фиберов. Все методы conn фибер-безопасны. Если десять фиберов делают запросы через один conn, все они мультиплексируются в один TCP-сокет: net.box присваивает каждому запросу уникальный sync-id (IPROTO_SYNC), отправляет их подряд не дожидаясь ответов (pipelining), а приходящие ответы раскладывает обратно по sync-id нужному фиберу. Меньше сокетов - меньше системных вызовов - выше throughput. Поэтому правильная практика: один общий conn на много фиберов, а не conn на каждый запрос.
call против eval - это разные IPROTO-команды. conn:call('func', {args}) отправляет IPROTO_CALL и вызывает заранее зарегистрированную глобальную функцию по имени. conn:eval('return ...', {args}) отправляет IPROTO_EVAL и выполняет произвольную строку Lua на сервере. Различие принципиально для безопасности: право execute можно выдать точечно на конкретную функцию (lua_call), а eval требует execute на lua_eval (фактически на universe) - это почти root. В проде наружу отдают call на белый список функций, eval оставляют для отладки.
Синхронность и yield. Любой удалённый запрос по сети делает yield фибера: он отдаёт управление, пока ждёт ответа. Это значит, что глобальные переменные и данные в базе могут измениться, пока ваш select висит в полёте - в отличие от локального box.space, который при чтении не уступает управление. Особый случай - net_box.self: встроенное "соединение" к самому себе, где запросы не уходят в сеть и опции is_async/timeout/on_push игнорируются.

Клиент-сервер net.box: call, eval, future
Ключевые команды и код
Сервер (sample_db, декларативный конфиг 3.x слушает 127.0.0.1:3301, у пользователя есть права на спейс bands и на функцию get_bands_older_than). Клиент:
Код: Выделить всё
local net_box = require('net.box')
-- ленивое подключение, ждём не дольше 1.5 c
local conn = net_box.connect('sampleuser:secret@127.0.0.1:3301',
{ wait_connected = 1.5 })
print(conn:ping()) -- true, если живо
-- работа со спейсом как с локальным
conn.space.bands:insert({1, 'Scorpions', 1965})
conn.space.bands:select({1})
-- call: вызов зарегистрированной функции
conn:call('get_bands_older_than', {1970})
-- eval: произвольный Lua (нужно право execute на lua_eval)
conn:eval('return box.info.version')
conn:close() -- close() - системный вызов, закрываем явно
Код: Выделить всё
local f = conn.space.bands:insert({10, 'Queen', 1970}, {is_async = true})
f:is_ready() -- true, когда ответ пришёл
f:result() -- результат (или nil, если не готов/ошибка)
f:wait_result(0.5) -- блокирующе ждать до 0.5 c, иначе ошибка
f:discard() -- забыть про ответ, освободить слот в таблице запросов
Код: Выделить всё
local futures = {}
for idx, conn in ipairs(shards) do
futures[idx] = conn:call('count_rows', {}, {is_async = true})
end
local total = 0
for _, f in ipairs(futures) do
total = total + f:wait_result(1.0)[1] -- ответ async - таблица!
end
Полезные опции connect. reconnect_after=N - прозрачно переподключаться каждые N секунд при обрыве (но box.session.id() после реконнекта меняется - нельзя полагаться на серверную сессию между обрывами). fetch_schema=false - не качать схему: ускоряет старт, если делаете только call/eval и спейсы не нужны (тогда conn.space недоступен). connect_timeout - таймаут установления соединения.Важно: результат async-запроса структурирован иначе, чем синхронного. Синхронный conn:call возвращает распакованные значения, а future:result() всегда возвращает их таблицей (как [val1, val2, ...]). Это частая причина "почему у меня nil вместо числа".
Частые заблуждения и грабли
- "net.box нужен для связи с внешней СУБД." Нет, это вариант для Tarantool-Tarantool. Для MySQL/PostgreSQL - отдельные коннекторы.
- Соединение на каждый запрос. connect/close - дорого (системные вызовы, auth, fetch_schema). Держите долгоживущий conn и шарьте между фиберами.
- eval наружу. Выдать execute на lua_eval - это открыть выполнение любого кода. Наружу - только call на конкретные функции.
- Забыть, что удалённый select делает yield. Логика "прочитал и тут же обновил" по сети не атомарна; данные могли уехать.
- Async без таймаута и без discard. Брошенные future копятся в таблице запросов и тормозят остальные; discard освобождает слот.
- timeout=nil в горячем цикле. Внутри без явного timeout net.box зовёт fiber.self() (C-call), что мешает JIT и просаживает скорость call примерно на 15%. В нагруженном пути указывайте timeout явно.
- is_async ради "ускорения" одиночного запроса. Профита нет - выгода только при пачке параллельных запросов (map-reduce) или очень высокой латентности.
Поднимите локальный инстанс на 127.0.0.1:3301 с функцией
Код: Выделить всё
function add(a, b) return a + b endКонтрольные вопросы
- Чем отличаются IPROTO_CALL (conn:call) и IPROTO_EVAL (conn:eval) с точки зрения механики и безопасности (какие права нужны)?
- Почему один conn можно безопасно использовать из многих фиберов и что при этом происходит с сетевым сокетом?
- Что именно возвращает запрос с is_async=true и какими четырьмя методами future из него достают результат?
- Чем форма результата future:result() отличается от результата синхронного conn:call и почему это ловушка?