Перейти к содержимому

Движки

Выбор движка

Все три движка хранят одни и те же данные: идентификатор UUIDv7 и массив данных, но размещают их на диске по-разному. От способа размещения зависят сильные стороны каждого движка:

DocStore SegmentedLog Queue
На диске один файл JSONC на запись, шардинг по хвосту UUID сегменты с добавлением только в конец файла, строки JSON с префиксами длины и контрольной суммы, а также манифест один журнал событий с добавлением только в конец файла
Запись атомарная замена файла для каждой записи записи с контрольными суммами; перезаписи и удаления добавляют новые записи, освобождение места выполняет compact() запись с контрольной суммой для каждого события очереди
Чтение get() по идентификатору, запросы по индексам чтение курсором и сканирование диапазона времени, get() через индекс состояния claim() в порядке постановки в очередь
Лучше всего подходит чтение отдельных записей и поиск по индексам в коллекциях любого размера потоки с частым добавлением, примерно до 1M записей надёжная отправка заданий между процессами
Слабое место сканирование неиндексированных полей при объёме свыше примерно 10k записей частая перезапись отдельных записей (старые версии занимают место до уплотнения) не является универсальным хранилищем записей; данные удаляются после завершения задачи

DocStore и SegmentedLog используют один и тот же гибкий API-запрос query API: одинаковая цепочка where() работает с обоими движками, различается только выполнение. DocStore может отвечать по вторичным индексам, а SegmentedLog фильтрует записи при сканировании сегментов. Queue основан на захвате задач и не поддерживает запросы: обработчики claim() берут следующую задачу, а не фильтруют задачи.

Практические рекомендации:

  • Записи, которые вы получаете, обновляете или удаляете по отдельности и запрашиваете по полю: DocStore. Добавьте индексы для полей, по которым выполняете фильтрацию.
  • События или версии, которые вы в основном добавляете и затем читаете по порядку или в заданном временном окне: SegmentedLog.
  • Задачи, передаваемые от производителей рабочим процессам: Queue. Захват и завершение задач безопасны при работе нескольких процессов, а claimMany()/completeMany() позволяют распределить затраты на блокировки и fsync.
  • Объединения, поиск или отчеты по любой коллекции: добавьте зеркало SQL. Файлы остаются каноническим источником данных; зеркало представляет собой проекцию, которую можно перестроить, в SQLite или MySQL.

Механизмы хранения можно комбинировать: у них общий только корневой каталог, поэтому одно приложение может хранить содержимое в DocStore, журнал аудита в SegmentedLog, а фоновые задачи в Queue — в пределах одного корневого каталога.

DocStore

DocStore предназначен для точечного чтения и простого сканирования долговечных записей. Для каждой записи создается отдельный файл JSONC:

PHP
$store = new Storh\DocStore($root, 'articles');
$record = $store->put(['slug' => 'hello', 'published' => true]);
$same = $store->get($record->id());

Файлы записей распределяются по каталогам по последним символам UUID, чтобы UUIDv7 всплески записи распределялись по каталогам:

Text
articles/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc

SegmentedLog

SegmentedLog предназначен для записей с частым добавлением, чтения по курсору и чтения за временной диапазон. Записи помещаются в активный сегмент, по одной в строке, в формате <byte-length>\t<xxh32>\t<json> (см. раздел Атомарность). Файлы сегментов имеют расширение .ndjson, но префикс с длиной и контрольной суммой не позволяет читать их обычными инструментами для NDJSON; используйте stream() или построитель запросов. Заполненные сегменты сменяются запечатанными сегментами с разреженными индексами.

PHP
$events = new Storh\SegmentedLog($root, 'events');
$events->put(['type' => 'user.created']);
$events->compact();

Очередь

Queue предназначен для высокопроизводительных долговечных очередей задач. Постановка в очередь, захват, завершение, повторная постановка и очистка добавляют события в queue.log; при открытии очереди ее состояние восстанавливается в памяти путем повторного воспроизведения событий. Пакетные методы распределяют затраты на блокировки и сброс данных по множеству задач. Журнал растет с каждым событием; stats() показывает его текущий размер в байтах.

PHP
$queue = new Storh\Queue($root, 'mail');
$queue->enqueue(['to' => 'person@example.test']);
$job = $queue->claim();

Надежность хранения и параллельная работа

storh публикует файлы через атомарное переименование внутри целевого каталога. При повторном открытии и repair() удаляются оставшиеся временные файлы, если создавший их процесс завершился, а временные файлы активных процессов записи сохраняются. Маркеры записи делают эту проверку постоянной по времени, поэтому коллекция сканируется при открытии хранилища только после сбоя.

SegmentedLog и Queue записывают строки журнала с защитой длиной и контрольной суммой. При повторном открытии или исправлении поврежденный хвост усекается до последнего зафиксированного события, а состояние в памяти восстанавливается по содержимому долговечного журнала. Записи в журналы и очереди сериализуются с помощью блокировок файловой системы.

Изменения в DocStore сериализуются блокировкой записи на уровне коллекции, поэтому файлы записей и вторичные индексы обновляются как единое целое во всех процессах. Параллельная запись записей с разными идентификаторами безопасна при тех же допущениях об атомарном переименовании файловой системой. При параллельной записи по одному идентификатору побеждает последнее переименование.

storh сбрасывает данные и вызывает fsync для файловых дескрипторов перед переименованием или завершением добавления, а после атомарного переименования пытается вызвать fsync для родительских каталогов. Надежность хранения при отключении питания по-прежнему зависит от поддержки этих операций файловой системой и параметрами монтирования.