CAP, консистентность и выбор хранилища
Мы прошли четыре модели NoSQL: документную, key-value, wide-column, графовую. У каждой свой способ моделирования. Финальный урок модуля связывает их в общую картину и отвечает на вопрос «почему вообще столько разных хранилищ и как выбирать». Ответ лежит в фундаментальном свойстве распределённых систем, которое описывает теорема CAP, и в понятии модели консистентности.
Главная идея урока: выбор хранилища — это не выбор «модного» инструмента, а выбор фундаментального компромисса. И этот компромисс диктует модель данных. CAP объясняет, почему компромисс неизбежен; модель консистентности — как именно вы его настраиваете; а вместе они определяют, как вы будете моделировать.
Теорема CAP
Теорема CAP — про распределённые системы хранения: те, где данные хранятся на нескольких узлах. Она утверждает, что у такой системы есть три желаемых свойства, но одновременно гарантировать все три нельзя.
C — Consistency (согласованность). Любое чтение возвращает самые свежие записанные данные. Записали новое значение — все последующие чтения с любого узла видят именно его, а не устаревшее. (Это не та же C, что в ACID; здесь — про согласованность копий между узлами.)
A — Availability (доступность). Каждый работающий узел отвечает на каждый запрос за разумное время. Система не «зависает» и не отказывает в ответе.
P — Partition tolerance (устойчивость к разделению). Система продолжает работать, даже если сеть между узлами разорвалась — часть узлов потеряла связь с другой частью (это называется network partition).
Суть теоремы: одновременно держать все три нельзя. Но формулировка «выбери любые два» немного упрощённая, и правильное понимание важнее. В реальной распределённой системе сетевые разрывы случаются — это не гипотеза, а факт жизни любого кластера. Значит, P нельзя «не выбрать»: устойчивость к разрыву обязательна, иначе система при первом же сбое сети просто развалится. Поэтому реальный выбор — не «любые два из трёх», а более узкий: когда разрыв сети уже произошёл, чем жертвовать — C или A?
Реальный выбор: CP или AP
Итак, разрыв сети случился. Два узла перестали видеть друг друга, а на один из них приходит запись. Система обязана решить:
Путь CP (жертвуем Availability ради Consistency). Узел, не уверенный, что у него свежие данные (он же потерял связь со вторым узлом), отказывается отвечать или отклоняет запись. Лучше не ответить, чем ответить неверно. Система остаётся согласованной, но часть запросов не обслуживается, пока сеть не восстановится.
Путь AP (жертвуем Consistency ради Availability). Узел отвечает в любом случае, даже рискуя отдать устаревшие данные или принять запись, которая разойдётся с другим узлом. Система остаётся доступной, но на время разрыва теряет строгую согласованность; копии разойдутся, и их придётся «помирить» позже.
# Разрыв сети. Узел A не видит узел B. На A приходит запрос:
# Выбор CP: лучше не ответить, чем ответить неверно
A: "не уверен, что данные свежие -> отклоняю запрос"
-> согласованность сохранена, доступность пострадала
# Выбор AP: лучше ответить, чем не ответить
A: "отвечаю тем, что есть, рассинхрон починим потом"
-> доступность сохранена, согласованность пострадала
Ни один путь не «правильный» вообще — правильный зависит от задачи. Банковский перевод: лучше отказать («попробуйте позже»), чем показать неверный баланс или провести двойное списание — это CP. Лента соцсети или счётчик лайков: лучше показать слегка устаревшее число, чем белый экран — это AP. Один и тот же разрыв сети, противоположные решения, потому что цена ошибки разная.
Модель консистентности: спектр, а не выключатель
CAP в формулировке «C или A» звучит как бинарный выбор. На практике консистентность — это спектр настроек, и реальные системы (особенно AP) дают её регулировать. Это понятие называется модель консистентности.
Два полюса спектра:
Strong consistency (строгая согласованность). Как только запись подтверждена, любое последующее чтение с любого узла гарантированно видит новое значение. Удобно для разработчика — никаких сюрпризов. Цена — координация между узлами на каждой операции, что бьёт по доступности и задержке.
Eventual consistency (итоговая согласованность). После записи узлы какое-то время могут отдавать разные значения, но со временем, если новых записей нет, все узлы сойдутся к одному значению. Узлы согласуются не мгновенно, а «в конце концов». Дёшево и доступно, но чтение сразу после записи может вернуть старое значение.
Многие NoSQL-хранилища дают настраиваемую (tunable) консистентность — выбирать уровень даже для отдельного запроса. В Cassandra, например, можно указать, сколько узлов-реплик должны подтвердить операцию: подтверждение от одного узла — быстро, но слабее по согласованности; подтверждение от большинства реплик — медленнее, но даёт строгую согласованность. Консистентность — это не «вкл/выкл», а ручка, которую крутят под конкретное требование.
Как выбор хранилища диктует модель данных
Теперь — главный вывод модуля. CAP-позиция и модель консистентности хранилища — это не абстрактная теория; они прямо определяют, как вы моделируете данные.
Связь работает так. Хранилище, выбравшее AP и eventual consistency (Cassandra, DynamoDB, многие документные базы), ради доступности и масштаба отказалось от части возможностей: нет дешёвого JOIN, нет глобальных транзакций по многим узлам, нет мгновенной согласованности. А раз этих возможностей нет — модель обязана с этим считаться. Отсюда напрямую вытекает всё, что вы изучили в модуле:
- Денормализация и дублирование (document, wide-column) — потому что JOIN недоступен, данные запроса должны лежать вместе заранее.
- Query-first проектирование (key-value, wide-column) — потому что нельзя задним числом дописать гибкий запрос, под который нет структуры.
- Embedding связанных данных (document) — потому что собрать их JOIN-ом на лету нечем.
- Таблица на каждый запрос и partition key (wide-column) — потому что данные размазаны по узлам и запрос должен попадать в одну партицию.
И наоборот: реляционная база (классически CP, со strong consistency и ACID) сохраняет JOIN, транзакции и гибкие запросы — поэтому она позволяет нормализацию и проектирование от связей, а не от запросов. Реляционные модули этого курса и NoSQL-модуль — это не «старое против нового». Это два конца одного компромисса CAP: разные приоритеты -> разные хранилища -> разные техники моделирования.
| Если хранилище выбрало… | То моделирование… |
|---|---|
| CP, strong consistency, ACID, JOIN (реляционные БД) | От связей, нормализация, гибкие запросы потом |
| AP, eventual consistency, без JOIN (Cassandra, DynamoDB) | Query-first, денормализация, дублирование, partition key |
| Документная модель (часто AP-склонная) | Embedding читаемого вместе, referencing по необходимости |
Финальный вывод курса по NoSQL. «Какую базу взять» — это на самом деле вопрос «какой компромисс CAP подходит задаче». Сначала поймите требования: нужна ли строгая согласованность (деньги, остатки) или важнее доступность и масштаб (лента, счётчики, кэш). Это определит CP- или AP-хранилище. А выбранное хранилище своими ограничениями уже продиктует технику моделирования. Модель данных — следствие выбора хранилища, а выбор хранилища — следствие требований задачи.
Попробуй сам
- Возьмите три системы: банковский счёт, лента новостей соцсети, корзина интернет-магазина. Для каждой решите, что важнее при разрыве сети — Consistency или Availability — и обоснуйте. К какому типу (CP/AP) вы склонитесь?
- Объясните своими словами, почему P (partition tolerance) в реальной распределённой системе нельзя «не выбрать», и почему поэтому реальный выбор — это C против A.
- Возьмите счётчик просмотров видео. Какая модель консистентности достаточна — strong или eventual? Что плохого, если выбрать strong? Что плохого при eventual?
- Свяжите всё с модулем: объясните, почему AP-хранилище без JOIN заставляет применять денормализацию и query-first проектирование. Приведите пример из уроков про document или wide-column.