vshard: router/storage, виртуальные бакеты

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

vshard: router/storage, виртуальные бакеты

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

Когда один инстанс Tarantool перестаёт справляться с объёмом данных или нагрузкой, его данные нужно размазать по нескольким машинам - это горизонтальное масштабирование (шардирование). Официальный модуль для этого - vshard. Он не встроен в ядро, ставится отдельно (минимальная версия 0.1.25):

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

tt rocks install vshard
Идея vshard в одном предложении: данные режутся не на физические узлы напрямую, а на большое фиксированное число виртуальных бакетов, и уже бакеты раскладываются по шардам. Этот слой косвенности и есть главное, что отличает vshard от наивного шардирования по hash(key) % N.

Три компонента архитектуры

Storage (хранилище) - узел, который держит часть данных. Несколько реплик одного и того же подмножества данных образуют replica set (он же shard). Внутри replica set один инстанс - master (читает и пишет), остальные - replica (только чтение). Рекомендуется минимум 2, а для отказоустойчивости 3+ инстанса на шард.

Router (маршрутизатор) - прокси без персистентного состояния. Все запросы приложения идут через router. Он скрывает от приложения топологию: сколько шардов, где они, идёт ли сейчас ребалансировка, случился ли failover. Router держит постоянный пул соединений ко всем storage (создаётся на старте, чтобы сразу отловить ошибки конфигурации) и кэширует таблицу маршрутизации.

Rebalancer (ребалансировщик) - фоновый процесс, который выравнивает число бакетов по шардам. В 3.x его можно не указывать явно: он автоматически выбирается из мастеров.

Виртуальные бакеты: как это устроено внутри

Весь датасет логически разбит на N бакетов с номерами от 1 до N. N задаётся один раз при бутстрапе кластера через sharding.bucket_count и не меняется после старта. Каждый бакет целиком принадлежит ровно одному replica set - бакет не может лежать на двух шардах сразу.

Bucket id хранится в двух местах одновременно, и это ключ к консистентности:
  • В отдельном поле каждого тапла каждой шардируемой спейс.
  • В системном спейсе _bucket на storage - там лежит сам факт "этот бакет тут и в таком состоянии".
Почему N делают на порядки больше числа узлов (правило: 100*N или 1000*N бакетов на N узлов)? Потому что единица миграции при ребалансировке - бакет. Мало бакетов - грубая гранулярность, нечем точно выровнять нагрузку. Слишком много - лишняя память под таблицу маршрутизации. Дефолт исторически маленький, поэтому на проде число почти всегда задают руками.

Router не знает соответствия bucket id -> primary key. Он знает только bucket id -> replica set (таблица маршрутизации). Чтобы найти бакет, приложение либо передаёт bucket id явно, либо router вычисляет его сам по данным запроса хэш-функцией.

Изображение
Архитектура vshard: router, бакеты, шарды

Жизненный цикл запроса

Единственная операция, которую понимает router, - это call. Всё DML идёт через него:

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

result = vshard.router.call(bucket_id, mode, func_name, {args}, {opts})
-- mode: 'read' или 'write'
-- удобные обёртки:
vshard.router.callrw(bucket_id, 'insert_band', {id, bucket_id, ...})
vshard.router.callro(bucket_id, 'get_band',    {id})
Что происходит по шагам:
  • Router берёт bucket_id и ищет в таблице маршрутизации нужный replica set. Если маппинг ещё не известен (discovery fiber не успел заполнить таблицу), router опрашивает все storage, чтобы найти бакет.
  • Storage проверяет: лежит ли бакет в его _bucket и в каком он состоянии. Для записи бакет должен быть ACTIVE или PINNED, для чтения допустим ещё SENDING.
  • Если проверки прошли - функция выполняется. Иначе запрос падает с ошибкой "wrong bucket".
Bucket id обычно вычисляют детерминированной хэш-функцией от ключа шардирования. Есть две функции:

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

-- старая, по строковому представлению; неконсистентна для cdata-чисел
local b = vshard.router.bucket_id_strcrc32(key)

