Модели согласованности

О лекции

Согласованность — обещание, что клиент увидит после записи. Strong, eventual, read-your-writes и monotonic reads различаются окном видимости и ценой координации. Это не то же самое, что транзакция в одной БД.

Распределённые системы architecture собеседование

Пока одна база и один процесс, «сохранил — сразу прочитал» кажется законом. Как только появляются реплики, кэш и несколько датацентров, закон пропадает.

Модель согласованности — контракт. Какие истории наблюдений допустимы. Это не синоним ACID. Это не свойство одной таблицы Postgres.

Клиент прав, когда говорит «я сохранил и открыл старое». Система выполнила более слабое обещание, чем он ждал.

Пример. Пользователь сменил аватар. Запись ушла на primary. Следующий запрос попал на отстающую реплику и вернул старую фотографию. Для него контракт сломан. Для eventual consistency это нормально, пока вы так и договорились.

Шкала обещаний

Линеаризуемость (strong): операции выглядят так, будто есть один регистр и глобальные часы. Это дорого. Нужен кворум или лидер. При партиции часто отказ.

Sequential consistency слабее: есть общий порядок, но он не обязан совпадать с реальным временем. Causal: «причина раньше следствия» видно. Независимые записи могут расходиться.

Eventual consistency обещает только сходимость. Если записи прекратятся или конфликты разрешатся, и реплики обменяются, они придут к одному состоянию. Пока поток записей идёт, клиент может увидеть смесь старого и нового.

Сессионные гарантии

  • Read-your-writes. Свой write виден своим следующим read. Липкая сессия на primary, версия в токене, чтение с той реплики, которая уже применила запись.
  • Monotonic reads. На одной сессии нельзя откатиться на более старую версию после более новой. Иначе профиль «прыгает» назад.
  • Monotonic writes. Записи одного клиента не переставляются местами. Иначе «сменил email, потом имя» на реплике применится наоборот.

Пример. Read-your-writes не даёт «все в мире видят мою запись сразу». Только автор видит свой эффект. Два телефона одного человека без общего токена сессии не обязаны видеть одно и то же.

package profile

type Session struct {
	UserID  int64
	SeenVer int64 // растёт на записи этого пользователя
}

func (s *Store) Get(sess Session) (Profile, error) {
	p, err := s.replica.Get(sess.UserID)
	if err != nil {
		return Profile{}, err
	}
	if p.Version < sess.SeenVer {
		return s.primary.Get(sess.UserID)
	}
	return p, nil
}

Версия в сессии ловит отсталую реплику. Иначе monotonic reads сломаны.

Где ломается в проде

Кэш перед БД ломает read-your-writes, если invalidate опаздывает: клиент читает ключ раньше, чем истечёт TTL.

Read replica без sticky-сессии — классика «сохранил профиль, открыл — старое».

Имеет смысл спрашивать: согласованность для какого ключа и какого клиента. Линеаризуемость корзины одного user_id дешевле, чем «все корзины мира в одном порядке».

Типичная ошибка

Сказать «у нас eventual consistency» и остановиться. Без окна сходимости, правила конфликтов и ответа на «что увидит автор через 200 мс» это не модель, а отказ обсуждать контракт.

На собеседовании

Назовите, чью сессию защищаете. Read-your-writes и monotonic reads — отдельные обещания. Их не даёт «eventual само». Кэш и replica — первые места, где контракт рвётся.