Движки хранения: memtx vs vinyl

Рейтинг: 77.2% · 13 голосов
Исчерпывающий курс по Tarantool 3.x: модель данных, движки memtx и vinyl, Lua и файберы, транзакции и MVCC, SQL, конфигурация (box.cfg и декларативная 3.x), репликация и Raft, шардирование vshard, эксплуатация, безопасность. 47 уроков со схемами.
Ответить
Аватара пользователя
denis_tnt
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Движки хранения: memtx vs vinyl

Сообщение denis_tnt »

Оглавление курса (47)
  1. Что такое Tarantool: in-memory СУБД и сервер приложений
  2. Архитектура изнутри: процесс, потоки, event-loop
  3. Установка и первый запуск: tt CLI, пакеты, Docker
  4. Интерактив: консоль, admin-консоль, первые команды
  5. Спейсы и кортежи: форматы, типы данных
  6. Типы индексов и их применимость
  7. Движки хранения: memtx vs vinyl (вы здесь)
  8. DDL: схема, создание спейсов и индексов, миграции
  9. DML и выборки: insert/update/upsert, итераторы
  10. Персистентность: WAL, снапшоты, recovery
  11. Внутренности memtx: аллокаторы slab/arena, память
  12. Внутренности vinyl: LSM, компакция, тюнинг
  13. Lua и LuaJIT в Tarantool: box, модули, rocks
  14. Файберы: кооперативная многозадачность, каналы
  15. Транзакции: ACID, изоляция, MVCC
  16. Хранимые процедуры, модули, организация приложения
  17. net.box: удалённые вызовы, async
  18. Пулы соединений, балансировка, реконнект
  19. Ошибки и диагностика: box.error, pcall
  20. Типы и сериализация: MsgPack, decimal, datetime, uuid
  21. SQL в Tarantool: возможности и связь с box
  22. SQL: таблицы, JOIN, подзапросы, представления
  23. SQL: подготовленные выражения, транзакции, Lua-интероп
  24. Классическая конфигурация box.cfg (legacy 1.x/2.x)
  25. Декларативная конфигурация 3.x: config.yaml, иерархия
  26. Роли и приложения в 3.x
  27. Централизованная конфигурация: etcd / config storage
  28. tt CLI глубоко: разработка, сборка, запуск
  29. Cartridge (официальный legacy) и миграция на 3.x
  30. Репликация: replicaset, топологии
  31. Механика репликации: WAL-стриминг, vclock
  32. Синхронная репликация и выборы лидера (Raft)
  33. Жизненный цикл узла: bootstrap, join, rejoin
  34. vshard: router/storage, виртуальные бакеты
  35. Решардинг и rebalancing бакетов
  36. Запросы поверх шардов: map-reduce, crud
  37. Мониторинг: метрики, Prometheus, Grafana
  38. Логирование и аудит
  39. Бэкапы и восстановление
  40. Безопасность: аутентификация, RBAC, TLS
  41. Производительность: профилирование, тюнинг
  42. Обновления: схема, rolling upgrade
  43. Деплой в продакшен: Docker, топология (официальные паттерны)
  44. Администрирование через официальный TCM (Tarantool Cluster Manager)
  45. Коннекторы: Python, Go, Java
  46. Ключевые модули (rocks): crud, metrics, queue, expirationd
  47. Capstone: шардированный отказоустойчивый кластер
Краткий обзор

Tarantool - это СУБД с двумя встроенными движками хранения, которые можно смешивать в одной инстанции и даже в одной схеме: движок задаётся для каждого спейса отдельно.
  • memtx - in-memory движок, используется по умолчанию. Все данные и индексы целиком лежат в оперативной памяти, диск нужен только для надёжности (WAL и снапшоты).
  • vinyl - дисковый движок на основе LSM-дерева (log-structured merge tree). В памяти держится только горячая часть и кеш, основной объём данных живёт на диске.
Базовое правило выбора: memtx даёт стабильно низкую задержку (типичный запрос - меньше 1 мс), но датасет ограничен объёмом RAM. vinyl берут тогда, когда данных больше, чем памяти, и докупать память нереалистично - на диске можно держать в десятки раз больше, чем помещается в RAM.
Движок - это не про "быстро/медленно вообще", а про компромисс. memtx платит памятью за латентность. vinyl платит латентностью и write amplification за объём. Выбор делается под профиль нагрузки, а не "по умолчанию".
Как это устроено внутри

