Цели главы. Всё, что курс доказывал на бумаге, надо уметь проверять в железе — своём собственном, не полагаясь на обещания вендора (главы 4–6 показали, сколько нюансов прячется за словом «consistency» в документации). Методика: сформулировать инварианты, вносить отказы, записывать историю операций, проверять историю алгоритмически. Разберём арсенал внесения отказов, подход Jepsen с его громкими находками, детерминированную симуляцию и хаос-инженерию. Завершает главу — и весь практикум курса — итоговая лабораторная: найти нарушение гарантий в системах из лабораторных 1–2 и предъявить его в отчёте, как это делает настоящий аудит.
Юнит- и интеграционные тесты проверяют счастливый путь и явные ошибки; баги распределённых систем живут в другом месте — в переплетениях: редкая перестановка «сообщение задержалось, лидер сменился, старый оффсет зафиксировался» воспроизводится раз на миллион прогонов и никогда — на стенде разработчика с идеальной сетью. Три системных ответа, по нарастающей: внести отказы намеренно (сделать редкое частым), проверять свойства, а не сценарии (не «после X будет Y», а «инвариант держится при любых историях»), контролировать недетерминизм (симуляция). Разберём все три.
Тест распределённой системы формулируется как инвариант над историей операций — и весь курс уже снабдил вас словарём:
Правило формулировки: инвариант должен быть проверяем по записанной истории, без веры во внутренности системы. Клиентские журналы «что я послал, что мне подтвердили, что я прочитал» — первичный материал; всё остальное — интерпретация.
Присмотритесь к списку: все перечисленные инварианты — свойства безопасности в смысле главы 3 (□: «плохое не случается никогда»), и это не случайный крен. Записанная история может опровергнуть безопасность — вот подтверждённая запись, вот её отсутствие после failover, нарушение предъявлено. Но конечная история принципиально не может опровергнуть живость (◇: «хорошее когда-нибудь произойдёт»): сколько бы вы ни ждали, «лидер ещё не избран» не доказывает «не будет избран никогда» — любой вердикт о живости из конечного наблюдения есть тайм-аут, то есть подозрение (глава 1), а не доказательство. Отсюда разделение труда в методике: чекеры доказательно проверяют безопасность по истории, а живость оценивается инженерными метриками — временем восстановления после немезиды, длительностью окон недоступности, — которые вы и будете снимать в лабораторной наряду с проверкой инвариантов.
Каждому виду отказа из главы 1 — инструмент; всё перечисленное доступно на одной Linux-машине с Docker:
Кайл Кингсбери превратил описанное в дисциплину и десятилетие подряд применял её к промышленным СУБД и брокерам. Схема канонична и воспроизводима вручную:
Урожай метода красноречив: за годы анализов Jepsen находил потери подтверждённых записей, расщепление кластера и нарушения заявленных уровней изоляции у самых известных систем — как правило, не в «экзотике», а в ребалансах, сменах лидера и асимметричных разделениях. Двойная мораль для выпускника курса: (1) заявлениям документации о гарантиях верят после проверки; (2) находки почти всегда сидят ровно в тех местах, на которые указывали главы 5–6 — теория курса и есть карта, где копать.
Следующая ступень строгости — убрать недетерминизм вовсе: вся система (сеть, диски, время, планировщик) исполняется в симуляторе с управляемым генератором случайности; каждый прогон — это seed, любой найденный баг воспроизводится побайтно по его seed. Так тестируются FoundationDB и TigerBeetle — их культура симуляционного тестирования стала эталоном отрасли; исследовательский вариант той же идеи — перебор переплетений модельными чекерами (TLA+, о котором стоит знать: спецификации Raft и многих систем верифицированы в нём). Домашняя ступень того же принципа — property-based тестирование (hypothesis в Python): генератор случайных последовательностей операций + инвариант, с автоматическим уменьшением контрпримера. Для лабораторных курса это уже реалистичный инструмент: сгенерировать тысячи случайных сценариев против вашей реализации согласованного хеширования (лабораторная 3) — час работы.
Последний рубеж — проверка не системы, а всей организации вместе с ней: намеренные отказы в продакшене (родоначальник — Chaos Monkey), по строгим правилам: формулируется гипотеза устойчивости («потеря одной зоны не влияет на доступность оформления заказа»), ограничивается радиус поражения, готовится мгновенный откат, эксперимент наблюдаем. Хаос-инженерия — это регулярная репетиция плановой деградации из статьи о надёжности; её незрелая имитация — «уронить сервер и посмотреть» без гипотезы и радиуса — дискредитирует идею. Начинать следует со стенда (лабораторная ниже), в прод выходить с процессом.
Ответы и указания. 1: блокировки — интервалы действительных ограждённых полномочий не пересекаются; корзина — подтверждённые добавления не исчезают без удаления и сходятся по выбранной модели; переводы — сумма сохраняется, остатки неотрицательны, идентификатор перевода применяется один раз; номера документов — два успешных ответа не содержат один номер. Журналу нужны invocation/completion, аргументы, результаты, идентификаторы сессий и времена. 2: если write(1) исполнился, а чекер объявил тайм-аут неуспехом, последующий read→1 покажется значением из ниоткуда. Если write(1) не исполнился, а чекер объявил успехом, последующий read→0 покажется устаревшим. Неопределённую операцию моделируют как pending: перебор рассматривает допустимое завершение или отсутствие эффекта. 3: kill разрывает соединения, STOP лишь заставляет живой процесс молчать. Скрипт без ограждения получит split brain; etcd после разморозки увидит новый терм и сложит полномочия, сохранив зафиксированные данные. 4: чтобы пропускать A→B и блокировать B→A, правило DROP ставят на исходящий трафик B к A либо на входящий трафик A от B, обязательно ограничив адреса и порты тестового кластера. Односторонняя потеря позволяет одному узлу видеть сердцебиения без обратных подтверждений и создаёт состояния, которых нет при симметричном разрыве. 5: грубая верхняя граница перебора — n! последовательностей. Реальный порядок, разбиение независимых ключей, кэш состояний и ранний откат резко сокращают поиск, но большой связный контрпример всё равно может быть дорогим; универсальной гарантии для десятков тысяч операций нет. 6: гипотеза — после убийства реплики API сохраняет успешные ответы без потери подтверждённых записей; отказ применяется к одной тестовой реплике и малой доле трафика; наблюдаются ошибки, p99, лаг и расхождение журналов; эксперимент прекращается при превышении заданной ошибки или задержки, после чего реплика восстанавливается по подготовленной процедуре.
Легенда. Вы — аудитор: для двух систем из прежних лабораторных надо найти и задокументировать нарушение гарантий (там, где оно есть по построению) и подтвердить гарантию (там, где она обещана). Результат — отчёт в стиле Jepsen-анализа: конфигурация, инварианты, немезида, история, вердикт. Инструменты — стенды лабораторных 1–2, bash/Python, арсенал 11.3.
Задание 1. Генератор и история. Напишите клиент-генератор (50–100 строк): N потоков выполняют случайные записи/чтения по 3–5 ключам против целевой системы, каждый пишет журнал строк вида «время, поток, операция, аргумент, результат|timeout». Отработайте на здоровом кластере etcd; убедитесь, что журналы пригодны для проверки (упражнения 1–2).
Задание 2. Долговечность PostgreSQL под немезидой. Стенд лабораторной 1, асинхронная репликация. Немезида: на фоне генератора (только записи с подтверждениями) — kill лидера + promote реплики. Чекер: каждое подтверждённое значение обязано присутствовать после failover. Предъявите нарушение (номера потерянных записей), затем повторите с синхронной репликацией — нарушение исчезает. В отчёт: оба прогона, формулировка «нарушен инвариант долговечности при конфигурации X, что соответствует документированному поведению» — аудит отличает баг от честно купленного компромисса.
Задание 3. Зомби-лидер: скрипт против etcd. Реализуйте «самодельный failover» для пары ключ-значение поверх двух узлов (простейший скрипт: пинг лидера, при трёх пропусках — назначить реплику лидером) — и сломайте его SIGSTOP-ом по сценарию упражнения 3: предъявите расхождение данных. Затем тот же сценарий против etcd (SIGSTOP лидеру на 60 с, генератор работает): покажите по истории, что зафиксированные записи целы, а нарушения линеаризуемости нет. В отчёт: обе истории и объяснение разницы через термы и кворумы главы 6.
Задание 4. Асимметричное разделение. Немезида упражнения 4 (iptables) против etcd на фоне генератора: 90 секунд одностороннего разрыва между лидером и одним ведомым. Зафиксируйте наблюдаемое (перевыборы? деградация? ничего?) и объясните. В отчёт: временная шкала «отказ → реакция кластера → восстановление».
Задание 5. Проверка линеаризуемости. Прогоните историю задания 3 (etcd) через готовый чекер (Porcupine — библиотека Go с простым интерфейсом; либо напишите переборный чекер для одного ключа сами — упражнение 5 даёт план). Затем снимите историю чтений с флагом --consistency=s (serializable, лабораторная 2) под той же немезидой и покажите чекером допустимое документацией устаревшее чтение, нарушающее линеаризуемость. В отчёт: вердикты чекера для обоих режимов.
Задание 6* (зачётное). Свободная охота. Выберите любую систему не из курса (брокер, KV-хранилище, «распределённую» библиотеку блокировок), сформулируйте по документации её гарантии, спланируйте немезиду и проведите аудит по схеме заданий 1–5. Найденное честное нарушение документированных гарантий — отличная оценка автоматом и, по традиции жанра, письмо разработчикам.