シャーディング
DocStoreはレコードごとに1つのファイルを書き込みます。シャーディングがないと、大規模なコレクションではすべてのファイルが1つのディレクトリに集まり、項目数が増えるにつれてディレクトリ操作が遅くなります。そのためstorhは、レコードIDの2文字を使ってレコードファイルを振り分けます。
pages/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
バケットには、UUIDの最後のグループの先頭(24文字目と25文字目)を使います。この部分はランダムなエントロピーです。16進数2文字で256個のバケットができるため、10万件のコレクションでは、1ディレクトリあたり平均400ファイル未満になります。
先頭ではなく末尾を使う理由
UUIDv7 IDは先頭に48ビットのミリ秒タイムスタンプを持つため、近い時刻に作成されたIDの先頭部分は共通します。プレフィックスでシャーディングすると、書き込みが集中するたびに同じ高負荷ディレクトリに書き込まれ、時間の経過とともにバケットが切り替わるだけです。末尾のバイトはレコードごとにランダムなため、最初のレコードから書き込みが均等に分散します。
シャーディングは読み取りやクエリからは見えません。get()はIDからバケットを計算し、stream()はすべてのバケットにまたがってレコードをID順に返します。そのためカーソルページネーションは、コレクションが1つの順序付きリストであるかのように動作します。
大規模運用時
バケット数は増えません。16進数2文字なら、コレクションのサイズにかかわらずディレクトリは256個です。増えるのは各バケット内のファイル数です。
| レコード数 | バケットあたりのファイル数 |
|---|---|
| 10k | ~40 |
| 100k | ~390 |
| 1M | ~3,900 |
| 10M | ~39,000 |
最新のファイルシステムはディレクトリエントリにインデックスを付けるため、1つのバケットあたり数千ファイルに抑えると、ポイント検索を高速に保てます。get()とインデックス付きクエリなら、100万件のレコードも無理なく扱えます。ストアを開く処理はコレクションのサイズにかかわらず一定時間です。孤立した一時ファイルの掃除は、前のプロセスが書き込み途中でクラッシュしたことを示す書き手の停止マーカーがある場合にのみ実行されます。規模に応じて線形に増えるのは、コレクション全体を処理する操作です。インデックスのないスキャン、reindex()、verify()はどれもすべてのレコードファイルをたどるため、バッチ処理として扱ってください。1つのコレクションが約1,000万件を超えると、バケットごとの件数と inode の総数が問題になり始めます。コレクションを分割するか、スキャン中心のデータには、ファイル数を構造上少なく保てるセグメントログを使ってください。
その他のエンジン
SegmentedLogとQueueはレコードごとにファイルを作成しないため、シャーディングを行いません。ログはsegments/配下のセグメントファイルに追記し、max_segment_bytes(既定では1 MiB)で上限を設けます。キューは単一のqueue.logにイベントを追記します。構造上、ファイル数は少なく保たれます。