データ登録処理に関する技術選定: Revision IDの生成アルゴリズム
data-registryによるRevision ID生成アルゴリズムにUUID v7を選択した。
背景
資源評価に利用するデータの永続化を担うdata-registryの開発をすすめている。
前回の記事では、冪等性キー生成に用いるハッシュアルゴリズムを選んだ。 本記事では、永続化処理が実行される際のRevision IDの生成方法について考える。
要件
現時点で、data-registryには下記のユースケースが見つかっている:
- 版の履歴閲覧: あるデータセットの変更履歴を時系列で表示したい
- データ登録要求の並行受信: 複数人のユーザーが複数シナリオを並列計算し、それらのクライアントからの登録要求を受信した
上記から、Revision IDの生成は、以下の要件を満たす必要がある:
| 要件 | 説明 | 要件ID |
|---|---|---|
| 一意性 | 各Revisionは一意のIDを持つ | SR001 |
| 不変性 | 一度生成されたら変更されない | SR002 |
| 時系列順序付け可能性 | 同じRevision chain内で自然にソート可能 | SR019 |
| 分散生成可能性 | Coordinatorなしで各プロセスから生成可能 | SR020 |
ここで、不変性(SR002)については実装の責務として考慮外とし、残りの要件を満たすID生成アルゴリズムを選定する。
アルゴリズムには複数の候補が考えうるが、詳細な検討の手間を軽減するには、まず時系列順序付け可能性を根拠に選択肢を刈り込むとよさそう。
候補アルゴリズム
ID群が時系列で順序付けされるためには、IDは時刻情報を含むtimestamp-basedな手法で生成される必要がある。
これに着目すると、例えばNanoidやUUID v3, v4, v5, v8は検討対象から除外でき、候補としては下記が残る:
なお、timestamp-basedな手法としてはUUID v1およびv6も含まれるが、これらと比較した場合にはv7が推奨されている(スーパー意訳)ため検討候補から除外した。
候補アルゴリズムの比較検討
連番(✘ 不採用)
その名の通り、連番の整数をIDに使う方法。
ID: 1, 2, 3, 4, ...
この手法の利点はストレージのオーバーヘッドが少ないことだろう。 表現はシンプルだが、それを実現するアーキテクチャや実装は必ずしもシンプルとは限らない。 2種類の実現方法「データベースレベル」「アプリケーションレベル」それぞれについて、pros/consを整理してみる。
方法A: データベースレベル
PostgreSQL の SERIAL/BIGSERIAL(自動採番機能)を利用する方法。
- pros:
- ✔ 採番の責務をデータベースに持たせられる
- ✔ ロック時間が短い
- cons:
- ✘ 単一のprimary writerに依存
- ✘ スケーラビリティ: 並行タスクが全て同じDBに集約
方法B: アプリケーションレベル
採番の責務をアプリケーションで持つ構成。 この方法をとるとデータベースについては水平スケールが自由になるが、結局ほアプリケーションレベルに移ってきた採番の責務がボトルネックになる。
つまり、連番形式は宿命的に「最後の値は何か」というグローバルな状態管理に依存する方法で、この状態は必ずどこかに集約されることになる。
結果として、SR020(分散生成可能性)を満たさないため不採用とする。
Snowflake ID(✘ 不採用)
Twitterなどの大規模分散システムで運用実績のある手法。
ID: 175638572750991360
├─ timestamp(ms)
├─ machine id
└─ sequence
- pros:
- ✔ 大規模分散システムでの運用実績あり
- ✔ ストレージ効率がよい
- cons:
- ✘ Machine IDの事前割り当てが必須(新規マシン追加時に管理が必要)
- ✘ 各machineが自身のsequenceを管理する必要がある
ストレージ効率の良さは魅力的だが、プラットフォームの潜在ユーザーの規模を考慮すると、machine IDの割り当てなどの運用オーバーヘッド増加がペイされるとは考えにくい。 data-registryにおける採用は見送ることにする。
UUID v7(✔ 採用)
UUIDの一つ。 タイムスタンプベースのフィールドを備えつつ、同タイプの先行バージョン(v1およびv6)に比べてランダム特性が向上している。
ID: 0189a5c5-a92b-7b47-b5ad-e6c3cb6c41e9
└─ from timestamp ─┘
- pros:
- cons:
- ✘ タイムスタンプ衝突にはランダム1部分のみで対処する必要がある
- TODO: ランダム部分のビット長が最悪ケースをカバーするか確認する
- ✘ タイムスタンプ衝突にはランダム1部分のみで対処する必要がある
短所もあるものの、これらはdata-registryのユースケースにおいては問題にならないかもしれないので、実際に試算してみる。
同タイムスタンプ内における一意性違反リスク
UUID v7では、同じミリ秒内に複数のIDを生成する場合、一意性の保証はランダム部分(74ビット)で対処する必要がある。 この問題をキューイングなどのアーキテクチャで解決しない場合において、UUID v7のエントロピー性能がdata-registryの最悪ケースをカバーできるかを見積もる。
この問題は誕生日問題と同じなので、並行プロセス数を\(n\)は、ランダム部分のビット長を\(b\)とすると、衝突確率は下式で表される:
$$ P(\text{1組以上が衝突}) = 1 - \frac{1}{e^{\frac{n (n - 1)}{2^{b+1}}}} $$いま1,000,000の並行プロセス(1,000ユーザー × 1,000シミュレーション)が同ミリ秒内にIDを生成する場合、\(n = 10^6\)、\(b = 74\)なので、衝突確率は
$$ \begin{aligned} P(\text{1組以上が衝突}) &= 1 - \frac{1}{e^{\frac{10^6 (10^6 - 1)}{2^{75}}}} \\ &\approx 1 - \frac{1}{e^{\frac{10^{12}}{3.8 \times 10^{22}}}} \\ &\approx \frac{10^{12}}{3.8 \times 10^{22}} \\ &\approx2.63\times10^{-11} \\ &= 2.63\times10^{-9} \text{\%} \end{aligned} $$となり、ピーク時リクエストに対してアーキテクチャによる緩衝を挟まない場合にも、UUID v7のエントロピー性能は充分な安全マージンを持つことがわかった。
以上から、data-registryにおけるRevision IDの生成したアルゴリズムにはUUID v7を採用する。
所感
本記事でも、前回の記事と同様に、最悪ケースに対するキャパシティ見積もりに基づいて技術選定した。 実際には、最悪ケースに対してアーキテクチャ的な解決を挟まない場合にはID衝突以前に別の問題(スループットなど)が生じそうだが、ひとまずの物理的な猶予は把握できた。 大規模アーキテクチャにおける成功例: Snowflake IDも一応は検討したが、個人開発における運用性を考えると選べなかった。
次回は、Revision IDと冪等性キーの永続化方法について考える予定。
-
疑似乱数であり、実際は決定的に生成する。 ↩︎