Ситуация: после успешной записи пользователь сразу читает данные из реплики и видит старое состояние. Какую гарантию должен обеспечить API?
API должен обеспечить гарантию read-your-writes, то есть пользователь после успешной записи должен видеть собственные изменения. Обычно это достигается чтением из ведущего узла после записи, маршрутизацией к реплике, которая уже догнала запись, или передачей версии записи для контроля причинной согласованности.
Одного факта успешного ответа записи недостаточно: при асинхронной репликации реплика может отставать. Если система не умеет ждать нужную версию или переключать чтение на ведущий узел, требуемую гарантию обеспечить нельзя.
Репликация появилась как способ увеличить производительность чтения, повысить доступность и распределить данные между узлами. При асинхронной репликации запись сначала фиксируется на ведущем узле, а затем распространяется на реплики.
Такой подход уменьшает задержку записи и снижает связанность между узлами, но создаёт окно репликационного отставания. Гарантия read-your-writes нужна именно для устранения пользовательского эффекта этого окна.
Пользователь отправляет изменение, получает успешный ответ, а следующий запрос попадает на реплику, которая ещё не получила новую версию данных. Для пользователя это выглядит как потеря изменения или нестабильная система, хотя запись на ведущем узле уже состоялась.
Неверное решение особенно опасно для заказов, платежей, настроек профиля и прав доступа. Оно может приводить к повторным действиям, ошибочным решениям пользователя и противоречивому поведению разных экранов одного приложения.
Сначала нужно явно определить гарантию: пользователь, выполнивший запись, должен увидеть эту запись в последующих чтениях в рамках заданного контекста. Это не обязательно означает, что все пользователи системы немедленно увидят изменение.
Простейший вариант — после записи направлять связанные чтения на ведущий узел. Он надёжен, но увеличивает нагрузку на ведущий узел и может ухудшить задержку или доступность чтений при его сбое.
Более гибкий вариант — вернуть клиенту версию записи или маркер позиции репликации. Последующий запрос передаёт этот маркер, а маршрутизатор выбирает реплику, достигшую нужной версии. Если подходящей реплики нет, запрос временно ждёт, переключается на ведущий узел или получает контролируемую ошибку.
Маршрутизация по сессии, при которой клиент некоторое время читает из ведущего узла после записи, проще в реализации, но хуже масштабируется и требует аккуратного поведения при смене узла. Кэш также нужно учитывать: устаревший ответ из кэша способен нарушить гарантию даже при корректной репликации.
Важно отличать read-your-writes от строгой глобальной согласованности. Первая гарантия касается наблюдений конкретного пользователя или сессии, тогда как строгая согласованность требует, чтобы все чтения во всей системе видели единый актуальный порядок операций. За более сильную гарантию обычно платят задержкой, стоимостью координации и снижением доступности при сетевых сбоях.
В сервисе заказов после изменения адреса доставки экран заказа иногда показывал старый адрес. Запись выполнялась на ведущем узле, но чтение через общий балансировщик попадало на одну из реплик с задержкой репликации.
Рассматривались три варианта. Постоянное чтение с ведущего узла было простым и надёжным, но создавало лишнюю нагрузку. Полная синхронная репликация устраняла окно рассогласования, но увеличивала задержку записи и усложняла работу при недоступности узла. Передача клиентом версии записи позволяла использовать реплики, но требовала доработки маршрутизации и обработки случая, когда реплика ещё не догнала нужную версию.
Выбрали версионный маркер: после записи сервис возвращал новую ревизию заказа, а последующее чтение требовало не более старой ревизии. Реплика обслуживала запрос только после достижения этой ревизии, иначе запрос направлялся на ведущий узел. Это сохранило масштабирование чтений и устранило отображение старого адреса сразу после изменения.
Нет, это не гарантирует read-your-writes. Реплика может отставать от ведущего узла, а при переключении на другую реплику пользователь снова увидит старое состояние. Такая маршрутизация может уменьшить вероятность проблемы, но не задаёт необходимую гарантию.
Система должна иметь явную политику: подождать ограниченное время, прочитать с ведущего узла или вернуть ошибку с возможностью повторить запрос. Бесконечное ожидание опасно для задержки и исчерпания ресурсов, а немедленная выдача устаревших данных нарушает обещанную гарантию.
Если изменение затрагивает несколько хранилищ или агрегатов, версия одного объекта не обязательно отражает готовность всех связанных данных. Нужен общий причинный маркер, согласованная транзакционная граница либо явно ограниченная семантика ответа. Иначе пользователь может увидеть обновлённый заказ, но старые связанные позиции или права доступа.