Как распределяется нагрузка между репликами базы данных
Что проверяют
Понимаете ли вы, кто именно решает, на какую реплику уйдёт запрос, и чем приложение платит за асинхронную репликацию. Следом почти всегда спрашивают: пользователь сохранил профиль, обновил страницу и увидел старые данные, почему.
Как отвечать
Классическая схема: один primary-узел принимает запись, реплики обслуживают чтение. Распределять запросы можно на трёх уровнях:
- в приложении: в Spring это
AbstractRoutingDataSource, который выбирает источник по признаку транзакции. Методы с@Transactional(readOnly = true)идут на реплику, остальные на primary. Роутинг оборачивают вLazyConnectionDataSourceProxy, чтобы соединение бралось, когда флаг только для чтения уже выставлен; - в драйвере: PostgreSQL JDBC принимает несколько хостов и параметры
targetServerType=preferSecondaryиloadBalanceHosts=true. MySQL Connector/J понимает URLjdbc:mysql:replication://и отправляет запрос на реплику приsetReadOnly(true); - в прокси: HAProxy, Pgpool-II, ProxySQL для MySQL. Приложение видит один адрес, а прокси разводит чтение и запись и следит за здоровьем узлов.
Между самими репликами чтение раскладывают по кругу (round-robin), по числу активных соединений или по весам, если машины разные.
Главная тонкость это задержка репликации. При асинхронной схеме реплика отстаёт на миллисекунды или секунды, под нагрузкой больше. Решения: читать с primary сразу после записи этого пользователя (read-your-writes), выводить из балансировки реплики с большим лагом, для критичных данных включать синхронную репликацию ценой более медленной записи.
Где спотыкаются
Путают репликацию с шардированием. Реплика хранит полную копию данных и разгружает чтение, шард хранит часть данных и разгружает запись.
Не учитывают лаг. После сохранения запрос уходит на отстающую реплику, и пользователь видит старое значение.
Ставят readOnly = true без LazyConnectionDataSourceProxy. Соединение берётся до того, как Spring отметит транзакцию как читающую, и все запросы молча идут на primary.