В прошлых уроках мы разобрали, что данные в vshard физически живут не в шардах напрямую, а в виртуальных бакетах (virtual buckets). Весь датасет нарезан на фиксированное число бакетов (sharding.bucket_count, обычно 100N-1000N от числа узлов), каждый бакет целиком лежит в одном replica set, а каждый тапл хранит свой bucket_id в индексированном поле. Решардинг - это процесс, при котором мы меняем состав кластера (добавили или убрали shard, поменяли его вес), а rebalancer фоном переносит бакеты так, чтобы их распределение снова стало ровным. Важно: количество бакетов не меняется при решардинге - меняется только их раскладка по шардам. Именно неизменность bucket_count и крупная гранулярность бакетов делают миграцию дешёвой и прозрачной для приложения.
Как rebalancer считает, кому сколько
У каждого replica set есть эталонное (etalon, идеальное) число бакетов. Оно вычисляется автоматически из общего bucket_count и весов шардов (sharding.weight). Вес - это относительная ёмкость: при весах 1, 0.5, 1.5 и 3000 бакетах эталоны будут 1000, 500 и 1500. Чем больше вес, тем больше бакетов осядет на шарде.
Rebalancer периодически просыпается и для каждого replica set считает disbalance (рассогласование) по формуле:
Код: Выделить всё
|etalon_bucket_number - real_bucket_number| / etalon_bucket_number * 100Решардинг инициируется конфигурацией, а не командой:
- Добавить shard: новый replica set с ненулевым весом - rebalancer увидит у него 0 бакетов при ненулевом эталоне и начнёт стягивать бакеты со всех остальных.
- Вывести shard: выставить ему sharding.weight: 0 - эталон станет 0, и все его бакеты разъедутся по остальным. После опустошения узел можно гасить.
Механика миграции одного бакетаГлавное про перезагрузку конфига: при локальном файле сначала перезагрузите конфигурацию на ВСЕХ роутерах, потом на ВСЕХ storage. При централизованном хранилище (etcd) Tarantool подхватывает изменения сам. Перепутаете порядок - роутер может слать запросы туда, где бакета ещё/уже нет.
Бакет переезжает не атомарно-мгновенно, а через цепочку состояний, которые хранятся в системном спейсе _bucket на каждом storage (поля: bucket id, status, destination - UUID шарда-получателя). Source - это шард-источник, destination - шард-приёмник.
- ACTIVE - бакет на месте, обслуживает чтения и записи.
- PINNED - как ACTIVE, но запрещён к переезду (закреплён вручную через bucket_pin).
- SENDING - на source данные копируются на destination; чтения ещё обслуживаются, записи - нет.
- RECEIVING - на destination бакет наполняется; любые запросы к нему отклоняются.
- SENT - на source копирование завершено, бакет начинает отвечать ошибкой переезда; через ~0.5 c уходит в GARBAGE.
- GARBAGE - данные уже не нужны (переехали либо приём оборвался ошибкой); их удаляет garbage collector по частям.
Роутеру про переезд никто заранее не сообщает. Он узнаёт о новом месте бакета через ошибку wrong bucket / код перенаправления, а discovery-фибер на роутере фоном обновляет routing table (карту bucket_id -> replica set). Поэтому миграция прозрачна для приложения: вы продолжаете звать vshard.router.callrw/callro, а роутер сам перенаведётся.

