Модели консистентности
Предпосылки: 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