2026 г.

Курс «Распределённые системы». Глава 11. Тестирование распределённых систем

Цели главы. Всё, что курс доказывал на бумаге, надо уметь проверять в железе — своём собственном, не полагаясь на обещания вендора (главы 4–6 показали, сколько нюансов прячется за словом «consistency» в документации). Методика: сформулировать инварианты, вносить отказы, записывать историю операций, проверять историю алгоритмически. Разберём арсенал внесения отказов, подход Jepsen с его громкими находками, детерминированную симуляцию и хаос-инженерию. Завершает главу — и весь практикум курса — итоговая лабораторная: найти нарушение гарантий в системах из лабораторных 1–2 и предъявить его в отчёте, как это делает настоящий аудит.

11.1. Почему обычные тесты бессильны

Юнит- и интеграционные тесты проверяют счастливый путь и явные ошибки; баги распределённых систем живут в другом месте — в переплетениях: редкая перестановка «сообщение задержалось, лидер сменился, старый оффсет зафиксировался» воспроизводится раз на миллион прогонов и никогда — на стенде разработчика с идеальной сетью. Три системных ответа, по нарастающей: внести отказы намеренно (сделать редкое частым), проверять свойства, а не сценарии (не «после X будет Y», а «инвариант держится при любых историях»), контролировать недетерминизм (симуляция). Разберём все три.

11.2. Инварианты: что проверять

Тест распределённой системы формулируется как инвариант над историей операций — и весь курс уже снабдил вас словарём:

  • Долговечность: подтверждённая запись не исчезает (глава 5 — где это ломается по определению; глава 6 — где это гарантия). Проверка: журнал подтверждений клиента ⊆ финальное состояние системы.
  • Линеаризуемость: история допускает точки линеаризации (глава 4). Проверка алгоритмическая — см. 11.4.
  • Сессионные гарантии: монотонность чтений, читай-свои-записи — простые проверки по истории одной сессии.
  • Сохранение величин: сумма счетов постоянна при переводах (глава 2, согласованные снимки); нет двойного исполнения (идемпотентность, глава 9).
  • Единственность полномочия: ресурс принимает команды только от актуального лидера или владельца блокировки по монотонному ограждающему номеру. Два процесса могут одновременно считать себя лидерами, поэтому проверять надо не их убеждения, а невозможность двух действительных полномочий (главы 5–6).

Правило формулировки: инвариант должен быть проверяем по записанной истории, без веры во внутренности системы. Клиентские журналы «что я послал, что мне подтвердили, что я прочитал» — первичный материал; всё остальное — интерпретация.

Присмотритесь к списку: все перечисленные инварианты — свойства безопасности в смысле главы 3 (□: «плохое не случается никогда»), и это не случайный крен. Записанная история может опровергнуть безопасность — вот подтверждённая запись, вот её отсутствие после failover, нарушение предъявлено. Но конечная история принципиально не может опровергнуть живость (◇: «хорошее когда-нибудь произойдёт»): сколько бы вы ни ждали, «лидер ещё не избран» не доказывает «не будет избран никогда» — любой вердикт о живости из конечного наблюдения есть тайм-аут, то есть подозрение (глава 1), а не доказательство. Отсюда разделение труда в методике: чекеры доказательно проверяют безопасность по истории, а живость оценивается инженерными метриками — временем восстановления после немезиды, длительностью окон недоступности, — которые вы и будете снимать в лабораторной наряду с проверкой инвариантов.

11.3. Арсенал внесения отказов

Каждому виду отказа из главы 1 — инструмент; всё перечисленное доступно на одной Linux-машине с Docker:

  • Смерть и рестарт: docker kill / stop / start; kill -9 процессу. Проверяет crash-recovery и failover.
  • Зависание (пауза GC, перегрузка): kill -STOP / kill -CONT — процесс замирает, не умирая: жесточайший тест детекторов отказов и зомби-лидеров (глава 5); большинство самодельных систем впервые ломаются именно на SIGSTOP.
  • Сеть: tc netem (задержка, потери, переупорядочение — практикум статьи о надёжности), iptables/nftables DROP (разделения, в т.ч. асимметричные: A видит B, B не видит A — коварнейший класс), docker network disconnect (лабораторная 2). Программируемые прокси (toxiproxy) портят отдельные соединения по сценарию.
  • Часы: сдвиг времени контейнера (libfaketime) — проверка LWW, TTL, аренд лидерства (глава 2 предупреждала).
  • Диск: заполнение, ошибки ввода-вывода (dm-error), потеря fsync — за кадром курса, но в реальном аудите обязательны.

11.4. Подход Jepsen

Кайл Кингсбери превратил описанное в дисциплину и десятилетие подряд применял её к промышленным СУБД и брокерам. Схема канонична и воспроизводима вручную:

  1. Генератор случайного потока операций от многих клиентов (чтения/записи/CAS по небольшому множеству ключей);
  2. Немезида — параллельный поток отказов из арсенала 11.3 по расписанию;
  3. История — полный журнал: вызов, ответ/тайм-аут, времена (важно: тайм-аут не значит «не выполнилось» — глава 1; операция остаётся в истории неопределённой);
  4. Чекеры — алгоритмическая проверка истории: линеаризуемость (в Jepsen — Knossos; для Go-программ доступен самостоятельный Porcupine), потерянные подтверждённые записи, аномалии транзакций.