Состояния бакета при миграции: source и destination
Параллельный rebalancing и as-is команды
Изначально rebalancer применял маршруты строго последовательно - бакет за бакетом - и на Vinyl это было мучительно медленно (стоимость чтения с диска сравнима с сетью, воркер большую часть времени спал). Сейчас каждый узел шлёт несколько бакетов параллельно, round-robin по получателям. Два параметра управляют нагрузкой:
Код: Выделить всё
sharding:
bucket_count: 3000
rebalancer_disbalance_threshold: 1
rebalancer_max_sending: 5 # сколько бакетов узел шлёт одновременно (default 1)
rebalancer_max_receiving: 100 # сколько бакетов узел принимает одновременноПолезные функции для наблюдения за процессом (запускать на storage):Помните: пока бакет в SENDING, он не принимает запись. rebalancer_max_sending напрямую ограничивает объём данных, заблокированных на запись по всему кластеру. Если у вас 100000 бакетов на 10 шардов и нельзя блокировать >0.1% данных - не ставьте max_sending выше 10 на узел (10 шардов * 10 = 100 бакетов = 0.1%).
Код: Выделить всё
-- сколько бакетов сейчас на этом storage
vshard.storage.buckets_count()
-- идёт ли применение маршрутов rebalancer прямо сейчас
vshard.storage.rebalancing_is_in_progress()
-- общая инфа: bucket.active/sending/receiving/garbage и состояние alerts
vshard.storage.info()
-- детально по конкретному бакету: статус, ref_rw/ref_ro
vshard.storage.buckets_info(42)Три механизма влияют на то, что rebalancer вообще может двигать:
- Replica set lock (sharding.lock: true) - шард невидим для rebalancer: не отдаёт и не принимает бакеты. Rebalancer пересчитывает эталоны остальных так, будто залоченного шарда нет.
- Bucket pin (vshard.storage.bucket_pin) - конкретный бакет приклеен к шарду. Если запиненных бакетов больше эталона, идеальный баланс недостижим, и rebalancer вытаскивает столько, сколько может.
- Bucket ref - НЕперсистентный счётчик ссылок на время выполнения запроса. RW-реф не даёт двигать бакет, RO-реф позволяет отправить, но не даёт удалить, пока жив последний читатель. callrw/callro делают ref/unref автоматически.
- "Решардинг меняет число бакетов." Нет. bucket_count фиксируется при создании кластера и не меняется онлайн. Двигаются только бакеты между шардами.
- "Можно делать rebalance командой." Нет ручного "rebalance now": вы меняете веса/состав в конфиге, rebalancer сам решает. Хотите изоляции - lock/pin.
- "Запиню весь шард = заблокирую его." Пин всех бакетов не равен lock. Незалоченный шард всё равно может ПРИНЯТЬ новые бакеты.
- "Бакеты застряли в SENDING/RECEIVING." Частая причина - max_sending высокий, а max_receiving низкий: бакеты пытаются переехать и фейлятся, жгут сеть и время. Согласуйте параметры. После reboot застрявшие бакеты чинит bucket recovery fiber.
- "Забыл bucket_unref в сыром API." При работе через router.route():callrw/callro ref/unref надо звать руками; забыли unref - бакет не уедет до рестарта инстанса.
- "Перезагрузил конфиг как удобно." При локальном конфиге порядок строгий: сначала роутеры, потом storage.
На учебном кластере из 2 storage (storage-a, storage-b) с bucket_count: 3000 добавьте третий шард storage-c с weight: 1. Реализуйте через конфиг, затем понаблюдайте миграцию:
- До reload снимите vshard.storage.buckets_count() на a и b (ожидаемо ~1500 + ~1500).
- Перезагрузите конфиг (сначала роутеры, потом storage).
- Раз в пару секунд вызывайте на каждом storage vshard.storage.rebalancing_is_in_progress() и vshard.storage.info() - поймайте ненулевые sending/receiving.
- Дождитесь окончания и убедитесь, что стало ~1000 на каждом из трёх шардов.
Контрольные вопросы
- По какой формуле rebalancer определяет, что шард рассогласован, и от чего зависит эталонное число бакетов?
- Перечислите состояния бакета при миграции по порядку: что на source, что на destination, и в какой момент бакет перестаёт принимать запись?
- Чем replica set lock отличается от пина всех бакетов шарда? Что сможет шард в каждом случае?
- Почему опасно ставить большой rebalancer_max_sending, и как с ним связана блокировка данных на запись?