Запросы поверх шардов: map-reduce, crud

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

Запросы поверх шардов: map-reduce, crud

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

В шардированном кластере приложение никогда не ходит на storage напрямую. Все запросы проходят через router, и у него есть ровно две модели доступа.
  • Точечный вызов по bucket id - вы знаете ключ шардирования, router находит нужный шард и зовёт функцию только на нём. Это vshard.router.call (и его варианты callrw/callro).
  • Map-reduce поверх всех шардов - bucket id неизвестен или нужны данные со всего кластера. Router рассылает одну функцию на все мастера и собирает ответы. Это vshard.router.map_callrw.
Поверх этих примитивов работает модуль crud: он прячет ручную работу с bucket id и сам решает, когда сделать точечный вызов, а когда map-reduce.
Главное правило: единственная операция, которую понимает router, - это call. Любой SELECT, INSERT, любая хранимка попадают на storage только как аргумент router-вызова. Router не знает вашу схему данных, он знает только карту bucket id -> replica set.
Механика точечного вызова

Router держит routing table - карту всех bucket id на replica set. При старте он создаёт постоянный пул соединений ко всем storage и кэширует эту карту. Фоновый discovery fiber периодически обновляет её, чтобы переезд бакета при ребалансе или фейловер мастера были прозрачны для приложения.

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

result = vshard.router.call(bucket_id, mode, func_name, {args}, {opts})
-- mode = 'read'  -> vshard.router.callro (можно на реплику)
-- mode = 'write' -> vshard.router.callrw (только мастер)
Что происходит шаг за шагом:
  • Router берёт bucket_id и ищет replica set в routing table. Если бакет ещё не известен (discovery не дошёл), router спрашивает у всех storage, где он.
  • Найдя шард, router зовёт функцию на нужном узле. На storage срабатывает проверка: бакет реально лежит в этом _bucket, и он в состоянии ACTIVE или PINNED (для чтения допустим ещё SENDING).
  • Если проверка провалилась (бакет уехал) - storage возвращает специальную ошибку с новым местоположением, router обновляет карту и сам ретраит. Прикладной код этого не видит.
Под капотом call автоматически делает bucket_ref/bucket_unref - ставит и снимает счётчик ссылок на бакет, чтобы ребалансер не утащил его прямо посреди вашего запроса.

Mode и предпочтения чтения. Для чтения mode можно расширить: prefer_replica=true (читать с реплики, разгрузить мастер), balance=true (раскидывать чтения по узлам по политике балансировки vshard). Запись всегда идёт на мастер.

Изображение
Router call и map-reduce поверх шардов

Механика map-reduce: Ref, Map, Reduce

vshard.router.map_callrw не принимает bucket id. Он рассылает одну и ту же функцию на мастера всех replica set, и эта функция работает со всеми локальными данными узла независимо от bucket id. Зачем три стадии? Чтобы гарантировать консистентность: ни один бакет не должен переехать между узлами, пока выполняется обход.
  • Ref - router сначала просит каждый storage поставить общую RO-ссылку на все свои бакеты разом. Пока ссылка жива, ребалансер не может вынести данные с узла. Если хоть один storage не смог зарефать (идёт миграция, мастер недоступен) - вся операция падает, частичных результатов нет.
  • Map - только после успешного Ref на всех узлах router зовёт вашу функцию на каждом мастере. Функция уже уверена, что видит полный и неподвижный срез своих данных.
  • Reduce - этой стадии в vshard нет. map_callrw отдаёт вам map вида {replicaset_uuid = результат}, а сворачивать (суммировать, склеивать, дедуплицировать) вы должны сами в прикладном коде.

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

