Получить доступ

CAP и согласованность

Точная формулировка CAP и типичные искажения, неизбежность разделения и выбор C/A по операции, спектр согласованности и сессионные гарантии, кворумы N/R/W и их пределы, PACELC и разрешение конфликтов.

Три буквы — точная формулировка

CAP пересказывают неточно чаще, чем любую другую теорему, и на собеседовании это слышно с первой фразы. Consistency здесь означает линеаризуемость: существует единый порядок операций, и после подтверждения записи ни один клиент уже не увидит предыдущее значение. Это свойство порядка, наблюдаемого снаружи, а не утверждение о том, что копии данных совпадают побайтово в каждый момент времени.

Availability в формулировке теоремы — это гарантия, что каждый запрос к работающему узлу получает не-ошибочный ответ за конечное время. Она не имеет отношения к процентам аптайма в соглашении об уровне сервиса: система с доступностью 99,99 процента может быть недоступной в смысле CAP, если при разделении часть узлов отвечает ошибкой.

Partition tolerance — способность продолжать работу, когда сообщения между частями системы теряются или задерживаются произвольно долго. Важно, что теорема доказана для асинхронной сети, где нельзя отличить медленный узел от мёртвого; именно из этой невозможности всё и следует.

Формула «выберите два из трёх» — вредное упрощение. Теорема утверждает лишь, что во время разделения нельзя одновременно оставаться линеаризуемым и отвечать на все запросы. Вне разделения никакого ограничения нет, и нормально работающая система даёт и согласованность, и доступность одновременно.

И ещё одна путаница, которую стоит развести вслух: буква C в CAP и буква C в ACID означают разное. В ACID это сохранение инвариантов схемы внутри транзакции, в CAP — линеаризуемость операций над одним объектом данных. Транзакционная база может быть строго ACID и при этом не быть линеаризуемой для читающих реплик.

Треугольник CAP: согласованность, доступность, устойчивость к разделению; при сетевом разделении приходится выбирать одно из C/A.
При сетевом разделении (P неизбежно) система выбирает между согласованностью и доступностью.

Разделение неизбежно — выбор возникает только при нём

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

Отсюда главный практический вывод: устойчивость к разделению не выбирают. Система либо обрабатывает разделения осознанно, либо ведёт себя при них непредсказуемо — и второе означает не «мы выбрали CA», а «мы не думали об этом».

Системы, выбирающие согласованность, при разделении останавливают обслуживание в меньшинстве: узлы, потерявшие кворум, перестают принимать запись, а иногда и чтение, чтобы данные не разошлись. Так устроены хранилища конфигурации и координации вроде etcd, ZooKeeper и Consul, а также кластеры с кворумной репликацией. Это правильный выбор там, где расхождение недопустимо: выбор лидера, распределённые блокировки, остатки на счетах, бронирование.

Системы, выбирающие доступность, продолжают отвечать из локальных реплик, принимая расхождение и сходясь позже. Так работают Cassandra и DynamoDB в мягких режимах, DNS, многие кэши. Это правильный выбор для ленты, корзины, счётчиков просмотров, пользовательских настроек — там, где показать слегка устаревшее лучше, чем не показать ничего.

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

Спектр согласованности

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

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

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

Отдельно стоит причинная согласованность: операции, связанные причинно, видны всем в одном порядке — ответ не появится раньше комментария, на который он отвечает. Это самая сильная модель, которая доказуемо совместима с доступностью при разделении, поэтому она интересна как верхняя граница возможного для AP-систем.

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

Кворумы N/R/W и их пределы

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

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

Расхождения лечат фоновыми механизмами: восстановление при чтении обновляет отставшую реплику, когда её застали за выдачей старого значения; антиэнтропия сравнивает деревья хешей и досылает недостающее; отложенная передача сохраняет запись для временно недоступного узла и доставляет её позже. Без этих механизмов кворум сам по себе не приводит систему к сходимости.

Важная тонкость уровня senior: пересечение кворумов даёт свежесть, но не порядок. Прерванная на середине запись, две конкурентные записи без согласования, а особенно нестрогий кворум, при котором подтверждения принимают от произвольных доступных узлов во время разделения, — всё это ломает гарантию. Линеаризуемость требует консенсуса (Raft, Paxos) или явной синхронизации, а не только арифметики R + W > N. Ответ, который останавливается на неравенстве, обычно и проверяют дополнительным вопросом.

Практический смысл в том, что уровень согласованности в таких хранилищах — это ручка, которую крутят по операции, а не настройка кластера. Чтение профиля можно взять с одной реплики, а списание бонусов — кворумом, в одном и том же приложении.

PACELC — то, о чём CAP молчит

CAP описывает поведение только во время разделения, то есть в редком режиме. PACELC достраивает картину: если есть разделение, выбирают между доступностью и согласованностью; иначе — между задержкой и согласованностью. Вторая половина важнее в повседневной жизни, потому что разделения редки, а задержка есть всегда.

Сильная согласованность стоит времени даже в идеально работающей сети: нужно дождаться подтверждения кворума реплик или сходить к лидеру, который может находиться в другой зоне доступности. Это физика расстояния и скорости света, а не качество реализации, и оптимизировать её кодом нельзя.

Классификация помогает говорить о конкретных хранилищах предметно: Cassandra по умолчанию предпочитает доступность при разделении и задержку в норме; MongoDB и реляционные кластеры с синхронной репликой предпочитают согласованность в обоих режимах; DynamoDB настраивается на уровне запроса. Формулировка «эта база AP» без второй половины — незаконченная характеристика.

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

Разрешение конфликтов

Конфликты появляются там, где записи принимаются в нескольких местах одновременно: репликация с несколькими лидерами, AP-хранилища во время разделения, офлайн-клиенты, синхронизирующиеся после восстановления связи. Вопрос не в том, можно ли их избежать, а в том, кто и по какому правилу их разрешает.

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

Версионные векторы фиксируют причинность: система отличает «эта запись новее» от «эти записи конкурентны» и во втором случае отдаёт обе версии приложению. Ничего не теряется, но появляется обязанность слить их по смыслу домена, и эту работу нельзя переложить на инфраструктуру.

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

Выбор всегда диктует домен. Для корзины потеря добавленного товара недопустима — нужен осмысленный слив. Для текущего статуса пользователя последняя запись обычно устраивает. А для бронирования конкретного места конфликт вообще не следует допускать: это тот случай, когда честнее заплатить за согласование и остаться недоступным при разделении, чем продать одно место дважды.

Как отвечать на собеседовании

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

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

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

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

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем