.NET Tutorial
Senior7 мин чтенияВопрос 3 из 12

Модели согласованности и консенсус: strong, eventual, кворумы, Raft?

Проблема

РепликаЛидер (primary)Пользователь«я же только чтосохранил!»UPDATE name = 'Анна'OKасинхронная репликация(задержка 200 мс)SELECT name (сразу после записи)'Аня' — старое значение
РепликаЛидер (primary)Пользователь«я же только чтосохранил!»UPDATE name = 'Анна'OKасинхронная репликация(задержка 200 мс)SELECT name (сразу после записи)'Аня' — старое значение
Чтение со «старой» реплики

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

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

Линеаризуемость
как будто одна копия,
операции мгновенны

Последовательная
единый порядок для всех,
не обязательно реальное время

Причинная
причина видна раньше
следствия

Read-your-writes,
monotonic reads
(гарантии сессии)

Итоговая (eventual)
когда-нибудь сойдутся

Линеаризуемость
как будто одна копия,
операции мгновенны

Последовательная
единый порядок для всех,
не обязательно реальное время

Причинная
причина видна раньше
следствия

Read-your-writes,
monotonic reads
(гарантии сессии)

Итоговая (eventual)
когда-нибудь сойдутся

Спектр моделей: от строгой к слабой
Модель Гарантия Цена Пример использования
Линеаризуемость Чтение видит последнюю завершённую запись Высокая задержка, недоступность при разделении сети Баланс, остаток на складе при продаже, уникальность логина, распределённые блокировки
Причинная Ответ на комментарий не появится раньше комментария Средняя Чаты, комментарии
Read-your-writes Пользователь видит свои изменения Низкая (привязка к лидеру после записи) Профиль, настройки
Monotonic reads Не «откатывается назад» при повторных чтениях Низкая (привязка к одной реплике) Ленты, списки
Eventual Сходимость когда-нибудь Минимальная Счётчики просмотров, лайки, рекомендации, поисковый индекс

Как обеспечить read-your-writes на практике

  • Читать с лидера в течение N секунд после записи пользователя (метка времени в сессии/cookie).
  • Передавать позицию репликации (LSN в PostgreSQL) и читать с реплики, которая её догнала.
  • На клиенте: оптимистично показывать отправленные данные, не перечитывая их.

Кворумы

не успела

Запись v2

Узел 1: v2

Узел 2: v2

Узел 3: v1

Чтение R=2

Видим v1 и v2 →
берём новейшую v2

не успела

Запись v2

Узел 1: v2

Узел 2: v2

Узел 3: v1

Чтение R=2

Видим v1 и v2 →
берём новейшую v2

Кворум N=3, W=2, R=2
Настройка (N=3) Свойство
W=3, R=1 Быстрое чтение, запись недоступна при падении одного узла
W=1, R=3 Быстрая запись, чтение недоступно при падении узла
W=2, R=2 Баланс; переживает отказ одного узла для обеих операций
W=1, R=1 Максимальная доступность, только eventual

Так работают Cassandra, ScyllaDB, DynamoDB-подобные системы (с настраиваемым уровнем согласованности на запрос).

Консенсус и Raft

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

Follower 2Follower 1Лидер (term 5)Клиентбольшинство (2 из3) подтвердило —записьзафиксированаSET key = valueзапись в журнал(индекс 42)AppendEntries(42)AppendEntries(42)OKOKOK (позже)
Follower 2Follower 1Лидер (term 5)Клиентбольшинство (2 из3) подтвердило —записьзафиксированаSET key = valueзапись в журнал(индекс 42)AppendEntries(42)AppendEntries(42)OKOKOK (позже)
Raft: лидер реплицирует журнал большинству

Основные идеи Raft:

  • Выбор лидера: если follower не получает heartbeat, он становится кандидатом, увеличивает term и просит голоса; лидер — тот, кто получил большинство.
  • Репликация журнала: запись считается зафиксированной, когда её сохранило большинство узлов.
  • Безопасность: новый лидер обязательно содержит все зафиксированные записи.
  • Кластер из 2f+1 узлов переживает отказ f узлов: 3 узла — 1 отказ, 5 узлов — 2 отказа. Чётное число узлов не добавляет отказоустойчивости.

Разработчику .NET редко нужно реализовывать Raft, но важно понимать его свойства в инструментах: etcd (Kubernetes), Consul, ZooKeeper, Kafka KRaft, CockroachDB/YugabyteDB, кластеры PostgreSQL с Patroni.

Разделение сети: split brain

Большинство: 3 узла

Узел 1 (лидер)

Узел 2

Узел 3

Меньшинство: 2 узла

Узел 4

Узел 5

не может выбрать лидера —
запись недоступна

Большинство: 3 узла

Узел 1 (лидер)

Узел 2

Узел 3

Меньшинство: 2 узла

Узел 4

Узел 5

не может выбрать лидера —
запись недоступна

Без кворума нельзя иметь двух лидеров

Без механизма кворума обе половины могли бы выбрать своего лидера и принимать противоречивые записи (split brain) — после восстановления связи данные невозможно корректно объединить.

Как применять в проектировании

  • Разделяйте операции: списание денег — строгая согласованность и транзакция в одной БД; счётчик просмотров — eventual через очередь; профиль пользователя — read-your-writes.
  • Между микросервисами согласованность почти всегда итоговая: события, саги, outbox. Интерфейс должен это учитывать («заказ принят, подтверждение придёт через минуту»).
  • Для уникальности (логин, номер заказа) — ограничение уникальности в одной БД или линеаризуемое хранилище, а не проверка «SELECT, затем INSERT» между сервисами.
  • Для конфликтующих параллельных записей в eventual-системах — стратегии: last-write-wins (теряет данные), версионные векторы, CRDT (счётчики, множества, которые сходятся математически).

Вопросы на засыпку

Чем линеаризуемость отличается от сериализуемости?

Линеаризуемость — свойство одного объекта (регистра, ключа) в распределённой системе: операции выглядят мгновенными и упорядоченными по реальному времени. Сериализуемость — свойство транзакций над многими объектами: результат как при каком-то последовательном выполнении. Их комбинация — strict serializability (Spanner, CockroachDB).

Почему в кластере etcd рекомендуют 3 или 5 узлов, а не 4?

Кворум — большинство: для 3 узлов это 2 (переживает 1 отказ), для 4 — 3 (тоже переживает 1 отказ), для 5 — 3 (переживает 2). Четвёртый узел не повышает отказоустойчивость, но увеличивает задержку записи (нужно больше подтверждений).

Что такое CRDT?

Conflict-free Replicated Data Types — структуры данных, которые можно изменять на разных репликах независимо, а затем объединить без конфликтов с гарантированной сходимостью: счётчики (G-Counter), множества (OR-Set), регистры. Используются в совместном редактировании, офлайн-приложениях, Redis Enterprise active-active.

Как пользователю объяснить eventual consistency в интерфейсе?

Показывать состояние процесса («платёж обрабатывается»), оптимистично отображать собственные действия, обновлять данные по событиям (SignalR), избегать сценариев, где пользователь сразу проверяет результат на другой странице, читающей с отстающей реплики.

Итог

Модель согласованности определяет, что увидит чтение после записи: линеаризуемость — как будто одна копия данных, eventual — сходимость со временем, между ними — причинная и гарантии сессии. Кворумы (R + W > N) и консенсус (Raft: лидер, журнал, большинство) обеспечивают согласованность при отказах ценой задержки и доступности меньшинства. Senior-разработчик выбирает уровень согласованности для каждой операции по бизнес-требованиям.