Шардирование
DocStore записывает по одному файлу на запись. Без шардирования все файлы большой коллекции окажутся в одном каталоге, а операции с каталогом замедляются по мере роста числа записей. Поэтому storh распределяет файлы записей по корзинам на основе двух символов идентификатора записи:
pages/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
Корзина определяется по началу последней группы UUID (символы 24 и 25), где содержится случайная энтропия. Два шестнадцатеричных символа дают 256 корзин, поэтому в коллекции из 100k записей на каталог приходится в среднем менее 400 файлов.
Почему хвост, а не начало
Идентификаторы UUIDv7 начинаются с 48-битной временной метки в миллисекундах, поэтому у идентификаторов, созданных почти одновременно, совпадают начальные символы. Шардирование по префиксу направляло бы каждый всплеск записей в один и тот же перегруженный каталог, а корзины менялись бы только с течением времени. Конечные байты случайны для каждой записи, поэтому записи равномерно распределяются с самого начала.
Шардирование незаметно для чтения и запросов: get() вычисляет корзину по идентификатору, а stream() возвращает записи, отсортированные по идентификатору во всех корзинах, поэтому пагинация по курсору работает так, будто коллекция представляет собой единый упорядоченный список.
При больших объемах данных
Число корзин не растет: два шестнадцатеричных символа означают 256 каталогов при любом размере коллекции. Растет число файлов в каждой корзине:
| Записи | Файлов на корзину |
|---|---|
| 10k | ~40 |
| 100k | ~390 |
| 1M | ~3,900 |
| 10M | ~39,000 |
Современные файловые системы индексируют записи каталогов, поэтому несколько тысяч файлов в корзине обеспечивают быстрый поиск по ключу; с 1M записей get() и индексированные запросы работают без проблем. Открытие хранилища занимает постоянное время независимо от размера коллекции: поиск осиротевших временных файлов выполняется только тогда, когда метка мертвого писателя указывает на сбой предыдущего процесса во время записи. При больших объемах линейно растут операции над всей коллекцией: сканирование без индексов, reindex() и verify() проходят по каждому файлу записей, поэтому рассматривайте их как пакетные задания. При объеме свыше примерно 10M записей в коллекции начинают иметь значение число записей в каждой корзине и общее использование inode: разделите коллекции или используйте сегментированный журнал для данных, рассчитанных на сканирование; он изначально поддерживает небольшое число файлов.
Другие движки
SegmentedLog и Queue не используют шардинг, поскольку не создают отдельный файл для каждой записи. Журнал дописывает данные в файлы сегментов в segments/, ограниченные размером max_segment_bytes (по умолчанию 1 MiB), а очередь дописывает события в один queue.log. Небольшое число файлов обеспечивается самой архитектурой.