АрхитектураМикросервисы и интеграцииАрхитектор распределённых систем

Пользователь после успешного чтения обращается к другой реплике и видит более старую версию данных. Как сох...

Пользователь после успешного чтения обращается к другой реплике и видит более старую версию данных. Как сохранить монотонное чтение в распределённой системе?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нужно гарантировать, что каждый следующий запрос пользователя обслуживается репликой, которая видела как минимум ту же версию данных, что и предыдущая. Обычно для этого используют привязку сессии к реплике, маркер версии в контексте запроса или чтение из лидера после фиксации наблюдённой версии.

Одной балансировки по репликам недостаточно: после переключения на отстающую реплику пользователь снова увидит старое состояние.

Исторический контекст

Монотонное чтение возникло как практическое требование для систем с репликацией и eventual consistency. Реплики обновляются не одновременно, поэтому глобально согласованное чтение часто заменяют более дешёвыми гарантиями на уровне пользовательской сессии.

Так появилась группа session guarantees: в частности, монотонное чтение, чтение собственных записей и монотонная запись. Они позволяют сохранить предсказуемое поведение для конкретного пользователя, не требуя полной синхронной согласованности всех реплик.

Постановка проблемы

После изменения данных лидер передаёт обновление репликам асинхронно. Если первый запрос попал на уже обновлённую реплику, а следующий — на отстающую, пользователь может увидеть последовательность версий 12, затем 10.

Такое поведение разрушает ожидание, что время не движется назад. Это особенно опасно для интерфейсов заказов, платежей и настроек: пользователь может решить, что подтверждённое действие отменилось, и повторить операцию.

Подробное решение

Система должна переносить между запросами минимальную наблюдённую версию. Это может быть номер журнала, версия агрегата, позиция репликации или другой монотонный маркер. Реплика имеет право ответить только после того, как достигла этой версии; иначе запрос направляется на более свежую реплику или ожидает догоняющей репликации.

Привязка сессии к реплике проста и уменьшает вероятность отката, но не даёт полной гарантии: реплика может отстать, выйти из строя, а после переключения новая реплика может быть старше требуемой версии. Поэтому при failover нужно проверять её позицию, а не только сохранять идентификатор узла.

Маркер версии надёжнее. Клиентский или серверный контекст содержит последнюю увиденную версию, а маршрутизатор выбирает реплику с версией не ниже неё. Если такая реплика временно недоступна, система выбирает между задержкой ответа, чтением из лидера и ослаблением гарантии; это должен быть явный продуктовый компромисс.

Монотонное чтение не означает, что пользователь всегда увидит самые свежие данные. Оно гарантирует только отсутствие возврата к более старой версии в рамках заданной сессии. Для критичных операций могут потребоваться более сильные гарантии: чтение из лидера, подтверждение репликации или согласованный кворум.

Ситуация из практики

В системе заказов экран сначала читает заказ с реплики, которая уже показала статус «Оплачен». Следующий запрос после обновления страницы попадает на другую реплику и показывает статус «Ожидает оплаты».

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

Выбранное решение — передавать в серверной сессии версию заказа, увиденную пользователем, и направлять запрос только на реплику, достигшую этой версии. При недоступности подходящей реплики запрос временно читается из лидера. В результате обычные чтения сохраняют масштабируемость, а пользователь не видит уже пройденное состояние снова.

Что кандидаты часто упускают

1. Достаточно ли использовать sticky session для гарантии монотонного чтения?

Нет. Sticky session снижает вероятность попадания на разные реплики, но не контролирует отставание выбранной реплики. После сбоя, балансировки или восстановления соединения запрос может попасть на другой узел, поэтому требуется проверка минимальной версии.

2. Чем монотонное чтение отличается от чтения собственных записей?

Монотонное чтение требует, чтобы последовательные чтения не показывали регресс версии, даже если пользователь ничего не изменял. Чтение собственных записей сильнее в другом отношении: после успешной записи пользователь должен увидеть именно результат своей записи. Для него может потребоваться ожидание репликации записи или чтение из источника, принявшего изменение.

3. Что делать, если ни одна реплика ещё не достигла требуемой версии?

Нельзя молча отдавать более старые данные, если гарантия обязательна. Система должна дождаться репликации, обратиться к лидеру или вернуть контролируемую ошибку/состояние ожидания. Выбор зависит от допустимой задержки и критичности данных; ослабление гарантии должно быть явным, иначе пользователь получит непредсказуемое поведение.