Загружаем научный разбор
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Автор: Казачкин Даниил Михайлович · Обновлено
Точный и практический разбор CAP-теоремы: что означают C, A и P, почему «выбрать два из трёх» неточно и как проектировать поведение API при разделении сети.
CAP-теорему часто пересказывают как меню из трёх пунктов: consistency, availability и partition tolerance, из которых распределённая система якобы может выбрать любые два. Эта мнемоника помогает запомнить буквы, но мешает проектировать реальные сервисы. Разделение сети — не функция продукта, которую команда по желанию выключает. Это условие отказа, при котором узлы перестают вовремя обмениваться сообщениями. Настоящий выбор возникает уже внутри такого отказа: ждать или отклонить часть запросов ради согласованного результата либо ответить всем достижимым клиентам, допуская расхождение реплик.
Разберём точные свойства из работы Сета Гилберта и Нэнси Линч, проиграем отказ на двух репликах и превратим теорему в практический список вопросов для API, хранилища и эксплуатации.
Буквы CAP легко спутать с более широкими бытовыми понятиями. В формальной постановке у них довольно узкий смысл.
Consistency — атомарная, или линеаризуемая, согласованность. Операции выглядят так, будто существует одна актуальная копия данных и все успешные чтения и записи выстроены в единый порядок, совместимый с реальным временем. Если запись завершилась до начала чтения, это чтение не должно вернуть предыдущее значение.
Availability — каждый запрос, который получил не отказавший узел, в конечном счёте завершается ответом. Это не обещание низкой задержки и не процент из SLA. Ошибка вида «сейчас не могу подтвердить результат» тоже не превращает недоступную операцию в доступную в смысле теоремы, если контракт требует нормального результата для каждого запроса.
Partition tolerance — система продолжает работать в модели, где сеть может потерять произвольное число сообщений между группами узлов. Partition — это не только оборванный кабель. Практически неотличимы друг от друга потеря пакетов, очень большая задержка, перегруженный канал, зависший процесс и пауза сборщика мусора: удалённый узел не может надёжно узнать, придёт ли ответ позже или не придёт вовсе.
Гилберт и Линч доказали, что в асинхронной сети при разделении нельзя одновременно гарантировать атомарную согласованность и доступность для каждого запроса. Теорема не утверждает, что любая распределённая система постоянно обладает ровно двумя буквами. Она описывает невозможность совмещать конкретные гарантии в конкретной модели отказа.
Пусть профиль пользователя хранится на репликах A и B. До отказа обе содержат значение email = old@example.test. Сеть разделилась: A и B доступны своим клиентам, но сообщения между ними не проходят.
Клиент рядом с A записывает new@example.test и получает подтверждение. Затем другой клиент читает профиль через B. У B нет способа выяснить, была ли запись на A: сообщение могло потеряться, а ответ нельзя ждать бесконечно.
Есть два принципиальных решения.
Третий вариант нельзя получить более умным таймаутом. Таймаут помогает выбрать момент перехода в аварийный режим, но не приносит информацию с недоступной стороны partition.
Следующий код не моделирует протокол репликации целиком. Он делает явным решение, которое часто прячется за настройкой consistency level:
from dataclasses import dataclass
@dataclass
class Replica:
value: str
version: int
peer_reachable: bool
def read_during_partition(
local: Replica,
*,
require_latest: bool,
) -> str:
if require_latest and not local.peer_reachable:
raise TimeoutError("нельзя подтвердить актуальность значения")
return local.value
replica_a = Replica("new@example.test", version=2, peer_reachable=False)
replica_b = Replica("old@example.test", version=1, peer_reachable=False)
print(read_during_partition(replica_b, require_latest=False))
# old@example.test — ответ есть, но он может быть устаревшим
read_during_partition(replica_b, require_latest=True)
# TimeoutError — актуальность защищена ценой отказа операцииВ промышленной системе вместо булева флага будут кворумы, сроки аренды лидера, fencing tokens, векторные часы, разрешение конфликтов или сессии с разными уровнями согласованности. Но источник компромисса остаётся тем же: локальный узел не знает состояние недоступной реплики.
У системы в нескольких зонах доступности нет надёжного режима «не учитывать P». Она либо определяет поведение при потере связи, либо получает его случайно. Поэтому полезнее говорить так:
Точная формулировка для практики: когда сеть разделена, для затронутой операции приходится пожертвовать атомарной согласованностью или полной доступностью.
В нормальном режиме, когда реплики общаются, система может одновременно давать согласованные ответы и обслуживать запросы. Более того, решение необязательно едино для всего продукта. Баланс пользователя можно блокировать без подтверждённого кворума, а счётчик просмотров — принимать локально и объединять позже. Один сервис способен выбрать разное поведение для разных команд, сущностей и этапов инцидента.
Ярлыки CP и AP поэтому описывают направление дизайна, а не исчерпывающую спецификацию.
В статье 2012 года Гилберт и Линч отдельно подчёркивают, что свойства можно рассматривать на интервалах времени и для частей системы, а между строгими крайностями существует пространство ослабленных гарантий. Это ещё одна причина не превращать CAP в постоянную наклейку на продукт.
Репликацию часто настраивают числами N, W и R:
Условие W + R > N создаёт пересечение наборов чтения и записи. Например, при N = 3 можно потребовать W = 2 и R = 2. Тогда успешное чтение встречает хотя бы одну реплику, участвовавшую в успешной записи. Однако при разделении «одна реплика против двух» меньшая сторона не соберёт кворум. Согласованность оплачена отказом части операций.
Само пересечение также недостаточно без правил сравнения версий и обработки конкурентных записей. Узел должен отличить новое значение от старого, а система — определить, что делать с двумя изменениями, которые не образуют простой порядок. Кворум — элемент протокола, не доказательство всех пользовательских гарантий.
Начинать стоит не с вопроса «наша база CP или AP?», а с последствий неверного ответа и временного отказа.
Для списания денег, выдачи уникального имени или смены владельца ресурса опасен конфликт двух одновременно подтверждённых операций. Часто разумнее вернуть повторяемую ошибку, поставить команду в очередь или направить её к подтверждённому лидеру. Клиенту понадобятся idempotency key, понятный статус и безопасный retry.
Для лайков, телеметрии, кэша рекомендаций или черновика текста временно устаревший ответ может быть приемлемее отказа. Тогда дизайн обязан объяснить последующее слияние: последнее изменение по физическому времени, монотонный счётчик, CRDT, предметное разрешение конфликта или ручная проверка дают разные результаты.
Полезно зафиксировать решение таблицей, а не одной буквой:
| Операция | Что делаем без кворума | Что видит клиент | Как восстанавливаемся |
|---|---|---|---|
| Списание | Не подтверждаем | Повторяемый статус «не определено» | Проверяем idempotency key и журнал |
| Чтение каталога | Возвращаем локальную копию | Метка времени или допустимая давность | Фоновая синхронизация |
| Уникальная регистрация | Блокируем запись | Явный временный отказ | Повтор после выбора лидера |
| Счётчик события | Принимаем локально | Приблизительное значение | Коммутативное объединение |
Такая таблица сразу раскрывает обязанности API и эксплуатации, которые скрывает фраза «мы выбрали AP».
Распределённый узел видит не «разделение сети», а отсутствие ожидаемого сообщения. Поэтому переход между режимами тоже является частью продукта:
Короткий timeout ускоряет реакцию, но увеличивает число ложных подозрений при обычных задержках. Длинный снижает ложные переключения, но дольше удерживает запросы. CAP не выбирает это число и не заменяет измерение latency tail.
Теорема не отвечает на множество соседних вопросов:
Она также не говорит, что eventual consistency означает «данные когда-нибудь сами станут правильными». Нужно определить условие сходимости, функцию объединения, доставку пропущенных изменений и поведение при новых конкурентных записях. Без этого слово eventual не является инженерной гарантией.
Наконец, CAP нельзя использовать как оправдание любого сбоя. Если сервис недоступен из-за исчерпанного пула соединений или медленного запроса к одной базе, это проблема ёмкости и реализации, а не неизбежное следствие теоремы.
Команда может проверить свой выбор контролируемым тестом:
Важно тестировать не только счастливый финал. Если запрос оборвался после фиксации записи, клиент может повторить его; без идемпотентности это отдельный источник двойного эффекта. Если обе стороны приняли изменение, автоматическое правило слияния может потерять предметно значимые данные. Если меньшая сторона отказала, балансировщик не должен продолжать направлять туда трафик бесконечно.
Перед выбором технологии и consistency level ответьте на восемь вопросов:
Точная польза CAP-теоремы не в классификации баз данных тремя буквами. Она заставляет признать предел информации во время сетевого отказа и заранее выбрать наблюдаемое поведение каждой важной операции. Когда этот выбор записан в контракте, тестах и runbook, абстрактная теорема становится практическим инструментом разработки.
Самостоятельный редакционный разбор ЯдроКода по первичным публикациям Seth Gilbert, Nancy Lynch и Eric Brewer. Факты и идеи изложены редакцией самостоятельно; фрагменты исходного текста, код и иллюстрации не воспроизводятся.
Иллюстрации, таблицы и программные примеры созданы редакцией; материалы и графика первоисточников не копируются.