Проект ЦИТадель
Обзоры • курсы • практикумы
Не «что нажать», а «как устроено»
Цели главы. Разобрать три классических отрицательных результата — задачу двух генералов, теорему FLP и теорему CAP — и научиться читать их правильно: не как приговоры, а как карту границ, внутри которых живёт всё проектирование. Каждую теорему мы сопровождаем ответом на два вопроса: что именно она запрещает (обычно меньше, чем принято думать) и какими предположениями запрет обходится на практике (обычно честной ценой, которую надо знать). Глава завершает теоретические основания курса; со следующей начинаются конструкции.
Отрицательный результат — самый полезный вид знания в инженерии: он закрывает целые направления поиска. Тот, кто знает FLP, не станет обещать заказчику «кластер, который гарантированно выбирает лидера за 500 мс при любых сетевых условиях»; тот, кто понимает двух генералов, не будет искать библиотеку с честной доставкой exactly-once; тот, кто читал не только аббревиатуру CAP, не будет требовать «строгую согласованность и стопроцентную доступность» в одном ТЗ. Невозможности экономят годы: каждая из теорем этой главы когда-то остановила индустриальную гонку за недостижимым.
Начнём с результата, доказываемого в четыре строки. Два генерала на холмах должны атаковать одновременно; связь — гонцы через долину, где их перехватывают (канал с потерями, глава 1). Требуется протокол, по завершении которого оба точно знают, что атака согласована.
Теорема. Конечный обмен сообщениями по каналу с потерями не может дать обоим генералам гарантированное общее знание о согласованной атаке. Доказательство — от противного. Предположим, существует успешное исполнение корректного протокола, и возьмём среди таких исполнений одно с минимальным числом доставленных сообщений; рассмотрим последнее доставленное в нём сообщение m (пусть от A к B). После отправки m A уже не получает подтверждений и потому принимает то же решение и в неотличимом для него исполнении, где m потерялось. Чтобы в этом исполнении не возникло расхождения, B также обязан принять прежнее решение без m. Значит, m не было необходимо — а это противоречит минимальности. (Ту же операцию можно повторять: удаляя последнее сообщение снова и снова, пришли бы к согласованной атаке вообще без связи, что невозможно при независимых исходных состояниях генералов.) ∎
Формально: по каналу с потерями недостижимо общее знание (я знаю, что ты знаешь, что я знаю... — до бесконечности). Практические следствия: обещанная в главе 1 теорема о недостижимости exactly-once-доставки — это два генерала в профиль (упражнение 1); двухфазная фиксация транзакций (глава 9) не «плохо спроектирована», а упирается в этот предел; TCP-рукопожатие завершается не абсолютной уверенностью, а «достаточной для практики». Обход у индустрии один: заменить «оба точно знают» на «расхождение обнаружимо и устранимо потом» — подтверждения, повторы, сверки, идемпотентность.
Центральный отрицательный результат области. Сформулируем задачу консенсуса (она же — сердце глав 6 и далее): каждый процесс предлагает значение; требуется, чтобы (1) все корректные процессы в итоге решили — завершаемость; (2) решили одно и то же — согласие; (3) решённое было кем-то предложено — обоснованность (без неё «всегда решай 0» — законный протокол).
Теорема (Фишер, Линч, Патерсон, 1985): в асинхронной системе с надёжными каналами ни один детерминированный протокол не решает консенсус, если хотя бы один процесс может отказать (crash-stop). Обратите внимание на скупость условий: каналы даже надёжны, отказ всего один — и всё равно невозможно.
Идея доказательства (эскиз, достаточный для понимания механики). Назовём конфигурацию системы бивалентной, если из неё ещё достижимы оба исхода (решение 0 и решение 1), и унивалентной — если исход предрешён. Два шага: (а) у любого корректного протокола существует бивалентная начальная конфигурация — иначе решение зависело бы только от входов, и тогда, меняя вход одного процесса по цепочке от «все предложили 0» к «все предложили 1», найдём соседние конфигурации с разным предрешённым исходом, различающиеся входом одного процесса; «убив» его, получим противоречие; (б) из любой бивалентной конфигурации, как ни планируй доставку сообщений, противник-планировщик может доставить их в таком порядке, что система останется бивалентной: решающий шаг — доставку сообщения, превращающего систему в унивалентную, — можно откладывать неограниченно, пользуясь тем, что в асинхронной модели «медленное» неотличимо от «мёртвого». Итог: существует бесконечное исполнение, в котором решение не принимается никогда — нарушена завершаемость. ∎
Что теорема означает: в честной модели интернета нельзя гарантировать консенсус за конечное время в худшем случае. Чего она не означает: что консенсус не работает на практике. Запрет касается детерминированных протоколов и гарантий худшего случая; худший случай — бесконечно изобретательный противник-планировщик, реальная сеть таковым не является. Легальные обходы, каждый со своей ценой:
Отступление: язык темпоральной логики. Утверждения о распределённых системах — это утверждения о поведении во времени: «плохое не случится никогда», «хорошее когда-нибудь произойдёт». Для них в литературе используются два оператора темпоральной логики, и оба уже неявно работали в этой главе:
Операторы комбинируются: ◇□A — «с некоторого момента A истинно всегда» (побыв ложным конечное время, A устанавливается навсегда). На этом языке точно формулируется деление свойств, которым мы пользуемся с раздела о FLP и будем пользоваться до конца курса (особенно в главе 11): безопасность — свойства вида □(не плохое): «два процесса не решают разные значения», «зафиксированное никогда не теряется»; живость (liveness) — свойства вида ◇(хорошее): «когда-нибудь решение будет принято». Для лидерских протоколов корректное утверждение обычно звучит как «не более одного лидера в одном терме» или «не более одного лидера способен фиксировать записи», а не как запрет двум узлам временно считать себя лидерами разных эпох. FLP на этом языке — теорема о том, что живость консенсуса недостижима с гарантией; безопасность она не затрагивает.
Теперь — детекторы отказов строго (Чандра–Туэг, 1996). Детектор отказов — это оракул при каждом процессе, выдающий список подозреваемых в отказе; протоколу разрешается им пользоваться, а качество оракула описывается двумя свойствами:
P (perfect, совершенный детектор) требует точности всегда (□): ни один живой процесс никогда не подозревается. В асинхронной системе P нереализуем — это переформулировка вывода главы 1: тайм-аут не доказывает смерть, значит, любой детектор на тайм-аутах иногда клевещет на живых. ◇P (eventually perfect, «в конце концов совершенный») ослабляет точность ромбом: детектору разрешено конечное время ошибаться как угодно — подозревать живых, снимать подозрения, снова подозревать, — но с некоторого момента и навсегда его точность становится безупречной. Формально приставка ◇ здесь означает «◇□»: когда-нибудь — и затем всегда.
◇P — это математический портрет тайм-аута в частично синхронной сети: пока сеть штормит, тайм-ауты срабатывают ложно (медленный узел объявляется мёртвым); когда сеть стабилизируется и задержки входят в границы, срабатывания становятся правдой. Результат Чандры–Туэга: при большинстве корректных процессов консенсус решается уже с детектором ◇P — то есть с оракулом, которому разрешено врать сколь угодно долго, лишь бы не вечно; более того, достаточно ещё более слабого ◇S, которому и после стабилизации разрешено клеветать на кого угодно, кроме хотя бы одного корректного процесса. (А слабейший детектор, при котором консенсус вообще разрешим, — Ω, «когда-нибудь все корректные доверяют одному и тому же живому лидеру» (Чандра, Хадзилакос, Туэг, 1996); узнаёте выборы Raft?) Практический смысл, ради которого всё отступление и затевалось: консенсусу не нужны точные тайм-ауты — достаточно тайм-аутов, когда-нибудь перестающих врать; именно поэтому Raft может позволить себе грубые рандомизированные тайм-ауты выборов, а инженер, подбирающий election timeout, настраивает не корректность (она безусловна — □), а скорость наступления того самого «когда-нибудь» (◇).
Самая известная и самая перевираемая. Дадим строгие определения — в них вся суть. C (согласованность) = линеаризуемость: система отвечает так, как будто копия одна (строго — в главе 4). A (доступность) = каждый запрос к любому неотказавшему узлу получает содержательный ответ (не ошибку, не вечное ожидание). P (устойчивость к разделению) = в модели допускаются разделения — сообщения между группами узлов могут теряться сколь угодно долго, — и система обязана иметь определённое поведение на это время.
Теорема (гипотеза Брюера 2000, доказательство Гилберта–Линч 2002): свойства C, A, P одновременно недостижимы. Доказательство — почти одна картинка: пусть сеть разделила узлы на группы G1 и G2. Клиент пишет x := 1 в G1; по доступности G1 обязана подтвердить (ждать G2 нельзя — ожидание неограниченно, это отказ в обслуживании). Затем клиент читает x из G2; по доступности G2 обязана ответить — но о записи она физически не могла узнать (сообщений через разрез нет) и ответит старым значением. Линеаризуемость нарушена. ∎
Как читать правильно. Во-первых, P — не опция: разделения в реальной сети случаются, «отказаться от P» означает лишь «не определить поведение системы при разделении» — худший из вариантов. Реальный выбор: при разделении жертвовать доступностью (CP: меньшинство отвечает ошибкой — так ведут себя системы на кворумах и консенсусе: etcd, ZooKeeper) или согласованностью (AP: отвечают все, копии расходятся, потом сливаются — Dynamo-наследники, кэши, DNS). Во-вторых, выбор не общесистемный, а по операциям и данным: одна и та же СУБД может отдавать линеаризуемые чтения с лидера и итогово-согласованные с реплик. В-третьих, полезное расширение PACELC: при разделении (P) — выбор A либо C, а в остальное время (E) — выбор между задержкой (L) и согласованностью (C): строгая согласованность стоит координационного round-trip в каждой записи, и эту цену платят всегда, а не только в аварию.
И предостережение от суеверия: CAP — теорема о двух конкретных сильных свойствах при разделении, не более. Она ничего не говорит о задержках без разделений, о долговечности, о поведении при отказах узлов без разрыва сети; сводить проектирование к «выбору двух букв» — значит потерять всё содержание глав 4–5, где спектр промежуточных гарантий и есть главный предмет.
Соберём часть I в одну таблицу «предположения → что достижимо»:
| Модель / предположение | Что невозможно | Что покупается |
|---|---|---|
| Канал с потерями | общее знание, exactly-once-доставка (два генерала) | at-least-once + идемпотентность, сверки и компенсации |
| Асинхронность + 1 отказ | детерминированный консенсус с гарантией завершения (FLP) | безопасность всегда (Raft/Paxos) |
| + частичная синхронность / ◇P, ◇S, Ω | — | живость в периоды стабильности |
| + рандомизация | детерминированная граница времени | завершение с вероятностью 1 |
| Сетевое разделение | C + A одновременно (CAP) | осознанный выбор CP или AP по классам данных и операциям |
| Отсутствие общих часов (гл. 2) | порядок по физическим меткам | причинный порядок (Лэмпорт, векторы), HLC |
Эта таблица — точный чертёж дизайн-пространства: каждая конструкция частей II–III курса занимает в ней своё место, честно объявляя, какими предположениями и какими жертвами она куплена.
Ответы и указания. 1: запрос и ответы играют роль гонцов. После потери последнего ответа отправитель не знает, был ли эффект, и выбирает между риском пропуска и риском повтора. Долговечная дедупликация даёт один эффект только внутри своей транзакционной границы, но не гарантирует единственную доставку сообщения. 2: ненадёжность используется при удалении последнего доставленного сообщения. Если канал гарантирует конечную доставку и нет отказов, узлы могут дождаться сообщений; если совместное действие требуется к сроку, но верхней границы задержки нет, своевременное общее знание всё равно не гарантируется. 3: при разделении две группы выбирают своих минимальных узлов — нарушена безопасность выбора лидера; при бесконечной череде ложных подозрений лидер не стабилизируется — нарушена живость. 4: безопасность Raft не требует границы задержки, а живость требует периода частичной синхронности. В полностью асинхронной сети сообщения можно задерживать так, чтобы выборы повторялись бесконечно; случайные тайм-ауты снижают вероятность одинаковых раундов, но не создают детерминированного предела. 5: доказательство ломается на обязанности чтения увидеть завершённую запись: итоговая модель разрешает старое значение, но обещает сходимость после прекращения обновлений, доставки версий и разрешения конфликтов. 6: линеаризуемые операции etcd при разделении доступны только стороне с кворумом; Dynamo-стиль W=1,R=1 может отвечать с разных реплик и затем разрешать конфликты; DNS и кэши обычно допускают устаревание; PostgreSQL с одним лидером прекращает запись без доступного лидера, а чтение с асинхронной реплики может быть устаревшим. 7: требование дословно противоречит CAP при разделении. Нужно разделить данные и операции по инвариантам, определить допустимое окно отказа и цену устаревшего ответа. Вариант с линеаризуемостью отказывает стороне без кворума; вариант с доступностью обеих сторон допускает расхождение и требует правил слияния. Для разных операций одной системы допустимы разные решения, и это следует зафиксировать в контракте.