Модуль: Распределённые системы · Уровень: Senior+

TL;DR#

  • Consistency model — это контракт между хранилищем и приложением: какие чтения допустимы при заданной истории операций. Сильнее модель ⇒ проще рассуждать ⇒ дороже по latency/availability.
  • Linearizability (strong/atomic consistency) — recency-гарантия над одним объектом: как только запись завершилась, все последующие чтения видят её (или более новое значение). Есть единый total order, согласованный с реальным временем.
  • Serializability — isolation-гарантия над транзакциями (мульти-объектными): результат конкурентного выполнения транзакций эквивалентен какому-то последовательному порядку. Не обязана уважать реальное время.
  • Strict serializability = serializability + linearizability — эквивалент последовательному порядку, который к тому же уважает реальное время (Spanner: external consistency).
  • Eventual consistency — слабейшая полезная модель: при прекращении записей реплики сходятся. Ничего не говорит про порядок «во время».
  • Causal consistency — сохраняет порядок причинно-связанных операций (happens-before); самая сильная модель, достижимая при сохранении доступности во время партиции.
  • Client-centric / session guarantees: read-your-writes, monotonic reads, monotonic writes, writes-follow-reads — гарантии в рамках сессии одного клиента, удобный компромисс.

Теория#

Зачем нужны модели согласованности#

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

Главный трейдофф: сильнее модель → больше синхронной координации → выше latency и ниже availability (см. CAP/PACELC).

Спектр моделей (от сильной к слабой)#

strict serializability   (Spanner external consistency)
        │  (txns + real-time)
linearizability           (single-object, real-time recency)   ← "strong consistency"
        │
sequential consistency    (общий порядок, но не обязан уважать real-time)
        │
causal consistency        (happens-before сохраняется)   ← макс. при сохранении availability
        │
session guarantees        (read-your-writes, monotonic reads/writes, writes-follow-reads)
        │
eventual consistency      (реплики сходятся "когда-нибудь")

Линейного «одного» спектра на самом деле нет (модели образуют частичный порядок), но как первое приближение порядок выше полезен.

Linearizability (strong / atomic)#

Гарантия: существует total order над всеми операциями такой, что:

  1. Он согласуется с real-time: если операция A завершилась до начала операции B (по абсолютному времени), то A предшествует B в порядке.
  2. Каждое чтение возвращает значение последней предшествующей записи в этом порядке.

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

Реальное время →
Клиент A:  ──[ write x=1 ]──┐
Клиент B:               ──[ read x ]──   должен вернуть 1
                              ↑ если read начался ПОСЛЕ завершения write,
                                linearizability обязывает вернуть 1 (или новее)
  • Это per-object свойство (один регистр/ключ). Не про транзакции над несколькими ключами.
  • Это «C» из CAP. Требует синхронной координации (кворум/лидер) ⇒ дорого.
  • Композируема: если каждый объект линеаризуем, система из этих объектов тоже линеаризуема (важное отличие от serializability).

Serializability#

Гарантия: результат конкурентного выполнения набора транзакций (каждая — над произвольным множеством объектов) эквивалентен некоторому последовательному (serial) их выполнению.

  • Это isolation-свойство (ACID-I), про транзакции, а не про recency.
  • Сериализуемость не уважает real-time: допустим, T1 закоммитилась до начала T2, но эквивалентный serial-порядок может поставить T2 перед T1. Чтение в T2 законно может «не увидеть» коммит T1.
  • Не композируема сама по себе так, как linearizability.

Linearizability vs Serializability — ключевое различие#

Это любимый вопрос на senior+.

LinearizabilitySerializability
Про чтоRecency (свежесть)Isolation (изоляция транзакций)
ОбластьОдин объект, одна операцияМного объектов, транзакции
Real-timeУважает (total order согласован с реальным временем)Не обязана уважать
Откуда терминРаспределённые системы (Herlihy & Wing)Теория БД (ACID-I)
КомпозируемостьДа (локальное свойство)Нет

Strict serializability = serializability ∧ linearizability: эквивалентный serial-порядок к тому же уважает real-time. Это то, что даёт Spanner (под именем external consistency) благодаря TrueTime. Самая сильная практическая модель для транзакционных систем.

                 single-object         multi-object/txn
recency only     linearizability       —
isolation only   —                     serializability
recency + iso     —                    strict serializability

Кратко: linearizability — про «когда» (свежесть), serializability — про «как переплелись транзакции» (изоляция). Strict serializability — оба сразу.

Sequential consistency#

