Sharding
DocStore scrive un file per record. Senza sharding, tutti i file di una raccolta grande finirebbero in un'unica directory e le operazioni sulla directory rallenterebbero all'aumentare delle voci. storh suddivide quindi i file dei record in bucket usando due caratteri dell'id del record:
pages/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
Il bucket si ricava dall'inizio del gruppo finale di UUID (caratteri 24 e 25), che contiene entropia casuale. Due caratteri esadecimali danno 256 bucket, quindi una raccolta di 100k record ha in media meno di 400 file per directory.
Perché la coda e non la testa
Gli id di UUIDv7 iniziano con un timestamp in millisecondi a 48 bit, quindi gli id creati a breve distanza condividono i caratteri iniziali. Suddividere per prefisso invierebbe ogni raffica di scritture alla stessa directory molto sollecitata, ruotando i bucket solo con il passare del tempo. I byte finali sono casuali per ogni record, quindi le scritture si distribuiscono uniformemente fin dal primo record.
Lo sharding è invisibile a letture e query: get() calcola il bucket dall'id e stream() restituisce i record ordinati per id in tutti i bucket, così la paginazione tramite cursore si comporta come se la raccolta fosse un unico elenco ordinato.
Su larga scala
Il numero di bucket non aumenta mai: due caratteri esadecimali corrispondono a 256 directory per qualsiasi dimensione della raccolta. Aumenta invece il numero di file per bucket:
| Record | File per bucket |
|---|---|
| 10k | ~40 |
| 100k | ~390 |
| 1M | ~3,900 |
| 10M | ~39,000 |
I filesystem moderni indicizzano le voci delle directory, quindi qualche migliaio di file per bucket mantiene veloci le ricerche puntuali; 1M di record è gestibile per get() e le query indicizzate. L'apertura di uno store richiede tempo costante indipendentemente dalla dimensione della raccolta: la pulizia dei file temporanei orfani viene eseguita solo quando un marcatore di writer inattivo indica che un processo precedente è terminato in modo anomalo durante una scrittura. Ciò che cresce linearmente su larga scala sono le operazioni sull'intera raccolta: le scansioni senza indice, reindex() e verify() visitano ciascuna tutti i file dei record, quindi vanno trattate come processi batch. Oltre circa 10M di record per raccolta, iniziano a contare i numeri per bucket e l'uso totale degli inode: suddividete le raccolte oppure usate il log segmentato per dati da scansionare, che mantiene basso il numero di file per sua struttura.
Gli altri motori
SegmentedLog e Queue non usano lo sharding perché non creano un file per record. Il log aggiunge dati ai file di segmento sotto segments/, con limiti definiti da max_segment_bytes (1 MiB per impostazione predefinita), mentre la coda aggiunge eventi a un singolo queue.log. Il numero di file rimane basso per sua struttura.