Модели консистентности

Предпосылки: CAP-теорема (strong consistency vs eventual consistency, linearizability, partition tolerance), репликация (leader/follower, sync/async, кворум).

CAP-теорема | Разрешение конфликтов

CAP-теорема ставит выбор при network partition: консистентность (linearizability) или доступность. AP-системы выбирают доступность и получают eventual consistency — гарантию «данные когда-нибудь синхронизируются». На практике двух крайностей недостаточно: каталогу онлайн-магазина хватит eventual consistency, но для списания с баланса нужна linearizability. Между этими полюсами лежит спектр промежуточных моделей, каждая из которых даёт конкретную гарантию за конкретную цену.

Eventual consistency            ← минимум гарантий, максимум доступности
    │
    │   Monotonic reads
    │   Read-your-writes
    │   Causal consistency
    │   Sequential consistency
    │
    v
Linearizability                 ← максимум гарантий, максимум координации

Eventual consistency: данные синхронизируются когда-нибудь

Если новых записей нет, рано или поздно все реплики вернут одно и то же значение. Когда именно — не определено: может через 10ms, может через 5 секунд. Нет гарантии порядка чтений и нет гарантии, что клиент увидит собственную запись.

Продавец добавляет товар в каталог онлайн-магазина. Запись попадает на Replica A, но покупатель на Replica B пока не видит этот товар.

Продавец:   POST "новый товар"     → Replica A
Покупатель: GET  каталог           → Replica B
Результат:  товара нет в списке

Для каталога это терпимо — товар появится через мгновение. Но тот же покупатель проверяет статус своего заказа. Первый запрос попадает на Replica A — статус «отправлен». Покупатель обновляет страницу, запрос уходит на Replica B, которая отстаёт — статус «в обработке». Заказ не отменялся, но интерфейс показал время, идущее назад.

Monotonic reads: время не откатывается назад

Eventual consistency не запрещает такой откат: она гарантирует конечную сходимость, но ничего не говорит о порядке промежуточных чтений.

Чтение 1 (Replica A): status = "отправлен"
Чтение 2 (Replica B): status = "в обработке"   ← более старое значение

Покупатель в панике звонит в поддержку: «Мой заказ отменили?»

Monotonic reads гарантирует: если клиент прочитал значение версии V, последующие чтения никогда не вернут значение старее V. Увидел «отправлен» — следующее чтение вернёт «отправлен» или более свежий статус, но не «в обработке».

Реализуется через отслеживание версии последнего прочитанного значения. Следующий запрос направляется только на реплики с версией не ниже.

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

Read-your-writes: видишь свои изменения

Покупатель меняет адрес доставки. Запись уходит на primary. Следующий GET профиля попадает на реплику, которая ещё не получила обновление. Старый адрес. Покупатель не понимает: изменение сохранилось или нет?

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

Типичные реализации:

Sticky sessions:
    Покупатель ──────────────────> Replica A (всегда)
    Записал на A → читает с A → видит своё изменение

Read from primary after write:
    Покупатель ── WRITE ──> Primary
    Покупатель ── READ  ──> Primary  (следующие N секунд)
    Покупатель ── READ  ──> Replica  (потом, когда реплика догнала)

При использовании кэша read-your-writes нарушается, если между записью и чтением данные отдаются из устаревшего кэша. Стратегии восстановления: инвалидация при записи, write-through, обход кэша для автора.

Для данных одного пользователя (корзина, профиль, настройки) read-your-writes часто достаточно: нет конкуренции за одни данные между разными клиентами. Проблема появляется, когда несколько пользователей взаимодействуют с общими данными и важен порядок их действий.

Causal consistency: причина перед следствием

Покупатель пишет в чат поддержки: «Где мой заказ?». Оператор читает вопрос и отвечает: «Отправлен вчера, трек-номер XYZ». Другой оператор, подключившийся к чату с другой реплики, видит:

Оператор:   "Отправлен вчера, трек-номер XYZ"    ← ответ
Покупатель: "Где мой заказ?"                      ← вопрос

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

Оператор прочитал вопрос покупателя и на его основе написал ответ — это причинная зависимость. Causal consistency гарантирует: если операция B причинно зависит от операции A, все узлы увидят A перед B.

Причинная зависимость возникает, когда клиент прочитал результат одной операции и на его основе выполнил другую, или когда два действия выполнены последовательно в одной сессии.

Независимые операции causal consistency не упорядочивает. Если два оператора одновременно пишут несвязанные сообщения в разные чаты — их порядок на разных узлах может отличаться, и это допустимо. Но что если независимые операции затрагивают одни и те же данные?

Sequential consistency: единый порядок для всех

Два продавца одновременно меняют цену одного товара: продавец A ставит 1000₽, продавец B — 1500₽. Эти операции не связаны причинно — продавцы не видели действий друг друга. Causal consistency не упорядочивает их, и разные покупатели могут увидеть разный порядок обновлений:

Покупатель X (Replica 1): цена 1000₽ → 1500₽   итого: 1500₽
Покупатель Y (Replica 2): цена 1500₽ → 1000₽   итого: 1000₽

Два покупателя видят разную текущую цену одного товара.

Sequential consistency гарантирует: существует единый порядок всех операций, и все клиенты наблюдают именно его. Этот порядок согласован с внутренним порядком операций каждого клиента: если продавец A сначала обновил описание, потом цену — все увидят описание перед ценой. Но порядок между операциями разных клиентов система определяет сама, и он не обязан совпадать с реальным временем.

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

Linearizability: как одна машина

Покупатель оплачивает заказ с одноразовым промокодом. Запрос обрабатывается на Replica A: промокод проверен, помечен как использованный, скидка применена. В тот же момент другой покупатель на Replica B пытается применить тот же промокод. Операция на A уже завершилась, но sequential consistency не гарантирует, что B видит результат — порядок между операциями разных клиентов не привязан к реальному времени, и система может расположить чтение B перед записью A в своём тотальном порядке. Промокод применяется дважды.

Linearizability — самая строгая модель. Система ведёт себя так, будто существует одна копия данных и все операции выполняются атомарно в реальном времени. Если операция A завершилась до начала операции B — все клиенты увидят результат A перед B. В отличие от sequential consistency, linearizability уважает реальное время.

Это поведение, которое даёт PostgreSQL на одной машине: записал — прочитал — увидел. Внутри одного узла PostgreSQL обеспечивает это через уровни изоляции. В распределённой системе linearizability стоит дорого: запись требует подтверждения от кворума, что добавляет latency, а при partition часть узлов становится недоступной.

Выбор модели определяется данными, а не системой

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

В том же онлайн-магазине разные данные живут под разными моделями. Каталог товаров — eventual consistency: товар появится через секунду, и это нормально. Статус заказа — monotonic reads: время не должно откатываться. Профиль пользователя — read-your-writes: человек должен видеть свои изменения. Чат поддержки — causal consistency: ответы не должны опережать вопросы. Цены при нескольких продавцах — sequential consistency: все покупатели должны видеть одинаковый порядок изменений и одинаковую итоговую цену. Баланс и промокоды — linearizability: цена ошибки несопоставимо выше цены задержки.

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

Sources

  • Kleppmann, 2017, Designing Data-Intensive Applications, Chapter 9: Consistency and Consensus — систематика моделей консистентности
  • Vogels, 2008, Eventually Consistent — классическая статья об eventual consistency и её вариациях
  • Herlihy, Wing, 1990, Linearizability: A Correctness Condition for Concurrent Objects — формальное определение linearizability
  • Lamport, 1979, How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs — определение sequential consistency

CAP-теорема | Разрешение конфликтов