CAP и PACELC: распределённые системы без магии
Третий закон Артура Кларка звучит красиво: любая достаточно развитая технология неотличима от магии. Инженеру придётся хуже: нет такой базы данных, такой сетевой топологии и такой архитектурной диаграммы, которая позволит системе быть одновременно всегда доступной, всегда согласованной и всегда быстрой без всякой цены. Архитектура распределённой системы начинается в тот момент, когда перестаёшь искать магию и начинаешь честно проектировать компромиссы.
Заметь, как сместился масштаб. До сих пор весь раздел жил внутри одного процесса: слои, порты, composition root из урока про DI и слои данных описывали одну программу с одной базой и одной транзакцией. С этого урока узлов становится больше одного, и появляются вопросы, которых в одном процессе просто не существует: что видит чтение, пока запись ещё едет на соседний узел, и что делать, когда узлы перестали слышать друг друга.
CAP, PACELC, модели консистентности, Jepsen и Cassandra, всё это разные способы проговорить одну и ту же реальность. В этом уроке мы пройдём по ним в таком порядке, чтобы каждый следующий инструмент уточнял предыдущий, а не заменял его.
Откуда берётся распределённость
Репликацию и шардирование ты уже видел в уроке про распределённый Postgres: данные копируются на реплики ради чтения и отказоустойчивости, делятся между шардами ради объёма и записи. Как только данных стало больше одного узла, между узлами появилась сеть. А сеть рвётся.
Насколько ненадёжна сеть? Принципиально. Классическая иллюстрация, задача двух генералов: две армии стоят по разные стороны долины и могут договориться о времени атаки только гонцами, которых перехватывают. Сколько подтверждений ни отправляй, последний гонец мог не дойти, и полной уверенности не будет ни у одной стороны. Доказано: протокола, который гарантирует согласие по ненадёжному каналу, не существует. Каждый ack в TCP и каждый таймаут в HTTP-клиенте, это инженерный обход этой невозможности, а не её решение.
И ещё одна неприятность, которая будет всплывать весь урок: по таймауту невозможно отличить мёртвый узел от медленного. Узел не ответил за секунду. Он упал? Перегружен? Или ответ потерялся по дороге? С точки зрения наблюдателя эти три ситуации неразличимы, а действовать как-то надо.
Правило Клеппмана
Классический способ изучать эту тему академичен: сначала CAP-теорема, потом её критика, потом BASE, потом PACELC. На это уходит чёртова куча времени, а практической пользы немного. Мартин Клеппман в статье Please stop calling databases CP or AP предлагает срезать путь, и мы начнём именно с него.
Центральная мысль: P не опция, а необходимость. Не существует ИТ-систем, в которых устойчивость к разделению сети можно было бы выключить. Сеть рвётся независимо от того, согласны мы на это или нет. Поэтому выбор, очень грубо, сводится к двум вариантам, и его можно сформулировать одним предложением:
В распределённой системе, устойчивой к потере связи между узлами, можно обеспечить либо согласованность, либо доступность, но не всё вместе.
Назовём это правилом Клеппмана. Его достаточно для большинства архитектурных решений, и оно понятно любому программисту без полуторачасовой лекции. Всё, что дальше в уроке, это уточнения для тех случаев, когда грубого правила перестаёт хватать.
Что на самом деле утверждает CAP
Теперь честная версия. CAP-теорема, это формальный результат с очень узкими определениями, и большая часть путаницы вокруг неё происходит из того, что слова согласованность и доступность в теореме означают не то, что в маркетинге баз данных.
- C, consistency. Не «данные корректны», а строго линеаризуемость: система ведёт себя так, будто копия данных ровно одна, и каждое чтение видит результат последней завершённой записи.
- A, availability. Не «99.9% аптайма», а формальное свойство: каждый запрос к любому живому узлу обязан получить не-ошибочный ответ. Без права ответить «попробуй позже» и без гарантии, что ответ будет свежим.
- P, partition tolerance. Система продолжает работать при произвольной потере сообщений между узлами. Не «узлы падают», а именно «сообщения не доходят»: оба узла живы, но друг друга не слышат.
Сама теорема скромнее своей славы: в системе, где сообщения могут теряться, нельзя одновременно гарантировать линеаризуемость и формальную доступность. Всё.
Из этих определений видно, почему расхожая формула «выбери 2 из 3» вводит в заблуждение, и Брюер сам признал это в статье CAP Twelve Years Later. Выбор не делается на этапе проектирования раз и навсегда, он происходит в момент разрыва. Узел получил запрос и не может связаться с остальными. Дальше ровно два варианта: ответить тем, что есть локально, рискуя отдать устаревшие данные (выбрали A), или отказать, сохранив гарантию согласованности (выбрали C). Пока сеть цела, противоречия нет и система спокойно даёт и то и другое.
Отсюда же вторая поправка: ярлык CP или AP на конкретной базе почти всегда враньё. Система может не быть ни тем ни другим: например, при разрыве и отдавать устаревшие данные, и отказывать части запросов, то есть провалить оба свойства сразу. Может вести себя по-разному для разных операций и настроек: чтение с реплики и запись через primary живут в разных точках компромисса. Клеппман в статье из ресурсов предлагает вообще перестать клеить эти ярлыки на продукты, и к концу урока ты увидишь почему: вопрос «эта база CP или AP» просто плохо сформулирован.
Consistency в CAP против consistency в ACID
Одна и та же буква C в двух аббревиатурах означает разные вещи, и на собеседованиях на этом регулярно ловят.
- В ACID согласованность, это сохранение инвариантов данных внутри одной базы: баланс не уходит в минус, внешние ключи целы. Причём следить за этим в значительной мере обязанность приложения, база даёт лишь инструменты (ограничения, транзакции).
- В CAP согласованность, это линеаризуемость: все реплики ведут себя так, будто данных ровно одна копия.
Один сюжет, два разных нарушения. Перевод 100 рублей со счёта A на счёт B:
- Сломали C из ACID. Транзакция списала 100 рублей с A, упала до зачисления на B, и база зафиксировала половину работы. Сумма по счетам уменьшилась на 100 рублей, инвариант «деньги не исчезают» нарушен внутри одной базы. Это лечится атомарностью транзакции.
- Сломали C из CAP. Перевод прошёл целиком и корректно на primary, но баланс ты читаешь с отставшей реплики и видишь цифры до перевода. Инварианты целы, сумма сходится, ни один байт не потерян. Просто твоё чтение отстало от последней записи, копий данных больше одной, и они на мгновение разъехались.
Первое, это баг или отсутствие транзакции. Второе, это нормальный режим работы любой асинхронной репликации, и вопрос лишь в том, заметит ли его пользователь.
PACELC: что мы платим, когда сеть цела
CAP молчит о самом частом режиме жизни системы: сеть в порядке. PACELC закрывает дыру: if Partition then Availability or Consistency, Else Latency or Consistency. Даже когда разрыва нет, за строгую согласованность платим задержкой: подтверждения от реплик не бесплатны.
Классификация знакомых систем выглядит так. Читается она «при разрыве / без разрыва»: PA значит «при разрыве выбирает доступность», EL значит «в обычной жизни выбирает латентность»:
| Система | При разрыве (P) | Без разрыва (E) | Почему |
|---|---|---|---|
| Postgres + асинхронная реплика | ближе к PC | EL | запись только через primary, реплика отстаёт ради скорости |
| DynamoDB | PA | EL | при разрыве отвечает тем, что есть; реплики догоняют потом |
| Cassandra | настраивается | настраивается | уровень согласованности задаётся на каждый запрос |
| CockroachDB, Spanner | PC | EC | кворум на каждую запись: строгость куплена задержкой |
Главное в этой таблице не буквы, а слово «настраивается» и слово «ближе». PACELC-класс это свойство конфигурации, а не бренда. Тот же Postgres с синхронной репликацией сдвигается из EL в сторону EC, а Cassandra одной строчкой в запросе проезжает всю ось. Поэтому правильный разговор не «какая буква у этой базы», а «какую букву мы включили этой настройкой и для этой операции».
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.