Раздел 9 · Backend

CAP и PACELC: распределённые системы без магии

middle-senior~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

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 + асинхронная репликаближе к PCELзапись только через primary, реплика отстаёт ради скорости
DynamoDBPAELпри разрыве отвечает тем, что есть; реплики догоняют потом
Cassandraнастраиваетсянастраиваетсяуровень согласованности задаётся на каждый запрос
CockroachDB, SpannerPCECкворум на каждую запись: строгость куплена задержкой

Главное в этой таблице не буквы, а слово «настраивается» и слово «ближе». PACELC-класс это свойство конфигурации, а не бренда. Тот же Postgres с синхронной репликацией сдвигается из EL в сторону EC, а Cassandra одной строчкой в запросе проезжает всю ось. Поэтому правильный разговор не «какая буква у этой базы», а «какую букву мы включили этой настройкой и для этой операции».

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.