Saltar al contenido

Fragmentación

DocStore escribe un archivo por registro. Sin fragmentación, una colección grande acumularía todos sus archivos en un solo directorio, y las operaciones del directorio se ralentizarían a medida que aumentara la cantidad de entradas. Por eso, storh distribuye los archivos de registros en grupos según dos caracteres del identificador:

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

El grupo se toma del principio del último bloque del UUID (caracteres 24 y 25), que contiene entropía aleatoria. Dos caracteres hexadecimales dan 256 grupos, de modo que una colección de 100k registros tiene, de media, menos de 400 archivos por directorio.

Por qué se usa la parte final y no la inicial

Los identificadores de UUIDv7 comienzan con una marca de tiempo de milisegundos de 48 bits, por lo que los identificadores creados en momentos cercanos comparten los caracteres iniciales. Fragmentar por prefijo enviaría cada ráfaga de escrituras al mismo directorio saturado y solo cambiaría de grupo con el paso del tiempo. Los bytes finales son aleatorios en cada registro, así que las escrituras se distribuyen uniformemente desde el primero.

La fragmentación es invisible para las lecturas y consultas: get() calcula el grupo a partir del identificador y stream() devuelve los registros ordenados por identificador entre todos los grupos, por lo que la paginación con cursor se comporta como si la colección fuera una única lista ordenada.

A escala

La cantidad de grupos nunca aumenta: dos caracteres hexadecimales equivalen a 256 directorios, sea cual sea el tamaño de la colección. Lo que aumenta es el número de archivos por grupo:

Registros Archivos por grupo
10k ~40
100k ~390
1M ~3,900
10M ~39,000

Los sistemas de archivos modernos indexan las entradas de directorio, por lo que unos pocos miles de archivos por bucket mantienen rápidas las búsquedas puntuales; 1M de registros es una cantidad manejable para get() y las consultas indexadas. Abrir un almacén lleva un tiempo constante, independientemente del tamaño de la colección: la limpieza de archivos temporales huérfanos solo se ejecuta cuando un marcador de escritor inactivo indica que un proceso anterior se bloqueó durante una escritura. Lo que sí crece linealmente a gran escala son las operaciones sobre toda la colección: los recorridos sin índice, reindex() y verify() recorren cada archivo de registro, así que considérelos trabajos por lotes. Por encima de unos 10M de registros por colección, empiezan a importar el número de archivos por bucket y el uso total de inodos: divida las colecciones o use el registro segmentado para datos que requieren recorridos, lo que mantiene bajo el número de archivos por diseño.

Los otros motores

SegmentedLog y Queue no fragmentan los datos porque no crean un archivo por registro. El registro añade datos a archivos de segmento en segments/, limitados por max_segment_bytes (1 MiB de forma predeterminada), y la cola añade eventos a un único queue.log. El número de archivos se mantiene bajo por diseño.