Tarantool - это in-memory СУБД и Lua-сервер приложений, объединённые в ОДНОМ процессе. То, что обычно делают две разные системы (например, PostgreSQL хранит данные, а отдельный сервис на Python/Java крутит бизнес-логику и ходит в Redis за кэшем), Tarantool делает внутри себя. Данные лежат в оперативной памяти рядом с кодом, который их обрабатывает, поэтому запрос не уходит по сети из приложения в базу и обратно - логика выполняется ТАМ ЖЕ, где данные.
Tarantool даёт ACID-хранилище: транзакции атомарны, изменения переживают перезапуск благодаря write-ahead log (WAL) и снапшотам. Он поставляется в двух редакциях - Community Edition (открытая, бесплатная) и Enterprise Edition (SDK с закрытыми модулями: шифрование трафика, аудит, supervised failover, сжатие кортежей и т.д.). В этом курсе мы работаем только с официальными компонентами CE.
Как это устроено внутриГлавная идея, которую надо унести из урока: Tarantool - это не "ещё один Redis" и не "ещё один Postgres". Это сервер приложений с встроенной транзакционной in-memory базой. Код живёт рядом с данными.
Один процесс - несколько ОС-потоков
Запрос, пришедший по сети, обрабатывают три типа потоков операционной системы:
- Сетевой поток (network / iproto) - принимает запрос по бинарному протоколу iproto, парсит его, проверяет и превращает в сообщение (исполняемый стейтмент с опциями).
- Транзакционный поток (TX thread) - получает это сообщение через lock-free шину сообщений и собственно работает с данными: ищет/меняет кортеж по индексу либо вызывает хранимую функцию. Lua-код исполняется прямо здесь, без отдельного парсинга.
- Поток WAL - на коммите записывает изменения в журнал на диск; пока идёт запись, файбер транзакции приостановлен, а после ответа WAL - возобновляется, и результат уходит обратно в сетевой поток клиенту.
Файберы и кооперативная многозадачность
Внутри единственного TX-потока крутятся тысячи файберов (fibers) - лёгких корутин. В отличие от ОС-потоков (вытесняющая многозадачность, ядро в любой момент может прервать), файберы используют кооперативную многозадачность: файбер выполняется до тех пор, пока сам не дойдёт до точки yield. На yield управление передаётся следующему готовому (ready) файберу. Состояния файбера: running, suspended, ready (и dead после смерти).
Yield бывает явный (fiber.yield(), fiber.sleep(t)) и неявный - на сетевом I/O, файловых операциях, и, самое важное, на коммите транзакции (запись в WAL). Для memtx-движка чтение не делает yield (данные уже в RAM), а изменение делает yield только на коммите. Отсюда вытекает фундаментальное свойство: пока в критической секции нет yield, никто не вклинится в выполнение - это делает программные блокировки и мьютексы ненужными. Нет гонок, нет проблем с консистентностью памяти. Расплата: если ваша функция делает тяжёлые вычисления и долго не yield-ит, она "захватывает" весь инстанс и тормозит всех остальных клиентов. Ответственность за дружелюбные yield-ы лежит на авторе функции (есть защитный механизм fiber slice).
Движки хранения и память
Данные лежат в спейсах (spaces, аналог таблиц), записи - это кортежи (tuples, по сути массивы полей в формате MsgPack), доступ к ним - через индексы. Движков два:
- memtx (по умолчанию) - всё в RAM, чтение обычно под 1 мс, индексы TREE/HASH/RTREE/BITSET, на чтении не yield-ит.
- vinyl - дисковый LSM-движок для данных, которые больше доступной RAM; только TREE-индексы, yield-ит даже на чтении (данные могут быть на диске).
Долговечность: WAL + снапшоты
In-memory не значит "потеряем при выключении". Каждое изменение пишется в write-ahead log (файлы .xlog). Периодически checkpoint-демон (это тоже файбер) делает снапшот - полную копию данных на момент времени (файлы .snap). При восстановлении Tarantool грузит свежий снапшот и доигрывает (redo) запросы из WAL поверх него. Режимы WAL: write (по умолчанию, без ожидания flush), fsync (гарантированно на диск), none (без журнала - данные не переживут рестарт).