Существует total order, согласованный с program order каждого клиента (операции одного клиента в порядке их выдачи), но не обязан согласовываться с real-time между разными клиентами. Слабее linearizability: чтение может вернуть устаревшее значение, если только не нарушает порядок внутри клиентов. Модель памяти многих языков/процессоров близка к этому.

Causal consistency#

Сохраняет порядок только для причинно-связанных (happens-before, Lamport) операций; конкурентные (causally independent) операции разные узлы могут видеть в разном порядке.

Happens-before (→):

  • операции одного процесса упорядочены;
  • send → receive того же сообщения;
  • транзитивность.
A: write post = "hi"   ──┐ (causal: коммент видит пост)
B: (видит post)          └─→ write comment = "re: hi"
   Любой узел, увидевший comment, ОБЯЗАН уже видеть post.
   Но два независимых поста разные узлы могут увидеть в разном порядке.

Важность: causal consistency — самая сильная модель, совместимая с высокой доступностью во время партиции (теорема: convergent + causal — потолок для AP-систем). Реализуется через vector clocks / version vectors. Предотвращает аномалии типа «вижу ответ, но не вижу исходное сообщение».

Session / client-centric guarantees#

Гарантии в рамках сессии одного клиента (не глобальные). Терпимы к слабой репликации, но устраняют самые раздражающие аномалии для конкретного пользователя.

  • Read-your-writes (read-my-writes): после того как клиент записал значение, его последующие чтения видят это значение (или новее). Без этого пользователь меняет аватар и видит старый.
  • Monotonic reads: если клиент прочитал значение, последующие чтения не вернут более старое. Без этого время «прыгает назад» при переключении реплик.
  • Monotonic writes: записи одного клиента применяются на репликах в порядке их выдачи. Без этого set name; set email может примениться в обратном порядке.
  • Writes-follow-reads (session causality): если клиент прочитал значение, а затем сделал запись, эта запись логически следует за прочитанным значением на всех репликах. Обеспечивает причинность на уровне сессии (комментарий после прочтения поста).

Session consistency обычно = read-your-writes + monotonic reads + monotonic writes в рамках сессии. Часто реализуется через sticky sessions (клиент закреплён за репликой) или передачу версии/токена (например, Cassandra LWT, MongoDB causal consistency tokens, Cosmos DB session tokens).

Эти гарантии слабее causal (только своя сессия), но сильнее голого eventual и закрывают 90% UX-проблем.

Eventual consistency#

Слабейшая практически используемая модель: если записи прекратятся, все реплики в конце концов сойдутся к одному значению (liveness-гарантия). Во время записей — почти никаких гарантий о порядке/свежести.

  • Не определяет, какое значение победит при конфликте — это решает разрешение конфликтов (LWT по timestamp, vector clocks + siblings в Riak, CRDT).
  • «Eventually» без верхней границы по времени; на практике — десятки мс — секунды.
  • Дёшево и максимально доступно ⇒ дефолт для Dynamo/Cassandra/DNS.
  • Strong eventual consistency (CRDT): реплики, получившие один набор обновлений (в любом порядке), гарантированно в одном состоянии — без координации, через коммутативные/идемпотентные merge.

Где это всплывает в Go#

В Go-сервисах модель согласованности обычно «спускается» из выбранного хранилища, но клиентский код обязан её учитывать:

// Анти-паттерн: read-after-write на eventually consistent реплике
_, err := db.ExecContext(ctx, "INSERT INTO orders ...") // пишем в primary
if err != nil { return err }
// читаем со read-replica — можем НЕ увидеть только что вставленное
row := replica.QueryRowContext(ctx, "SELECT ... FROM orders WHERE id=$1", id)

Решения: читать с primary для read-your-writes критичных путей; передавать версию/LSN и ждать её на реплике; sticky routing; или строить инвариант так, чтобы stale-read был безопасен (идемпотентность, повтор).

Подводные камни / gotchas#

  • Linearizability ≠ serializability. Первое — recency над одним объектом, второе — isolation над транзакциями. Их регулярно путают; strict serializability — это их объединение.
  • Serializability не уважает real-time. Транзакция может законно «не увидеть» только что закоммиченную другую транзакцию, если эквивалентный serial-порядок ставит её раньше. Для real-time нужна strict serializability.
  • Eventual consistency не разрешает конфликты сама. «Сойдутся» — да, но к чему зависит от стратегии (LWW теряет записи; vector clocks дают siblings; CRDT мержит без потерь).
  • Read-your-writes легко сломать read-репликами. Запись в primary, чтение с асинхронной реплики — классический баг «обновил профиль, вижу старый».
  • Monotonic reads ломается при балансировке между репликами без sticky/версионности: пользователь видит то новые, то старые данные.
  • Causal consistency требует трекинга зависимостей (vector clocks), что добавляет метаданные и сложность; «дешёвая» causal-репликация — нетривиальна.
  • Сильная модель на запись ≠ сильная на чтение. Многие системы тюнят отдельно (Cassandra: write CL и read CL независимы; ZooKeeper: линеаризуемые записи, но чтения могут быть stale без sync()).
  • Линеаризуемость дорога и не всегда нужна. Навешивать strong consistency на всё — это лишняя latency; senior выбирает минимально достаточную модель на каждый путь.

