Jepsen Testing — fault injection framework by Kyle Kingsbury for verifying claimed consistency guarantees of distributed databases. Shows Jepsen control node (op generator, nemesis fault injector, history recorder, Knossos/Elle checker) connected to a 5-node system-under-test. Three scenarios: setup with concurrent ops, partition-induced lost write, Knossos finding non-linearizable history. References real findings: MongoDB lost writes, Redis Sentinel split-brain, Cassandra LWT, Etcd recovery bug.
Вендоры пишут на лендинге "ACID", "linearizable", "exactly-once". Реальность мягче: MongoDB до 3.4 терял подтверждённые writes под partition, Redis Sentinel разваливался на два мозга, Cassandra LWT не была линеаризуемой, Etcd 3.0 ломался при recovery. Дефолтные уровни консистентности почти всегда слабее маркетинга, а edge-cases — partition, leader re-election, clock skew, GC-пауза — никто не тестирует в обычных юнит-тестах, потому что они не воспроизводятся в одном процессе.
Jepsen — методология (и одноимённая Clojure-библиотека Кайла Кингсбери), которая делает воспроизводимыми именно эти сценарии. Идея простая: поднять реальный кластер на 5 нодах, гонять конкурентные операции от нескольких клиентов, одновременно инжектить отказы (network partition, kill -9, clock jump, disk full), записывать историю всех invoke/ok/fail/info с wall-clock таймстемпами и потом скармливать историю чекеру, который математически проверит, могла ли такая последовательность вообще случиться у заявленной модели консистентности.
Если не могла — у тебя баг. Не "performance issue", не "edge case", а доказанное нарушение контракта, который вендор обещал на главной странице. С таким репортом разработчики не отмахнутся.
Jepsen — это state machine fuzzing для распределённых систем. Три части:
{:type :invoke, :f :write, :value 42}) и раздаёт их N процессам-клиентам, каждый говорит со своей нодой кластера.iptables -A INPUT -s n3 -j DROP, убивает процессы, двигает часы через libfaketime, забивает диск.Для линеаризуемости чекер называется Knossos: перебирает все возможные total orders, совместимые с реальным временем (invoke до ok), и пытается прогнать их через эталонную state machine (register, mutex, set). Если хотя бы один порядок подходит — ок. Если ни один — линеаризуемости нет, точка.
Для транзакций (snapshot isolation, serializable, read-committed) используется Elle: строит граф зависимостей (write-write, write-read, read-write) и ищет циклы конкретного типа — каждый тип цикла соответствует конкретной аномалии (G0 dirty write, G1a aborted read, G-single read skew, G2 anti-dependency cycle).
Ключевая фраза: "ok" в истории — это обещание клиенту, что write committed. Если позже read не видит этот write (или leader откатил лог через truncate) — это уже не "eventual consistency", это lost update, и Knossos на этом ловит.
Слева — Jepsen control node, отдельная машина, которая дирижирует тестом:
Op generator — конкурентно посылает invoke-ops на каждую ноду кластера;Nemesis — инжектит partition / kill / clock skew (на диаграмме рисует красные стрелки в SUT);History recorder — собирает все invoke/ok/fail/info в history.edn с миллисекундными тайстемпами;Knossos / Elle checker — после прогонки гоняет историю через model checker.Справа — System Under Test: 5 нод базы (n1..n5), где n1 — текущий лидер. Между нодами — replication links (n1→n2, n1→n3), которые порвёт partition-сценарий. Каждая нода пишет операции в recorder параллельно.
Это не прод-архитектура — это тестовый harness. В реальном тесте control node поднимает SUT через SSH/Docker/EC2, ставит пакет, конфигурит, гоняет workload 60–600 секунд, сносит SUT и пишет отчёт store/<run-id>/{timeline.html, linear.svg, latency.png, history.edn, results.edn}.
setup — boot и нормальная нагрузка. Generator поднимает 5 нод, синхронизирует часы через NTP, ставит SUT и фанит конкурентные ops (write x=1, read x, cas x 1→2) на все ноды одновременно. Каждый клиент работает в своём процессе и не координируется с другими — это создаёт реальную concurrency, а не "одна транзакция в test runner". History recorder получает invoke/ok с точностью до миллисекунд. На этом этапе всё зелёное — система просто работает.
partition-test — инжектим partition, ловим lost write. Самый частый паттерн: клиент шлёт write x=42 на лидера n1. Одновременно nemesis рвёт сеть — n1 оказывается отрезан от большинства (n3, n4, n5), остаётся только с n2. n1 реплицирует write на n2 (минорити), считает что всё ок, и возвращает клиенту ok — это критический момент. Дальше nemesis залечивает раздел, majority видит, что n1 отстал, и в re-election выигрывает n3. n1 делает log truncate всех записей, которые не реплицировались на majority, — включая x=42. Write, который вернул ok клиенту, исчез. Это и есть тот баг, который Knossos поймает.
check-history — Knossos находит non-linearizable schedule. Recorder отдаёт history.edn (12k операций за 60 секунд) в checker. Knossos строит state machine для register и перебирает все совместимые с real-time линеаризации. Если ни одна не работает — verdict valid? false. На выходе HTML-таймлайн с цветными op-rectangles, linearizability graph (SVG), latency histogram и список конкретных операций, которые сломали порядок. Этот артефакт прикладывается к bug-репорту вендору, и обычно его принимают без споров — потому что отчёт воспроизводим: lein test :only jepsen.mongodb.tests/test-partition и через 15 минут тот же fail.
Когда инвестировать в Jepsen-тесты. Решение фиксируется в первой ноде диаграммы как ADR-001. Коротко:
| Стоит | Не стоит |
|---|---|
| Распределённые БД (Mongo, Cassandra, CockroachDB) | App-код поверх протестированной БД |
| Consensus-библиотеки (etcd, Consul, custom Raft) | Stateless микросервис с одной Postgres |
| Очереди с exactly-once / at-least-once гарантиями | Cache-only сервис |
| Lock services, leader election | Read-only API, batch ETL |
| Custom replication (sharded counters, CRDT) | Стандартный CDN + S3 |
Правило большой пальца: если потеря или дублирование одной записи стоит дороже, чем 30-нод-кластер на EC2 на полдня (~$50), — пиши Jepsen-тест. Для app-уровня хватает Chaos Engineering: kill pods через chaoskube, partition AZ через iptables, не Knossos.
Цена. Setup долгий: написать workload + checker + nemesis-стратегию для нового SUT — это 1–4 недели опытного инженера. Прогон — 15–60 минут на тест-кейс, 5–30 GB логов на nightly. CI обычно гоняет одну smoke-конфигурацию (5 минут, partition + healthy) на каждый PR, и полный matrix (5 nemesis × 3 workload × 4 consistency level = 60 прогонов, 12 часов) — раз в ночь на отдельной machine pool.
Альтернатива. Если совсем нет ресурсов — TLA+/PlusCal (статический model checker на спеке, без реальных бинарников). Найдёт проблемы в дизайне, но не в реализации. Лучше всего работает в паре: TLA+ доказывает что протокол корректен, Jepsen — что код этот протокол реализует.
w=1 (и при w=majority в некоторых конфигурациях из-за rollback bug). Серия отчётов Кингсбери в 2013–2017 заставила пересмотреть write concern defaults.localhost. Это не тестирует partition — сеть в loopback всегда работает. Нужны минимум 3 (а лучше 5) VM с настоящей сетью, чтобы iptables -A INPUT -s ... имел эффект.:info операции. В Jepsen :info означает "клиент не знает, прошла ли операция" — таймаут или потеря соединения после invoke. Knossos обязан считать что op могла либо случиться, либо нет. Если в коде игнорируешь :info и считаешь их :fail — спрячешь баги.pumba kill.