CAP-теорема

Предпосылки: базовое понимание репликации (leader/follower, синхронная и асинхронная), понятие network partition, ACID (достаточно знать, что Consistency в ACID — это про constraints).

Шардинг | Модели консистентности

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

Два узла: San Francisco и New York. Оба хранят balance = 100. Клиент в San Francisco делает UPDATE balance = 50. В этот момент связь между городами рвётся.

    San Francisco                New York
    ┌─────────────┐      X      ┌─────────────┐
    │ balance=50  │             │ balance=100 │
    └─────────────┘  partition  └─────────────┘

Клиент в New York хочет прочитать balance. Узел должен решить: что делать?

Network partition — данность, не выбор

Network partition — это не partitioning (шардирование данных). Network partition — ситуация, когда узлы системы не могут общаться друг с другом: сеть разделилась на изолированные части.

ДО partition:                 ПОСЛЕ partition:

Node A ←──────→ Node B        Node A    ✕    Node B
   │               │             │               │
   └───────┬───────┘             │               │
           │                     │               │
        Node C                Node C         (изолирован)

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

CAP-теорема формулирует ограничение: когда происходит network partition, система может сохранить либо consistency, либо availability — но не оба сразу.

«Выбери 2 из 3» — распространённое, но неверное упрощение. P — не опция, а данность. Реальный выбор — между C и A в момент partition.

Два варианта ответа

У узла в New York ровно два пути.

Вариант CP — отказать: «Я не могу связаться с San Francisco. Возможно, там были изменения. Отдавать потенциально устаревшие данные я не имею права.» Клиент получает ошибку. Данные остаются консистентными, но доступность потеряна.

Вариант AP — ответить: «Отдам то, что знаю: balance = 100.» Клиент получает ответ, но значение устаревшее (реальное — 50). Доступность сохранена, консистентность потеряна.

Третьего варианта нет. Нельзя одновременно отдать правильные данные и отдать хоть какие-то данные, когда неизвестно, какие данные правильные.

Consistency в CAP — линеаризуемость

CAP Consistency — это не ACID Consistency. ACID Consistency означает, что транзакция переводит базу из одного валидного состояния в другое, не нарушая constraints (NOT NULL, FOREIGN KEY, CHECK). Это правила внутри одной базы.

CAP Consistency (она же linearizability) — требование к распределённой системе: после завершения записи любое последующее чтение с любого узла вернёт это значение или более новое. Система ведёт себя так, будто копия данных одна. Именно это свойство нарушается, когда New York отдаёт balance = 100 вместо актуальных 50.

Availability в CAP — ответ от каждого узла

CAP Availability — это не SLA availability (99.9% uptime за период). CAP Availability означает: каждый запрос к работающему узлу получает ответ с данными. Не ошибку, не таймаут — ответ. Узел не имеет права сказать «подожди, пока я свяжусь с остальными». CP-вариант нарушает именно это: New York отказывает клиенту, хотя сам узел работает.

Кворум: кто должен продолжать работу

CP-система отказывает изолированному узлу. Но кто определяет, кто изолирован? Правило большинства — кворум: узел обслуживает запросы, только если связан с большинством кластера. Большинство может быть только одно — split brain исключён на уровне арифметики. Поэтому кластеры обычно нечётные (3, 5, 7): при чётном числе возможно разделение без большинства ни у кого.

CP на практике: PostgreSQL с синхронной репликацией

Кворум определяет, кто работает, а кто нет. Но CP-поведение проявляется и на уровне каждой отдельной записи: при синхронной репликации primary не подтверждает COMMIT, пока синхронная реплика не подтвердила получение WAL (Write-Ahead Log).

Клиент           Primary              Replica
   │                │                    │
   │── INSERT ─────→│                    │
   │                │── WAL ────────────→│
   │                │←── ACK ────────────│
   │←── COMMIT OK ──│                    │

Если связь с репликой рвётся, primary не может получить ACK — и блокирует все записи. Клиент ждёт, получает таймаут, затем ошибку. Если падает primary — реплика read-only и записи тоже невозможны.