Архитектура Tarantool: СУБД и сервер приложений в одном процессе
Ключевые команды и код
Запустить интерактивную консоль и создать спейс с индексом (классический трек, box.cfg - работает в 1.x/2.x/3.x):
Код: Выделить всё
-- стартуем базу, отдаём ей до 256 МБ под данные
box.cfg{ listen = 3301, memtx_memory = 256 * 1024 * 1024 }
-- создаём спейс и первичный индекс
local s = box.schema.space.create('users')
s:format({ {name='id', type='unsigned'}, {name='name', type='string'} })
s:create_index('primary', { parts = {'id'} })
-- пишем и читаем
s:insert{1, 'alice'} -- INSERT (yield на коммите в WAL)
s:get{1} -- SELECT по PK (memtx: без yield)
s:replace{1, 'alice2'} -- UPSERT-подобная замена по PK
Код: Выделить всё
groups:
group001:
replicasets:
replicaset001:
instances:
instance001:
iproto:
listen:
- uri: '127.0.0.1:3301'
memtx:
memory: 268435456 # те же 256 МБ
Код: Выделить всё
box.execute([[ CREATE TABLE items (id INT PRIMARY KEY, qty INT) ]])
box.execute([[ INSERT INTO items VALUES (1, 10) ]])
box.execute([[ SELECT * FROM items WHERE id = 1 ]])
Код: Выделить всё
Ниша Что даёт Tarantool
----------- --------------------------------------------------
Кэш write-behind кэш + вторичные индексы + сложная
инвалидация; часто заменяет связку Postgres+Redis
Очередь smart queue: планирование задач, lifecycle,
архивация выполненных (модуль queue)
Real-time данные в RAM + логика рядом, латентность под 1 мс,
сотни тысяч RPS на инстанс
Primary store ACID, durable (WAL+снапшоты), не нужна вторичная БД
- "Это просто быстрый Redis." Нет. Redis - внешнее хранилище, к которому ходят из любого языка. Tarantool - сервер приложений: вы заталкиваете бизнес-логику в слой данных через Lua-хранимки, получаете вторичные индексы и НАСТОЯЩИЕ ACID-транзакции.
- "In-memory - значит, потеряю данные при сбое." Нет, при wal_mode write/fsync есть WAL + снапшоты и репликация (в т.ч. синхронная, на базе Raft).
- "Добавлю потоков - станет быстрее." TX-поток один. Тяжёлый CPU-цикл без yield заблокирует ВЕСЬ инстанс. Масштабирование - шардирование на инстансы (vshard).
- "select() и insert() по очереди в консоли внутри box.begin() - это одна транзакция." Без MVCC консоль делает неявный yield после каждой команды, и такая транзакция оборвётся. Оборачивайте логику в box.atomic()/функцию.
- Out-of-memory при "свободной" памяти. Из-за фрагментации slab-аллокатора (много разных классов размеров) можно упереться в лимит, даже когда утилизация невысокая. Мониторьте box.slab.info() и box.slab.stats().
- Операции модуля os НЕ кооперативные - os.* монопольно блокирует весь TX-поток. Для I/O используйте fio, socket, popen.
Контрольные вопросыЗапустите Tarantool в интерактивном режиме (tarantool без аргументов либо tt). Выполните box.cfg{} (память по умолчанию), создайте спейс events с первичным индексом по полю id (unsigned). Вставьте 3 кортежа. Затем выполните box.snapshot() и посмотрите в рабочей директории появившийся файл .snap. Бонус: вызовите box.slab.info() и найдите поля arena_used / arena_size - это и есть та самая Arena из урока.
- Сколько TX-потоков в одном инстансе Tarantool и почему из этого следует, что тяжёлый цикл без yield опасен?
- Чем кооперативная многозадачность файберов отличается от вытесняющей многозадачности ОС-потоков и почему это делает мьютексы ненужными?
- Как Tarantool, будучи in-memory, не теряет данные при перезапуске? Назовите два механизма и опишите процесс восстановления.
- В каких трёх нишах Tarantool заменяет связку "СУБД + отдельный кэш + сервер логики" и за счёт какого архитектурного свойства?