user@elrise.ru:~
2026-07-227 min readphpsymfonydoctrinedbalshardingarchitecture

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 CLIshard:add для нового шарда, shard:migrate для обновления схемы. Не shell-скрипты, о которых знает только девопс.

Бандл закрывает все четыре. Это стартовая архитектурная база, на которой строится сервис. Если вы пришли к шардингу после того, как таблица выросла, бандл тоже подойдёт, но вы получите головную боль, которую могли бы не получить.

Прозрачный routing

Главный архитектурный сдвиг — это не введение шардов как таковых, а прозрачность для бизнес-логики. Разработчик пишет ShardedRepository::findOneById(), а бандл сам смотрит на конфигурацию User-класса, вычисляет шард по ключу (через одну из трёх стратегий), переключает активные Connection и EntityManager на время операции, и возвращает результат. Бизнес-логика не знает, что в проекте N шардов — она знает только, что работает с User.

Большинство самописных обёрток прячут шардинг внутри Repository, но требуют, чтобы вызывающий код знал, какой шард ему нужен. Бандл прячет шардинг так, что вызывающий код вообще не думает о шардах. Это и есть инвариант, который разворачивает всю остальную архитектуру: можно менять стратегию, добавлять шарды, переезжать в другую инфраструктуру, не трогая ни строчки в use-case-сервисах.

Граница с инфраструктурным движком

Шардирование решает задачу «куда и как положить данные». Бандл не делает bulk-операции, потоковое чтение, advisory-locks и тонкий SQL-контроль — это задача соседнего elrise/dbal-bundle. Разделение ролей:

Гибридная архитектура получается естественно: 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.

Не делает:

Когда брать, а когда не брать

Брать, если:

Не брать, если:

Где смотреть реализацию

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 и не ответ на вопрос «что делать, когда таблица уже выросла». Это стартовая база для архитектуры, в которой рост данных не станет катастрофой.


Полезные ссылки:

Обсуждение

Комментарии (0)

Пока никто не комментировал.