Движки
Выбор движка
Все три движка хранят одни и те же данные: идентификатор 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:
$store = new Storh\DocStore($root, 'articles');
$record = $store->put(['slug' => 'hello', 'published' => true]);
$same = $store->get($record->id());
Файлы записей распределяются по каталогам по последним символам UUID, чтобы UUIDv7 всплески записи распределялись по каталогам:
articles/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
SegmentedLog
SegmentedLog предназначен для записей с частым добавлением, чтения по курсору и чтения за временной диапазон. Записи помещаются в активный сегмент, по одной в строке, в формате <byte-length>\t<xxh32>\t<json> (см. раздел Атомарность). Файлы сегментов имеют расширение .ndjson, но префикс с длиной и контрольной суммой не позволяет читать их обычными инструментами для NDJSON; используйте stream() или построитель запросов. Заполненные сегменты сменяются запечатанными сегментами с разреженными индексами.
$events = new Storh\SegmentedLog($root, 'events');
$events->put(['type' => 'user.created']);
$events->compact();
Очередь
Queue предназначен для высокопроизводительных долговечных очередей задач. Постановка в очередь, захват, завершение, повторная постановка и очистка добавляют события в queue.log; при открытии очереди ее состояние восстанавливается в памяти путем повторного воспроизведения событий. Пакетные методы распределяют затраты на блокировки и сброс данных по множеству задач. Журнал растет с каждым событием; stats() показывает его текущий размер в байтах.
$queue = new Storh\Queue($root, 'mail');
$queue->enqueue(['to' => 'person@example.test']);
$job = $queue->claim();
Надежность хранения и параллельная работа
storh публикует файлы через атомарное переименование внутри целевого каталога. При повторном открытии и repair() удаляются оставшиеся временные файлы, если создавший их процесс завершился, а временные файлы активных процессов записи сохраняются. Маркеры записи делают эту проверку постоянной по времени, поэтому коллекция сканируется при открытии хранилища только после сбоя.
SegmentedLog и Queue записывают строки журнала с защитой длиной и контрольной суммой. При повторном открытии или исправлении поврежденный хвост усекается до последнего зафиксированного события, а состояние в памяти восстанавливается по содержимому долговечного журнала. Записи в журналы и очереди сериализуются с помощью блокировок файловой системы.
Изменения в DocStore сериализуются блокировкой записи на уровне коллекции, поэтому файлы записей и вторичные индексы обновляются как единое целое во всех процессах. Параллельная запись записей с разными идентификаторами безопасна при тех же допущениях об атомарном переименовании файловой системой. При параллельной записи по одному идентификатору побеждает последнее переименование.
storh сбрасывает данные и вызывает fsync для файловых дескрипторов перед переименованием или завершением добавления, а после атомарного переименования пытается вызвать fsync для родительских каталогов. Надежность хранения при отключении питания по-прежнему зависит от поддержки этих операций файловой системой и параметрами монтирования.