Artifact Registry ADR 008: コンテンツアドレッサブルストレージ
コンテキスト
Artifact Registry は次のバランスを取らなければなりません。
- 重複排除の効率性: 同一コンテンツの冗長なストレージを削減すること
- 運用上の複雑さ: 異なるスコープにまたがる重複排除の管理
- コスト按分: namespace ごとのストレージコストの明確な追跡
- パフォーマンス: アーティファクトの高速なアップロードとダウンロード
- 整合性: アーティファクトが破損したり改ざんされたりしていないことの検証
アーティファクトのストレージは通常、同じコンテンツを複数回保存することを伴います。
- 同一の依存関係を持つパッケージの複数バージョン
- 多くのイメージにわたって共有されるベースコンテナレイヤー
- バージョン間で変更されていないバイナリファイル
Artifact Registry のストレージは namespace を中心に編成されます。namespace は、不変で顧客が選択するスラッグと、内部の UUIDv7 ID を持つ内部エンティティです。これは分離および重複排除の境界です。当面の間、各 namespace は組織に 1:1 でマッピングされます。スラッグはすべての URL に現れます。内部 ID のハッシュがストレージパスの分離に使用されます。詳細は ADR-022 を参照してください。
決定
私たちは、SHA256 ベースの識別と namespace スコープの重複排除を備えたコンテンツアドレス指定ストレージを使用します。
私たちはすべてのアーティファクトをその SHA256 コンテンツハッシュで識別し、次を可能にします。
- namespace 内での自動的な重複排除(一意の blob ごとに 1 つのコピー) - ADR-002、ADR-022 を参照
- 不変のコンテンツパス(コンテンツハッシュによる整合性検証)
- namespace 境界内での効率的なストレージ利用
- ハッシュ比較による明確な整合性検証
重複排除のスコープは個々の namespace 内に限定され、インスタンス全体ではありません(ADR-022 を参照)。namespace は初回リリースでは組織に 1:1 でマッピングされ、セキュリティ、予測可能なパフォーマンス、コスト按分のための明確な境界を維持しつつ、各 namespace 内でのストレージ効率を提供します。
コンテンツハッシュは OCI Content Descriptors の慣例に従い、algorithm:hex 形式(例: sha256:a3b5c7d9...)を使用します。SHA256 はローンチ時にサポートされる唯一のアルゴリズムです。アルゴリズムの移行(例: SHA256 から SHA-3 へ)は容易ではありません。既存コンテンツの再ハッシュ、移行中のデュアルハッシュルックアップのサポート、アルゴリズムをまたいだ重複排除の解決を伴います。これはこの ADR のスコープ外です。
私たちは SHA256 を衝突がないものとして扱い、重複排除時に二次的なコンテンツ比較(例: サイズやバイト比較)を行いません。ADR-003 のスケール目標を大きく上回る、一意の blob を毎秒 1,000 件持続的にアップロードしたとしても、保存オブジェクトが 1 兆個に達するまでに約 30 年かかり、その時点で 1 回の衝突が発生する確率は約 10^-53 です。衝突確率 50% に対する誕生日限界には約 2^128(約 3.4 × 10^38)個のオブジェクトが必要であり、これは現実的に想定しうるいかなる運用規模でも到達不可能です。
ストレージパス戦略
オブジェクトストレージプロバイダーは、キープレフィックスに基づいてデータをシャードにまたがってパーティション分割します。多くのオブジェクトが共通のプレフィックスを共有すると、書き込みが少数のシャードに集中し、ホットパーティションを生み出します。GCS は高書き込みのシーケンシャルワークロードに対し、負荷を均等に分散するための ハッシュ化を推奨しています。S3 は 2018 年以降 自動パーティショニングを改善 していますが、プレフィックスごとのレート制限は依然として書き込み 3,500 件/秒、読み取り 5,500 件/秒のままです。フラットな識別子をトップレベルのプレフィックスとして使用すると、namespace のすべての操作が単一のプレフィックスの下に集中し、アクティブな namespace ではボトルネックになる可能性があります。
GCS と S3 はどちらも、オブジェクトキー長の上限を 1,024 バイト(UTF-8 エンコード)に課しています。シャーディングのオーバーヘッド(余分な / セパレータと繰り返されるプレフィックス文字)は、この上限の十分に範囲内です。
ストレージパス戦略は、GitLab.com コンテナレジストリの本番メトリクスに基づいています。現在の操作レートは GCS のプレフィックスごとの制限内ですが、Artifact Registry は、バイトあたりの操作数がより多い追加のアーティファクトフォーマット(Maven、npm)、namespace 全体にわたる偏ったトラフィック分布、増大する顧客採用を考慮し、10 倍の成長に向けて設計されるべきです。
ストレージ階層は、効率的なアクセスパターンと namespace の分離を確保するために多段階のシャーディングを使用します。トップレベルの artifact_registry/ プレフィックスは root_directory ストレージ構成フィールド(デフォルト: "artifact_registry")で構成可能であり、ADR-006 に従ってパスプレフィックスを備えた共有バケットのデプロイをサポートします。
{root_directory}/
├── {namespace_hash[0..1]}/
│ └── {namespace_hash[2..3]}/
│ └── {namespace_hash}/
│ ├── objects/
│ │ └── {content_hash[0..1]}/
│ │ └── {content_hash[2..3]}/
│ │ └── {content_hash}
│ └── uploads/
│ └── {upload_id}/
ここで:
namespace_hash: トップレベルのストレージ分離のための Namespace の内部 UUIDv7 ID(ADR-022)の SHA256 ハッシュ(後述の トップレベルストレージのアイデンティティ を参照)。最初の 2 つのレベル(それぞれ 2 桁の 16 進数、65,536 個のプレフィックスバケット)は書き込みをストレージパーティションにまたがって分散します。3 番目のレベルは namespace の分離のためにフルハッシュを使用します。content_hash: blob コンテンツの SHA256 ハッシュ。ストレージキーとして直接使用されます。同じシャーディングパターン: 2 つの分散レベルに続いてフルハッシュ。upload_id: 一時的なアップロードセッションのための UUID
objects/ と uploads/ はどちらも同じ namespace パーティションの下にあります。これは、uploads/{upload_id} から objects/{content_hash} への移動が常にパーティション内であり、アップロードフロー中にパーティション間のデータ移動が発生しないことを意味します。
ストレージパスは可逆的に解析可能です。オブジェクトキーが与えられれば、データベースのルックアップなしにパスコンポーネントを解析することで namespace ハッシュとコンテンツハッシュを抽出できます。ストレージウォークにより、各 blob をどの namespace が所有しているか、そしてそのコンテンツハッシュが何であるべきかを特定でき、データベースレコードとの比較が可能になります。
パスの例(73475cb... が SHA256(namespace_id)、a3ed95c... が SHA256(blob content) の場合):
# Stored object
artifact_registry/73/47/73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049/objects/a3/ed/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
# In-progress upload
artifact_registry/73/47/73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049/uploads/550e8400-e29b-41d4-a716-446655440000
アップロードパスの構造
アップロードパスは namespace パーティションの下でフラット(upload_id のシャーディングなし)であり、多段階のシャーディングを使用する objects/ とは異なります。これは次の理由で許容されます。
- 列挙が不要: アップロードセッションはデータベースで追跡されるため、アップロードを発見するためにオブジェクトストレージの
uploads/プレフィックスをリストする必要が決してありません。シャーディングはリスト処理のパフォーマンスに利点がありますが、私たちはそれを完全に回避します。 - オブジェクトストレージはフラットなキースペースを扱える: S3 と GCS はキーを内部でハッシュによって分散します。数千の並行する UUID を持つフラットな
uploads/プレフィックスはホットパーティションを生み出しません。 - 短命なオブジェクト: アップロードパスは一時的です。
objects/のように永続的ではなく、アップロードセッションの期間(数分から数時間)の間だけ存在します。
検討された代替案:
- シャード化されたアップロード(例:
uploads/{id[0..1]}/{id[2..3]}/{id[4..-1]}): リスト処理のパフォーマンスを改善しますが、データベース追跡があればリスト処理は不要です。運用上の利点なしにパスの複雑さを追加します。 - namespace パーティション外のグローバルアップロード: 完了時にパーティション間の移動が必要となり、レイテンシーと障害モードを追加します。パーティション内の移動の方がシンプルで高速であるため却下しました。
トップレベルのストレージパスは、namespace スラッグなどの人間が読める識別子ではなく、namespace の内部 UUIDv7 ID の SHA256 ハッシュを使用します。ハッシュ化されたパスは構造上不変です。変更すべき人間が読める値がないため、伝播すべきリネームイベントがありません。これは GitLab のハッシュ化ストレージ標準 に一致しており、リネーム/移動操作における Geo 同期の失敗 と分散システムにおけるパフォーマンスペナルティに遭遇した後、人間が読めるプロジェクトパスから移行したものです。
トップレベルストレージのアイデンティティ
トップレベルのストレージパスは Namespace の内部 UUIDv7 ID をハッシュ化します(ADR-022)。各 namespace は独自のトップレベルのストレージパーティションを取得します。
これはストレージの境界を重複排除の境界に整合させます。namespace 内の同一の blob は自然に同じ場所に配置され、コスト按分は 1 つのハッシュプレフィックスの下の blob の合計です。組織のマージにはストレージの移動が不要です。各 namespace はそのパーティションを保持し、存続する組織は単に複数のパーティションを保持します。namespace ID は内部的であり、構造上不変です。
初回リリース(組織ごとに 1 つの Artifact Registry namespace)では、これは組織 ID をハッシュ化することと機能的に同一です。将来、組織が複数の Artifact Registry namespace を持つ場合、重複排除は namespace 境界をまたぎません。同じ組織内の異なる namespace にアップロードされた同一の blob は 2 回保存されます。これは ADR-022 で認められている明示的なトレードオフです。コンテナレジストリの分析 によると、blob の約 95% が単一のトップレベル namespace に存在しており、namespace 間の重複が少ないことを示唆しています。この数値はコンテナイメージのレイヤー共有パターンを反映したものです。パッケージレジストリは通常、アーティファクトがバージョンごとに一意であるためリポジトリ間の重複排除率が低く、これらのフォーマットでは namespace 間の重複がさらに低いと予想されます。
オブジェクトキーとしてのコンテンツハッシュ
私たちは SHA256 コンテンツハッシュをオブジェクトキーとして直接使用します(例: {hash[0..1]}/{hash[2..3]}/{hash})。これはコンテンツの識別子とストレージの場所の間の直接的なマッピングを提供します。重複排除は調整ステップではなく、ストレージレイヤーの構造的な特性です。宛先パスがすでに存在する場合、その blob は重複であり、新しいオブジェクトは書き込まれません。重複排除のチェック自体にはデータベースの調整は不要です。これはまた、ストレージパスがデータベースのルックアップなしにコンテンツハッシュから導出できるため、デバッグを簡素化します。
検討された代替案: ランダムな識別子ベースのパス。ランダムに生成された UUID をオブジェクトキーとして使用し、コンテンツハッシュをデータベースのメタデータに保存します。これは潜在的な情報漏洩(ハッシュ列挙)を回避し、ストレージの再編成に柔軟性を提供します。しかし、すべての重複排除チェックに対して必須のデータベース調整ポイントを導入し、すべての読み取りに対して間接参照レイヤーを追加し、この ADR で説明されている並行アップロードと移動のセマンティクスを変更します。二段階アップロード戦略、並行アップロードの解決、移動時のアトミックな重複排除はすべて、コンテンツハッシュベースのパスを構造的な特性として依存しています。ランダムな識別子を採用すると、ストレージレベルの収束ではなくデータベース仲介の重複排除を中心にこれらのフローを再設計する必要があり、初回リリースにおいて明確な運用上の利点なしに複雑さを追加します。
2 段階アップロード戦略
私たちは移動ベースの二段階アップロード(コンテナレジストリのパターン)を使用します。コンテンツはまず uploads/{upload_id} にアップロードされ、ハッシュ計算と重複排除チェックの後に objects/{content_hash} へ移動されます。この blob ごとのモデルはすべてのアーティファクトフォーマットに適用されます。複数ファイルのパッケージデプロイ(例: Maven JAR + POM)はファイルごとに別個のアップロードセッションです。バージョン内の関連ファイル間のアトミック性は API レベルの関心事であり、ADR-009 で扱われます。
コンテナレジストリの blob アップロード実装 は、このアプローチに対して 10 年にわたり本番で鍛えられたリファレンスを提供します。その実装からの教訓:
- ストリーミングハッシュ計算: ハッシュは、アップロード完了後の別個のパスとしてではなく、ストリーミングハッシュ計算を使用して書き込み中に計算されます。これにより blob 全体をストレージから再読み込みすることを回避でき、数 GB のアーティファクトにとって重要です。
- 再開可能なダイジェスト状態: 再開可能なアップロードでは、部分的にアップロードされたコンテンツを最初から再ハッシュする必要がないように、ハッシュ状態が永続化および復元されます。コンテナレジストリの検証ロジックは、本番で遭遇する実際のエッジケースを反映した複数のフォールバックパス(再開可能なダイジェストが利用可能、アルゴリズムの不一致、完全な再ハッシュ)を扱います。Artifact Registry はストリーミングハッシュと再開可能な状態を保持しますが、その完全な再ハッシュのフォールバックを「乖離時に終了」に置き換えます。後述の 再開可能なアップロードとハッシュ状態 を参照してください。
- 移動時のアトミックな重複排除: 移動先はコンテンツハッシュのパスです。パスがすでに存在する場合、移動はスキップされます。重複排除は、別個の調整ステップではなく、コンテンツアドレス指定パスの構造的な帰結です。
- 長さゼロの blob の安全性不変条件: 空のコンテンツの SHA256 ハッシュ(
sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)は既知の定数です。長さゼロの書き込みはその特定のハッシュパスにのみ許可されます。この不変条件がなければ、失敗したアップロード(ゼロバイトを生成する)と移動ステップの間の競合により、無関係なハッシュパスにある既存の blob コンテンツが空のオブジェクトで黙って上書きされる可能性があります。システムは移動の前に既知の空コンテンツハッシュ定数と照合することでこれを強制します。
検討された代替案:
2 つの代替アップロードパターンが評価されました。
インプレース確認方式(CI アーティファクトパターン)
コンテンツは一時的な識別子でアップロードされ、重複排除チェックの後に Redis 追跡を使用してその場で確認されます。このパターンは、重複排除が目標ではない CI artifacts のために設計されました。各アーティファクトはアップロード時に一意のパスを取得し、そこにとどまります。Redis 追跡は、コンテンツの識別子ではなく受信を確認します。コンテンツハッシュはアップロードが完了するまで判明しないため、これをコンテンツアドレス指定システムに適用すると問題が変わります。
投機的アップロード(ハッシュが判明する前にコンテンツハッシュのパスへ直接書き込む)がない限り、オブジェクトはハッシュ計算後に最終的なコンテンツアドレス指定の場所へ移動する必要があり、結果として移動ベースのアプローチと同じオブジェクトストレージ操作になります。confirm-in-place パターンは、ストレージ操作を削減することなくアップロード中の必須依存として Redis を追加し、同じハッシュパスを目指して競合する同一コンテンツの並行アップロードを扱うために Redis ベースの調整を必要とします。これは、移動ベースのステージング領域が各アップロードを UUID で分離することにより構造的に解決するものです。
最終パスへの直接アップロード
ステージング領域と移動操作を完全にスキップし、コンテンツハッシュのパスへ直接アップロードします。クライアントがアップロード前にコンテンツハッシュを提供するフォーマット(例: digest パラメータを持つ OCI blob プッシュ)では、サーバーはアップロードの開始時に最終パスを知っています。これは移動操作とそれに関連するコストおよび障害モードを排除します。
このアプローチはすべてのアーティファクトフォーマットにわたって一般化できません。多くのパッケージレジストリのフォーマット(Maven、npm、generic)はアップロード前にコンテンツハッシュを提供しないため、サーバーはハッシュ計算が完了するまで最終パスを決定できません。これらのフォーマットでは、コンテンツは依然として一時的な場所に書き込まれて移動される必要があり、結果として同じ二段階フローになります。2 つのアップロードコードパス(ハッシュ既知の場合は直接、ハッシュ未知の場合はステージング)を保守することは、移動ベースのパスを排除することなく複雑さを追加します。
direct-to-final-path のアップロードは、並行アップロードモデルも変更します。2 つのクライアントが同じコンテンツを同時にアップロードすると、両方が同じ宛先パスに書き込みます。最初に完了したものが成功し、2 番目は上書きする(作業が無駄になる)か、競合で失敗します。ステージング領域は、各アップロードを UUID で分離し、移動ステップで重複排除を解決することでこれを回避します。
放棄されたアップロードが重複排除されたオブジェクトストア(objects/)に直接到達すると、孤立した部分的なアップロードを有効な blob から区別するには、uploads/ プレフィックスの単純な経過時間ベースのスイープではなく、データベースのクロスリファレンスが必要になります。ステージング領域は構造的な境界を提供します。uploads/ 内のセッション TTL より古いものはすべて安全に削除できます。
2 段階移動を選んだ理由
移動ベースのアプローチは、GitLab.com 上のコンテナレジストリ規模で、関連するインシデントなしに実証された本番挙動を持っています。移動のオーバーヘッドは低いです。GCS では、アトミックな Objects.move は同一バケット操作に対してメタデータのみです。S3 では、CopyObject は PUT($0.005/1K リクエスト)として課金され、削除は無料です。コンテナレジストリの本番ボリュームでは、追加の API コストはストレージ支出総額に対して無視できます。
コンテナレジストリの blob アップロード実装は、Artifact Registry の blob 処理のリファレンス実装です。二段階アップロードロジック、ストリーミングハッシュ計算、移動セマンティクスは、ストレージパス構造(インスタンスベースではなく namespace ベースのシャーディング)のみが変わるだけで、両システム間で同一です。同じアプローチから始めることで乖離が減り、AI 支援によるコード生成のための明確なリファレンスが提供されます。
これは一方通行ではありません。運用データが、移動コストが大規模でボトルネックになることを示した場合、direct-to-final-path のアプローチは、ストレージレイアウトを変更することなくハッシュ既知のフォーマットに対する明確な最適化パスを提供します。
オブジェクトストレージにおける移動セマンティクス
GCS はアトミックな Objects.move 操作を提供しており、GitLab.com に対して以下の障害モードを排除します。S3 は RenameObject を提供しますが、標準バケットではなく Express One Zone でのみです。S3 標準バケットとネイティブの移動を持たないプロバイダーでは、「移動」はサーバーサイドのコピーに続いてソースの削除を行うものであり、これはアトミックではない 2 つの別個の操作です。これは次の障害モードを導入します。
- コピーは成功、削除は失敗: blob が一時パスと最終パスの両方に存在します。一時オブジェクトは孤立し、アップロードパージ(Blob ライフサイクル のステップ 5 を参照)によってクリーンアップされます。データの損失や破損はありません。最終オブジェクトは正しいです。
- コピーが失敗: アップロードが失敗します。クライアントはエラーを受け取り、再試行しなければなりません。一時オブジェクトはアップロードパスに残り、クライアントがセッションを放棄した場合は最終的にパージによってクリーンアップされます。部分的な成功状態はありません。blob は最終的なコンテンツアドレス指定パスに存在するか、しないかのいずれかです。
- 並行完了: 宛先がすでに存在する場合(同じコンテンツの別のアップロードが先に完了した場合)、コピーは完全にスキップされます。ソースは削除され、既存の blob への参照が追加されます。
copy+delete のパスでは、GCS と S3 はコピー後の削除の失敗を異なって扱います。GCS は削除エラーを黙って飲み込み、S3 はそれを呼び出し元に伝播します。私たちはこの挙動を正規化します。コピー成功後の削除の失敗は、すべてのバックエンドで致命的ではありません。blob は宛先にあり、アップロードパスにある孤立したソースはアップロードパージ(Blob ライフサイクル のステップ 5 を参照)によって処理されます。これは、ストレージバックエンドに関係なく移動のエラー処理が同一であることを意味し、呼び出し元はプロバイダー固有の障害モードで分岐しません。
同一バケット、同一リージョンの操作(パーティション内アップロードパス設計 によって保証される)では、サーバーサイドのコピーは高速であり、クライアントを経由してデータを転送しません。
ここで説明した移動セマンティクスは、強い read-after-write 整合性に依存します。コピーが成功したら、後続の読み取りに即座に可視でなければなりません。公式にサポートされているすべてのオブジェクトストレージプロバイダー(AWS S3、GCS、Azure Blob Storage)はこの保証を提供します。公式にサポートされていない S3 互換バックエンド(例: Ceph RadosGW)は、特定の操作に対して弱い整合性を持つ可能性があり、これが並行アップロード中に誤った重複排除挙動を引き起こす可能性があります。
S3 の CopyObject API は最大 5 GB のオブジェクトをサポートします。このしきい値より大きいアーティファクト(例: ML モデル、大きなコンテナレイヤー)には、マルチパートのサーバーサイドコピー(UploadPartCopy)が必要です。具体的な実装はストレージドライバーに依存し、この ADR のスコープ外ですが、移動ステップは任意のサイズの blob を扱わなければなりません。マルチパートコピーは、上記の障害モードを超えた追加の中間状態(部分的にコピーされたパート)を導入します。これらはストレージドライバー固有であり、ドライバーレベルで解決されます。
同一コンテンツの並行アップロード
2 つのクライアントが同じコンテンツを同時にアップロードするとき、各アップロードは独立して追跡されます。
- 両方のクライアントが、一意の
upload_id値を持つ独立したデータベースのアップロードセッションレコードを作成します。 - 両方が独立した一時パス(
uploads/{uuid_a}およびuploads/{uuid_b})に書き込みます。 - 両方がストリーミング書き込み中に同じ SHA256 ハッシュを計算します。
- 最初に完了したものが
objects/{hash_path}へ移動(コピー)し、blob レコードを作成し、参照を追加します。 - 2 番目に完了したものは宛先がすでに存在することを発見し、コピーをスキップし、自身の一時オブジェクトを削除し、既存の blob への自身の参照を追加します。
- 両方のアップロードセッション行が、データベースから独立して削除されます。
各セッションは独自のライフサイクルを持ちます。独立した追跡、独立したクリーンアップです。重複排除は、アップロード間の調整ステップとしてではなく、コンテンツアドレス指定パスの構造的な帰結として移動ステップで発生します。
ストレージレベルの重複排除チェック(上記のステップ 4-5)とデータベースレベルの参照作成は、互いに対してアトミックではありません。最初のアップロードが宛先へコピーしたがまだ blob レコードをコミットしていない場合、2 番目のアップロードはストレージに宛先が存在することを確認しますが、参照すべき blob レコードがありません。 ADR-007 は、並行アップロードがデータベースレベルで正しく解決されるよう、適切な一意性制約、upsert セマンティクス、または再試行戦略でこれに対処しなければなりません。
再開可能なアップロードとハッシュ状態
数 GB のアーティファクトサイズ(コンテナレイヤー、ML モデル)では、クライアントはすでに転送されたコンテンツを再アップロードしたり再ハッシュしたりすることなく、中断されたアップロードを再開する能力を必要とします。これにはリクエスト間で中間ハッシュ状態を永続化することが必要です。KB-MB 範囲の典型的なパッケージアップロードでは、クライアントは再開可能な状態を必要とせずに単一のリクエストでアップロードを完了しますが、このメカニズムはすべてのフォーマットで利用可能です。
私たちはハッシュ状態をデータベースに永続化します。 ハッシュ状態は、シリアライズされた中間 SHA-256 状態として upload_sessions.hash_state カラム(BYTEA、null 許容)に保存されます。
代替案は、uploads/{upload_id}/hashstates/{algorithm}/{offset} にあるオブジェクトストレージのサイドカー(コンテナレジストリのパターン)です。私たちはコストの理由でこれを却下します。サーバーが Range レスポンスを生成して正しく再開できるよう、upload_sessions.size_bytes はオブジェクトストレージで確認されたバイトと常に最新の状態に保たれなければならないため、アップロードの書き込みパスでは upload_sessions に対する単一行の UPDATE が避けられません。その同じ UPDATE に hash_state を同居させることは、HASH(namespace_id) の下ですでにパーティションローカルである書き込みに約 100 バイト(マーシャルされた SHA-256 状態)を追加し、サイドカーが必要とするチャンクごとの 2 つのオブジェクトストレージのラウンドトリップ(追加時の書き込み、再開時のリストと取得)を排除します。
並行書き込みとクラッシュリカバリーは、コンテナレジストリの黙った再ハッシュフォールバックではなく、乖離時に終了 モデルを使用します。size_bytes が権威であり、データベースはバックエンドが永続的に受信していないオフセットを記録することを決して許可されず、再開時に記録されたオフセットとバックエンドのステージングオブジェクトの間に乖離があればセッションを終了してクライアントが最初からやり直します。再ハッシュによる救済は意図的に採用されていません。孤立したバックエンドのバイトを認証できず(正しいサイズだが誤ったコンテンツのリスク)、黙った破損がコンテンツアドレス指定パスに到達することを許してしまうためです。dirty カラム(ADR-007)はそのための前方互換なフックです。完全なモデル(横断的な不変条件、size_bytes の compare-and-swap、dirty のポイズンピル、DB 更新の粒度、障害ごとのリカバリー分岐)は、Artifact Registry S06 storage-layer spec の「Consistency & Crash-Recovery Model」で定義されています。
ハッシュ状態のライフサイクルはアップロードセッションのライフサイクルに紐付けられます。セッション行が削除される(完了またはパージ時)と、ハッシュ状態もそれと共に削除されます。アップロードのステージングパス(uploads/{upload_id})には blob データのみが含まれます。サイドカーファイルはありません。
一時オブジェクトのクリーンアップ
アップロードが正常に完了すると、uploads/{upload_id} の一時オブジェクトは、objects/{content_hash} への移動とデータベースセッションの更新の直後に、ベストエフォートの操作として削除されます。これは通常のケース(blob が宛先へ移動される)と並行重複排除のケース(宛先がすでに存在し、コピーがスキップされる)の両方に適用されます。インラインの削除が失敗した場合、ホットパスでの再試行は試みられません。blob はすでにコンテンツアドレス指定ストアへ移動されデータベースに記録されており、アップロードパージャー(Blob ライフサイクル のステップ 5 を参照)が次のスイープでそれを処理します。
これは、パージャーのワークロードが、正常に完了する一般的なケースではなく、真に孤立したセッション(放棄されたアップロード、クラッシュしたクライアント)に限定されることを意味します。
アップロードセッション追跡
アップロードセッションは、ファイルシステムやオブジェクトストレージのウォークを通じてではなく、データベースで追跡されます。セッションスキーマとライフサイクル管理は ADR-007 で定義されています。
読み込みパス
コンテンツアドレス指定ストレージにより、読み取りパスはコンテンツハッシュから直接ストレージの場所を導出でき、コンテンツハッシュをストレージパスに解決するためのデータベースのルックアップは不要です。blob の SHA256 ハッシュと namespace が与えられれば、完全なオブジェクトストレージキーは決定論的です: artifact_registry/{namespace_hash_shard}/objects/{content_hash_shard}(省略形。完全なシャード化レイアウトは ストレージパス戦略 を参照)。
これにより、クライアントは受信したコンテンツの SHA256 ハッシュを計算し、期待されるハッシュと比較することで、ダウンロード時に整合性を検証できます。不一致があれば、転送中または保存時の破損や改ざんを示します。
コンテンツハッシュは自然なキャッシュキーとしても機能します。HTTP キャッシュレイヤー(CDN、リバースプロキシ)は、所定のハッシュにあるコンテンツが不変であるため、長い TTL でコンテンツハッシュをキーとして blob レスポンスをキャッシュできます。異なるハッシュは常に異なるコンテンツを意味します。
仮想レジストリでは、アップストリームのリモートからキャッシュされたコンテンツは同じ CAS モデルに従います。キャッシュされたアーティファクトはそのコンテンツハッシュに保存されるため、異なるアップストリームから取得された同一のコンテンツは自然に重複排除されます。CAS はバージョンパスではなくコンテンツをキーとするため、コンテンツが変わる可変のアップストリームバージョン(例: Maven SNAPSHOT)は異なるハッシュと新しいストレージオブジェクトを生成します。キャッシュの無効化とアップストリームの鮮度チェックは API レベルの関心事であり、ADR-009 で定義されています。
API レベルの読み取りセマンティクス(エンドポイント設計、認証、署名付き URL の生成)も ADR-009 で定義されています。
Blob ライフサイクル
コンテンツアドレス指定 blob は、各 namespace 内で参照カウント方式のライフサイクルに従います。
- アップロード: クライアントが一時パス(
uploads/{upload_id})にコンテンツをアップロードします。サーバーは書き込み中にコンテンツをハッシュ関数にストリーミングすることで SHA256 ハッシュを計算します。 - 重複排除チェック: 移動先はコンテンツハッシュによって決定されます。宛先パスにオブジェクトがすでに存在する場合、その blob は重複であるため、新しいオブジェクトはストレージに書き込まれず、一時アップロードは破棄されます。パスが空の場合、blob は一時パスから最終的な場所へ移動されます。これはストレージレベルの操作です。重複排除のチェック自体にデータベースの調整は不要です(ストレージパス戦略 を参照)。
- 参照追跡: 同じ namespace 内の複数のレジストリとリポジトリが、同じ物理 blob を参照できます。参照追跡のスキーマとクエリパターンは ADR-007 で定義されています。
- 参照解除: アーティファクトが削除される、またはライフサイクルポリシーがバージョンを削除すると、対応する参照が削除されます。この時点では blob 自体はストレージから削除されません。
- 孤立したアップロードのパージ: 完了しなかったアップロード(クライアントのクラッシュ、ネットワーク障害、放棄されたセッション)は
uploads/パスにオブジェクトを残します。バックグラウンドプロセスが、有効期限を過ぎたアップロードセッションをデータベースで照会し、対応するストレージオブジェクトを削除します。これは調整されたジョブ(インスタンスごとではない)として実行され、コンテナレジストリが遭遇した 冗長なスイープの問題 を回避します。セッションがデータベースで追跡されるため、パージはストレージ内のオブジェクトを列挙する必要がありません。境界が定められた期限切れレコードのセットを照会し、すべての候補を一度にロードする メモリスケーリングの問題 を回避します。 - ガベージコレクション: バックグラウンドプロセスが定期的に残存参照がゼロの blob を特定し、オブジェクトストレージから削除します。GC は単一の namespace 境界内で動作するため、namespace 間の調整は不要です(ADR-022 を参照)。GC はデータベースレコードを削除する前にストレージオブジェクトを削除するため、最悪のケースは孤立したストレージ(無駄なスペース)であり、ぶら下がったデータベース参照(壊れた pull)ではありません。ガベージコレクションと並行アップロードの間の競合を防ぐメカニズム(例: 猶予期間、参照ロック)は ADR-007 で定義されています。
帰結
ポジティブ
不変のコンテンツ: コンテンツアドレス指定ストレージはアーティファクトを不変に保ちます。ハッシュベースの場所にコンテンツを保存すると、いかなる変更も異なるハッシュ、したがって異なるストレージの場所を生成し、改ざんを防ぎます
暗号学的な整合性検証: SHA256 ハッシュにより、クライアントは計算したハッシュを期待されるハッシュと比較することでアーティファクトの整合性を検証し、ダウンロード中の破損や改ざんを検出できます
アップロードの最適化: アップロードの前に、クライアントはハッシュによってコンテンツがすでに存在するかどうかを確認し、同一コンテンツの冗長なアップロードをスキップします
リポジトリ間の blob 共有: namespace 内で、別のリポジトリにすでに存在する blob を参照することはデータベースのみの操作であり、データ転送はありません。blob がコンテンツアドレス指定パスにすでに存在する場合、ストレージオブジェクトをコピーすることなく新しい参照が追加されます。コンテナレジストリでは、これは OCI の
mount操作(リポジトリ間の blob マウント)を可能にします。パッケージレジストリでは、これは複数のプロジェクトに公開された同一のアーティファクト(例: 共有内部ライブラリ)が namespace ごとに一度だけ保存されることを意味します。どちらの場合も、ストレージの場所はリポジトリではなくコンテンツハッシュによって決定されます。簡素化された並行アップロード: 同一コンテンツの複数の並行アップロードは、競合なしに自然に同じストレージの場所へ収束します
コンテンツベースのキャッシュ: HTTP キャッシュレイヤーがコンテンツハッシュをキャッシュキーとして使用し、効率的な CDN およびプロキシのキャッシュ戦略を可能にします
ネガティブ
ハッシュ計算のオーバーヘッド: すべてのアップロードがコンテンツ全体の SHA256 ハッシュを計算します。ストリーミング計算(書き込み中のハッシュ化)はレイテンシーを緩和しますが、CPU コストはコンテンツサイズに比例したままです
二段階アップロードの複雑さ: アップロードはまず一時的な場所に行き、ハッシュを計算し、その後最終的なコンテンツアドレス指定パスへ移動するため、実装の複雑さが増します
一部のオブジェクトストレージバックエンドにおける非アトミックな移動: S3 標準バケットとネイティブの移動を持たないプロバイダーでは、一時パスから最終パスへの移動はサーバーサイドのコピーに続いて削除を行うものであり、これはアトミックではありません。コピーと削除の間の失敗は孤立した一時オブジェクトを残します。GCS は GitLab.com に対してこれを排除する アトミックな移動 を提供しますが、システムは他のバックエンドに対して依然としてバックグラウンドのアップロードパージャーに依存します。パージャーが遅れたり失敗したりすると、孤立したストレージが黙って蓄積します
参照追跡のリスク: blob のライフサイクルは、namespace 内のレジストリとリポジトリにまたがる正確な参照追跡に依存します。参照追跡のバグは、早すぎるガベージコレクション(データ損失)や決して回収されない blob(ストレージリーク)を引き起こす可能性があります。これは正しさが重要なコンポーネントです
namespace 間の重複: namespace スコープの重複排除は、同じ組織内の異なる namespace にアップロードされた同一の blob が別々に保存されることを意味します。これは分離と運用上のシンプルさのための明示的なトレードオフです(ADR-022 を参照)
データベースとストレージの乖離: システムは 2 つの信頼できる情報源を維持します。データベース内の blob レコードと、ストレージ内のオブジェクトです。これらは乖離する可能性があります(例: ストレージ書き込みとデータベースコミットの間のクラッシュは、どのデータベースレコードも参照しない孤立したオブジェクトを
objects/に残します)。突合には別個のプロセスが必要ですデータベースのルックアップが必要: 重複排除のチェック自体はストレージレベルですが、各アップロード操作には参照追跡とセッション管理のためのデータベースクエリが必要です
代替案
代替案 1: インスタンス全体のコンテンツアドレッサブルストレージ
SHA256 を使った CAS を使用しますが、インスタンス上のすべての組織にまたがって重複排除し、同一のコンテンツをグローバルに一度だけ保存します。
長所: インスタンス全体の重複排除は、組織にまたがる同一の blob を一度だけ保存し、オブジェクトの総数を削減します。
短所:
- 組織間の参照カウント: blob を削除するにはすべての組織にまたがる参照を確認する必要があり、無制限のクエリスコープを生み出します
- セキュリティリスク: 共有された blob 参照は、他の組織が使用しているコンテンツを明らかにします(コンテンツ存在オラクル)
- コスト按分の複雑さ: 共有 blob を使用する組織にまたがってストレージコストを分割しなければなりません
- GC の調整: ガベージコレクションが組織間の依存関係によってブロックされます。インスタンス全体の重複排除に関するコンテナレジストリの 運用経験 は、これらの問題を大規模に示しています
- 災害復旧の結合: 1 つの組織を復元するには、他の組織が参照する blob が必要になる場合があります
却下理由: 分析によると、namespace スコープの重複排除と比べて 1 桁台前半の節約しかありません(ADR-002 を参照)。運用上の複雑さとセキュリティリスクは、わずかなストレージの利点を上回ります。
代替案 2: 重複排除なし(シンプルストレージ)
すべてのアーティファクトのアップロードを、コンテンツハッシュ化や重複排除なしに別個のオブジェクトとして保存します。
長所:
- 最もシンプルな実装: ハッシュ計算なし、参照カウントなし、重複排除の調整なし
- 二段階アップロードが不要: コンテンツは最終的なストレージの場所へ直接行きます
- GC の複雑さなし: アーティファクトを削除するとそのストレージオブジェクトが直接削除されます
短所:
- コンテナイメージに対して namespace スコープの重複排除と比べて 約 36% 以上のストレージオーバーヘッド(リポジトリレベルでの コンテナレジストリ分析 に基づく)。パッケージレジストリは、アーティファクトが通常バージョンごとに一意であるため重複排除率が低いですが、整合性検証とアップロードの最適化は重複排除率に関係なく価値があります。
- 整合性検証なし: コンテンツハッシュ化がないと、クライアントが独自の検証を実装しない限り、転送中の破損や改ざんが検出されません
- 不公平な課金: 顧客はリポジトリにまたがる同一コンテンツの重複ストレージに対して支払います
- アップロードの最適化なし: クライアントはすでに保存されたコンテンツのアップロードをスキップできません
- 競争力のなさ: JFrog Artifactory は チェックサムベースのストレージ をコア機能として使用し、すべてのリポジトリにまたがって同一のバイナリを重複排除しています。重複排除を欠くことは、この分野の市場リーダーに対する競争上の不利になります
却下理由: ストレージオーバーヘッドと整合性検証の欠如は、確立されたアーティファクトレジストリと競合する企業向け製品にとって受け入れられません。CAS はこの問題領域に対する業界標準のアプローチです。
参考文献
- ADR-002: 重複排除のスコープ - 重複排除スコープに関する詳細な決定
- ADR-022: Namespace の分離 - Namespace の抽象化、重複排除境界の namespace スコープへの移行
- ADR-007: データベーススキーマ - レジストリのルートテーブルとデータベーススキーマ
- ストレージ非効率性の分析 - イメージの約 95% がトップレベル namespace 間で共有されていないことを示す
- コンテナレジストリの重複排除の複雑さ
- コンテナレジストリの blob アップロード実装 - ストリーミングハッシュ計算を備えた移動ベースの二段階アップロードに対する、本番で鍛えられたリファレンス
- GitLab のハッシュ化ストレージ - 人間が読めるストレージパスからハッシュ化ストレージパスへの GitLab の移行
- コンテナレジストリのリネーム/移動に対する Geo 同期 - 分散システムにおける人間が読めるストレージパスの運用コストを示す
- コンテナレジストリのアップロードパージのメモリ問題 - ファイルシステムベースのアップロード追跡がすべての候補をメモリにロードしていた
- コンテナレジストリのアップロードパージのスケーリング問題 - インスタンスごとのアップロードスイープが大規模で冗長な作業を引き起こした
- GitLab のオブジェクトストレージプロバイダーサポート - 公式にサポートされるオブジェクトストレージバックエンドと整合性保証
- Cells 設計ドキュメント
c955a93f)