АрхитектураРаспределённые системыРазработчик распределённых сервисов

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

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

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

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

Нужно обеспечить гарантию read-your-writes — чтение после собственной успешной записи не должно возвращать более старое состояние. Для этого запрос можно направлять на лидера, читать только реплику, подтвердившую применение записи, либо передавать между запросами позицию записи и выбирать реплику, которая уже достигла этой позиции.

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

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

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

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

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

Рассмотрим профиль пользователя. Запрос на изменение имени попадает к лидеру, лидер подтверждает запись, а следующий запрос на получение профиля направляется балансировщиком на реплику.

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

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

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

Read-your-writes — это гарантия, при которой клиент после успешно завершённой собственной записи не читает состояние, предшествующее этой записи. Она слабее полной линейризуемости: гарантия относится к наблюдениям конкретного клиента, а не требует, чтобы все клиенты немедленно видели единый результат.

Есть несколько способов её реализовать.

  • Маршрутизация чтения на лидера. После записи последующие чтения сессии направляются на узел, который принимает записи. Это простой и надёжный вариант, но он увеличивает нагрузку на лидера и может повысить задержку для географически удалённых клиентов.
  • Ожидание отставания реплики. Клиент или сервис передаёт идентификатор позиции записи, например номер журнала или версию объекта. Реплика обслуживает чтение только после применения изменений как минимум до этой позиции. Такой подход сохраняет распределение чтений, но требует поддержки маркеров прогресса и корректной обработки тайм-аутов.
  • Сессионная привязка к реплике. После записи запросы временно направляются на лидера или на реплику, известную как достаточно свежая. Это проще маркерной схемы, но плохо работает при сбоях, балансировке и длительном отставании.
  • Синхронное подтверждение репликации. Лидер ждёт подтверждения от одной или нескольких реплик. Это уменьшает окно расхождения, но само по себе не гарантирует корректное чтение: реплика могла подтвердить получение записи, но ещё не применить её к состоянию, из которого выполняется чтение. Кроме того, конкретная семантика подтверждения зависит от реализации.

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

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

Гарантия read-your-writes также не делает все чтения глобально согласованными. Другой пользователь может ещё увидеть старую версию, если его запрос обслуживает отстающая реплика. Если же бизнес-требование гласит, что каждый клиент должен немедленно видеть общий порядок операций, потребуется более сильная гарантия, например линейризуемое чтение, что обычно дороже по задержке и доступности.

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

В сервисе заказов клиент после оплаты сразу открывает страницу заказа. Запись о платеже выполняется на лидере, а чтение заказа — через географически ближайшую реплику. При межрегиональной задержке пользователь периодически видит статус «ожидает оплаты» и нажимает кнопку повторно.

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

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

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

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

1. Вопрос: Достаточно ли направлять чтение на ту же реплику, на которую ранее попала запись?

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

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

2. Вопрос: Делает ли чтение с лидера систему линейризуемой?

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

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

3. Вопрос: Что произойдёт с гарантией read-your-writes после переключения на новый лидер?

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

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