Doctrine ORM + шардирование: с чего начать строить сервис
Шардирование — это не то, что прикручивают, когда таблица переросла. Это архитектурное решение, которое нужно заложить в сервис с самого начала: ключ шардирования, стратегия, routing, bootstrap шардов и миграции. Если на старте это не продумано, через год вы будете не шардировать, а переписывать.
Здесь разобран архитектурный сдвиг, который я для себя зафиксировал, и бандл elrise/doctrine-shard-manager-bundle как один из способов закрепить эти инварианты в коде. Реализация — contracts, стратегии, конфигурация, CLI, бенчмарки — описана на странице проекта.
Не сравнение подходов
Я не сравниваю разные способы шардирования. Здесь предложен один вариант: заложить в архитектуру проекта с первого коммита ключ, стратегию, routing и per-shard-операции. Бандл — готовая реализация этого подхода поверх Doctrine ORM и DBAL.
Почему шардирование — это решение на старте
Путь «сначала один EntityManager, потом шардирование» выглядит экономно. Через год таблица users — десятки миллионов строк, ещё через год — сотни миллионов. EXPLAIN показывает, что индекс уже не спасает, INSERT ... ON DUPLICATE KEY UPDATE упирается в один первичный ключ по всем записям, и команда идёт «делать шардирование». К этому моменту бизнес-логика уже насыщена findBy(['user_id' => ...]) в десятках мест, ID — автоинкремент, отчёты собирают данные «со всех мест», а JOIN между шардами уже не работает. Поменять стратегию — это переписать половину приложения, выкатить в прод и надеяться, что ничего не упало.
Другой путь — заложить четыре инварианта с самого начала:
| Инвариант | Что это значит |
|---|---|
| Ключ шардирования | Выбирается при проектировании модели данных и не меняется. UUIDv7 с зашитым шардом или хеш от естественного PK (user-id, order-id). |
| Стратегия | Зафиксирована в коде и конфиге. Даже если физически шард сейчас один, routing уже работает. |
| Прозрачный routing | Бизнес-логика не знает, сколько шардов в системе. ShardedRepository или ShardContext::withEntity() сами выбирают нужный Connection и EntityManager. |
| First-class CLI | shard:add для нового шарда, shard:migrate для обновления схемы. Не shell-скрипты, о которых знает только девопс. |
Бандл закрывает все четыре. Это стартовая архитектурная база, на которой строится сервис. Если вы пришли к шардингу после того, как таблица выросла, бандл тоже подойдёт, но вы получите головную боль, которую могли бы не получить.
Прозрачный routing
Главный архитектурный сдвиг — это не введение шардов как таковых, а прозрачность для бизнес-логики. Разработчик пишет ShardedRepository::findOneById(), а бандл сам смотрит на конфигурацию User-класса, вычисляет шард по ключу (через одну из трёх стратегий), переключает активные Connection и EntityManager на время операции, и возвращает результат. Бизнес-логика не знает, что в проекте N шардов — она знает только, что работает с User.
Большинство самописных обёрток прячут шардинг внутри Repository, но требуют, чтобы вызывающий код знал, какой шард ему нужен. Бандл прячет шардинг так, что вызывающий код вообще не думает о шардах. Это и есть инвариант, который разворачивает всю остальную архитектуру: можно менять стратегию, добавлять шарды, переезжать в другую инфраструктуру, не трогая ни строчки в use-case-сервисах.
Граница с инфраструктурным движком
Шардирование решает задачу «куда и как положить данные». Бандл не делает bulk-операции, потоковое чтение, advisory-locks и тонкий SQL-контроль — это задача соседнего elrise/dbal-bundle. Разделение ролей:
elrise/doctrine-shard-manager-bundle— архитектура данных: какой шард, какая стратегия, какая конфигурация, как добавить новый шард, как накатить миграцию. Это карта данных.elrise/dbal-bundle— инфраструктурное взаимодействие: bulk-insert/update/upsert, потоковое чтение через генераторы, advisory-locks,SELECT ... FOR UPDATE, caller-trace, PSR-3. Это движок для bulk-операций.
Гибридная архитектура получается естественно: ORM остаётся mapping-системой для описания структуры и CRUD-логики, шардовый routing идёт поверх ORM, DBAL закрывает инфраструктурные задачи внутри шарда. Бизнес-логика при этом не меняется — она по-прежнему работает с сущностями.
Что бандл решает и что нет
Закрывает: routing Connection и EntityManager per shard, три reference-стратегии (hash, range, uuid), PSR-6 кэш с graceful degradation, конфиг через #[Sharding] атрибут или YAML, cross-shard чтение через ShardedFinder::find(), shard:add для greenfield и shard:migrate для additive.
Не делает:
- middleware-уровень (Vitess, ProxySQL, Citus) — это application-level routing, а не SQL-aware proxy;
- кросс-шардовые транзакции (XA, 2PC) — по дизайну, как anti-pattern;
- автоматический live-решардинг — добавление пятого шарда к четырём остаётся ручной операцией;
- Doctrine native sharding (
ShardFilterи т.п.) — это альтернативный контракт с собственной совместимостью.
Когда брать, а когда не брать
Брать, если:
- таблица переросла шарды одного сервера БД, и нужно горизонтальное разделение;
- уже используется Doctrine ORM/DBAL и не хочется выбрасывать его ради шардинга;
- есть детерминированный ключ шардирования, по которому запись всегда попадает на один шард;
- вы готовы управлять шардами как first-class-сущностями: provisioning, per-shard миграции, мониторинг.
Не брать, если:
- одна БД и вертикальное масштабирование ещё не исчерпано — шардинг это сложность, и она не нужна раньше времени;
- нет естественного ключа для шардирования — никакой бандл не поможет, пока это архитектурное решение не принято;
- нужны кросс-шардовые транзакции — они не покрываются бандлом и не должны;
- нужна live-решардизация — миграция данных между шардами без даунтайма остаётся отдельным проектом.
Где смотреть реализацию
API, контракты, стратегии hash/range/uuid, конфигурация, CLI-команды, бенчмарки на трёх вендорах и known issues — на странице проекта. В dummy-market-agent можно поднять reference Symfony-сервис через Docker и посмотреть, как routing, ORM и DBAL-уровень собираются в одном приложении.
Итог
Шардирование — это не оптимизация поверх существующего сервиса. Это архитектурное решение, которое ложится в самый первый коммит, иначе через год придётся переписывать половину. Бандл elrise/doctrine-shard-manager-bundle — один из способов формализовать эти инварианты в коде: ключ, стратегия, прозрачный routing, first-class CLI для шардов.
Это не замена Doctrine ORM/DBAL и не ответ на вопрос «что делать, когда таблица уже выросла». Это стартовая база для архитектуры, в которой рост данных не станет катастрофой.
Полезные ссылки:
- doctrine-shard-manager-bundle: реализация, контракты, бенчмарки
- Doctrine ORM Sharding — для контекста
- UUIDv7 (RFC 9562) — формат ключа для
uuid-стратегии - Doctrine ORM и Doctrine DBAL — то, поверх чего работает бандл
- Doctrine Migrations — для
shard:migrate - PSR-6: Caching Interface — мемоизация стратегий
- PSR-3: Logger Interface — structured-логи резолвинга
- Doctrine Migrations Bundle для Symfony — базовая интеграция