Система жертвует доступностью ради консистентности: данные не расходятся, но при partition записи останавливаются. Настройки, контролирующие это поведение — synchronous_commit и synchronous_standby_names — описаны в репликации PostgreSQL.

PostgreSQL с асинхронной репликацией

Синхронная репликация — не единственный режим PostgreSQL. При асинхронной репликации primary не ждёт подтверждения от реплики и отвечает «COMMIT OK» сразу. Если реплика отвалится — primary продолжает работать. Но если primary упадёт до того, как WAL дошёл до реплики — данные потеряны. Это сдвигает PostgreSQL ближе к AP-поведению.

AP на практике: Redis

Redis-реплики получают данные от master асинхронно. При потере связи с master Sentinel выбирает новый master из доступных реплик.

San Francisco (изолирован)       New York
┌────────────┐         X         ┌────────────┐
│ Old Master │                   │  Replica1  │
│            │                   │  Replica2  │
└────────────┘                   └────────────┘

Sentinel в New York обнаруживает недоступность master, набирает кворум среди Sentinel-ов и промоутит одну из реплик. Как именно Sentinel обнаруживает сбой (SDOWN → ODOWN), набирает кворум и выбирает реплику — в Sentinel. Но старый master в San Francisco не знает, что его заменили — он продолжает принимать записи от локальных клиентов.

San Francisco                     New York
┌──────────────┐        X        ┌──────────────┐
│ Old Master   │                 │  NEW Master  │
│ balance=500  │                 │ balance=100  │
└──────────────┘                 └──────────────┘
       ↑                                ↑
 клиент пишет                     клиент пишет

Когда сеть восстанавливается, старый master узнаёт о новом, становится репликой и перезаписывает свои данные данными нового master. Все записи, принятые старым master за время partition, теряются безвозвратно.

CP-система в такой ситуации отказала бы в записи изолированному узлу — клиент получил бы ошибку, но данные не потерялись бы. Redis выбирает доступность: клиент всегда получает «OK», но часть записей может исчезнуть.

Спектр, а не бинарный переключатель

Реальные системы не делятся строго на «чистый CP» и «чистый AP». PostgreSQL со синхронной репликацией — CP, но переключение на асинхронную сдвигает поведение к AP. Redis по умолчанию AP, но команда WAIT позволяет дождаться подтверждения от реплик — приближая к CP-поведению на отдельных операциях. Cassandra идёт дальше: consistency level настраивается per-query — ONE даёт AP-поведение, QUORUM приближает к CP, ALL — жёсткий CP.

Более того, разные части одной системы могут иметь разные требования. Банковская платформа может использовать CP для списаний и зачислений (ошибка в балансе — юридические и финансовые последствия), и AP для отображения истории транзакций (показать данные секундной давности допустимо). Выбор CP или AP определяется ценой ошибки в конкретной операции.

Eventual Consistency

AP-системы сохраняют доступность ценой расхождения данных между узлами. Расхождение не может длиться вечно — когда partition заканчивается, узлы должны синхронизироваться. Гарантия, которую дают AP-системы: данные когда-нибудь станут одинаковыми на всех узлах, но не определено когда — через 10ms, через 5 секунд или позже.

Запись: новый пост
    │
    v
  Node A: видит пост сразу
  Node B: ещё не видит ──→ (100ms) ──→ видит
  Node C: ──→ ──→ (200ms) ──→ видит

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

Когда partition заканчивается и узлы AP-системы снова видят друг друга, им нужно разрешить конфликты — ситуации, когда разные узлы приняли разные записи для одних и тех же данных. Типичные стратегии: last-write-wins (по timestamp; одна запись теряется), merge (объединение изменений; работает не для всех типов данных), передача конфликта приложению. Идеального решения нет — это цена AP. Подробнее о стратегиях — в разрешении конфликтов.

Sources

  • Gilbert, Lynch, 2002, Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services: формальное доказательство CAP-теоремы
  • Kleppmann, 2017, Designing Data-Intensive Applications, Chapter 9: Consistency and Consensus
  • Brewer, 2012, CAP Twelve Years Later: How the “Rules” Have Changed: уточнение от автора теоремы — CAP как спектр, а не бинарный выбор

Шардинг | Модели консистентности