memtx: всё в Arena, один TX-поток

В Tarantool фиксированное число независимых потоков, которые не делят состояние, а обмениваются сообщениями через очереди. С базой данных работает ровно один поток - transaction processor (TX-поток). Все транзакции в нём выполняются строго последовательно, поэтому в типичных ситуациях memtx работает без блокировок (lock-free): конкуренции за данные просто нет.

Внутри TX-потока есть область памяти под данные - Arena. В ней аллокаторы (проект small) размещают т uples, спейсы и индексы. Основной аллокатор таплов - slab allocator, его статистику отдаёт модуль box.slab. Параллелизм внутри TX-потока обеспечивают файберы - кооперативные сопрограммы, которые сами уступают управление в точках yield.

Надёжность memtx обеспечивают другие потоки:
  • iproto - сетевой поток: принимает запрос, парсит, формирует сообщение и отдаёт в TX-поток.
  • WAL - пишет каждый изменяющий запрос в write-ahead log (файлы .xlog) на диск.
  • снапшот-файбер периодически снимает целый снимок Arena в файл .snap.
При рестарте Tarantool читает последний снапшот, затем все .xlog после него, восстанавливает данные в памяти и строит индексы (сначала первичные, потом вторичные). Важно: индексы не хранятся в снапшоте, они каждый раз строятся заново.

vinyl: LSM-дерево, уровни и компакция

vinyl хранит не значения, а операции: REPLACE, DELETE, UPSERT. Каждой операции присвоен LSN (монотонный номер). Дерево упорядочено по ключу по возрастанию, а внутри одного ключа - по LSN по убыванию (свежее сверху).

Свежие записи попадают в L0 - уровень в памяти (в Tarantool это B+*-дерево), размер которого ограничен параметром vinyl.memory. Когда L0 переполняется, он целиком сбрасывается на диск в файл-run (операция dump) и очищается. Раны образуют пирамиду уровней L1, L2 ...: чем ниже, тем старее и крупнее данные.

Компакция (compaction) - это сборка мусора: несколько соседних ранов сливаются в один новый. При встрече двух версий одного ключа остаётся только свежая; пара "вставка + удаление" выкидывается совсем. Удаление физически делается через tombstone (маркер-надгробие) в L0, который проходит вниз по уровням и исчезает при major-компакции на нижнем уровне.

Чтобы не сканировать все уровни при поиске несуществующего ключа (это нужно, например, при вставке в уникальный индекс - "паразитный" скрытый read), vinyl использует Bloom-фильтр на каждый ран: если бит ноль - ключа точно нет. Плюс page index (первый ключ каждой страницы) в кеше RAM, сжатие страниц через zstd, и range tuple cache - кеш диапазонов значений, а не страниц.

Большой индекс делится на ranges (размер - vinyl.range_size): split режет диапазон пополам при переполнении, coalesce склеивает почти пустые. Какой run какому range принадлежит, отслеживает метажурнал .vylog через лёгкие ссылки-slice.

Изображение
memtx в RAM против дискового LSM vinyl

Ключевые команды и код

Классический box.cfg (1.x/2.x и совместимый режим)

Код: Выделить всё

box.cfg{
    memtx_memory = 2 * 1024 * 1024 * 1024,  -- 2 GiB на Arena memtx
    vinyl_memory = 512 * 1024 * 1024,       -- 512 MiB на L0 всех LSM
}

-- спейс в памяти (по умолчанию)
box.schema.space.create('hot', { engine = 'memtx' })
box.space.hot:create_index('pk', { type = 'TREE', parts = {1, 'unsigned'} })

-- дисковый спейс на vinyl
box.schema.space.create('archive', { engine = 'vinyl' })
box.space.archive:create_index('pk', { parts = {1, 'unsigned'} })
box.space.archive:insert{1, 'cold data'}
Декларативный конфиг 3.x (YAML)

В 3.x те же настройки задаются под группами memtx и vinyl:

Код: Выделить всё

groups:
  default:
    replicasets:
      r1:
        instances:
          i1: {}
        memtx:
          memory: 2147483648      # = memtx.memory
          allocator: small
        vinyl:
          memory: 536870912       # = vinyl.memory (размер L0)
          bloom_fpr: 0.05         # вероятность ложного срабатывания фильтра
          run_count_per_level: 2
          run_size_ratio: 3.5
Сам спейс с движком в 3.x обычно создают в коде приложения тем же box.schema, движок берётся из engine = 'vinyl'.

