Fragmentação
DocStore grava um arquivo por registro. Sem fragmentação, uma coleção grande
colocaria todos os arquivos em um único diretório, e as operações no diretório
ficariam mais lentas à medida que o número de entradas aumentasse. Portanto,
storh distribui os arquivos dos registros em diretórios com base em dois
caracteres do id do registro:
pages/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
O bucket é extraído do início do último grupo de UUID (caracteres 24 e 25), que contém entropia aleatória. Dois caracteres hexadecimais geram 256 buckets, portanto uma coleção com 100k registros tem, em média, menos de 400 arquivos por diretório.
Por que a cauda, e não o início
Os ids de UUIDv7 começam com um timestamp em milissegundos de 48 bits, então ids criados próximos uns dos outros compartilham os primeiros caracteres. Fragmentar pelo prefixo enviaria todos os picos de gravação para o mesmo diretório muito acessado, e os buckets só mudariam com o passar do tempo. Os bytes finais são aleatórios por registro, então as gravações se distribuem uniformemente desde o primeiro registro.
A fragmentação é invisível para leituras e consultas: get() calcula o bucket a partir do id, e stream() retorna os registros ordenados por id em todos os buckets, fazendo com que a paginação por cursor funcione como se a coleção fosse uma única lista ordenada.
Em grande escala
A quantidade de buckets nunca aumenta: dois caracteres hexadecimais correspondem a 256 diretórios, qualquer que seja o tamanho da coleção. O que aumenta é a quantidade de arquivos por bucket:
| Registros | Arquivos por bucket |
|---|---|
| 10k | ~40 |
| 100k | ~390 |
| 1M | ~3,900 |
| 10M | ~39,000 |
Os sistemas de arquivos modernos indexam as entradas de diretório, então alguns milhares de arquivos por bucket mantêm rápidas as buscas pontuais; 1M de registros é um volume tranquilo para get() e consultas indexadas. Abrir um armazenamento leva tempo constante, independentemente do tamanho da coleção: a varredura de arquivos temporários órfãos só é executada quando um marcador de processo encerrado indica que um processo anterior falhou durante uma gravação. O que cresce linearmente em grande escala são as operações sobre a coleção inteira: varreduras sem índice, reindex() e verify() percorrem cada arquivo de registro, então trate-as como tarefas em lote. Acima de cerca de 10M de registros por coleção, a quantidade de arquivos por bucket e o uso total de inodes começam a importar: divida as coleções ou use o log segmentado para dados que exigem varreduras, que mantém a quantidade de arquivos baixa por construção.
Os outros mecanismos
SegmentedLog e Queue não fragmentam os dados porque não criam um arquivo por registro. O log acrescenta dados a arquivos de segmento em segments/, limitados por max_segment_bytes (1 MiB por padrão), e a fila acrescenta eventos a um único queue.log. A quantidade de arquivos permanece baixa por construção.