Распределённая база обещает строгую консистентность. Клиент пишет значение, запрос отваливается по таймауту, клиент решает, что запись не прошла. А через секунду читает тот же ключ — и там лежит ровно то «непрошедшее» значение. Закоммиченное значение пришло из записи, которая отчиталась об ошибке. Это ровно тот класс багов, который ищет Jepsen, и именно такой баг его ночные прогоны нашли в CockroachDB.
На этом задокументированном кейсе и разберём, как устроена проверка консистентности, что Jepsen реально поймал и почему путь от симптома до причины занял два месяца.
Из чего собран тест: нагрузка, немезида, checker
Тест Jepsen складывается из трёх частей.
Первая — нагрузка: набор клиентов, которые конкурентно дёргают систему и пишут журнал операций с метками времени. Каждая операция фиксируется как вызов, а потом как успех, отказ или неопределённый исход.
Самая простая нагрузка — register: один ключ, чтения и записи, проверка, что чтение видит последнее записанное.
; фрагмент истории Jepsen: журнал операций с метками времени {:process 3 :type :invoke :f :write :value 4} {:process 3 :type :fail :f :write :value 4} ; запись отчиталась об ошибке {:process 7 :type :invoke :f :read} {:process 7 :type :ok :f :read :value 4} ; а прочиталось всё равно 4
Вторая часть — немезида: процесс, который впрыскивает отказы, потому что распределённые баги живут именно в сбоях. Сетевые разделения, паузы и убийства процессов, перекос часов, смена состава кластера, принудительные разбиения диапазонов — всё это немезида включает по ходу теста.
Третья часть — checker: он берёт записанную историю и модель консистентности и проверяет, есть ли допустимый порядок операций, который объясняет наблюдения. Для линеаризуемости это в общем случае NP‑трудно, и когда ни одного законного объяснения не находится, Jepsen печатает свою фирменную строку
Analysis invalid! (ノಥ益ಥ)ノ ┻━┻— это реальное нарушение.
Плюсом когда узел убивают посреди операции, клиент не знает, успела ли она закоммититься. Jepsen это понимает и при разборе истории держит в уме обе ветки: операция могла как пройти, так и нет, и история считается валидной, если хоть один расклад исходов укладывается в модель. Как раз на этом стыке баг и вылез.
Модели консистентности и чем их проверяют
Чтобы понимать, что именно нарушено, Jepsen опирается на иерархию моделей, которую сам же во многом и прояснил для индустрии. Сериализуемость пришла из мира СУБД и говорит о транзакциях: есть ли последовательный порядок, эквивалентный наблюдаемому конкурентному исполнению. Линеаризуемость пришла из распределённых систем и говорит об отдельных операциях: выглядит ли система как один узел, где каждая операция мгновенна и видит самое свежее значение. Это разные оси, и их постоянно путают, сериализуемость не обещает свежести, линеаризуемость не обещает атомарности транзакций. На вершине строгая сериализуемость, где есть и то, и другое.
Проверяют это разные чекеры. Линеаризуемость одиночных операций исторически проверял Knossos, перебирая допустимые порядки, — отсюда и тяжесть на длинных историях. Транзакционные аномалии ловит Elle: он строит граф зависимостей между транзакциями по тому, кто чьё значение прочитал и перезаписал, и ищет в нём циклы, потому что цикл определённого вида — это конкретная аномалия изоляции вроде потерянного обновления. Elle показывает сами транзакции, которые образуют цикл, и эта диагностируемость отличает его от обычного «тест покраснел».
Для самопроверки можно пройти вступительный тест по NoSQL: в нём есть вопросы по распределённым базам и связанным с ними концепциям. По результату будет проще понять, какие темы стоит разобрать глубже.
Кейс: ночной Jepsen ловит баг в CockroachDB 2.1
CockroachDB гоняет Jepsen с до‑релизных бет и перезапускает тесты каждую ночь. В наборе семь нагрузок в связке с разными немезидами; ночью поднимается кластер из пяти узлов, и каждая пара «нагрузка плюс немезида» крутится по шесть минут. Долго почти все падения были не багами базы, а сбоями самой автоматики, она поднимает облачные виртуалки, зависит от сети и потому склонна мигать. На отделение настоящих инцидентов от шума ушло заметное время, после которого тесты стали надёжными.
29 сентября 2018-го — ровно через два года после релиза беты, которую когда‑то тестировал Aphyr, — ночной прогон сообщил о падении в тесте register со split‑немезидой, и в логах стояло то самое |
Дальше — расследование.
Для упавших прогонов сохранялись сетевые дампы, и разбор трафика в Wireshark указал на недавно добавленную транзакционную конвейеризацию записей. 24 октября, почти через месяц, тест упал снова, но уже не со split‑немезидой, а с другой, с перекосом часов и кольцевым разделением большинства. Это сразу сказало две вещи: подозрение на split было ложным, а сам баг редкий и мог дремать месяцами до первого проявления. Чтобы ловить больше одного воспроизведения в месяц, инженеры собрали стенд, гонявший пятьдесят копий теста параллельно, и подкрутили логирование.
Корень: утечка сигнала о коммите
Сдвинулось дело, когда в одной из трасс заметили утечку побочного эффекта: ожидающая транзакция получала разрешение продолжить ровно тогда, когда транзакция, которую она ждала, шла на второй виток цикла перепредложения. Потянули за нить — и нашли суть: транзакция, не закоммитившаяся с первой попытки, всё равно сигналила ожидающим, будто закоммитилась. Завязано всё на конвейерные записи, появившиеся в CockroachDB 2.1: запись идёт асинхронно, транзакция движется дальше, не дожидаясь её завершения, и проверяет наличие записанных намерений только перед коммитом.
Дальше по шагам.
Транзакция A начинает конвейерную запись
UPDATE test SET val=4 WHERE id=167и оставляет на ключе 167 намерение записи.Транзакция B пытается прочитать ключ 167, видит висящее намерение и встаёт в очередь ожидания транзакций. A пробует закоммититься и при коммите обязана убедиться, что её ранние намерения на месте; первая попытка проверку проходит, но в этот момент происходит передача лидерства, старый лидер прерывает работу, и коммит автоматически переедет на нового лидера.
Тут и срабатывает баг: A ошибочно сообщает очереди ожидания, что закоммитилась. B просыпается и, раз ей сказали об успехе A, разрешает намерение на ключе 167, для внешнего наблюдателя в ключе теперь 4, хотя A ещё не завершилась. A делает вторую попытку коммита на новом лидере, и та проваливается: конвейерного намерения уже нет, его разрешила B. В итоге A считает, что упала, и отдаёт клиенту однозначную ошибку, а B сделала её результат фактически состоявшимся.
Будь транзакции многоключевыми, B могла бы разрешить часть намерений, пока A отменяет остальные, — и атомарность транзакции развалилась бы, хотя такой тяжёлый сценарий в дикой природе не встречали.
Починка и масштаб
После двух месяцев охоты починка оказалась почти неловко простой — один оператор if (в патче, куда вошли и сопутствующие правки тестов). По срокам не повезло: расследование наложилось на релиз CockroachDB 2.1 от 30 октября, так что баг уехал в этот релиз и был закрыт через пару недель в 2.1.1.
При дальнейшем разборе выяснилось, что баг старше, он жил в коде с версии 2.0, где появилась очередь ожидания транзакций, и для той ветки чинился в 2.0.7; конвейерные записи не создали баг, а резко подняли вероятность его проявления.
Чему учит кейс
Здесь видно и силу Jepsen, и его предел. С одной стороны, он нашёл баг, который не поймало больше ничто, — рассогласование, прятавшееся месяцами в редком стечении передачи лидерства и ожидания транзакций. С другой, Jepsen хорош ровно настолько, насколько хороши нагрузки в наборе: имевшаяся нагрузка была мало чувствительна именно к этой ошибке, между попаданиями проходил месяц, а симптом — «упавшая» запись, которая закрепилась, — был далеко от причины, утечки сигнала о коммите.
Отсюда и практические уроки про редкие конкурентные баги.
Первый: «упало» не равно «не произошло». Неопределённый исход — полноправная часть модели, клиент обязан трактовать неоднозначную ошибку как «возможно, закоммичено», и баг был ровно местом, где система схлопнула неоднозначный исход в неверный определённый.
Второй: чтобы поймать такой баг, нужны впрыск отказов, checker, который замечает тонкие нарушения, массовое повторение ради воспроизведения (те самые пятьдесят копий) и полные трассы вроде сетевых дампов. Воспроизводимость и наблюдаемость тут сложнее самого факта обнаружения.
Другие находки Jepsen
CockroachDB — не исключение, а иллюстрация закономерности: вендоры, уверенные, что пройдут Jepsen, нередко не проходят. По публичным разборам Jepsen ловил потерю закоммиченных записей в MongoDB. В MariaDB Galera Cluster версий с 12.1.2 по 12.2.2 нашли два сценария потери закоммиченных транзакций и нарушение заявленного уровня изоляции — там допускается потерянное обновление, которого при обещанной изоляции быть не должно.
Бывает и наоборот: etcd 3.4.3 в тестах удержал строгую сериализуемость под паузами, убийствами, перекосом часов и сменой состава, а сотрудничество с Jepsen свелось в основном к исправлению документации, которая занижала реальные гарантии.
Список длиннее: в ScyllaDB давний баг #7611 нашёлся в коде добавления в список — он генерировал свои метки времени вместо Paxos‑меток, а без них линеаризуемость операций со списком не держалась; в Dgraph всплыли дедлоки, крэши и потеря с порчей записей даже в здоровых кластерах; RethinkDB ловили на грязных чтениях, а Redis Cluster — на потере данных без всякого падения узлов. Вывод из всей этой коллекции простой: отсутствие публичного теста — не признак надёжности, а просто отсутствие свидетельств.
Если хочется покопаться самому — вот первоисточники: разбор в блоге Cockroach Labs, issue #30792, сайт Jepsen с анализами.
Что ещё почитать о тестировании
Почему классический подход к QA больше не работает — и виновата ли в этом эпоха ИИ
О том, почему проверки перед релизом уже не дают полной картины качества и как меняется роль QA в современных системах.Процесс тестирования: от анализа до завершения
Разбор основных этапов тестирования, организации процесса и задач команды на каждом из них.

Редкие сбои в распределённых системах сложно не только найти, но и правильно воспроизвести и диагностировать. На бесплатных открытых уроках можно глубже разобрать работу высоконагруженных систем и подходы к расследованию инцидентов, познакомиться с преподавателями и задать вопросы по теме.
16 июня, 20:00 — «Асинхронная обработка данных в высоконагруженных системах». Записаться
24 июня, 20:00 — «Инцидент-менеджмент в SRE. Как быстро находить, устранять и предотвращать сбои в системе». Записаться
Все бесплатные уроки, которые пройдут в июне, собрали в отдельном дайджесте.