Урожай метода красноречив: за годы анализов Jepsen находил потери подтверждённых записей, расщепление кластера и нарушения заявленных уровней изоляции у самых известных систем — как правило, не в «экзотике», а в ребалансах, сменах лидера и асимметричных разделениях. Двойная мораль для выпускника курса: (1) заявлениям документации о гарантиях верят после проверки; (2) находки почти всегда сидят ровно в тех местах, на которые указывали главы 5–6 — теория курса и есть карта, где копать.

11.5. Детерминированная симуляция и проверка свойств

Следующая ступень строгости — убрать недетерминизм вовсе: вся система (сеть, диски, время, планировщик) исполняется в симуляторе с управляемым генератором случайности; каждый прогон — это seed, любой найденный баг воспроизводится побайтно по его seed. Так тестируются FoundationDB и TigerBeetle — их культура симуляционного тестирования стала эталоном отрасли; исследовательский вариант той же идеи — перебор переплетений модельными чекерами (TLA+, о котором стоит знать: спецификации Raft и многих систем верифицированы в нём). Домашняя ступень того же принципа — property-based тестирование (hypothesis в Python): генератор случайных последовательностей операций + инвариант, с автоматическим уменьшением контрпримера. Для лабораторных курса это уже реалистичный инструмент: сгенерировать тысячи случайных сценариев против вашей реализации согласованного хеширования (лабораторная 3) — час работы.

11.6. Хаос-инженерия

Последний рубеж — проверка не системы, а всей организации вместе с ней: намеренные отказы в продакшене (родоначальник — Chaos Monkey), по строгим правилам: формулируется гипотеза устойчивости («потеря одной зоны не влияет на доступность оформления заказа»), ограничивается радиус поражения, готовится мгновенный откат, эксперимент наблюдаем. Хаос-инженерия — это регулярная репетиция плановой деградации из статьи о надёжности; её незрелая имитация — «уронить сервер и посмотреть» без гипотезы и радиуса — дискредитирует идею. Начинать следует со стенда (лабораторная ниже), в прод выходить с процессом.

Итоги главы

  • Баги распределённых систем живут в редких переплетениях; их делают частыми намеренные отказы, а проверяют — инварианты над записанной историей, не сценарии.
  • Арсенал: kill/рестарт, SIGSTOP (главный разоблачитель зомби), netem/iptables (включая асимметричные разделения), сдвиг часов.
  • Jepsen-схема: генератор + немезида + история + алгоритмические чекеры; Jepsen использует Knossos, а Porcupine предоставляет сходную проверку Go-программам.
  • Детерминированная симуляция (seed = воспроизводимость) — эталон строгости; property-based тесты — её домашняя ступень.
  • Хаос-инженерия = гипотеза + ограниченный радиус + наблюдаемость + откат; это репетиция деградации, а не вандализм.

Упражнения

  1. Сформулируйте проверяемые по истории инварианты для: (а) сервиса блокировок; (б) корзины интернет-магазина на кворумном хранилище; (в) переводов между счетами; (г) выдачи уникальных номеров документов. Для каждого укажите, какие записи обязан вести клиент-тестировщик.
  2. Почему операцию с тайм-аутом нельзя заранее считать ни неуспешной, ни успешно завершённой? Постройте два примера ложного нарушения: когда запись исполнилась и видна последующему чтению; когда запись не исполнилась и последующее чтение вернуло старое значение. Как чекер должен моделировать такую операцию?
  3. Объясните, почему SIGSTOP жёстче docker kill для тестирования систем с лидером, и предскажите (по главам 5–6) поведение при 60-секундном SIGSTOP лидеру: (а) самодельного failover-скрипта без ограждения; (б) etcd. Проверка предсказания — задание 3 лабораторной.
  4. Спроектируйте немезиду «асимметричное разделение» на iptables для трёхузлового кластера (A→B ходит, B→A — нет). Какие протокольные ситуации она провоцирует и почему симметричные разрывы её не заменяют (подумайте о сердцебиениях в одну сторону)?
  5. Оцените сложность наивной проверки линеаризуемости истории из n операций (перебор порядков) и объясните, какие отсечения делают её практичной (совместимость с реальным временем режет пространство; независимые ключи проверяются раздельно).
  6. Составьте план первого хаос-эксперимента для сервиса из статьи о надёжности (клиент → API → БД с репликой): гипотеза, вносимый отказ, радиус, метрики наблюдения, критерий остановки.

Ответы и указания. 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, лаг и расхождение журналов; эксперимент прекращается при превышении заданной ошибки или задержки, после чего реплика восстанавливается по подготовленной процедуре.

Лабораторная работа 5 (итоговая). Аудит гарантий своими руками

Легенда. Вы — аудитор: для двух систем из прежних лабораторных надо найти и задокументировать нарушение гарантий (там, где оно есть по построению) и подтвердить гарантию (там, где она обещана). Результат — отчёт в стиле 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. Найденное честное нарушение документированных гарантий — отличная оценка автоматом и, по традиции жанра, письмо разработчикам.

Литература к главе

  1. K. Kingsbury, Jepsen: методология и анализы. jepsen.io
  2. Porcupine — чекер линеаризуемости. github.com/anishathalye/porcupine
  3. W. Wilson, "Testing Distributed Systems w/ Deterministic Simulation" (FoundationDB), доклад StrangeLoop 2014.
  4. A. Rosenthal, N. Jones, "Chaos Engineering," O'Reilly, 2020.
  5. L. Lamport, "Specifying Systems" (TLA+), Addison-Wesley, 2002 — для желающих следующей ступени строгости.

Предыдущая глава || Содержание курса || Следующая глава

404 Not Found

404 Not Found


nginx/1.24.0 (Ubuntu)

Связь с редакцией