Поведенческие отличия (моноширинно)

Код: Выделить всё

+-------------------+---------------------------+----------------------------+
| Свойство          | memtx                     | vinyl                      |
+-------------------+---------------------------+----------------------------+
| Где данные        | всё в RAM (Arena)         | в основном на диске (LSM)  |
| Типы индексов     | TREE, HASH, RTREE, BITSET | только TREE                |
| Временные спейсы  | да                        | нет                        |
| len()             | точное число таплов       | приблизительный максимум   |
| count()           | константа по времени      | переменное время           |
| delete()          | вернёт удалённый тапл     | всегда nil                 |
| select            | без yield (до коммита)    | делает yield (get/pairs)   |
| index:random()    | есть                      | нет                        |
+-------------------+---------------------------+----------------------------+
Частые заблуждения и грабли
  • "vinyl - это просто memtx, который не влезает в RAM". Нет. vinyl - LSM-движок, заточенный под запись: чтения дороже, особенно по холодным данным. У него своя семантика (см. таблицу): len() приблизителен, delete() возвращает nil, count() не константный.
  • "vinyl не ест память". Ест: L0 целиком в RAM (vinyl.memory), плюс page index, Bloom-фильтры и range cache. Если памяти под L0 мало, дампы идут чаще и write amplification растёт.
  • Write amplification. vinyl физически пишет больше, чем вы изменили - реально 5, 10, иногда 20x. Это плата за LSM. Уменьшается тюнингом run_size_ratio (выше отношение - меньше уровней, но дороже компакция) и автоматическим zstd-сжатием.
  • Компакция не успевает за дампами. Если потоки компакции (vinyl_write_threads) отстают, растёт read и space amplification: запросы тормозят, диск пухнет. Это главная эксплуатационная боль vinyl - следите за box.stat.vinyl().
  • UPSERT не бесплатен. upsert выполняется отложенно при компакции и не делает скрытого read - но если по одному ключу накопились десятки тысяч непросквошенных upsert, чтение вынуждено "проигрывать" всю их историю. Классические грабли горячего ключа.
  • Вторичные индексы и уникальность в vinyl провоцируют скрытые reads. Обновление поля под вторичным индексом требует прочитать старое значение из первичного индекса "под капотом". upsert хорош только без вторичных ключей и без рисков ошибок при отложенном применении.
  • Индексы memtx не в снапшоте. При рестарте они строятся заново - на больших спейсах это удлиняет старт.
Мини-лаба

Задание. Создайте два спейса с одинаковой схемой, но разными движками, и сравните поведение len() и delete().

Код: Выделить всё

box.cfg{}

-- memtx
box.schema.space.create('s_mem', { engine = 'memtx' })
box.space.s_mem:create_index('pk', { parts = {1, 'unsigned'} })
box.space.s_mem:insert{1, 'a'}
print('memtx len =', box.space.s_mem:len())          -- точное: 1
print('memtx delete =', box.space.s_mem:delete{1})   -- вернёт {1, 'a'}

-- vinyl
box.schema.space.create('s_vin', { engine = 'vinyl' })
box.space.s_vin:create_index('pk', { parts = {1, 'unsigned'} })
box.space.s_vin:insert{1, 'a'}
print('vinyl len =', box.space.s_vin:len())           -- приблизительный максимум
print('vinyl delete =', box.space.s_vin:delete{1})    -- nil
Дополнительно: вызовите box.stat.vinyl() и найдите в выводе разделы про disk, memory и scheduler (dump/compaction). Попробуйте создать в vinyl HASH-индекс - убедитесь, что движок отвергнет всё, кроме TREE.

Контрольные вопросы
  • Почему memtx в типичных ситуациях работает без блокировок, и какой поток у него отвечает за все транзакции?
  • Что физически происходит при удалении строки в vinyl и почему vinyl хранит операции, а не значения? Что такое tombstone?
  • Что такое write amplification в vinyl, какой порядок он имеет на практике и какими параметрами на него влияют?
  • Почему len() в vinyl приблизителен, delete() возвращает nil, а select делает yield - в отличие от memtx?
👍2 ❤️2 🔥 😄 🤔2
Ответить
← Предыдущая глава
Типы индексов и их применимость
Следующая глава →
DDL: схема, создание спейсов и индексов, миграции

Все главы курса «Tarantool: in-memory СУБД и сервер приложений с нуля до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Tarantool: СУБД и сервер приложений»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость