Внутренности memtx: аллокаторы slab/arena, память

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

Внутренности memtx: аллокаторы slab/arena, память

Сообщение 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: шардированный отказоустойчивый кластер
О чём этот урок

Движок memtx держит все данные в оперативной памяти, и от того, как устроено управление этой памятью, зависят пределы по объёму, фрагментация и стабильность под нагрузкой. В этом уроке мы разберём память memtx сверху вниз: общий quota и arena, slab-аллокатор для нарезки крупных блоков, small-аллокатор для кортежей и отдельный аллокатор matras для индексов. Цель - чтобы вы понимали, куда уходит каждый байт и что показывают цифры в box.slab.info().
Ключевая мысль: в memtx есть НЕ один аллокатор, а целая иерархия. Кортежи и индексы живут в разной памяти, и считаются они тоже по-разному.
Иерархия памяти memtx

Память выделяется уровнями, каждый следующий нарезает блоки предыдущего на более мелкие куски.

1. Quota - это глобальный лимит на память под данные memtx. Задаётся параметром memtx_memory (по умолчанию 256 МБ = 268435456 байт, минимум 32 МБ). Quota реализована как lock-free счётчик: ни один нижележащий аллокатор не получит память, если упрётся в этот потолок. Когда лимит достигнут, INSERT/UPDATE начинают падать с ошибкой ER_MEMORY_ISSUE. Лимит можно поднять на лету, но НЕ уменьшить.

2. Slab arena - выделяет у ОС крупные выровненные блоки (слабы) фиксированного размера. В memtx размер слаба MEMTX_SLAB_SIZE = 4 МБ. Arena предвыделяет память кусками в рамках quota, кэширует освобождённые слабы в lock-free LIFO-стеке и переиспользует их.
Важно: arena НИКОГДА не возвращает память операционной системе. Освобождённый слаб остаётся в кэше arena до перезапуска процесса. Поэтому RSS процесса после пиковой нагрузки не уменьшится - это не утечка, это дизайн.
3. Slab cache - надстройка над arena, которая умеет резать 4-мегабайтный слаб на слабы меньшего размера степенями двойки (buddy-система). Из него питаются small-аллокатор (кортежи) и mempool (индексы).

4. Small allocator - аллокатор кортежей. Это и есть значение small у параметра memtx_allocator (по умолчанию). Внутри он держит набор пулов (mempool), каждый под свой класс размера. Запрос на N байт округляется вверх до ближайшего класса, объект берётся из соответствующего пула. Один слаб пула нарезан на ячейки одного размера - отсюда название slab allocator.

Изображение
Иерархия памяти memtx: quota, arena, small и matras

Как small подбирает класс размера

Маленькие классы идут с шагом slab_alloc_granularity (по умолчанию 8 байт, должен быть степенью двойки и >= 4): 8, 16, 24, 32 ... Это stepped-пулы. Дальше, чтобы не плодить тысячи классов, размеры растут геометрически с множителем slab_alloc_factor (по умолчанию 1.05, диапазон 1..2): это factored-пулы. Множитель 1.05 значит, что каждый следующий класс примерно на 5 процентов больше предыдущего.

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

Запрос 40 байт  -> класс 40  -> пул A (шаг 8)
Запрос 41 байт  -> класс 48  -> пул B   (7 байт overhead)
Запрос 1000 байт -> класс ~1024 -> пул из factored-зоны
Запрос 1100 байт -> класс ~1075..1129 (следующий по factor 1.05)
Границы кортежа сверху и снизу задают memtx_max_tuple_size (по умолчанию 1 МБ) и memtx_min_tuple_size (по умолчанию 16 байт). Если кортеж не влезает в max - вставка падает.

Matras: память под индексы

Индексы (деревья TREE, хэши HASH) живут в ОТДЕЛЬНОЙ памяти, выделяемой через matras (Memory Address TRAnSlation). Это аллокатор фиксированных блоков-экстентов размером MEMTX_EXTENT_SIZE = 16 КБ. Экстенты он берёт из mempool поверх того же slab cache, так что общая arena у кортежей и индексов одна.

Зачем нужен отдельный механизм. Matras выдаёт не просто указатель, а 32-битный целочисленный идентификатор блока, а сам адрес транслирует через трёхуровневое дерево (своего рода TLB в userspace): корневой блок L0 хранит указатели на экстенты L1, те - на L2, а уже L2 содержит сами блоки данных. Это даёт две вещи:
  • Стабильные id вместо абсолютных адресов - удобно для компактных ссылок внутри индекса.
  • Версионирование (read views). При создании снапшота индекс вызывает matras_create_read_view(). Дальше работает copy-on-write: пока фоновый поток пишет снапшот по согласованному снимку, изменённые экстенты копируются, а старые версии остаются видны read view. Поэтому во время box.snapshot() потребление памяти под индексы может временно вырасти.
Под транзакционные операции matras заранее резервирует экстенты, чтобы операция гарантированно завершилась без out-of-memory на полпути: для replace/update резервируется порядка 16 экстентов, для delete - около 8.

Команды и наблюдаемость

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

-- агрегированный отчёт по всей arena
box.slab.info()
--[[ ключевые поля:
  quota_size   лимит = memtx_memory
  quota_used   занято под slab cache
  arena_size   выделено под кортежи + индексы
  arena_used   реально используется
  items_size   выделено только под кортежи
  items_used   используется только кортежами
  *_ratio      три коэффициента заполнения
]]

-- разбивка по классам размера (пулам)
box.slab.stats()
-- mem_used/mem_free/item_size/slab_count по каждому классу

-- сколько памяти под индексы конкретного спейса
box.space.my_space.index[0]:bsize()
box.space.my_space:bsize()  -- под кортежи спейса
Декларативный конфиг 3.x (тот же small, но через YAML):

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

groups:
  default:
    replicasets:
      rs-1:
        instances:
          storage-1: {}
        memtx:
          memory: 1073741824        # quota, 1 GB
          allocator: small          # small | system
          slab_alloc_factor: 1.05
          slab_alloc_granularity: 8
          min_tuple_size: 16
          max_tuple_size: 1048576
Классический box.cfg для 1.x/2.x - те же имена с префиксом:

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

box.cfg{
  memtx_memory = 1073741824,
  memtx_allocator = 'small',
  slab_alloc_factor = 1.05,
  slab_alloc_granularity = 8,
}
Частые заблуждения и грабли
  • memtx_memory ограничивает всю память Tarantool - нет. Это лимит ТОЛЬКО на arena (кортежи + индексы). Сетевые буферы, fiber-стеки, Lua-heap, метаданные живут вне quota. Реальный RSS процесса всегда заметно больше memtx_memory.
  • Высокий quota_used_ratio = пора паниковать - не обязательно. Опасно, когда ОДНОВРЕМЕННО высоки и quota_used_ratio, и arena_used_ratio, и items_used_ratio. Если items_used_ratio в районе 0.5..0.9 при высоком quota - это фрагментация: много классов размера, в каждом слабе мало занятых ячеек.
  • Уменьшил memtx_memory - освободил память - параметр динамический только в сторону увеличения. Уменьшить нельзя, и arena всё равно не отдаст слабы ОС.
  • Small всегда лучше - на нагрузках с дрейфом размеров (кортежи постепенно растут, миграция между классами) small подвержен неустранимой фрагментации: старые пулы держат пустые слабы, которые нельзя переиспользовать под другой класс. В таких случаях помогает memtx_allocator = system (на базе malloc) - он гибче по фрагментации, но без преимуществ пулов.
  • Снапшот не ест память - ест. Read view + copy-on-write в matras на время box.snapshot() увеличивают потребление под индексы.
  • Память кончилась внезапно при свободной quota - классический эффект фрагментации: новый кортеж требует слаб нового класса, свободных слабов в arena нет, попытка добрать quota упирается в потолок -> ER_MEMORY_ISSUE.
Мини-лаба

Поднимите инстанс с небольшим лимитом и понаблюдайте за работой пулов.

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

box.cfg{ memtx_memory = 64 * 1024 * 1024 }
box.schema.space.create('t')
box.space.t:create_index('pk')

-- снимем базовую картину
local before = box.slab.info()

-- зальём 50k кортежей разного размера
for i = 1, 50000 do
    box.space.t:insert{ i, string.rep('x', i % 200 + 10) }
end

print('arena_used MB:', box.slab.info().arena_used / 1024 / 1024)
print('items_used_ratio:', box.slab.info().items_used_ratio)

-- сколько классов размера задействовано
print('число пулов:', #box.slab.stats())
Задание: посмотрите box.slab.stats(), найдите 2-3 класса с большим mem_free при малом mem_used - это и есть фрагментация. Затем сравните bsize() кортежей и индекса: box.space.t:bsize() против box.space.t.index.pk:bsize(). Объясните, почему сумма не равна arena_used из box.slab.info().

Контрольные вопросы
  • Почему RSS процесса Tarantool не падает после того, как вы удалили половину кортежей и вызвали сборку? Какой уровень иерархии за это отвечает?
  • Чем выделение кортежа (small) отличается от выделения узла индекса (matras) - по размеру блока, по способу адресации и по поддержке read view?
  • items_used_ratio = 0.6, quota_used_ratio = 0.95. Что это означает и какое действие корректно: поднять memtx_memory, сменить аллокатор или ничего не делать?
  • Как slab_alloc_factor и slab_alloc_granularity влияют на число классов размера и на overhead на кортеж? В какую сторону их крутить, если все кортежи примерно одного небольшого размера?
👍3 ❤️2 🔥 😄 🤔1
Ответить
← Предыдущая глава
Персистентность: WAL, снапшоты, recovery
Следующая глава →
Внутренности vinyl: LSM, компакция, тюнинг

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

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

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

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

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