Seed-driven стресс-тест PG-машинерии (src/test/test_pg_chaos.cpp): ОДИН настоящий
osd_t (primary) с настоящим хранилищем на заглушке диска (полностью в RAM, ringloop_mock)
владеет одной PG (номер 1, пул 1, объекты — 4 шт по 128 К, гранула 4 К). Остальные члены PG —
фейковые вторичные OSD внутри теста. PG: реплицированная x3 (members 2,3, spare 4) или
EC 2+2 (members 2…4, spare 5, объект = один parity-stripe). Сид однозначно задаёт
конфигурацию и всю последовательность событий; упавший прогон воспроизводится тем же --seed.
Проверяется в первую очередь PG-машинерия (subop-веер, peering/LIST/rollback/stabilize, repeer-демоны, stop_pg, INCOMPLETE, recovery) и частично хранилище первичного OSD; однако, проверка хранилища — не главная цель теста, его покрывает хаос-тест blockstore.
Конфигурации (10)
- replicated/EC ×
- immediate_commit none/all ×
- csum_block нет/4096/16384
--recovery включает recovery/rebalance после set-change (по умолчанию выкл, чтобы
гонять degraded-состояния).
Модель мира
Для каждого объекта — упорядоченная история версий; версия пишется в модель в момент SUBMIT (дельта = точные байты/длина/битмап гранул запроса), состояние:
V_SUBMITTED— операция отправлена, ответа ещё нет (выполняется). Ведёт себя как V_OPTIONAL во всём, кроме одного: параллельные чтения вправе не показывать результат этой записи, и это не будет маркировать её DEAD.V_OPTIONAL— запись отправлена, но не подтверждена, и может быть либо применена (но тогда целиком), либо не применена вообще. Версии попадают в это состояние из V_SUBMITTED и, в режимах с явными fsync, V_ACKED при гибели клиентской сессии до запроса SYNC (это считается маркером, что запись не подтверждена). Наблюдение эффекта OPTIONAL записи в последующем чтении восстанавливает её в ACKED; наблюдение её отсутствия (когда она не видна в своём диапазоне) ⇒ DEAD.V_ACKED— запись подтверждена; запоминаетсяack_live(живой набор на момент ack — используется для проверки превышения модели отказов). При immediate_commit=all сразу считается зафиксированной на диске. ACKED запись не должна откатываться, пока сессия клиента жива.V_SYNCED— подтверждена и SYNC-нута ⇒ зафиксирована на диске и более не должна откатываться вообще.V_PARTIAL— удалена, но, возможно, не со всех OSD (PG осталась в LEFT_ON_DEAD).V_DEAD— версия либо точно отказана (-EAGAIN, возможно -ENOSPC в будущем), либо была OPTIONAL, но после этого стало точно известно, что она откачена, так как последующее чтение её уже НЕ наблюдало. Повторное появление DEAD-версии — провал теста.
Содержимое объектов самоидентифицируется версией: паттерн кодирует объект, версию и смещение — прочитанное можно декодировать обратно в «какая версия записала эти байты».
Верификация читает ТОЛЬКО через первичный OSD и проверяет, что ответ объясняется слиянием ОДНОГО согласованного подмножества записанных версий, где все зафиксированные точно присутствуют, все откаченные точно отсутствуют, а содержимое не врёт об авторстве. При неполном удалении разрешается INCOMPLETE в EC, а в репликах допускается воскрешение объекта.
Всё выводится из состояний версий и событий, без дополнительных флагов: ACKED не может исчезнуть, кроме как через событие, которое сначала ломает гарантию сессии — краш (уносит сессию вместе с процессом) или repeer/stop_pg (останавливают dirty-сессии ДО начала пиринга).
Проверки в тесте смотрят ТОЛЬКО на то, как процесс записи видит клиент, и не учитывают в модели, например, падения парных OSD сами по себе, так как клиент не может о них знать.
Симулируемая реальность
- Симулятор диска: запросы с задержками;
power_loss()= потеря питания — незафиксированные записи теряются, зафиксированные остаются. Неизвестный FD/opcode → падение теста (защита заглушки). - Симулятор etcd: ключ/значение + lease; revoke/TTL эмулируются при крахе (ключи OSD’а удаляются, lease отзывается).
- Симулятор парных OSD: отвечает в соответствии с контрактом вторичного OSD / blockstore.
Чтение/запись/удаление, SYNC, LIST. При падении парного OSD не-SYNC-нутые записи гибнут
с вероятностью
peer_cache_loss% (по умолчанию 50 — моделирует волатильный кэш диска), SYNC-нутые выживают всегда. Модель мира на краш пира сама НЕ реагирует: она знает только то, что видит клиент. Краш реперит PG, репер убивает dirty-сессии клиентов (volatile ACKED — это и есть dirty-клиент), сессия гибнет — вот это клиент наблюдает (PEER_STOPPED/EPIPE), и только session_died() делает demote. Если краш откатит что-то при живой сессии — это ненаблюдаемый клиентом откат, проверка 5 его ловит. - Симулятор сети: service_peers, каждый раунд. Задержка на этапе запроса — применяются случайные
--p-wire% (по умолчанию 70) очередей subop’ов пира; задержка на этапе ответа — доставляются случайныеp_reply% применённых ответов. Ответ принадлежит соединению: соединение умерло — недоставленные ответы гибнут с-EPIPE(применение остаётся). Однако, при гибели экземпляра OSD мутирующие операции вsent_opsкопируются в отдельную очередь «сети»; применяются позже (--p-wire%) БЕЗ ответа (соединение упало). Немутирующие операции (READ/LIST) отбрасываются (отвечать некому), очередь собранных операций ограничивается 64.
Операции (step)
На каждом шаге (веса --p-*; 0 выключает действие):
- wait (500) — ничего не слать, прокрутить симуляцию (событийные окна).
- write (60) — запись случайного диапазона случайного объекта; увеличение версии; дельта запоминается как OPTIONAL; ≤ max_inflight (8) одновременно.
- delete (6) — удаление случайного объекта; версия запоминается как OPTIONAL с нулевой длиной (полное удаление); LEFT_ON_DEAD/ack отслеживаются в account().
- read (40) — чтение объекта, полное с вероятностью
full_read%, иначе произвольный диапазон; при полном чтении объект проверяется. - sync (30) — синхронизация кэша (fsync); при подтверждении версии помечаются SYNCED (зафиксированными).
- restart (1) — ЧИСТАЯ перезагрузка OSD: очистка очереди (все inflight закончены), ожидание
простоя диска, уничтожение, пересоздание, ожидание подъёма OSD и, с шансом
--p-post, чтение и проверка содержимого всех объектов («чистый перезапуск, который ничего не потерял»). - takeover (1) — передача PG: сначала остановка PG через смену первичного OSD на
фейковый в заглушке etcd → stop_pg останавливает PG; далее сразу после удаления ключа
/pg/state/1/1(означающего, что OSD остановил PG) → пересоздание OSD и его переустановка в качестве первичного → перепроверка объектов, как в restart. - crash (2) — падение первичного OSD: потеря питания диска (потеря не-синкнутых записей), отмена лизы + удаление ключей OSD без отмены операций в полёте к парным OSD, запуск нового экземпляра, перепроверка объектов.
- peer_down (3) — крах парного OSD: выбирается жертва, не ломающая кворум ни одного объекта (проверяются все последние OPTIONAL версии до первой SYNCED), парный OSD останавливается, соединение с ним разрывается.
- peer_up (3) — подъём парного OSD (соединение — если PG или её история его ещё касается); после — settle(): ждать подъёма PG (ACTIVE|INCOMPLETE + покой), затем прогнать recover_pending — отложенные проверки объектов, чьи цепи могли перегласоваться.
- set_change (1) — переезд PG на другой OSD-набор через etcd; с
--recovery— восстановительные чтения; без — деградация легальна, мир «выживших копий» защищён проверками.
В конце — ожидание завершения: очистка до «PG в порядке и все операции завершены», максимум до MAX_STEPS=1e6 шагов → если превышается, то падение с ошибкой «did not settle».
Учёт результатов операций (account)
- Завершена с ожидаемым кодом возврата → V_ACKED. Удаление без LEFT_ON_DEAD закрывает окно частичного удаления. Удаление с LEFT_ON_DEAD требует повтора после подъёма пиров, а с EC допускает будущий INCOMPLETE, если объект не перезаписан вновь.
- Завершена с EAGAIN → V_DEAD: версия точно не применена; в тесте сейчас недостижимо, оставлено как заготовка на будущее, также применимо, например, к ENOSPC.
- Завершена с EPIPE → версия остаётся OPTIONAL: отказ — не ack, но операция могла примениться (а могла и нет). Опциональное удаление также требует повтора после подъёма пиров и тоже разрешает EC INCOMPLETE до следующей перезаписи.
- Завершена с другой ошибкой → падение теста («failed with retval»).
Проверки (все точки abort)
Согласованность данных (world_check):
- world_check — ответ обязан объясняться подмножеством версий: все обязательные (зафиксированные) присутствуют; байты отвечают ровно своей версией («record obj/seq/off mismatch», битмап-гранула «carries data from wrong version»).
- «shows %s but the highest…» — прочитана старая версия объекта, последние записи потеряны (монотонность нарушена).
- «read shows seq over acked v’s granule» — содержимое гранулы (obj/seq/off в record) противоречит версии, которой гранула принадлежит по модели: сборное/порванное содержимое вместо подтверждённых байт.
- «DEAD version showed again» — откаченные версии не должны возвращаться.
- «committed version read fully absent over floor» / «established version read absent over its granule» — подтверждённые операции не должны исчезать.
- read -EIO «OSD deems unrecoverable» — легален ровно в одном окне: незавершённое EC-удаление. Вне его -EIO — падение теста, в т.ч. «чистый» ack удаления + INCOMPLETE после revival (OSD соврал о полноте) и откат цепей при живой сессии без окна удаления; прочие retval ≠ len → падение теста.
- bitmap_len ≠ ожидаемому в ответах чтения → падение теста.
- «came back INCOMPLETE» после set_change/crash — тот же критерий: окно незавершённого удаления ⇒ INCOMPLETE легален и удаление повторяется; в ином случае → падение теста.
- «came back … which no world of the recorded versions explains» — финальная проверка корректности содержимого объектов после завершения теста.
- «changed across a clean restart that lost nothing» — неизменность объектов после перезапуска.
Живость/тайминг:
- «did not settle in MAX_STEPS» — операции не завершились за большое число шагов (возможна самоблокировка);
- «operations did not complete» — залипшие операции, печатаются в дампе; «a read of object N did not complete in MAX_STEPS … deadlock?»; «OSD did not load PGs from etcd in MAX_STEPS» / «OSD did not finish peering in MAX_STEPS» — проверка на старте OSD.
- «did not release the PG» — OSD не отпустил PG по команде;
- «disk did not go idle» — проверка против use-after-free, что все операции диска завершены перед уничтожением OSD;
- «PG did not become healthy after reviving every peer» — PG не поднялась после подъёма всех OSD;
- «pending recovery/repeated deletion could not complete on a healthy PG» — удаление/восстановление не завершено на живой PG.
Защиты тестовых заглушек:
- Неожиданный SQE (fd/opcode) на заглушке диска; «unexpected secondary opcode» у пира;
аргументы
--p-reply ≥ 1,--max-inflight ≥ 1и т.п.
Проверки покрытия:
peer_cache_loss<100+ volatile-конфиг + ≥8 смертей пира ⇒ проверка, что хотя бы раз выполнен вариант, в котором парный OSD не потерял журнал.- ≥10 грязных смертей OSD/передач PG ⇒ wire_harvested > 0: хотя бы одна операция первичного OSD, бывшая в полёте, обязана была собраться в «сеть» при остановке — путь «доставки от мёртвого OSD» выполнен.
Отчёт и воспроизводимость
OK-строка: writes(refused)/deletes/syncs/crashes/restarts/takeovers/peer deaths (kept journal)/recoveries/set changes/verified reads/stragglers (applied/captured/lost)/disk ops.
Детерминизм: один seed ⇒ та же конфигурация и то же расписание; -v — трасса операций
диска и парных OSD, захватов операций при падении пиров и изменений etcd.