Pular para o conteúdo

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:

Text
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.