local map, err = vshard.router.map_callrw(
    'count_local_bands',   -- имя функции, объявленной на storage
    {},                    -- аргументы
    {timeout = 5}          -- таймаут на ВСЮ операцию
)
-- success: map = { ['uuid-1'] = {42}, ['uuid-2'] = {37} }
-- fail:    map = nil, err, [uuid]  -- где случилась ошибка
local total = 0
for _, res in pairs(map) do total = total + res[1] end
Самое тонкое место - планировщик (scheduler) на storage. Refs от map-reduce и переносы бакетов ребалансером конкурируют за одно и то же: первые требуют, чтобы данные стояли на месте, второй - чтобы они двигались. Scheduler честно делит время между ними, поэтому долгий map-reduce тормозит ребаланс и наоборот. Отсюда практическое правило: таймауты map_callrw - это секунды, не минуты.

Модуль crud: map-reduce без ручного bucket id

Писать router.call с вычислением bucket id под каждый запрос утомительно. Модуль crud (официальный rock) даёт привычный CRUD-интерфейс поверх vshard и сам выбирает стратегию.

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

local crud = require('crud')

-- INSERT: crud сам считает bucket_id из ключа шардирования
crud.insert('bands', {1, box.NULL, 'Scorpions', 1965})

-- GET по первичному ключу: точечный вызов в нужный шард
crud.get('bands', 1)

-- SELECT с условием по НЕ ключу шардирования: map-reduce!
crud.select('bands', {{'>=', 'year', 2000}}, {first = 100})
Ключевая развилка внутри crud.select:

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

условие содержит ключ шардирования -> один шард (точечно)
условие НЕ содержит ключ шардирования -> обход всех шардов (map-reduce)
Во втором случае crud зовёт select на каждом storage, мерджит результаты по индексу, режет лишнее по first/after и возвращает единый отсортированный набор. У select есть опции prefer_replica, balance (читать с реплик) и mode='write' (читать строго с мастера, когда нужна максимальная свежесть).

Частые заблуждения и грабли
  • map_callrw - не аналог callro. Несмотря на имя rw, его используют и для чтений по всему кластеру. rw здесь означает, что он ходит на мастера и держит refs; на реплики он не пойдёт.
  • map_callrw требует живых мастеров везде. Если хоть в одном replica set мастер лёг, Ref не пройдёт - вся операция упадёт. Точечный callro в это время продолжит работать с реплик.
  • Reduce за вас никто не сделает. Частая ошибка - ждать готовую сумму. Вы получаете map по узлам и сворачиваете сами.
  • Огромные таймауты на map-reduce - вредны. Refs на все бакеты блокируют ребаланс на это время. Минуты держать refs - плохая практика (кроме тестов).
  • crud.select без ключа шардирования - это скрытый map-reduce по всему кластеру. На больших данных это дорого: лучше фильтровать по ключу шардирования или ограничивать first.
  • Запись неидемпотентна. callrw нельзя слепо ретраить: ответ мог потеряться по таймауту уже после применения. Встроенной дедупликации нет - проверку "применено ли уже" пишете сами (например, по уникальному id вставки). Для чтений безопасный ретрай даёт пара timeout + request_timeout (timeout > request_timeout).
Мини-лаба

На развёрнутом sharded-кластере из учебных примеров (router + 2 storage с пространством bands):
  • Сделайте точечный crud.get('bands', N) и crud.insert - убедитесь, что данные легли в один шард (посмотрите bucket_id в результате).
  • Сделайте crud.select('bands', {{'>=', 'year', 1990}}) без ключа шардирования и заметьте, что ответ собран с обоих storage (это и есть map-reduce).
  • Бонус: на storage объявьте функцию count_local() и вызовите vshard.router.map_callrw('count_local', {}, {timeout=5}); сложите счётчики из map вручную.
Контрольные вопросы
  • Почему единственная операция router - это call, и как тогда выполняется SELECT приложения?
  • Что делает стадия Ref в map_callrw и почему без неё нельзя гарантировать консистентность?
  • По какому признаку crud.select выбирает между точечным запросом и обходом всех шардов?
  • Почему нельзя ставить таймаут в минуты на map_callrw и какой компонент storage за это отвечает?
👍 ❤️5 🔥 😄 🤔2
Ответить
← Предыдущая глава
Решардинг и rebalancing бакетов
Следующая глава →
Мониторинг: метрики, Prometheus, Grafana

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

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

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

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

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