-- рекомендуемая: CRC32 от MessagePack-представления,
-- bucket_id не зависит от Lua-типа числа (123 и 123ULL дадут одно и то же)
local b = vshard.router.bucket_id_mpcrc32(key)
Шардируемый спейс обязан иметь поле под bucket id (тип unsigned/number/integer, non-nullable) и индекс по нему (по умолчанию имя индекса - bucket_id):

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

box.once('schema', function()
  box.schema.space.create('bands')
  box.space.bands:format({
    {name = 'id',        type = 'unsigned'},
    {name = 'bucket_id', type = 'unsigned'},
    {name = 'band_name', type = 'string'},
  })
  box.space.bands:create_index('id', {parts = {'id'}})
  box.space.bands:create_index('bucket_id',
    {parts = {'bucket_id'}, unique = false})
end)
Состояния бакета и ребалансировка

При миграции бакет проходит через состояния: ACTIVE (доступен на чтение/запись), PINNED (как ACTIVE, но запрещён к переезду), SENDING (копируется, источник ещё отдаёт чтения), RECEIVING (наполняется, все запросы отвергаются), SENT (уехал; router по нему вычисляет новое место; через 0.5 c уходит в GARBAGE), GARBAGE (удаляется сборщиком).

Ребалансировщик считает для каждого шарда эталонное (идеальное) число бакетов исходя из bucket_count и весов. Запуск происходит, когда дисбаланс превышает порог:

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

|etalon - real| / etalon * 100  >  rebalancer_disbalance_threshold
Вес (weight) задаёт ёмкость шарда: больше вес - больше бакетов. При bucket_count = 3000 и весах 1, 0.5, 1.5 эталоны выйдут 1000 / 500 / 1500. Вес 0 - это сигнал "слей все бакеты с этого шарда" (удобно для вывода узла из эксплуатации).

Частые заблуждения и грабли
bucket_count нельзя поменять после бутстрапа. Заложите запас сразу: люди ставят 3k бакетов, а потом каждый бакет распухает до гигабайта, и ребалансировать становится невозможно нормально.
  • Router на каждом storage-узле - технически можно, но на проде категорически не рекомендуется: это разные по профилю нагрузки роли, селите их раздельно.
  • bucket_id_strcrc32 для чисел из FFI - вернёт разные бакеты для 123, 123LL, 123ULL. Берите bucket_id_mpcrc32. И не передавайте cdata как bucket_id в call (в свежих версиях это вообще запрещено).
  • Уникальность вторичного индекса в шардируемом спейсе гарантируется только в пределах одного шарда, не глобально.
  • SELECT в хранимке выполняется локально: storage не знает маппинга bucket_id -> primary key, поэтому распределённые выборки без bucket_id - это map-reduce по всем шардам (за этим обычно идут в модуль crud).
  • TRANSFER_IS_IN_PROGRESS - не баг: бакет переезжает, запрос надо просто повторить.
  • pin != lock: lock прячет весь replica set от ребалансировщика; pin фиксирует один бакет. Запинить все бакеты не равно залочить шард - незалоченный шард всё равно может принимать новые бакеты.
Мини-лаба

На любом запущенном инстансе с установленным vshard выполните в консоли и сравните результаты:

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

local vshard = require('vshard')
print(vshard.router.bucket_id_mpcrc32(123))
print(vshard.router.bucket_id_mpcrc32(123ULL))
print(vshard.router.bucket_id_strcrc32(123))
print(vshard.router.bucket_id_strcrc32(123ULL))
Задание: убедитесь, что mpcrc32 даёт одинаковый бакет для 123 и 123ULL, а strcrc32 - разный. Объясните себе, почему для шардирования это критично.

Контрольные вопросы
  • Зачем vshard вводит слой виртуальных бакетов вместо прямого hash(key) % число_узлов? Что это даёт при добавлении шарда?
  • В каких двух местах хранится bucket id и как это обеспечивает консистентность при ребалансировке и failover?
  • Что произойдёт, если выставить вес replica set в 0? А если число бакетов выбрали слишком маленьким?
  • Почему распределённый SELECT без bucket_id превращается в опрос всех шардов, а DML через vshard.router.call - нет?
👍2 ❤️3 🔥4 😄 🤔3
Ответить
← Предыдущая глава
Жизненный цикл узла: bootstrap, join, rejoin
Следующая глава →
Решардинг и rebalancing бакетов

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

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

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

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

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