
# 分片

`DocStore` 为每条记录写入一个文件。如果不进行分片，大型集合的所有文件都会堆积在同一个目录中，随着条目数量增加，目录操作会变慢。因此，storh 按记录 id 的两个字符对记录文件进行分桶：

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

分桶依据取自 UUID 最后一组的开头（第 24 和第 25 个字符），这些字符是随机熵。两个十六进制字符可形成 256 个桶，因此包含 100k 条记录的集合平均每个目录不到 400 个文件。

## 为什么取末尾而不是开头

UUIDv7 id 以 48 位毫秒时间戳开头，因此相近时间创建的 id 具有相同的前导字符。按前缀分片会让每一阵写入都集中到同一个繁忙目录中，只有随着时间推移才会切换桶。每条记录的末尾字节都是随机的，因此从第一条记录开始，写入就会均匀分散。

分片对读取和查询不可见：`get()` 根据 id 计算桶，而 `stream()` 会将所有桶中的记录按 id 排序后返回，因此游标分页的效果就像集合是一份有序列表。

## 大规模场景

桶的数量不会增加：无论集合有多大，两个十六进制字符始终对应 256 个目录。增长的是每个桶中的文件数：

| 记录数 | 每个桶中的文件数 |
| ------- | ---------------- |
| 10k     | ~40              |
| 100k    | ~390             |
| 1M      | ~3,900           |
| 10M     | ~39,000          |

现代文件系统会为目录条目建立索引，因此每个桶中只有几千个文件时，点查找依然很快；对于 `get()` 和索引查询而言，1M 条记录也能轻松应对。无论集合大小如何，打开存储所需的时间都是固定的：只有在发现失效的写入者标记表明先前进程在写入中途崩溃时，才会清理孤立的临时文件。随着规模扩大而线性增长的是整集合操作：未建立索引的扫描、`reindex()` 和 `verify()` 都会遍历每个记录文件，因此应将它们作为批处理任务。每个集合超过约 10M 条记录后，每桶记录数和 inode 总用量就开始变得重要：拆分集合，或者对以扫描为主的数据使用分段日志，其设计本身就能保持较低的文件数。

## 其他引擎

`SegmentedLog` 和 `Queue` 不会进行分片，因为它们不会为每条记录创建一个文件。日志会将内容追加到 `segments/` 下的分段文件中，并受 `max_segment_bytes` 限制（默认 1 MiB）；队列则将事件追加到单个 `queue.log` 中。其设计本身就能保持较低的文件数。