Вопросы на собеседовании#

В: В чём разница между linearizability и serializability? О: Linearizability — recency-гарантия над одним объектом: единый порядок операций, согласованный с реальным временем, чтение видит последнюю завершившуюся запись. Serializability — isolation-гарантия над транзакциями (много объектов): результат эквивалентен какому-то последовательному порядку, но real-time не обязан соблюдаться. Первое из распределённых систем, второе — из теории БД (ACID-I).

В: Что такое strict serializability и кто её даёт? О: Это serializability + linearizability: эквивалентный serial-порядок транзакций к тому же уважает реальное время (если T1 закоммитилась до начала T2, то T1 в порядке раньше). Google Spanner предоставляет её под названием external consistency, используя TrueTime для синхронизации часов.

В: Может ли сериализуемая система вернуть устаревшие данные? О: Да. Сериализуемость не обязана уважать real-time: транзакция T2, начавшаяся после коммита T1, может законно оказаться в эквивалентном serial-порядке перед T1 и не увидеть её изменений. Чтобы это исключить, нужна strict serializability.

В: Что гарантирует eventual consistency и чего не гарантирует? О: Гарантирует, что при прекращении записей реплики сойдутся к одному состоянию. Не гарантирует порядок или свежесть во время записей и сама по себе не определяет, какое значение победит в конфликте — это задача стратегии разрешения (LWW, vector clocks, CRDT).

В: Что такое read-your-writes и как её обеспечить при наличии read-реплик? О: Гарантия, что клиент после своей записи видит её (или новее) в последующих чтениях. При асинхронных репликах обеспечивается чтением с primary для своих недавних записей, sticky-сессией к одной реплике, или передачей версии/LSN записи и ожиданием, пока реплика догонит её.

В: Чем monotonic reads отличается от read-your-writes? О: Monotonic reads: последующие чтения клиента не возвращают более старое значение, чем уже прочитанное (время не идёт назад). Read-your-writes: чтения видят собственные записи клиента. Первое про монотонность чтений, второе про видимость своих записей — это разные session guarantees.

В: Почему causal consistency особенная среди слабых моделей? О: Это самая сильная модель согласованности, совместимая с полной доступностью во время сетевой партиции (доказано, что «причинность + сходимость» — потолок для AP-систем). Она сохраняет порядок причинно-связанных операций (happens-before), устраняя аномалии вроде «вижу ответ без исходного сообщения», и при этом не требует глобальной синхронной координации.

В: Композируемы ли linearizability и serializability? О: Linearizability — да: если каждый объект линеаризуем, вся система из этих объектов линеаризуема (локальное свойство). Serializability сама по себе не композируема таким образом — её надо обеспечивать на уровне всего набора транзакций.

В: Что такое session consistency и как её реализуют на практике? О: Набор гарантий в рамках сессии одного клиента — обычно read-your-writes + monotonic reads + monotonic writes. Реализуется через sticky-сессии (закрепление клиента за репликой) либо через session/version-токены, которые клиент носит с собой и которые заставляют реплику обслужить запрос не старее указанной версии (Cosmos DB session tokens, MongoDB causal consistency).

На что копают на senior+#

  • Чёткое разведение linearizability (recency, single-object) vs serializability (isolation, multi-object) и понимание, что strict serializability — их объединение (Spanner / external consistency).
  • Понимание, что serializability не уважает real-time, и умение привести конкретную аномалию, которую это допускает.
  • Знание спектра и того, что это частичный порядок, а не одна линия; умение разместить sequential, causal, session guarantees.
  • Понимание causal как потолка для AP и механики happens-before / vector clocks.
  • Знание session guarantees поимённо (read-your-writes, monotonic reads/writes, writes-follow-reads) с конкретными UX-аномалиями каждой и способами обеспечить (sticky, version tokens).
  • Понимание стоимости: сильная согласованность = синхронная координация = latency/availability-цена (связь с CAP/PACELC), и умение выбрать минимально достаточную модель на конкретный путь.
  • Практика: read-after-write на read-репликах, разрешение конфликтов при eventual (LWW теряет данные, CRDT/vector clocks — нет), независимый тюнинг read/write consistency.