Рушії
Вибір рушія
Усі три рушії зберігають одне й те саме: ідентифікатор UUIDv7 та масив даних, але записують їх на диск по-різному. Від цього залежить, для чого найкраще підходить кожен рушій:
| DocStore | SegmentedLog | Queue | |
|---|---|---|---|
| На диску | один файл JSONC на запис, шардований за кінцевими символами UUID | сегменти лише для додавання з рядками JSON, перед якими вказано довжину й контрольну суму, а також маніфест | один журнал подій лише для додавання |
| Записування | атомарна заміна файлу для кожного запису | додавання із контрольними сумами; перезаписування та видалення додають нові записи, а звільнене місце повертається під час compact() |
додавання із контрольною сумою для кожної події черги |
| Читання | get() за ідентифікатором, запити за індексом |
сканування курсором і за часовим діапазоном, get() через індекс стану |
claim() у порядку додавання в чергу |
| Найкраще підходить | точкове читання та пошук за індексом у колекціях будь-якого розміру | потоки з інтенсивним додаванням, приблизно до 1M записів | довговічна передача завдань між процесами |
| Слабке місце | сканування неіндексованих полів у колекціях понад приблизно 10k записів | часте перезаписування окремих записів (старі версії займають місце до стиснення) | не є універсальним сховищем записів; дані видаляються після завершення завдання |
DocStore і SegmentedLog використовують один ланцюжок викликів запит 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 батьківських каталогів. Стійкість до втрати живлення все одно залежить від того, чи файлову систему та параметри монтування налаштовано на виконання цих операцій синхронізації.