
# 샤딩

`DocStore`는 레코드마다 파일 하나를 씁니다. 샤딩이 없으면 대규모 컬렉션의 모든 파일이 한 디렉터리에 쌓이고, 항목 수가 늘수록 디렉터리 작업이 느려집니다. 따라서 storh는 레코드 ID의 두 문자를 기준으로 레코드 파일을 버킷에 분배합니다:

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

버킷은 UUID의 마지막 그룹 시작 부분(문자 24와 25)에서 가져오며, 이 부분은 무작위 엔트로피입니다. 16진수 문자 두 개는 256개의 버킷을 제공하므로 레코드 100k 컬렉션에서는 디렉터리당 평균 400개 미만의 파일이 들어갑니다.

## 앞부분이 아닌 꼬리를 사용하는 이유

UUIDv7 ID는 앞부분에 48비트 밀리초 타임스탬프가 있으므로 비슷한 시점에 생성된 ID끼리는 앞쪽 문자가 같습니다. 접두사로 샤딩하면 쓰기 급증이 같은 핫 디렉터리에 집중되고 시간이 지나야 버킷이 바뀝니다. 꼬리 바이트는 레코드마다 무작위이므로 첫 레코드부터 쓰기가 고르게 분산됩니다.

샤딩은 읽기와 쿼리에 영향을 주지 않습니다. `get()`는 ID에서 버킷을 계산하고, `stream()`는 모든 버킷의 레코드를 ID 순으로 반환하므로 커서 페이지네이션에서도 컬렉션이 정렬된 하나의 목록처럼 동작합니다.

## 규모 확장

버킷 수는 늘어나지 않습니다. 16진수 문자 두 개를 사용하므로 컬렉션 크기와 관계없이 디렉터리는 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`에 이벤트를 추가합니다. 따라서 파일 수는 처음부터 적게 유지됩니다.
