Artifact Registry ADR 007: データベーススキーマ
背景
Artifact Registry は次の点を考慮したデータベース構成を必要とします。
- 異なるアクセスパターン: アーティファクト管理クライアントは独自のプロトコルを使用し、それはフォーマットごとに大きく異なります。
- スケーラビリティ: アーティファクトのストレージは、数百万行が保存される規模にすぐに到達し得ます。
- パフォーマンス: 前述の 2 点を踏まえつつ、それでも私たちは操作の大部分を占める読み取りクエリにおいて高速な実行時間を維持したいと考えています。
- 過去の落とし穴: 現行のコンテナレジストリおよびパッケージレジストリのデータ構成にはいくつかのほころびが見られており(例、例)、ここではそれらを回避します。
決定事項に踏み込む前に、以下のスキーマに関するいくつかの注記を述べます。これらは主に、提示するテーブルの数を考慮して可読性を高めるためのものです。
- このドキュメントは機能のコアとなるテーブルについて記述します。サブ機能には追加のテーブルが必要になりますが、ここでは記述しません。例えば、blob ストレージのクリーンアップに必要な補助テーブルの背景については クリーンアップタスク を参照してください。
- テーブル名は可読性のために短縮しています。これらはここには示していない共通のプレフィックスを共有します(例:
artifacts_registry_container_repositories)。 - Artifact Registry はネームスペースにスコープされます。根拠については ADR-001 を参照してください。
- 主キーやタイムスタンプなど、いくつかの共通カラムは明確さのために省略しています。
- すべてのテーブルは
namespace_idカラムを含みます。Cells のシャーディングキー要件 はサテライトサービスのデータベースには適用されません。行はネームスペースのアンカータプル(platform、entity_type、entity_id)を通じて間接的に Organization に帰属します。このカラムは以下のすべてのテーブル定義に明示的に示しています。 - すべての
jsonbカラムは、無制限なペイロードを防ぎ期待される構造を強制するために、永続化前に厳格な JSON スキーマに対して検証されなければなりません。これはこのドキュメント内のすべてのjsonbカラムに適用されます(例:rule_configurationおよびpackage_json)。 - リモートリポジトリテーブルの暗号化された認証情報カラムは、単独では成り立ちません。各行には、認証情報を復号するためのラップされたデータ暗号化キーと、それをラップしたネームスペースキーの識別情報(
wrapped_dek、ns_key_id、ns_key_version)も保持されるため、この 3 つがなければ暗号文を読み取れません。この 3 つとテーブルの暗号文カラムは一括して有無が決まる単位を構成し、CHECK 制約により、この単位のすべてのカラムが設定されているか、すべてがNULLであるかのどちらかを強制しなければなりません。部分的な認証情報(例: パスワードのないユーザー名)は受け付けません。また、暗号文のないラップ済み DEK や、ラップ済み DEK のない暗号文など、この 3 つによって生じ得る部分的な状態も受け付けません。npm_remote_repositoriesのようにテーブルが 1 つの認証情報属性しか保存しない場合でも、このような部分的な状態はレコードが保持する属性数に左右されないため、制約は必要です。キーテーブルと共通のカラム構成については、暗号化キーを参照してください。 - リモートリポジトリテーブルの暗号化された認証情報カラム(
encrypted_username、encrypted_password、encrypted_auth_token)は、平文の入力を 2048 文字に制限し、暗号化前に Go の検証レイヤーで強制します。この上限は平文に対するものであり、平文はアプリケーションレイヤーにのみ存在します。データベースが見るのはbyteaの暗号文だけなので、DB 側の CHECK(例:octet_length(...) <= N)は、暗号化方式の固定オーバーヘッド(IV、認証タグ、key-id ヘッダー)を介して間接的に平文を制約することしかできず、上限の近似となり、必須の Go チェックと冗長になります。CHECK を省略することで、スキーマを暗号フレーミングから切り離した状態にも保てます。暗号、key-id レイアウト、エンベロープ構造の変更がスキーママイグレーションを必要としません。 - すべての
idカラムは、Artifact Registry インスタンスのスコープ内で一意でなければなりません。namespaces.idは UUIDv7(RFC 9562)を使用し、すべての Artifact Registry デプロイにまたがるグローバルな一意性を保証します。PostgreSQL の各バージョンで利用可能な生成方法を含む完全な根拠については、ネームスペース ID の型を参照してください。その他すべての API 公開テーブルもidに UUIDv7 を使用し、アプリケーションレイヤーで生成します(カラムデフォルトもシーケンスも使用しません)。これにより、API に公開されるすべてのエンティティで単一の識別子型を維持し、調整なしでデプロイ間のグローバル一意性を保証します。アプリケーション生成の UUIDv7 は論理レプリケーションにも適しています(出典)。サブスクライバー間で調整すべきサーバー側シーケンスやGENERATEDカラムがなく、UUIDv7 の時刻順序性により B-tree の挿入局所性はBIGSERIALに近く保たれます。内部 blob ストレージ層(blob_storage_attachments、blob_storage_blobs、upload_sessions)は意図的な例外です。API には決して公開されず、最も大量の行を保持するため、bigint DEFAULT nextval('<table>_id_seq')の ID を維持します。その一意性は単一の Artifact Registry データベース内でローカルに強制されます。これらの行は常にネームスペース配下にスコープされるため、それで十分です。サーバー側デフォルトを持たないすべてのuuididカラムには値をバージョン 7 に制限する CHECK 制約があり、ジェネレーターを信頼するのではなくスキーマがバージョンを強制します。
決定事項
データの領域は 6 つあります。
- ネームスペーステーブル。不変のスラッグと仮想アンカータプルを持つ内部ネームスペースエンティティを導入することで、Artifact Registry を外部識別子から切り離します。完全な根拠については ADR-022 を参照してください。
- リポジトリコレクションテーブル。ネームスペース内のリポジトリの論理的なグループです。スキーマには初日から存在しますが、まだユーザーには公開されていません。すべてのネームスペースには「default」リポジトリコレクションが作成され、すべてのリポジトリが自動的にそこへ割り当てられます。
- ネームスペースレベルのテーブル。これらは ライフサイクルポリシーの設定とルール、ネームスペースレベルのストレージ統計、およびこのネームスペースに保存されたシークレットをラップする 暗号化キーを、ネームスペースに直接スコープして扱います。
- リポジトリ親テーブル。すべてのフォーマットにまたがるすべてのリポジトリ(ホスト型、仮想型、リモート型)の統一されたレジストリであり、ランディングページのハイブリッドリストとクロスフォーマットクエリを支えます。
- アーティファクトフォーマットレベルのテーブル。ここには各フォーマット専用のテーブルがあります。ホスト型リポジトリ(Container、Maven、NPM)、リモートリポジトリ(Container、Maven、NPM)、仮想リポジトリ(Container、Maven、NPM)。それぞれ
repository_idを介して親のrepositoriesテーブルを参照します。 - blob ストレージレベルのテーブル。実際のストレージメタデータと 進行中のアップロードセッションの追跡 を扱います。
ネームスペース
erDiagram
namespaces {
uuid id PK "UUIDv7, globally unique across Artifact Registry deployments"
text slug "NOT NULL, UNIQUE, immutable, limit 255"
text platform "NOT NULL, limit 255"
text entity_type "NOT NULL, limit 255"
text entity_id "NOT NULL, opaque string, limit 255"
text billing_entity_type "NOT NULL, limit 255"
text billing_entity_id "NOT NULL, opaque string, limit 255"
smallint delivery_mode_override "NULLABLE, 0=redirect, 1=proxy; per-namespace override of the instance default"
timestamptz deleted_at "NULLABLE; set on soft-delete, reclaimable until purge (ADR-015)"
timestamptz purged_at "NULLABLE; set on hard-delete, slug retired, anchor preserved for audit (ADR-015)"
timestamptz blocked_at "NULLABLE; set on security-block, slug reserved but not serving (ADR-015)"
timestamptz disabled_at "NULLABLE; set while the owning organization has the registry turned off, cleared on re-enable"
timestamptz suspended_at "NULLABLE; set while billing-suspended, read-only service, cleared when resolved"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}- namespaces: 他のすべてのテーブルが
namespace_idを介して参照するルートエンティティです。各ネームスペースは、URL やクライアント設定で使用される不変かつグローバルに一意なslugを持ちます(スラッグの設計とグローバルな一意性の強制については ADR-022 を参照)。(platform, entity_type, entity_id)タプルは、そのセマンティクスを解釈することなく、ネームスペースを外部エンティティ(デフォルトでは Organization)にリンクします。entity_idは、基となる値が数値であってもTEXTとして保存され、アンカー型間でスキーマを統一して保ちます。Organizations v1 では、すべての行が('gitlab', 'organization', '<organizations.uuid>')、すなわち数値のorganizations.idではなく organization の UUIDv7 を持ちます。IAM の Relationships API は UUIDv7 でない object ID が現れるリクエスト全体を拒否するため、数値 ID では organization の祖先を名指しできません。billing_entity_typeとbilling_entity_idは、使用量イベントのための課金アンカーを識別します。外部から提供されるカラム(platform、entity_type、entity_id、billing_entity_type、billing_entity_id)のいずれもスキーマレベルのデフォルトを持ちません。その根拠については ADR-022 を参照してください。delivery_mode_overrideカラムは、ADR-005 で定義されたネームスペースごとのアーティファクトデリバリーのオーバーライドを保持します。NULLはインスタンスのデフォルト(StorageConfig.delivery_mode)を継承し、0(redirect)はこのネームスペースに対してリダイレクトを強制し、1(proxy)はプロキシを強制します。ダウンロードリクエストの有効なデリバリーパターンはnamespace.delivery_mode_override ?? instance.delivery_modeです。このカラムは、リクエストハンドラーが認可とルーティングのために実行する既存のネームスペース検索の一環として読み取られるため、別個のクエリやインデックスは必要ありません。カラム型はSMALLINTで、整数からラベルへのマッピングは Go アプリケーションで定義されます(0 = redirect、1 = proxy)。これは enum スタイルのカラムに関する Artifact Registry のデータベース規約 に従っています(PostgreSQL のENUM型は安全に変更するのが難しいため避けています)。アーティファクトデリバリーの選択を保存する将来のカラム(例: S17 がリポジトリごとのオーバーライドを導入する場合)は、同じ整数マッピングを再利用します。 - ライフサイクルイベント(
deleted_at、purged_at): ADR-015(内部)で定義されたスラッグのライフサイクルを実装します。deleted_atはソフト削除されたネームスペースを示し、ADR-010 のソフト削除ウィンドウ内で回収できます。purged_atはハード削除されたネームスペースを示します。スラッグは永久に廃止され、アンカーと課金のカラムは監査とフォレンジックのために保持されます。これら 2 つは一方向のイベント記録であり、一度設定されると設定されたままになります。purge 済みの行では両方が設定されます。 - サービス条件(
blocked_at、disabled_at、suspended_at): それ以外は稼働中のネームスペースに対する、元に戻せる制限です。タイムスタンプは条件が課された時刻を記録します。NULLは条件が有効でないことを意味し、繰り返しサイクルの履歴は行ではなく監査イベントストリームに保持されます。blocked_atはセキュリティ上のブロックを示します(スラッグは予約され、リクエストは処理されませんが、ネームスペースはサブスクライブされたままです)。disabled_atは、所有 Organization がレジストリを無効にしたことを示します。リクエストは処理されず、データは保持されます。Rails 側の Organization レベルの設定に基づき、内部 API を通じて設定および解除されます。suspended_atは、内部 API を通じて課された課金上の一時停止を示します。サービスは読み取り専用に制限され、ダウンロードは処理されますが、書き込みは拒否されます。そのため、支払いの途絶によって本番環境のコンシューマーが機能しなくなることはありません。各カラムは独立しているため、複数の条件が共存でき、1 つを解除しても他の条件は解除されません。 - サービス提供可否の述語: 書き込みを処理する検索(プッシュ、変更)では、5 つすべてのカラムが
NULLである必要があります。読み取りを処理する検索(ルーティング、認証、ダウンロード)では、deleted_at、purged_at、blocked_at、disabled_atがNULLである必要があります。一時停止中のネームスペースは引き続き読み取りを処理します。サブスクリプションライフサイクルのクエリ(課金、保持、およびサブスクリプション状態だけを読むスケジュール検索)は、ブロック中、無効化中、一時停止中のネームスペースもサブスクライブされたままであるため、deleted_atとpurged_atのみを除外します。述語は何が検索をスケジュールしたかではなく、検索が何を行うかに従います。クライアントがすでに要求した書き込みを完了するバックグラウンドジョブは書き込み処理の検索であり、そのリクエストを受け付けたときと同じく 5 つすべてのカラムがNULLである必要があります。これは、受け付け済みクライアント書き込みの延期された半分だけを対象とします。回収、カウンターの調整、ポリシー駆動の削除など、プラットフォーム駆動の書き込みはこの項目では分類しません。API は導出されたステータス(優先順位: purged、deleted、blocked、disabled、suspended、active)を公開し、Rails は表示用にこれをキャッシュします(ADR-022)。
スラッグの不変性
PostgreSQL にはネイティブな不変カラムのサポートがありません。スラッグの不変性(ADR-022)は、値が変更された場合に例外を発生させる BEFORE UPDATE OF slug トリガーによってデータベースレベルで強制されます。これは、アプリケーションレイヤーをバイパスするあらゆるコードパス(直接的なデータベースアクセス、管理ツール、マイグレーション)を捕捉します。このトリガーは、スラッグの変更を必要とする緊急操作のために無効化できます(例: ALTER TABLE namespaces DISABLE TRIGGER trg_namespaces_immutable_slug)。
インデックス
namespaces:(slug)に対するユニークインデックス — スラッグによってネームスペースを検索します。(platform, entity_type, entity_id) WHERE purged_at IS NULLに対する部分ユニークインデックス — purge されていない行の間でアンカーの重複を防ぎます。アクティブな行とソフト削除済みの行は対象になりますが、purge 済みの行は対象外です。そのため、以前 purge された Organization は新しいネームスペース行で再オンボーディングでき、purge 済みの行は監査のためにアンカーデータを保持できます。delivery_mode_override、deleted_at、purged_at、blocked_at、disabled_at、suspended_atにはインデックスを設けません。これらのカラムはidまたはslugをキーとする既存のネームスペース検索の一環として読み取られ、サービス提供可否の述語は取得された単一行に対してフィルタリングします。
ADR-015(内部)で定義された予約済みスラッグリストはデータベースには保存しません。
リポジトリコレクション
リポジトリコレクションは、ネームスペース内のリポジトリの論理的なグループであり、チーム、セキュリティドメイン、製品ラインによってアーティファクトを整理します。リポジトリコレクションを UI と API に公開することは MVP のスコープ外です。このエンティティは純粋に将来の互換性のために初日から存在します。MVP の間、すべてのネームスペースには作成時に単一の「default」リポジトリコレクションが作成され、すべてのリポジトリがそこへ割り当てられます。MVP 後にリポジトリコレクションの概念が公開されると、ユーザーは追加のリポジトリコレクションを作成し、リポジトリをそこへ再割り当てできます。
erDiagram
namespaces ||--o{ repository_collections : "has many"
repository_collections {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
text name "NOT NULL, limit 255"
boolean is_default "NOT NULL, DEFAULT false"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}- repository_collections: ネームスペース内のリポジトリの論理的なグループです。
nameはネームスペース内で一意な、人間が読めるラベルです。is_defaultは、すべてのネームスペースとともに自動的に作成され、MVP の間にすべてのリポジトリが割り当てられるリポジトリコレクションを示します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。
すべてのネームスペース作成時に、デフォルトのリポジトリコレクション行をアトミックに挿入しなければなりません。
INSERT INTO repository_collections (namespace_id, name, is_default)
VALUES (<new_namespace_id>, 'default', true)
ON CONFLICT (namespace_id, name) DO NOTHING;
インデックス
repository_collections:(id, namespace_id)に対する主キー —HASH(namespace_id)パーティショニングに必要な複合 PK であり、repositoriesからの複合外部キーのターゲットも兼ねます。(namespace_id, name)に対するユニークインデックス — ネームスペース内で名前によってリポジトリコレクションを検索します。(namespace_id) WHERE is_default IS TRUEに対する部分ユニークインデックス — ネームスペースごとにデフォルトのリポジトリコレクションが最大 1 つであることを強制します。
クエリ例
ネームスペースのデフォルトのリポジトリコレクションを取得する。
SELECT * FROM repository_collections WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND is_default = true;ネームスペースのすべてのリポジトリコレクションを一覧する。
SELECT id, name, is_default, created_at FROM repository_collections WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' ORDER BY created_at;新しい(デフォルトではない)リポジトリコレクションを作成する。
INSERT INTO repository_collections (namespace_id, name) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'team-backend');
リポジトリ
repositories テーブルは、フォーマットや種類に関係なくシステム内のすべてのリポジトリを登録する統一された親テーブルです。これはランディングページのハイブリッドリスト、すなわちすべてのフォーマットにまたがるホスト型・仮想型・リモート型のリポジトリを表示する単一のソート可能・フィルター可能・ページネーション可能なビューを支えます。各フォーマット固有のリポジトリテーブル(ホスト型、仮想型、リモート型)は、repository_id を介してここの単一の行を参照します。
このモデル(ホスト型、リモート型、仮想型を対等なスタンドアロンの型とし、参照によって構成する)は、JFrog Artifactory、Sonatype Nexus、Google Cloud AR がすべて採用しているものです。ただし各社で型の名前は異なります。
erDiagram
namespaces ||--o{ repositories : "has many"
repository_collections }o--o{ repositories : "linked via repository_collection_repositories"
repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
text name "NOT NULL, limit 255"
text description "nullable, limit 1024"
smallint format "NOT NULL, 0=docker, 1=maven, 2=npm, 3=oci"
smallint kind "NOT NULL, 0=hosted, 1=virtual, 2=remote"
smallint visibility "NOT NULL, 0=public, 1=private, 2=internal"
bigint artifacts_count "NOT NULL, DEFAULT 0, buffered counter"
bigint downloads_count "NOT NULL, DEFAULT 0, buffered counter"
bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
timestamptz last_updated_at "nullable"
timestamptz last_reconciled_at "NOT NULL, DEFAULT 'epoch', reconciliation bookkeeping"
text gitlab_last_updated_by_user_id "nullable, opaque string, limit 255"
timestamptz soft_deleted_at "nullable"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
text gitlab_created_by_user_id "nullable, opaque string, limit 255"
}- repositories: すべてのリポジトリの親エンティティです。
formatはアーティファクトフォーマット(Docker、Maven、npm、OCI)を識別します。kindはリポジトリの種類(ホスト型、仮想型、リモート型)を識別します。リポジトリはrepository_collection_repositories結合テーブルを介してリポジトリコレクションにリンクされ、リポジトリがそのネームスペース内の 1 つ以上のリポジトリコレクションに属することを可能にします。MVP の間、すべてのリポジトリはネームスペースのデフォルトのリポジトリコレクションにリンクされます。nameはネームスペース内で一意でなければならず、これはすべての競合製品と一致しています。カウンターカラム(artifacts_count、downloads_count、size_bytes)は、ホット行の競合を避けるために バッファ書き込み/非同期書き込み を介して維持されます。last_updated_atは、ダウンロードではなくコンテンツの変更(アーティファクトの公開/変更/削除、キャッシュイベント)を追跡します。last_reconciled_atは、同名のnamespace_statisticsカラムに対応するリポジトリレベルのカラムです(ストレージ使用量の計算を参照)。これは調整用の記録であり、リポジトリ自身の書き戻し時に記録され、API には決して表示されず、カウンターでもありません。ネームスペースレベルの対応するカラムとは異なり、独自のインデックスはありません。インデックスを必要とする古さのスキャンはネームスペースを選択し、リポジトリにはその独自の新しさではなく、選択済みのネームスペースを通じて到達するためです。'epoch'デフォルトによって、すでに行が存在するテーブルでもこのカラムをNOT NULLにできます。カラムを追加した時点ですべての既存行が有効になり、バックフィルも NULL 許容の暫定状態も不要です。gitlab_created_by_user_idとgitlab_last_updated_by_user_idは、どの GitLab ユーザーがリポジトリを作成し最後に変更したかを記録します。どちらも NULL 許容の不透明な参照で、外部キーもアプリケーション側の検証もありません。なぜなら、ユーザーレコードはモノリスに存在するためです。ユーザーハンドルとアバターのレンダリングはコンシューマーの責任であり、AR スキーマは ID のみを保存します。これらはnamespaces.entity_idと同じ理由でTEXTとして保存されます。上流のユーザー ID 形式が将来変更されても(例: UUID への変更)、スキーママイグレーションは不要です。descriptionは、UI が仮想リポジトリだけでなくすべてのリポジトリ型の説明を表示するため、親テーブルにあります。soft_deleted_atタイムスタンプは、リポジトリがソフト削除された時刻を記録し、必要に応じて復元を可能にします。ソフト削除を親テーブルに置くことで、すべてのリポジトリ型(ホスト型、仮想型、リモート型)がフォーマット固有の処理なしに同じ削除セマンティクスを共有します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。
リポジトリをハード削除すると、その構造上の子へカスケードします。関連する外部キーは、次の参照アクションを持ちます。
- フォーマットの子テーブル(
container_repositories、npm_repositories、maven_repositories、およびそれぞれの仮想型とリモート型のバリアント)で、repositoriesを参照する FK(repository_id, namespace_id):ON DELETE CASCADE。 repository_collection_repositories(コレクション結合テーブル)で、repositoriesを参照する FK(repository_id, namespace_id):ON DELETE CASCADE。- アーティファクトテーブル(
container_images、npm_packages、maven_packages)で、そのフォーマットの子テーブルを参照する FK(<format>_repository_id, namespace_id):NO ACTION。 - 仮想 upstream 結合テーブル(
container_virtual_repository_upstreams、maven_virtual_repository_upstreams、npm_virtual_repository_upstreams)で、repositoriesを参照する FK(upstream_repository_id, namespace_id):NO ACTION。
したがって、1 回の DELETE FROM repositories によってフォーマットの子行とすべてのコレクションリンクが削除されます。NO ACTION キーはそれを拒否でき、いずれか 1 つの拒否でステートメント全体が中止されます。アーティファクトから子へのキーは、リポジトリがまだアーティファクトを保持している間は拒否します。結合キーは、いずれかの仮想リポジトリがフォーマットを問わず、それを upstream として一覧に含めている間は拒否します。
上記の一覧は、このドキュメントがアクションを明記するキーを対象としており、図示するすべてのキーではありません。さらに 2 つの ERD エントリ、artifact_type_repository_lifecycle_policy_settings と upload_sessions は repositories 外部キーを宣言しますが、アクションは明記していません。出荷済みスキーマにはどちらの外部キーもないため、現在はどちらも削除を拒否しません。ERD エントリの説明どおりに追加されれば、明記されないアクションは NO ACTION なので、どちらも拒否します。
接続のアクションは 2 つのテストで決まり、カスケードするには両方を通過する必要があります。1 つ目は方向です。親から子への接続だけがカスケードするため、削除は repositories から構造チェーンを下り、横方向には進みません。2 つ目は子が保持するものです。下向きの接続のうち、子が純粋な構造(フォーマットの子行、コレクションリンク)ならカスケードし、ユーザーデータ(アーティファクト)なら拒否します。upstream_repository_id は 2 つ目ではなく 1 つ目のテストに失敗します。自身が所有しないリポジトリを名指すため、結合行が何を保持するかをどう解釈してもカスケードにはなりません。これらは合わせて、前述のソフト削除パスと、アプリケーションが管理する blob_storage_attachments のクリーンアップに対応する宣言的な仕組みです。
どちらの拒否も、参照アクションだけではオペレーターに届かず、どのように失敗するかはステートメントの実行時期に依存します。削除が同期的な場合、拒否は失敗したステートメントとして到着し、API レイヤーが認識して変換する必要があります。削除が非同期、つまり今 tombstone を付け、後の purge で DELETE FROM repositories を実行する場合、拒否は停止した purge として現れ、呼び出し元には届きません。したがって、オペレーターが頼れる拒否は、どちらのパスよりも前の削除リクエスト時に管理 API から返されます。Repository Deletion を参照してください。これらのキーはそのチェックの下にあるバックストップであり、シグナルではありません。
インデックス
repositories:(namespace_id, name)に対するユニークインデックス — アクティブなリポジトリとソフト削除されたリポジトリの両方にまたがって名前の一意性を強制し、名前の衝突によって復元が失敗することがないようにします。名前を再利用するには、まずハード削除が必要です。(namespace_id, name) WHERE soft_deleted_at IS NULLに対するインデックス — アクティブなリポジトリの検索と名前順の一覧表示のために最適化されたスキャンパスです。(namespace_id, format) WHERE soft_deleted_at IS NULLに対するインデックス — アクティブなリポジトリをフォーマットでフィルタリングします。(namespace_id, kind) WHERE soft_deleted_at IS NULLに対するインデックス — アクティブなリポジトリを種類でフィルタリングします。(namespace_id, visibility) WHERE soft_deleted_at IS NULLに対するインデックス — リポジトリを可視性レベルでフィルタリングします(可視性監査クエリ「このネームスペースで現在公開されているリポジトリはどれか?」を支えます)。ランディングページでソート可能なカラムごとに 1 つのインデックスを設け、すべてにWHERE soft_deleted_at IS NULLと末尾のid DESCキーセットのタイブレーカーを付けます。対象は(namespace_id, artifacts_count DESC, id DESC)、(namespace_id, downloads_count DESC, id DESC)、(namespace_id, size_bytes DESC, id DESC)、および式インデックス(namespace_id, COALESCE(last_updated_at, created_at) DESC, id DESC)です。ROW(<col>, id)キーセット境界のid側は、インデックス内に存在する必要があります。カウンターカラム(artifacts_count、downloads_count、size_bytes)はデフォルトが0でカーディナリティが低いため、インデックスにタイブレーカーがないと、planner はスキャン後のソートまたはフィルターとして適用し、深いページではネームスペースに対して O(offset) に劣化します。式インデックスにもid DESCを含めます。これはカーディナリティのためではなく(created_atは行ごとに一意であるため、COALESCE(last_updated_at, created_at)はカーディナリティが高い)、キーセットに決定的なカーソルを与えるためです。新しさによるソートでは、生のlast_updated_atではなくCOALESCE(last_updated_at, created_at)で並べます。更新されたことのないリポジトリ(新しいリポジトリで一般的なlast_updated_atがNULLの状態)は、作成時刻に基づいて順位付けされ、NULLS LASTの下に沈むのではなく「最近更新された」一覧の上位に並びます。また、結合されたキーは決して NULL にならないため、キーセット境界は単純なROW(<col>, id)比較となり、NULL 領域の特別な処理なしでインデックス範囲に組み込まれます。シリアライズされるlast_updated_atフィールドは nullable のままで、結合するのはソートキーだけです。nameによるソートにはタイブレーカーが不要です。(namespace_id, name)ユニークインデックスが、すでに決定的なキーセットだからです。(namespace_id, format, name) WHERE soft_deleted_at IS NULLに対するインデックス — フォーマットでフィルタリングした名前順の一覧表示(主要な閲覧ビュー: フォーマットでフィルタリングし、名前でソート)に使用します。formatをインデックス適用後のフィルターとして使用し、異なるフォーマットの行を飛ばしながら(namespace_id, name)ユニークインデックスを走査するのではなく、(format, name)の範囲を直接検索してインデックスオンリーの状態を維持します。インデックスを設けるのはフォーマットと名前の組み合わせだけです。使用頻度の低いカウンターまたはタイムスタンプによるソートでは、ソートとフィルターの組み合わせごとに複合インデックスを増やすのではなく、formatまたはkindをインデックス適用後のフィルターとして維持します。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — このネームスペース内のソフト削除されたリポジトリを削除時刻順に一覧します(ゴミ箱一覧クエリ「ゴミ箱には何があり、いつ削除されたか?」を支えます)。この逆の部分述語は、前述のアクティブ行に対する部分インデックスを反映したものです。このテーブルの他の部分インデックスはすべてゴミ箱を除外し、完全な(namespace_id, name)ユニークインデックスはsoft_deleted_atをキーにしないため、これがなければゴミ箱の一覧表示では、フィルタリングとソートのためにネームスペース内のすべての行を確認しなければなりません。GC の適格性は ADR-010 に従い、soft_deleted_at + retention_windowから導出します。別個のカラムは不要です。
MVP の間、すべてのリポジトリは単一のデフォルトのリポジトリコレクションにリンクされるため、(namespace_id, ...) ソートインデックスはネームスペース全体のクエリとコレクションでフィルターされたクエリの両方に対応します。MVP 後、ネームスペースが複数のリポジトリコレクションを持つようになると、コレクションでフィルターされたクエリは repository_collection_repositories を介して JOIN します。追加のサポートインデックスは、リポジトリコレクションが公開される際に評価されます。
クエリ例
ネームスペースのすべてのリポジトリ(すべてのリポジトリコレクション)を最終更新順に一覧する。
SELECT id, name, description, format, kind, artifacts_count, downloads_count, size_bytes, last_updated_at FROM repositories WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND soft_deleted_at IS NULL ORDER BY COALESCE(last_updated_at, created_at) DESC LIMIT 20;リポジトリコレクションでフィルターしたネームスペースのリポジトリを最終更新順に一覧する。
SELECT r.id, r.name, r.description, r.format, r.kind, r.artifacts_count, r.downloads_count, r.size_bytes, r.last_updated_at FROM repositories r JOIN repository_collection_repositories rcr ON rcr.namespace_id = r.namespace_id AND rcr.repository_id = r.id WHERE r.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND rcr.repository_collection_id = '019a1b2c-0456-7abc-8def-000000000456' AND r.soft_deleted_at IS NULL ORDER BY COALESCE(r.last_updated_at, r.created_at) DESC LIMIT 20;リポジトリコレクションとフォーマットでフィルターしたリポジトリを一覧する。
SELECT r.id, r.name, r.description, r.format, r.kind, r.artifacts_count, r.downloads_count, r.size_bytes, r.last_updated_at FROM repositories r JOIN repository_collection_repositories rcr ON rcr.namespace_id = r.namespace_id AND rcr.repository_id = r.id WHERE r.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND rcr.repository_collection_id = 456 AND r.format = 0 AND r.soft_deleted_at IS NULL ORDER BY r.name LIMIT 20;名前で単一のリポジトリを検索する。
SELECT * FROM repositories WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND name = 'my-repo' AND soft_deleted_at IS NULL;可視性監査: ネームスペース内のすべての公開リポジトリを一覧する(
(namespace_id, visibility) WHERE soft_deleted_at IS NULLの部分インデックスを使用)。SELECT id, name, format, kind FROM repositories WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND visibility = 0 AND soft_deleted_at IS NULL ORDER BY name;ゴミ箱一覧: ネームスペース内のすべてのソフト削除されたリポジトリを、最近削除された順に一覧する(
(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLの部分インデックスを使用)。スコープはネームスペース全体なので、管理者は「今すぐ復元可能なものは何か?」を 1 つのクエリで回答できます。親ごとのゴミ箱ビューは別個の UI の関心事であり、必要であれば後から親をキーとするインデックスを追加することで対応できます。SELECT id, name, format, kind, soft_deleted_at FROM repositories WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND soft_deleted_at IS NOT NULL ORDER BY soft_deleted_at DESC LIMIT 50;
リポジトリコレクションリポジトリ
repository_collection_repositories 結合テーブルは、リポジトリを、それが属するリポジトリコレクションにマッピングします。リポジトリはそのネームスペース内の 1 つ以上のリポジトリコレクションのメンバーになることができ、共通のユーティリティリポジトリを複数のチームのリポジトリコレクションを通じて公開するといった共有アクセスのシナリオを可能にします。
erDiagram
repository_collections ||--o{ repository_collection_repositories : "has many"
repositories ||--o{ repository_collection_repositories : "has many"
repository_collection_repositories {
uuid namespace_id PK,FK "NOT NULL, references namespaces(id), part of composite PK (namespace_id, repository_collection_id, repository_id)"
uuid repository_collection_id PK,FK "NOT NULL, (repository_collection_id, namespace_id) references repository_collections(id, namespace_id)"
uuid repository_id PK,FK "NOT NULL, (repository_id, namespace_id) references repositories(id, namespace_id)"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}- repository_collection_repositories: リポジトリをリポジトリコレクションにリンクします。MVP の間、すべてのリポジトリはちょうど 1 つのリポジトリコレクション(ネームスペースのデフォルト)にリンクされますが、スキーマは複数のリンクを許可するため、MVP 後にリポジトリをリポジトリコレクション間で共有できます。アプリケーションは、すべてのリポジトリが少なくとも 1 つのリポジトリコレクションリンクを持つという不変条件を強制します。Postgres はこれを宣言的に表現できません。複合外部キーにより、リポジトリコレクションとリポジトリは同じネームスペース内でのみリンクできることが保証されます。
HASH(namespace_id)で 64 パーティションにパーティショニングされます。
インデックス
repository_collection_repositories:(namespace_id, repository_collection_id, repository_id)に対する主キー — リンクの一意性を強制し、リポジトリコレクションによる検索に対応します。(namespace_id, repository_id)に対するインデックス — 特定のリポジトリが属するすべてのリポジトリコレクションを検索します。
クエリ例
リポジトリが属するすべてのリポジトリコレクションを一覧する。
SELECT repository_collection_id FROM repository_collection_repositories WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND repository_id = '019a1b2c-0789-7abc-8def-000000000789';リポジトリをリポジトリコレクションにリンクする。
INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', '019a1b2c-0789-7abc-8def-000000000789') ON CONFLICT (namespace_id, repository_collection_id, repository_id) DO NOTHING;
ライフサイクルポリシー
erDiagram
lifecycle_policy_settings ||--o{ lifecycle_rules : "has many"
lifecycle_policy_settings {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, UNIQUE, references namespaces(id)"
boolean enabled "NOT NULL"
}
lifecycle_rules {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid lifecycle_policy_settings_id FK "NOT NULL, (lifecycle_policy_settings_id, namespace_id) references lifecycle_policy_settings(id, namespace_id)"
smallint rule_type "NOT NULL, 0=keep_last_downloaded_at, 1=keep_last_n, 2=keep_regex"
jsonb rule_configuration "NOT NULL"
}- lifecycle_policy_settings: ネームスペースレベルでライフサイクル管理の設定を定義し、すべてのリポジトリのデフォルトポリシーとして機能します。有効化されると、関連するライフサイクルルールがネームスペース全体に適用されます。これらのポリシーはリポジトリレベルのポリシーによって オーバーライド できます。
HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - lifecycle_rules: ネームスペースレベルで特定のアーティファクトのライフサイクル動作を統制する、個々の保持・クリーンアップルールを指定します。これらのルールは、リポジトリレベルで オーバーライド されない限り、すべてのリポジトリに適用されます。ポリシーレコードごとのライフサイクルルールの数は、ルール評価時のパフォーマンス低下を防ぐために制限されます。これは、例えば特定のアーティファクトをどのくらいの期間保持するかをユーザーが指定するために使用されます(例: Maven スナップショットファイルは 1 か月だけ保持するなど)。
HASH(namespace_id)で 64 パーティションにパーティショニングされます。
インデックス
lifecycle_policy_settings:(namespace_id)に対するユニークインデックス — ネームスペースごとに 1 つのポリシー設定レコード。lifecycle_rules:(namespace_id, lifecycle_policy_settings_id)に対するインデックス — 特定のポリシーのすべてのルールを取得します。
リポジトリレベルのオーバーライドテーブルは同じパターンに従います。設定テーブルには (namespace_id, repository_id) に対するユニークインデックス、ルールテーブルには (namespace_id, <format>_repository_lifecycle_policy_settings_id) に対するインデックスです。
クエリ例
特定のネームスペースのポリシーを取得する
SELECT lp.* FROM lifecycle_policy_settings lp WHERE lp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8';特定のアーティファクトリポジトリのポリシーを取得する
SELECT * FROM container_repository_lifecycle_policy_settings WHERE container_repository_lifecycle_policy_settings.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND container_repository_lifecycle_policy_settings.repository_id = '019a1b2c-0123-7abc-8def-000000000123';新しいライフサイクルルールを作成する
INSERT INTO lifecycle_rules (namespace_id, lifecycle_policy_settings_id, rule_type, rule_configuration) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0123-7abc-8def-000000000123', 1, '{"count": 10}'::jsonb);ライフサイクルルールを更新する
UPDATE lifecycle_rules SET rule_configuration = '{"count": 20}'::jsonb WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0123-7abc-8def-000000000123';ライフサイクルルールを破棄する
DELETE FROM lifecycle_rules WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0123-7abc-8def-000000000123';
リポジトリレベルのオーバーライド
各リポジトリ型(container、maven、npm)は、ネームスペースレベルの値に対するオーバーライドを提供する、同様の名前のテーブルを持ちます。これにより優先順位システムが構築されます: ネームスペース(最低)-> リポジトリ(最高)。オーバーライドは repository_id を介して親の repositories テーブルを参照します。
erDiagram
artifact_type_repository ||--|| artifact_type_repository_lifecycle_policy_settings : "has one"
artifact_type_repository ||--o{ artifact_type_repository_lifecycle_rules : "has many"
artifact_type_repository_lifecycle_policy_settings {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid repository_id FK "NOT NULL, (repository_id, namespace_id) references repositories(id, namespace_id)"
boolean enabled "NOT NULL"
}
artifact_type_repository_lifecycle_rules {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid artifact_type_repository_lifecycle_policy_settings_id FK "NOT NULL, (artifact_type_repository_lifecycle_policy_settings_id, namespace_id) references artifact_type_repository_lifecycle_policy_settings(id, namespace_id)"
smallint rule_type "NOT NULL, 0=keep_last_downloaded_at, 1=keep_last_n, 2=keep_regex"
jsonb rule_configuration "NOT NULL"
}(各アーティファクトフォーマットにオーバーライドテーブルがあるため、artifact_type は container、maven、npm に置き換える必要があります。これらのオーバーライドはホスト型、仮想型、リモート型のリポジトリに同様に適用されます。repository_id 外部キーは親の repositories テーブルを参照し、フォーマット固有のテーブルはリポジトリの format カラムによって決まります。)
これらのテーブルは、ある意味で カスケード設定 のように動作します。それらの説明は ネームスペースレベル の同様の名前のテーブルとまったく同じであり、パーティショニングも同様です。すべてのオーバーライドテーブルは HASH(namespace_id) で 64 パーティションにパーティショニングされます。現在の 2 層の優先順位システム(ネームスペース → リポジトリ)は、MVP 後にリポジトリコレクションが公開される際に 3 層(ネームスペース → リポジトリコレクション → リポジトリ)に拡張できます。これには同じパターンに従ってリポジトリコレクションレベルのオーバーライドテーブルを追加する必要がありますが、既存のネームスペースレベルやリポジトリレベルのテーブルへの変更は不要です。
暗号化キー
erDiagram
namespaces ||--o{ namespace_encryption_keys : "has many"
namespace_encryption_keys {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
int version "NOT NULL, CHECK >= 1, UNIQUE (namespace_id, version)"
bytea wrapped_key "NOT NULL, the namespace key wrapped by the deployment root key; emptied to zero length on crypto-shred"
text root_key_uri "NOT NULL, limit 255, names the root key this row was wrapped under"
boolean active "NOT NULL, DEFAULT true"
timestamptz shredded_at "nullable, crypto-shred tombstone marker"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}- namespace_encryption_keys: ネームスペースが保持したキー暗号化キーごとに 1 行を保存し、各キーをデプロイのルートキーでラップします。ネームスペースが一度に持つアクティブなキーは 1 つです。以前のバージョンでラップされた行は引き続きそのバージョンを介してキーを解決するため、置き換えられたバージョンは削除せず、非アクティブ化してテーブルに残します。
idは参照用の識別情報であり、認証情報の行はns_key_idでこれを指定します。一方、versionはネームスペース単位の序数で、ネームスペースのキーを順序付け、ローテーションの走査を制御します。この 2 つは相互に置き換えられません。暗号学的に消去されたネームスペースを再度有効にするとversionは 1 から再開するため、ネームスペースの存続期間全体にわたってキーを識別できず、読み取りではns_key_idによってラップ用キーを解決します。暗号学的消去では、行を削除せず tombstone として保持します。wrapped_keyを長さ 0 のbyteaで上書きし、activeを解除してshredded_atに時刻を記録するため、消去済みの状態が再起動後も維持され、すべてのインスタンスに適用されます。2 つの行単位の CHECK 制約によって、この構成を維持します。一方は tombstone がアクティブのままになることを拒否し、もう一方はキー素材と未消去状態を双方向に関連付けて、shredded_atがNULLの場合に限りキー素材が存在するようにします。いずれかの tombstone が存在すればネームスペース全体が消去済みであるという、ネームスペース全体の不変条件はアプリケーションレイヤーで強制します。これは行単位の CHECK で対応できる範囲の限界です。HASH(namespace_id)で 64 パーティションにパーティショニングされます。
インデックス
namespace_encryption_keys:(namespace_id, version)に対するユニークインデックスは、ネームスペース単位のバージョン序数を維持します。(namespace_id) WHERE active = trueに対する部分ユニークインデックスは、ネームスペースごとにアクティブなキーが 1 つだけであることを保証するとともに、認証情報を書き込む際にラップに使用するキーを検索するために使われます。(namespace_id) WHERE shredded_at IS NOT NULLに対する部分インデックスは、読み取りパスでキー履歴を走査せずに、ネームスペースが消去済みかを検出するために使われます。(root_key_uri) WHERE shredded_at IS NULLに対する部分インデックスは、特定のルートキーで現在もラップされている有効な行を検索するために使われ、ルートキーのローテーションで走査し、完了確認で数える対象となります。消去済みの行は、最後のインデックスから意図的に除外します。消去済みのネームスペースは以前のroot_key_uriを永続的に保持し、そのwrapped_keyはすでに空になっているため、ローテーションでこれらの行を対象にしてはいけません。4 つのインデックスのうち、namespace_id以外のカラムで始まるのはこれだけです。このインデックスが扱うクエリはルートキーを指定し、ネームスペースを指定しないためです。パーティショニングされたテーブルのすべてのインデックスと同様、各パーティションにローカルなため、この読み取りは 1 つのパーティションに絞られず、64 パーティションすべてに分散します。ルートキーのローテーションはバックグラウンドジョブとして一定数の行をまとめて走査するため、リクエストパスでは許容できないこの分散も、ここでは許容できます。
リモートリポジトリテーブルの認証情報カラム
3 つのリモートリポジトリテーブル(Container、Maven、NPM)は、行単位のデータ暗号化キーによる暗号文として上流の認証情報を保存し、それぞれが暗号文とともに次の 3 つのカラムを持ちます。
wrapped_dek(bytea): ネームスペースキーでラップされた、行のデータ暗号化キー。ns_key_id(uuid): このキーをラップしたnamespace_encryption_keysの行。(ns_key_id, namespace_id)に対する複合外部キーでnamespace_encryption_keys(id, namespace_id)を参照し、ON DELETE RESTRICTを指定します。これにより、認証情報の行が引き続き依存している間は、キーのバージョンを削除できません。ns_key_version(int): 行のラップに使われたネームスペースキーのバージョン。ネームスペースキーのローテーション処理が、キーテーブルを結合せずに古いバージョンのままの行を選択できるよう、キーの行から非正規化します。この処理はネームスペースを指定するため、後述の(namespace_id, ns_key_id)インデックスを通じてそのネームスペースの行へ到達し、バージョンでフィルタリングします。ns_key_versionから始まるインデックスはありません。namespace_idプレフィックスによって 1 つのパーティションに絞られ、ネームスペースが保持する認証情報の行はリモートリポジトリごとに 1 つであり、これらのテーブルは管理操作でしか変更されないため、フィルターが走査する行は少数です。読み取りでは常にns_key_idによってラップ用キーを解決し、このカラムは使用しません。
認証情報を必要としないリモートリポジトリも有効なため、この 3 つはいずれも NULL 許容です。背景で説明した一括有無の CHECK により、行が単位の一部だけを保持することを防ぎます。また、各テーブルは (namespace_id, ns_key_id) に対するインデックスを持ちます。PostgreSQL は外部キーの参照される側にはインデックスを作成しますが、参照する側には作成しません。そのため、このインデックスがなければ、キーの行を削除しようとするたびに、ON DELETE RESTRICT を強制するため認証情報テーブル全体が走査されます。このアクションは ON DELETE RESTRICT と記述しますが、このドキュメントでは、削除を拒否する他のキーには ON DELETE NO ACTION と記述しています。これらのキーはいずれも DEFERRABLE ではないため、2 つの表記は同じように動作します。RESTRICT を選ぶのは、チェックを遅延させる可能性も排除し、キーの行を削除するステートメントで拒否されるようにして、トランザクションのコミット時に持ち越さないためです。
行の上位には 2 段階のキーがあります。デプロイのルートキーがネームスペースキーをラップし、ネームスペースキーが各行の DEK をラップします。各段階は個別にローテーションされ、いずれも認証情報の暗号文を再暗号化しません。ルートキーをローテーションすると、namespace_encryption_keys.wrapped_key を新しいルートキーでラップして root_key_uri を更新しますが、認証情報の行には一切触れません。ネームスペースキーをローテーションすると、各認証情報の行の wrapped_dek を新しいネームスペースキーでラップし、その ns_key_id と ns_key_version を更新しますが、暗号文カラムは変更しません。暗号文を置き換えるのは認証情報への書き込み時だけで、そのたびに新しい DEK を生成します。
コンテナリポジトリ
この部分の課題は、OCI Distribution Spec v1.1 に準拠することです。
このアプローチは GitLab Container Registry のスキーマ から大きな着想を得ています。
erDiagram
repositories ||--|| container_repositories : "has one"
container_repositories ||--o{ container_images : "has many"
container_images ||--o{ container_blobs : "has many"
container_images ||--o{ container_manifests : "has many"
container_images ||--o{ container_manifest_relationships : "has many"
container_images ||--o{ container_tags : "has many"
container_tags ||--|| container_manifests : "has one"
container_blobs ||--|| blob_storage_attachments : "has one"
container_manifests ||--|| blob_storage_attachments : "has one"
container_manifest_relationships ||--|| container_manifests : "has one (parent_id)"
container_manifest_relationships ||--|| container_manifests : "has one (child_id)"
container_repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
}
container_images {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_repository_id FK "NOT NULL, (container_repository_id, namespace_id) references container_repositories(id, namespace_id)"
text name "NOT NULL, limit 255"
timestamptz last_downloaded_at "nullable, buffered"
timestamptz soft_deleted_at "nullable"
}
container_blobs {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_image_id FK "NOT NULL, (container_image_id, namespace_id) references container_images(id, namespace_id)"
bytea digest "NOT NULL, CHECK octet_length = 32"
text media_type "NOT NULL, limit 255"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, CHECK octet_length = 32, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
timestamptz soft_deleted_at "nullable"
}
container_manifests {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_image_id FK "NOT NULL, (container_image_id, namespace_id) references container_images(id, namespace_id)"
bytea digest "NOT NULL, CHECK octet_length = 32"
text media_type "NOT NULL, limit 255"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, CHECK octet_length = 32, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
bigint size "NOT NULL, precomputed at push time"
text gitlab_user_id "nullable, opaque string, limit 255"
text gitlab_project_id "nullable, opaque string, limit 255"
bytea gitlab_git_commit_sha "nullable"
timestamptz soft_deleted_at "nullable"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}
container_manifest_relationships {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_image_id FK "NOT NULL, (container_image_id, namespace_id) references container_images(id, namespace_id)"
uuid parent_container_manifest_id FK "NOT NULL, (parent_container_manifest_id, namespace_id) references container_manifests(id, namespace_id)"
uuid child_container_manifest_id FK "NOT NULL, (child_container_manifest_id, namespace_id) references container_manifests(id, namespace_id)"
}
container_tags {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_image_id FK "NOT NULL, (container_image_id, namespace_id) references container_images(id, namespace_id)"
uuid container_manifest_id FK "NOT NULL, (container_manifest_id, namespace_id) references container_manifests(id, namespace_id)"
text name "NOT NULL, limit 255"
}- container_repositories: 複数のイメージのコンテナです。各リポジトリは独立したバージョニングを持つ複数のイメージをホストできます。名前、可視性、クロスフォーマットクエリのために
repository_idを介して親のrepositoriesテーブルを参照します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_images: リポジトリ内の名前付きコンテナイメージ(例:
myapp、backend)を表します。last_downloaded_atはイメージが最後にプルされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。keep_last_downloaded_atライフサイクルルールによって、ダウンロードベースの保持を評価するために使用されます(ADR-010)。soft_deleted_atタイムスタンプは、イメージがソフト削除された時刻を記録し、必要に応じて復元を可能にします。イメージの削除は、リクエスト時間内のトランザクションで完了できない唯一のコンテナ削除でもあります。タグの削除では 1 行が削除され、マニフェストの削除は ADR-004 の 1,000 タグ上限によって制限されますが、1 つのイメージには最大 25,000 個のマニフェストを格納でき、各マニフェストは 200 個の参照を持つことができます。そのため、イメージを削除すると行をマークし、バックグラウンド処理に回収を委ねます。このため、このテーブルはイメージレベルの削除マーカー検出インデックスを持ち、マニフェストレベルの削除マーカーインデックスでは代用できません(以下のインデックス一覧を参照してください)。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_blobs: コンテナイメージを構成する、個々のコンテンツアドレス可能なレイヤーと設定オブジェクトを保存します。マニフェストとその構成要素であるレイヤー(blob)の関係は暗黙的であり(実行時にマニフェストの内容を解析することで決定される)、データベースの外部キーとしてはモデル化されません。
soft_deleted_atタイムスタンプは、blob がソフト削除された時刻を記録し、必要に応じて復元を可能にします。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_manifests: 特定のイメージバージョンの設定とレイヤーを記述するイメージマニフェストを表します。
sizeカラムは、ここをルートとするマニフェストツリーの合計バイトサイズを保持します。すなわち、このマニフェスト自体のペイロードに加えて、そこから到達可能なすべての blob(マニフェストリストや OCI インデックスの場合は子マニフェストを通じて推移的に到達するものを含む)です。gitlab_user_idはどの GitLab ユーザーがこのマニフェストをプッシュしたかを記録します。NULL 許容の不透明なテキスト参照で外部キーはなく、repositories の同等のカラムと同じ根拠によります。ユーザーレコードはモノリスに存在し、ユーザーハンドルとアバターのレンダリングはコンシューマーの責任であり、AR スキーマは ID のみを保存し、TEXTはスキーマを上流のユーザー ID 形式の将来の変更から隔離します。gitlab_project_idとgitlab_git_commit_shaは、その帰属に公開コンテキストの残りを加えて拡張します。gitlab_project_idはプッシュ元の GitLab プロジェクト(例:CI_PROJECT_ID)であり、gitlab_user_idと同じモノリス参照の理由から NULL 許容の不透明テキストとして保存されます。gitlab_git_commit_shaは公開時の Git コミット(例:CI_COMMIT_SHA)で、ハッシュカラムのスキーマ規約に従って NULL 許容のbyteaとして保存されます。可変長で、SHA-1(20 バイト)と SHA-256(32 バイト)の両方に収まります。これはモノリス参照ではなく公開時の事実なので、外部キーは不要です。両方とも、CI コンテキストなしでプッシュが到着した場合(例: 開発者のワークステーションからの手動プッシュ)には NULL になります。soft_deleted_atタイムスタンプは、マニフェストがソフト削除された時刻を記録し、必要に応じて復元を可能にします。created_atはマニフェストが最初にプッシュされた時刻を記録します。ネームスペースごとの時刻順インデックスと組み合わせることで、公開履歴と時間範囲のアーティファクト出所クエリ(例: 「午前 2 時から午前 8 時の間にこのネームスペースにプッシュされたものは何か?」)を支えます。公開イベント自体は削除によって消去されないため、ソフト削除された行も公開履歴に引き続き表示されます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_manifest_relationships: Docker マニフェストリストと OCI インデックス(マルチアーキテクチャイメージなど)を扱います。ここでは親マニフェストが複数の他のマニフェストを参照できます。
HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_tags: 特定のマニフェストを指す、人間が読める名前(例:
latest、v1.2.3)を提供します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。
container_blobs テーブルは、他のコンテナレジストリアーキテクチャがそうするかもしれない方法で、コンテナレジストリの物理的な blob を直接保存することはしません。ここでの違いは、blob ストレージが blob ストレージ テーブルで処理される(重複排除とガベージコレクションも含む)ことです。したがって、container_* レベルでは、単に blob_storage_attachments レコードへの参照を保存するだけで済みます。
インデックス
container_repositories:(namespace_id, repository_id)に対するユニークインデックス — 親リポジトリ参照によってコンテナリポジトリを検索します。container_images:(namespace_id, container_repository_id, name) WHERE soft_deleted_at IS NULLに対するユニークインデックス — イメージ名はリポジトリ内で一意なイメージを識別します。重複すると OCI の名前ベースの検索が壊れます。部分条件により、ソフト削除後に同じ名前のイメージを再作成できます。(namespace_id, container_repository_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULLに対するインデックス —keep_last_downloaded_atライフサイクルルールの評価をサポートします。リポジトリ内のすべてのイメージをスキャンして行ごとにフィルターするのではなく、境界のある範囲スキャンによって期限切れになったイメージのみを返します。NULLS FIRSTは、一度もダウンロードされていないイメージを最も古い行とグループ化し、両方が同じ範囲スキャンで返されるようにします。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — ソフト削除されたイメージを削除時刻の順に一覧表示し、イメージ粒度のゴミ箱一覧とリーパーによるイメージレベルのスキャンを支えます。この形のcontainer_manifestsインデックスは、どちらにも使用できません。このインデックスはマニフェストをキーとし、ソフト削除されたイメージのマニフェストには独自のsoft_deleted_atがないため、マニフェストのスキャンでもリポジトリの走査でも、リポジトリがまだ有効な削除マーカー付きのイメージには到達できません。container_blobs:(namespace_id, container_image_id, digest) WHERE soft_deleted_at IS NULLに対するユニークインデックス — blob のダイジェストはコンテンツアドレス指定です。同じイメージ内の同じダイジェストは定義上同じ blob です。部分条件により、ソフト削除後に同じダイジェストを再プッシュできます。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによって blob を検索します。(namespace_id, digest)に対するインデックス — コンテンツダイジェストによるイメージ横断の検索で、blob のマウントに使用します。コンテナ blob はコンテンツアドレス指定であるため、そのdigestは保存されたblob_sha256と等しくなります。このため、このインデックスは別個の(namespace_id, blob_sha256)インデックスなしに、クロスフォーマットのチェックサム検索と脆弱性影響クエリ「この侵害されたダイジェストが与えられたとき、どのイメージがそれを参照しているか?」にも使用されます。このドキュメントの逆引きインデックスは無条件(soft_deleted_at述語なし)なので、かつて参照されたダイジェストは監査証跡に引き続き表示されます。現在影響を受けているアーティファクトのみを必要とする脆弱性影響は、クエリ時にsoft_deleted_at IS NULLを追加します。これは小さな中間集合に対する安価な後置フィルターです。Maven と npm のファイルはコンテンツアドレス指定ではなく、同等のダイジェストカラムもないため、これらのテーブルには専用の(namespace_id, blob_sha256)逆引きインデックスがあり、コンテナテーブルにはありません。この等価性は sha256 が唯一のダイジェストアルゴリズムである間に成り立ちます。digestとblob_sha256はどちらもoctet_length = 32CHECK の下で 32 バイトの値であり、コンテンツは書き込み時にダイジェストに対して検証されるため、(namespace_id, digest)インデックスはblob_sha256インデックスとまったく同じ行を返します。将来、sha256 以外のダイジェストアルゴリズムが導入される場合は、2 つのカラムを分離し、再検討します。container_manifests:(namespace_id, container_image_id, digest) WHERE soft_deleted_at IS NULLに対するユニークインデックス — マニフェストのダイジェストはコンテンツアドレス指定です。同じイメージ内の同じダイジェストは定義上同じマニフェストです。部分条件により、ソフト削除後に同じダイジェストを再プッシュできます。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによってマニフェストを検索します。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — ソフト削除されたマニフェストを削除時刻順に一覧し、コンテナイメージのアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC)に対するインデックス — ネームスペース全体の時系列スキャンで、公開履歴のページネーションと時間範囲のアーティファクト出所クエリを支えます。無条件(soft_deleted_at述語なし)なので、後でソフト削除された公開イベントも監査証跡に引き続き表示されます。container_blobsとは異なり、container_manifestsにはスタンドアロンの(namespace_id, digest)インデックスがありません。ダイジェストはイメージ内でのみ、ユニークな(namespace_id, container_image_id, digest)によってインデックス化されます。そのため、マニフェストペイロードのイメージ横断チェックサム検索(マニフェストのdigestは保存されたblob_sha256と等しい)は、ネームスペースで絞り込まれたパーティションをスキャンします。これは許容されています。これを発行する MVP エンドポイントはなく、脆弱性影響の検索はcontainer_blobsの(namespace_id, digest)インデックスによって提供されるレイヤー/設定ダイジェスト検索であるためです。container_manifest_relationships:(namespace_id, parent_container_manifest_id, child_container_manifest_id)に対するユニークインデックス — 親子関係の重複を防ぎ、特定の親マニフェストのすべての子を見つけます。(namespace_id, child_container_manifest_id)に対するインデックス — 特定の子マニフェストのすべての親を見つけます。(namespace_id, container_image_id)に対するインデックス — 特定のイメージのすべてのマニフェスト関係を見つけます。container_tags:(namespace_id, container_image_id, name)に対するユニークインデックス — イメージ内で名前によってタグを検索します。(namespace_id, container_manifest_id)に対するインデックス — 特定のマニフェストを指すすべてのタグを見つけます。
クエリ例
名前でイメージを取得する
SELECT * FROM container_images WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND container_repository_id = '019a1b2c-0123-7abc-8def-000000000123' AND name = 'myapp/backend' AND soft_deleted_at IS NULL;リポジトリ ID に対してダイジェストで blob を取得する
SELECT cb.* FROM container_blobs cb JOIN container_images ci ON cb.container_image_id = ci.id AND cb.namespace_id = ci.namespace_id WHERE ci.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND ci.container_repository_id = '019a1b2c-0123-7abc-8def-000000000123' AND cb.digest = 'sha256:abcd1234...'::bytea AND ci.soft_deleted_at IS NULL AND cb.soft_deleted_at IS NULL;リポジトリ ID に対してダイジェストでマニフェストを取得する
SELECT cm.* FROM container_manifests cm JOIN container_images ci ON cm.container_image_id = ci.id AND cm.namespace_id = ci.namespace_id WHERE ci.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND ci.container_repository_id = '019a1b2c-0123-7abc-8def-000000000123' AND cm.digest = 'sha256:efgh5678...'::bytea AND ci.soft_deleted_at IS NULL AND cm.soft_deleted_at IS NULL;チェックサム検索と脆弱性影響: 保存された blob の
sha256が与えられたとき、それを参照するネームスペース内のすべてのアーティファクトを見つけます。namespace_idの等価性により、テーブルごとに単一のパーティションに絞り込みます。Maven と npm のファイルは(namespace_id, blob_sha256)インデックスを使用し、パーティションをスキャンせずに一致する行を直接返します。コンテナテーブルはコンテンツアドレス指定です。blob またはマニフェストのdigestは保存されたblob_sha256と等しいため、container_blobsは(namespace_id, digest)インデックスを通じてこれに応答します。container_manifestsにはスタンドアロンのダイジェストインデックスがないため、マニフェストペイロードの検索はネームスペースで絞り込まれたパーティションをスキャンします。チェックサム検索はすべての参照を返します。脆弱性影響(「この侵害されたダイジェストによって現在影響を受けているアーティファクトはどれか?」)は、結果をアクティブなアーティファクトに限定するためにsoft_deleted_at IS NULLを追加します。-- Single format: container layer/config blobs referencing the digest. -- container_blobs is content-addressed (digest = blob_sha256) and has a -- (namespace_id, digest) index, so this reverse lookup rides that index -- rather than a dedicated blob_sha256 one. container_manifests has no -- standalone digest index. See the cross-format query below. SELECT cb.id, cb.container_image_id, cb.digest FROM container_blobs cb WHERE cb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND cb.digest = 'sha256:abcd1234...'::bytea; -- Cross-format: every artifact referencing the digest, active rows only (vulnerability impact) SELECT 'container_blob' AS artifact_kind, cb.id AS artifact_id, cb.container_image_id AS parent_id FROM container_blobs cb WHERE cb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND cb.digest = 'sha256:abcd1234...'::bytea AND cb.soft_deleted_at IS NULL UNION ALL -- container_manifests has no standalone (namespace_id, digest) index, so this -- arm scans the namespace-pruned partition (accepted: no MVP endpoint issues -- a cross-image manifest-payload checksum search). SELECT 'container_manifest', cm.id, cm.container_image_id FROM container_manifests cm WHERE cm.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND cm.digest = 'sha256:abcd1234...'::bytea AND cm.soft_deleted_at IS NULL UNION ALL SELECT 'maven_file', mf.id, mf.maven_version_id FROM maven_files mf WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.blob_sha256 = 'sha256:abcd1234...'::bytea AND mf.soft_deleted_at IS NULL UNION ALL SELECT 'npm_file', nf.id, nf.npm_version_id FROM npm_files nf WHERE nf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND nf.blob_sha256 = 'sha256:abcd1234...'::bytea AND nf.soft_deleted_at IS NULL;同じ
(namespace_id, blob_sha256)アクセスパスは、キャッシュ側のテーブル(container_remote_blobs、container_remote_manifests、maven_remote_files、npm_remote_files)と、npm_metadata_files/npm_remote_metadata_filesにも適用されます。キャッシュされた参照もカバーするには、UNION ALLをそれらのテーブルに拡張してください。
コンテナリモートリポジトリ
リモートリポジトリは、プロキシおよびキャッシュできる外部のコンテナレジストリを表します。これらは独自のライフサイクルを持つスタンドアロンエンティティであり、複数の仮想リポジトリ間で共有できます。これらは親の repositories テーブルを介して仮想リポジトリの上流から参照されます。
erDiagram
repositories ||--|| container_remote_repositories : "has one"
container_remote_repositories ||--o{ container_remote_images : "has many"
container_remote_images ||--o{ container_remote_blobs : "has many"
container_remote_images ||--o{ container_remote_manifests : "has many"
container_remote_images ||--o{ container_remote_manifest_relationships : "has many"
container_remote_images ||--o{ container_remote_tags : "has many"
container_remote_tags ||--|| container_remote_manifests : "has one"
container_remote_blobs ||--|| blob_storage_attachments : "has one"
container_remote_manifests ||--|| blob_storage_attachments : "has one"
container_remote_manifest_relationships ||--|| container_remote_manifests : "has one (parent_id)"
container_remote_manifest_relationships ||--|| container_remote_manifests : "has one (child_id)"
container_remote_repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
text url "NOT NULL, limit 1024"
text auth_url "nullable, limit 1024"
bytea wrapped_dek "nullable, this row's data-encryption key, wrapped by the namespace key"
uuid ns_key_id FK "nullable, (ns_key_id, namespace_id) references namespace_encryption_keys(id, namespace_id), ON DELETE RESTRICT"
int ns_key_version "nullable, the namespace key version this row was wrapped under"
bytea encrypted_username
bytea encrypted_password
smallint cache_validity_hours "NOT NULL, DEFAULT 24"
smallint last_health_status "NOT NULL, DEFAULT 0, 0=unknown, 1=healthy, 2=unhealthy"
timestamptz last_health_checked_at "nullable"
}
container_remote_images {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_remote_repository_id FK "NOT NULL, (container_remote_repository_id, namespace_id) references container_remote_repositories(id, namespace_id)"
text name "NOT NULL, limit 255"
timestamptz last_downloaded_at "nullable, buffered"
timestamptz soft_deleted_at "nullable"
}
container_remote_blobs {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_remote_image_id FK "NOT NULL, (container_remote_image_id, namespace_id) references container_remote_images(id, namespace_id)"
bytea digest "NOT NULL, CHECK octet_length = 32"
text media_type "NOT NULL, limit 255"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, CHECK octet_length = 32, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
timestamptz soft_deleted_at "nullable"
}
container_remote_manifests {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_remote_image_id FK "NOT NULL, (container_remote_image_id, namespace_id) references container_remote_images(id, namespace_id)"
bytea digest "NOT NULL, CHECK octet_length = 32"
text media_type "NOT NULL, limit 255"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, CHECK octet_length = 32, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
bigint size "NOT NULL, updated as children are cached"
timestamptz soft_deleted_at "nullable"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}
container_remote_manifest_relationships {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_remote_image_id FK "NOT NULL, (container_remote_image_id, namespace_id) references container_remote_images(id, namespace_id)"
uuid parent_container_remote_manifest_id FK "NOT NULL, (parent_container_remote_manifest_id, namespace_id) references container_remote_manifests(id, namespace_id)"
uuid child_container_remote_manifest_id FK "NOT NULL, (child_container_remote_manifest_id, namespace_id) references container_remote_manifests(id, namespace_id)"
}
container_remote_tags {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid container_remote_image_id FK "NOT NULL, (container_remote_image_id, namespace_id) references container_remote_images(id, namespace_id)"
uuid container_remote_manifest_id FK "NOT NULL, (container_remote_manifest_id, namespace_id) references container_remote_manifests(id, namespace_id)"
text name "NOT NULL, limit 255"
timestamptz upstream_checked_at "NOT NULL, DEFAULT NOW()"
text upstream_etag "nullable, limit 255"
}- container_remote_repositories: 外部のコンテナレジストリを表します。URL、オプションの認証 URL(
auth_url)、認証情報、キャッシュ TTL(cache_validity_hours)を含みます。ヘルスチェックのステータスは監視のために追跡されます。repository_idを介して親のrepositoriesテーブルを参照します。リモートリポジトリはスタンドアロンであるため、同じリモートを使用する 2 つの仮想リポジトリは 1 つのキャッシュを共有します。上流の認証情報は、それを復号するラップ済みデータ暗号化キーとともに暗号文として保存します。カラムの構成と制約については、暗号化キーを参照してください。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_remote_images: リモートリポジトリ内のキャッシュされたコンテナイメージです。
container_imagesを反映します。last_downloaded_atはキャッシュされたイメージが最後にプルされた時刻を記録し、ホット行の競合を避けるためにバッファ書き込み/非同期書き込み(repositories.downloads_countと同じパターン)を介して維持されます。keep_last_downloaded_atライフサイクルルールとキャッシュ保持の評価に使用されます(ADR-010)。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_remote_blobs: キャッシュされたレイヤーまたは設定 blob です。
HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_remote_manifests: キャッシュされたイメージマニフェストです。
sizeカラムは、このキャッシュが把握しているサブツリーのバイトフットプリントを保持します。すなわち、キャッシュ時のマニフェスト自体のペイロードに加えて、子が到着するにつれて各子のsizeが加わります。イメージマニフェストの場合、値はキャッシュ時に完全です。マニフェストリストと OCI インデックスの場合、子がフェッチされるにつれて段階的にツリー全体のフットプリントに収束し、一部の子が一度もプルされなければ部分的なままになることもあります。この段階的なセマンティクスは遅延リモートキャッシングを反映しています。sizeを完全に保つためだけに積極的に子をフェッチすると、遅延設計が損なわれます。created_atはマニフェストが最初にキャッシュされた時刻を記録し、ホスト型の同等のもの(container_manifests)と同じ公開履歴・時間範囲出所スキャンを支えます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_remote_manifest_relationships: キャッシュされたマルチアーキテクチャのマニフェストリスト関係です。ホスト型の同等のものと同じ構造です。
HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_remote_tags: キャッシュされたタグからマニフェストへのマッピングです。タグは可変なポインターであり、キャッシュ再検証時にタグが新しいマニフェストに再ポイントされることがあります。
upstream_checked_atはタグが上流レジストリに対して最後に検証された時刻を記録し、cache_validity_hoursと比較して再検証が必要かどうかを判断します。upstream_etagは上流が返した ETag を保存し、タグがまだ同じマニフェストを指している場合に完全なマニフェスト解決を回避するための条件付きリクエスト(If-None-Match)を可能にします。マニフェストと blob は暗号学的ハッシュによってコンテンツアドレス指定されるため、鮮度の追跡は不要です。保存されたバイトがダイジェストと一致すれば、コンテンツが正しいことが保証されます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。
インデックス
container_remote_repositories:(namespace_id, repository_id)に対するユニークインデックス — 親参照によってリモートリポジトリを検索します。(namespace_id, ns_key_id)に対するインデックス — 暗号化キーの外部キーを支え、キーの行を削除する際にテーブルを走査せず参照元の行を確認できるようにします。container_remote_images:(namespace_id, container_remote_repository_id, name) WHERE soft_deleted_at IS NULLに対するユニークインデックス — 名前によってキャッシュされたイメージを検索します。部分条件により、ソフト削除後に同じ名前のイメージを再作成できます。container_remote_blobs:(namespace_id, container_remote_image_id, digest) WHERE soft_deleted_at IS NULLに対するユニークインデックス — イメージ内でダイジェストによってキャッシュされた blob を検索します。部分条件により、ソフト削除後に同じダイジェストを再キャッシュできます。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによって blob を検索します。(namespace_id, blob_sha256)に対するインデックス — 保存された blob の sha256 から、それを参照するすべてのキャッシュされた blob への逆引きで、チェックサム検索と脆弱性影響がキャッシュ側の参照もカバーするようにします。このインデックスはリモートキャッシュ固有です。ホスト型のcontainer_blobsテーブルはコンテンツアドレス指定であり、同じ検索を(namespace_id, digest)インデックスで処理するため、これを反映するblob_sha256インデックスはありません。リモートテーブルには、逆引きに再利用できるスタンドアロンの(namespace_id, digest)インデックスもありません。これはプル専用で、blob マウントパスを正当化するものがないためです。そのため、チェックサム検索とサイズ照合クエリがblob_storage_blobs(namespace_id, sha256)と JOIN するカラムであるblob_sha256を直接インデックス化します。container_remote_manifests:(namespace_id, container_remote_image_id, digest) WHERE soft_deleted_at IS NULLに対するユニークインデックス — イメージ内でダイジェストによってキャッシュされたマニフェストを検索します。部分条件により、ソフト削除後に同じダイジェストを再キャッシュできます。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによってマニフェストを検索します。(namespace_id, blob_sha256)に対するインデックス — マニフェストペイロードの保存された blob の sha256 から、それを参照するすべてのキャッシュされたマニフェストへの逆引きで、チェックサム検索と脆弱性影響がキャッシュ側の参照もカバーするようにします。このインデックスはリモートキャッシュ固有です。ホスト型のcontainer_manifestsテーブルには同等のインデックスがありません。また、リモート blob テーブルと同様に、リモートマニフェストテーブルにも再利用できるスタンドアロンの(namespace_id, digest)インデックスがないため、逆引きにはblob_sha256を直接インデックス化します。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — ソフト削除されたキャッシュ済みマニフェストを削除時刻順に一覧し、キャッシュされたコンテナイメージのアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC)に対するインデックス — ネームスペース全体の時系列スキャンで、ホスト型のcontainer_manifestsインデックスを反映し、キャッシュ側の公開履歴と出所をカバーします。ホスト型のインデックスと同じ監査証跡の理由から無条件(soft_deleted_at述語なし)です。container_remote_manifest_relationships:(namespace_id, parent_container_remote_manifest_id, child_container_remote_manifest_id)に対するユニークインデックス — 親子関係の重複を防ぎます。(namespace_id, child_container_remote_manifest_id)に対するインデックス — 特定の子マニフェストのすべての親を見つけます。(namespace_id, container_remote_image_id)に対するインデックス — 特定のイメージのすべてのマニフェスト関係を見つけます。container_remote_tags:(namespace_id, container_remote_image_id, name)に対するユニークインデックス — イメージ内で名前によってタグを検索します。(namespace_id, container_remote_manifest_id)に対するインデックス — 特定のマニフェストを指すすべてのタグを見つけます。
クエリ例
リモートリポジトリを作成する
-- Resolve the default repository collection for the namespace SELECT id FROM repository_collections WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND is_default = true; -- Create the parent repository INSERT INTO repositories (namespace_id, name, format, kind, visibility) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'docker-hub', 0, 2, 1) RETURNING id; -- Link the repository to the repository collection INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <repository_collection_id>, <returned_id>); -- Then create the format-specific record INSERT INTO container_remote_repositories (namespace_id, repository_id, url, wrapped_dek, ns_key_id, ns_key_version, encrypted_username, encrypted_password) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>, 'https://registry.hub.docker.com', $1, $2, $3, $4, $5);キャッシュされたマニフェストが新鮮かどうかを確認する
SELECT crm.digest FROM container_remote_manifests crm JOIN container_remote_tags crt ON crt.container_remote_manifest_id = crm.id AND crt.namespace_id = crm.namespace_id JOIN container_remote_images cri ON crt.container_remote_image_id = cri.id AND crt.namespace_id = cri.namespace_id WHERE cri.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND cri.container_remote_repository_id = '019a1b2c-0789-7abc-8def-000000000789' AND cri.name = 'library/nginx' AND crt.name = 'latest' AND cri.soft_deleted_at IS NULL AND crm.soft_deleted_at IS NULL;ダイジェストでキャッシュされた blob をプルする(blob ストレージへの読み取りパスのショートカット)
SELECT bsb.object_storage_key, bsb.size FROM container_remote_blobs crb JOIN blob_storage_blobs bsb ON bsb.namespace_id = crb.namespace_id AND bsb.sha256 = crb.blob_sha256 WHERE crb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND crb.container_remote_image_id = '019a1b2c-0456-7abc-8def-000000000456' AND crb.digest = 'sha256:abcd1234...'::bytea AND crb.soft_deleted_at IS NULL;
仮想コンテナリポジトリ
erDiagram
repositories ||--|| container_virtual_repositories : "has one"
container_virtual_repositories ||--o{ container_virtual_repository_upstreams : "has many"
container_virtual_repository_upstreams ||--|| repositories : "references upstream"
container_virtual_repository_upstreams ||--o{ container_virtual_upstream_rules : "has many"
container_virtual_repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE CASCADE"
}
container_virtual_repository_upstreams {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
uuid container_virtual_repository_id FK "NOT NULL, (container_virtual_repository_id, namespace_id) references container_virtual_repositories(id, namespace_id) ON DELETE CASCADE"
uuid upstream_repository_id FK "NOT NULL, (upstream_repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE NO ACTION"
int position "NOT NULL, CHECK position >= 1"
}
container_virtual_upstream_rules {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
uuid container_virtual_repository_upstream_id FK "NOT NULL, (container_virtual_repository_upstream_id, namespace_id) references container_virtual_repository_upstreams(id, namespace_id) ON DELETE CASCADE"
smallint rule_type "NOT NULL, 0=allow, 1=deny, CHECK rule_type IN (0, 1)"
text pattern "NOT NULL, CHECK char_length(pattern) <= 255"
smallint target_field "NOT NULL, 0=image, 1=tag, CHECK target_field IN (0, 1)"
}- container_virtual_repositories: コンテナイメージの仮想リポジトリです。名前、可視性、クロスフォーマットクエリのために
repository_idを介して親のrepositoriesテーブルを参照します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_virtual_repository_upstreams: 仮想リポジトリとその上流を結合するテーブルです。各仮想リポジトリは上流の順序付きリストを持ちます。各エントリは
upstream_repository_idを介して上流リポジトリを参照し、これはrepositories(id, namespace_id)を指します。複合外部キー(namespace_id, upstream_repository_id)は、上流が同じネームスペース内にあることを強制します。これはレジストリがネームスペースにスコープされること(ADR-001)と一貫しています。positionは 1 始まりで連続します。CHECK position >= 1が下限を定め、管理サーフェスは一覧を変更するトランザクション内で番号を振り直すため、値に隙間はありません。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - container_virtual_upstream_rules: 上流の許可/拒否フィルタールールを定義します。各ルールは、この上流を通じて解決する際にどのアーティファクトが含まれるか除外されるかを制御するために、ワイルドカードパターンと対象フィールドを指定します。パターンは MVP ではワイルドカードのみです。正規表現のサポートは顧客のフィードバックがそれを正当化するまで延期されます(ディスカッション)。ルールは(リモートリポジトリごとではなく)上流参照ごとのままで、包含/除外パターンが仮想-上流の関連付けごとに設定される JFrog モデルと一致します。データベースは 2 つの列挙型とパターン長を、
CHECK rule_type IN (0, 1)、CHECK target_field IN (0, 1)、CHECK char_length(pattern) <= 255で強制します。これは境界での拒否であり、範囲外の識別子は読み取りパスに処理を任せず、書き込み時に拒否されます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。
これら 3 つのテーブルの外部キーには、次の参照アクションがあります。
container_virtual_repositoriesのrepositoriesを参照する FK(repository_id, namespace_id): フォーマット子テーブルの一般ルールで定められたON DELETE CASCADE。container_virtual_repositoriesのnamespacesを参照する FKnamespace_id:NO ACTION。container_virtual_repository_upstreamsのnamespacesを参照する FKnamespace_id:NO ACTION。container_virtual_repository_upstreamsのcontainer_virtual_repositoriesを参照する FK(container_virtual_repository_id, namespace_id):ON DELETE CASCADE。container_virtual_repository_upstreamsのrepositoriesを参照する FK(upstream_repository_id, namespace_id):NO ACTION。container_virtual_upstream_rulesのnamespacesを参照する FKnamespace_id:NO ACTION。container_virtual_upstream_rulesのcontainer_virtual_repository_upstreamsを参照する FK(container_virtual_repository_upstream_id, namespace_id):ON DELETE CASCADE。
container_virtual_repository_upstreams の 2 つの外部キーが反対のアクションを取るのは意図的です。container_virtual_repository_id は親から子へのリンクなので、仮想リポジトリを削除すると upstream の関連付けも削除されます。upstream_repository_id は兄弟参照なので、まだ upstream として一覧に含まれるリポジトリの削除は拒否されます。そこでカスケードすると、一覧に含めるすべての仮想リポジトリが縮小し、空になる可能性もありますが、オペレーターには見えません。キーはシグナルを出さずに縮小を拒否するため、リポジトリの削除パスがそれを扱います。upstream の削除は意図的な管理操作でなければならず、以下の逆引きインデックスもそのために存在します。
インデックス
container_virtual_repositories:(namespace_id, repository_id)に対するユニークインデックス — 親参照によって仮想リポジトリを検索します。container_virtual_repository_upstreams:(namespace_id, container_virtual_repository_id, position)に対するDEFERRABLE INITIALLY DEFERREDユニーク制約 — 仮想リポジトリの順序付き上流を取得します。トランザクション内での並べ替えを可能にするために遅延可能(deferrable)です。(namespace_id, container_virtual_repository_id, upstream_repository_id)に対するユニークインデックス — 同じ上流が仮想リポジトリに二度追加されるのを防ぎます。(namespace_id, upstream_repository_id)に対するインデックス — 特定のリポジトリを upstream として一覧に含めるすべての仮想リポジトリを検索します。これは、上記のNO ACTIONガードと関連付けサーフェスが読み取るものです。どちらのユニークエントリにも、この列順を使用可能な接頭辞として持つものはなく、PostgreSQL は外部キーカラムを自動的にインデックス化しません。container_virtual_upstream_rules:(namespace_id, container_virtual_repository_upstream_id)に対するインデックス — 特定の上流のすべてのルールを取得します。
順序付き upstream のエントリはユニークインデックスではなくユニーク制約です。遅延は制約の属性なので、ALTER TABLE ... ADD CONSTRAINT ... UNIQUE (...) DEFERRABLE INITIALLY DEFERRED で追加します。PostgreSQL では CREATE UNIQUE INDEX ... DEFERRABLE は構文エラーです。npm の仮想テーブルには同じ方法で追加された unique_nvru_ns_id_repository_id_position がすでにあります。コンテナのマイグレーションはパターンを新設せず、兄弟のものをコピーします。ただし、npm では 0、ここでは 1 である position の下限はコピーしません。遅延可能なユニーク制約は ON CONFLICT の調停にも使えないため、position の書き込みはこのキーへの upsert ではなく、UPDATE の後に INSERT を行います。
クエリ例
仮想リポジトリを作成する
-- First create the parent repository INSERT INTO repositories (namespace_id, name, format, kind, visibility) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'my-virtual-repo', 0, 1, 1) RETURNING id; -- Link the repository to a repository collection INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>); -- Then create the format-specific record INSERT INTO container_virtual_repositories (namespace_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>);仮想リポジトリを上流に関連付ける
INSERT INTO container_virtual_repository_upstreams (namespace_id, container_virtual_repository_id, upstream_repository_id, position) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0123-7abc-8def-000000000123', '019a1b2c-0789-7abc-8def-000000000789', 1);
Maven リポジトリ
Maven パッケージは、ファイルの集合(.jar、.pom、maven-metadata.xml)を表します。したがって、単一の Maven パッケージのダウンロードは 4 〜 15 個の API リクエストを表すことがあります。
erDiagram
repositories ||--|| maven_repositories : "has one"
maven_repositories ||--o{ maven_packages : "has many"
maven_packages ||--o{ maven_versions : "has many"
maven_packages ||--o{ maven_files : "has many"
maven_versions ||--o{ maven_files : "has many"
maven_files ||--|| blob_storage_attachments : "has one"
maven_repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
}
maven_packages {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid maven_repository_id FK "NOT NULL, (maven_repository_id, namespace_id) references maven_repositories(id, namespace_id)"
text group_id "NOT NULL, limit 255"
text artifact_id "NOT NULL, limit 255"
timestamptz last_downloaded_at "nullable, buffered"
timestamptz soft_deleted_at "nullable"
}
maven_versions {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid maven_package_id FK "NOT NULL, (maven_package_id, namespace_id) references maven_packages(id, namespace_id)"
text version "NOT NULL, limit 255"
bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
timestamptz last_downloaded_at "nullable, buffered"
text gitlab_user_id "nullable, opaque string, limit 255"
text gitlab_project_id "nullable, opaque string, limit 255"
bytea gitlab_git_commit_sha "nullable"
timestamptz soft_deleted_at "nullable"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}
maven_files {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid maven_package_id FK "NOT NULL, (maven_package_id, namespace_id) references maven_packages(id, namespace_id)"
uuid maven_version_id FK "nullable, (maven_version_id, namespace_id) references maven_versions(id, namespace_id)"
text file_name "NOT NULL, limit 255"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
bytea sha1 "NOT NULL"
bytea md5 "nullable"
bytea sha512 "NOT NULL"
timestamptz soft_deleted_at "nullable"
}- maven_repositories: 複数のパッケージのコンテナです。各リポジトリはグループ ID とアーティファクト ID で識別される複数のパッケージをホストできます。名前、可視性、クロスフォーマットクエリのために
repository_idを介して親のrepositoriesテーブルを参照します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - maven_packages: そのグループ ID とアーティファクト ID で識別される Maven パッケージ(例:
com.example:myapp)を表します。last_downloaded_atはパッケージのいずれかのファイルが最後にダウンロードされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。NULLはパッケージが一度もダウンロードされていないことを意味し、keep_last_downloaded_atライフサイクルルールの評価では可能な限り最も古いダウンロード時刻として扱われます(すなわち、ダウンロードベースの保持の下で削除の対象となります)。keep_last_downloaded_atライフサイクルルールによって、ダウンロードベースの保持を評価するために使用されます(ADR-010)。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - maven_versions: Maven パッケージの個々の バージョン(例:
1.0.0、2.1.3-SNAPSHOT)を保存します。last_downloaded_atはバージョンのいずれかのファイルが最後にダウンロードされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。keep_last_downloaded_atライフサイクルルールによって使用されます。gitlab_user_id、gitlab_project_id、gitlab_git_commit_shaは、どの GitLab ユーザーがこのバージョンを公開したか、および公開の背後にある CI コンテキスト(プロジェクト、コミット)を記録し、container_manifestsの同等のカラムと同じ形状・根拠によります。created_atはバージョンが最初に公開された時刻を記録し、container_manifestsと同じ公開履歴・時間範囲出所スキャンを支えます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - maven_files: Maven パッケージに関連付けられた個々のファイルを表します。ファイルは、
maven_version_idが設定されたバージョン固有のもの(JAR、POM、ソース、Javadoc、チェックサム)か、maven_version_idが NULL のパッケージレベルのもの(maven-metadata.xmlとそのチェックサムなど)のいずれかです。maven_package_idは常に設定されており、パッケージからそのすべてのファイルへの直接のパスを提供します。また、レジストリがパフォーマンスのボトルネックを改善するために使用する補助ファイルである場合もあります。sha1とmd5カラムは、整合性検証のために Maven プロトコルが要求するチェックサム を保存します。Maven クライアントは、すべてのアーティファクトとともに.sha1と.md5のサイドカーファイルを期待します。これらのカラムはblob_storage_blobsではなくmaven_filesにあります。なぜなら、これらは Maven プロトコルの関心事であり、普遍的な blob プロパティではないためです。他のフォーマット(OCI コンテナ)は SHA256 のみを使用します。これらをここに置くことで、blob_storage_blobsをフォーマット固有のカラムやインデックスを持たないフォーマット非依存のテーブルとして保ちます。sha1は Maven プロトコルがそれを要求するためNOT NULLです。md5は Maven 3.9+ が MD5 チェックサムを非推奨にした ため nullable です。sha512はNOT NULLです。なぜなら、Maven プロトコルはレジストリが提供できなければならない.sha512サイドカーを公開しており、その値はバイトが永続化される前にハンドラーを通過する際のアップロード中に常に計算可能だからです。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。
私たちは、パッケージ名(この場合はグループ ID とアーティファクト ID)とバージョンを同じテーブルに保存していません。その理由は、UI がこのデータにパッケージ名でアクセスするためです。パッケージ名がフォルダであり、それを開くと各バージョンに 1 つずつサブフォルダがあるツリー状の UI を想像してください。この最初のリクエストはフォルダ、すなわちパッケージ名を一覧する必要があります。フォルダを開くと、すべてのサブフォルダ、すなわちパッケージバージョンを一覧するリクエストがトリガーされます。したがって、このアクセスパターンを容易にするために、2 つの専用テーブル(maven_packages と maven_versions)を持っています。
インデックス
maven_repositories:(namespace_id, repository_id)に対するユニークインデックス — 親リポジトリ参照によって Maven リポジトリを検索します。maven_packages:(namespace_id, maven_repository_id, group_id, artifact_id) WHERE soft_deleted_at IS NULLに対するユニークインデックス — リポジトリ内で Maven 座標によってパッケージを検索します。部分条件により、ソフト削除後に同じ座標のパッケージを再作成できます。(namespace_id, maven_repository_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULLに対するインデックス —keep_last_downloaded_atライフサイクルルールの評価をサポートします。リポジトリ内のすべてのパッケージをスキャンして行ごとにフィルターするのではなく、境界のある範囲スキャンによって期限切れになったパッケージのみを返します。NULLS FIRSTは、一度もダウンロードされていないパッケージを最も古い行とグループ化し、両方が同じ範囲スキャンで返されるようにします。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — ソフト削除されたパッケージを削除時刻順に一覧し、reaper のパッケージレベルスキャンを支えます。maven_versionsの同形インデックスでは対応できません。パッケージ削除はパッケージ行をマークしますが子行を書き込まないため、tombstone が付いたパッケージ配下のバージョンは自身のsoft_deleted_atを持たず、リポジトリが生存している間はバージョンレベルスキャンでもリポジトリ走査でもそのパッケージに到達できません。maven_versions:(namespace_id, maven_package_id, version) WHERE soft_deleted_at IS NULLに対するユニークインデックス — パッケージ内の特定のバージョンを検索します。この部分条件により、ソフト削除後に同じ識別子でバージョンを再作成できます。(namespace_id, maven_package_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULLに対するインデックス —maven_packagesと同じ範囲スキャン戦略を使用し、パッケージのバージョンにスコープされたkeep_last_downloaded_atライフサイクルルールの評価を支えます。(namespace_id, maven_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULLに対するインデックス — ランディングページのリポジトリソートと同様に、バージョン一覧表示でパッケージのバージョンをサイズ順に並べます。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — ソフト削除されたバージョンを削除時刻順に一覧し、Maven アーティファクトに対するアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC)に対するインデックス — ネームスペース全体を時系列でスキャンし、公開履歴のページネーションと時間範囲を指定したアーティファクトの来歴クエリを支えます。ソフト削除された公開イベントも監査証跡に表示されるよう、無条件のインデックスとします。maven_files:(namespace_id, maven_version_id, file_name) WHERE soft_deleted_at IS NULL AND maven_version_id IS NOT NULLに対するユニークインデックス — バージョン固有のファイル名はバージョン内で一意でなければなりません。部分条件はソフト削除された行とパッケージレベルのファイルを除外します。(namespace_id, maven_package_id, file_name) WHERE soft_deleted_at IS NULL AND maven_version_id IS NULLに対するユニークインデックス — パッケージレベルのファイル名(maven-metadata.xmlなど)はパッケージ内で一意でなければなりません。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによってファイルを検索します。(namespace_id, blob_sha256)に対するインデックス — 保存された blob の sha256 から、それを参照するすべての Maven ファイルへの逆引きで、クロスフォーマットのチェックサム検索を支えます。既存の親をキーとするインデックスはバージョンまたはパッケージをキーとするため、ダイジェストをキーとするスキャンを直接満たすことはできません。
クエリ例
特定のリポジトリ ID とパッケージ名のパッケージバージョンを取得する。
SELECT mv.* FROM maven_versions mv JOIN maven_packages mp ON mv.maven_package_id = mp.id AND mv.namespace_id = mp.namespace_id WHERE mp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mp.maven_repository_id = '019a1b2c-0123-7abc-8def-000000000123' AND mp.group_id = 'com.example' AND mp.artifact_id = 'myapp' AND mv.version = '1.0.0' AND mp.soft_deleted_at IS NULL AND mv.soft_deleted_at IS NULL;バージョン ID とファイル名でファイルを取得する。
SELECT mf.* FROM maven_files mf WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.maven_version_id = '019a1b2c-0456-7abc-8def-000000000456' AND mf.file_name = 'myapp-1.0.0.jar' AND mf.soft_deleted_at IS NULL;特定のパッケージのパッケージレベルのファイル(例:
maven-metadata.xml)を取得する。SELECT mf.* FROM maven_files mf WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.maven_package_id = '019a1b2c-0123-7abc-8def-000000000123' AND mf.maven_version_id IS NULL AND mf.soft_deleted_at IS NULL;ゴミ箱一覧: ネームスペース内のすべてのソフト削除された Maven バージョンを、最近削除された順に一覧する(
(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLの部分インデックスを使用)。コンプライアンスのユースケースはネームスペース全体(「今ゴミ箱に何があるか?」)です。親にスコープしたビュー(「このパッケージのゴミ箱に入ったバージョン」)には、別個の(namespace_id, maven_package_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLインデックスが有益で、その UI が構築される場合には後から追加できます。同じパターンはnpm_versions、container_manifests、およびそれらのリモート版に適用されます。SELECT mv.id, mv.maven_package_id, mv.version, mv.soft_deleted_at FROM maven_versions mv WHERE mv.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mv.soft_deleted_at IS NOT NULL ORDER BY mv.soft_deleted_at DESC LIMIT 50;
Maven リモートリポジトリ
erDiagram
repositories ||--|| maven_remote_repositories : "has one"
maven_remote_repositories ||--o{ maven_remote_packages : "has many"
maven_remote_packages ||--o{ maven_remote_versions : "has many"
maven_remote_packages ||--o{ maven_remote_files : "has many"
maven_remote_versions ||--o{ maven_remote_files : "has many"
maven_remote_files ||--|| blob_storage_attachments : "has one"
maven_remote_repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
text url "NOT NULL, limit 1024"
bytea wrapped_dek "nullable, this row's data-encryption key, wrapped by the namespace key"
uuid ns_key_id FK "nullable, (ns_key_id, namespace_id) references namespace_encryption_keys(id, namespace_id), ON DELETE RESTRICT"
int ns_key_version "nullable, the namespace key version this row was wrapped under"
bytea encrypted_username
bytea encrypted_password
smallint cache_validity_hours "NOT NULL, DEFAULT 24"
smallint metadata_cache_validity_hours "NOT NULL, DEFAULT 24"
smallint last_health_status "NOT NULL, DEFAULT 0, 0=unknown, 1=healthy, 2=unhealthy"
timestamptz last_health_checked_at "nullable"
}
maven_remote_packages {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid maven_remote_repository_id FK "NOT NULL, (maven_remote_repository_id, namespace_id) references maven_remote_repositories(id, namespace_id)"
text group_id "NOT NULL, limit 255"
text artifact_id "NOT NULL, limit 255"
timestamptz last_downloaded_at "nullable, buffered"
timestamptz soft_deleted_at "nullable"
}
maven_remote_versions {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid maven_remote_package_id FK "NOT NULL, (maven_remote_package_id, namespace_id) references maven_remote_packages(id, namespace_id)"
text version "NOT NULL, limit 255"
bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
timestamptz last_downloaded_at "nullable, buffered"
timestamptz soft_deleted_at "nullable"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}
maven_remote_files {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid maven_remote_package_id FK "NOT NULL, (maven_remote_package_id, namespace_id) references maven_remote_packages(id, namespace_id)"
uuid maven_remote_version_id FK "nullable, (maven_remote_version_id, namespace_id) references maven_remote_versions(id, namespace_id)"
text file_name "NOT NULL, limit 255"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
bytea sha1 "NOT NULL"
bytea md5 "nullable"
bytea sha512 "NOT NULL"
timestamptz upstream_checked_at "NOT NULL, DEFAULT NOW()"
text upstream_etag "nullable, limit 255"
timestamptz soft_deleted_at "nullable"
}- maven_remote_repositories: 外部の Maven リポジトリを表します。URL、認証情報、アーティファクトのキャッシュ TTL(
cache_validity_hours)、およびmaven-metadata.xmlなどのメタデータレスポンス用の別個の TTL(metadata_cache_validity_hours)を含みます。ヘルスチェックのステータスは監視のために追跡されます。repository_idを介して親のrepositoriesテーブルを参照します。上流の認証情報は、それを復号するラップ済みデータ暗号化キーとともに暗号文として保存します。カラムの構成と制約については、暗号化キーを参照してください。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - maven_remote_packages: グループ ID とアーティファクト ID で識別される、キャッシュされた Maven パッケージです。
maven_packagesを反映します。last_downloaded_atはパッケージのいずれかのキャッシュされたファイルが最後にダウンロードされた時刻を記録し、ホット行の競合を避けるためにバッファ書き込み/非同期書き込みを介して維持されます。keep_last_downloaded_atライフサイクルルールとキャッシュ保持の評価に使用されます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - maven_remote_versions: Maven パッケージのキャッシュされたバージョンです。
maven_versionsを反映します。last_downloaded_atはバージョンのいずれかのキャッシュされたファイルが最後にダウンロードされた時刻を記録し、ホット行の競合を避けるためにバッファ書き込み/非同期書き込みを介して維持されます。keep_last_downloaded_atライフサイクルルールとキャッシュ保持の評価に使用されます。created_atはバージョンが最初にキャッシュされた時刻を記録し、maven_versionsを反映してキャッシュ側の公開履歴と出所スキャンを支えます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - maven_remote_files: キャッシュされたファイル(JAR、POM、チェックサム、
maven-metadata.xml)です。NULL 許容のmaven_remote_version_idは、ホスト型と同じパターン(バージョン固有のファイル対maven-metadata.xmlのようなパッケージレベルのファイル)を保ちます。sha1とmd5は、コンテンツがホストされているかキャッシュされているかに関係なく Maven プロトコルがこれらのチェックサムの提供を要求するため保持されます。sha512は、ホスト型のmaven_filesのカラム形状を反映するために整合性の観点から追加され、Maven Virtual 仕様(S30)が単一のクエリパスでどちらのバックエンドからも.sha512サイドカーを提供できるようにします。値はプロキシ書き込みステップ中に他のチェックサムとともにキャッシュされたバイトから計算されるため、初日からNOT NULLを達成できます。upstream_checked_atはファイルが上流リポジトリに対して最後に検証された時刻を記録し、アーティファクトファイルについてはcache_validity_hours、メタデータファイル(例:maven-metadata.xml)についてはmetadata_cache_validity_hoursと比較して再検証が必要かどうかを判断します。upstream_etagは上流が返した ETag を保存し、変更されていないファイルの再ダウンロードを回避するための条件付きリクエスト(If-None-Match)を可能にします。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。
インデックス
maven_remote_repositories:(namespace_id, repository_id)に対するユニークインデックス — 親参照によってリモートリポジトリを検索します。(namespace_id, ns_key_id)に対するインデックス — 暗号化キーの外部キーを支え、キーの行を削除する際にテーブルを走査せず参照元の行を確認できるようにします。maven_remote_packages:(namespace_id, maven_remote_repository_id, group_id, artifact_id) WHERE soft_deleted_at IS NULLに対するユニークインデックス — Maven 座標によってキャッシュされたパッケージを検索します。部分条件により、ソフト削除後に同じ座標のパッケージを再作成できます。maven_remote_versions:(namespace_id, maven_remote_package_id, version) WHERE soft_deleted_at IS NULLに対するユニークインデックス — パッケージ内のキャッシュされたバージョンを検索します。この部分条件により、ソフト削除後に同じ識別子でバージョンを再作成できます。(namespace_id, maven_remote_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULLに対するインデックス — バージョン一覧表示で、キャッシュされたパッケージのバージョンをサイズ順に並べます。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — ソフト削除されたキャッシュ済みバージョンを削除時刻順に一覧し、キャッシュされた Maven アーティファクトに対するアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC)に対するインデックス — ネームスペース全体を時系列でスキャンします。ローカルのmaven_versionsインデックスを反映し、キャッシュ側の公開履歴と来歴を対象にします。ローカルインデックスと同じ監査証跡上の理由から、無条件(soft_deleted_atの述語なし)のインデックスとします。maven_remote_files:(namespace_id, maven_remote_version_id, file_name) WHERE soft_deleted_at IS NULL AND maven_remote_version_id IS NOT NULLに対するユニークインデックス — バージョン固有のファイル名は、バージョン内で一意でなければなりません。(namespace_id, maven_remote_package_id, file_name) WHERE soft_deleted_at IS NULL AND maven_remote_version_id IS NULLに対するユニークインデックス — パッケージレベルのファイル名は、パッケージ内で一意でなければなりません。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによってファイルを検索します。(namespace_id, blob_sha256)に対するインデックス — 保存済み blob の sha256 から、それを参照するすべてのキャッシュ済み Maven ファイルを逆引きします。ローカルのmaven_filesインデックスを反映し、チェックサム検索でキャッシュ側の参照も対象にします。
クエリ例
リモートリポジトリを作成する
-- First create the parent repository INSERT INTO repositories (namespace_id, name, format, kind, visibility) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'central', 1, 2, 0) RETURNING id; -- Link the repository to a repository collection INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>); -- Then create the format-specific record INSERT INTO maven_remote_repositories (namespace_id, repository_id, url, wrapped_dek, ns_key_id, ns_key_version, encrypted_username, encrypted_password) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>, 'https://repo.maven.apache.org/maven2', $1, $2, $3, $4, $5);座標でキャッシュされた Maven ファイルを検索する
SELECT mrf.*, bsb.object_storage_key FROM maven_remote_files mrf JOIN maven_remote_versions mrv ON mrf.maven_remote_version_id = mrv.id AND mrf.namespace_id = mrv.namespace_id JOIN maven_remote_packages mrp ON mrv.maven_remote_package_id = mrp.id AND mrv.namespace_id = mrp.namespace_id JOIN blob_storage_blobs bsb ON bsb.namespace_id = mrf.namespace_id AND bsb.sha256 = mrf.blob_sha256 WHERE mrp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mrp.maven_remote_repository_id = '019a1b2c-0789-7abc-8def-000000000789' AND mrp.group_id = 'com.example' AND mrp.artifact_id = 'myapp' AND mrv.version = '1.0.0' AND mrf.file_name = 'myapp-1.0.0.jar' AND mrp.soft_deleted_at IS NULL AND mrv.soft_deleted_at IS NULL AND mrf.soft_deleted_at IS NULL;パッケージのキャッシュされた
maven-metadata.xmlを検索するSELECT mrf.* FROM maven_remote_files mrf JOIN maven_remote_packages mrp ON mrf.maven_remote_package_id = mrp.id AND mrf.namespace_id = mrp.namespace_id WHERE mrp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mrp.maven_remote_repository_id = '019a1b2c-0789-7abc-8def-000000000789' AND mrp.group_id = 'com.example' AND mrp.artifact_id = 'myapp' AND mrf.maven_remote_version_id IS NULL AND mrf.file_name = 'maven-metadata.xml' AND mrp.soft_deleted_at IS NULL AND mrf.soft_deleted_at IS NULL;
Maven 仮想リポジトリ
erDiagram
repositories ||--|| maven_virtual_repositories : "has one"
maven_virtual_repositories ||--o{ maven_virtual_repository_upstreams : "has many"
maven_virtual_repository_upstreams ||--|| repositories : "references upstream"
maven_virtual_repository_upstreams ||--o{ maven_virtual_upstream_rules : "has many"
maven_virtual_repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
}
maven_virtual_repository_upstreams {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid maven_virtual_repository_id FK "NOT NULL, (maven_virtual_repository_id, namespace_id) references maven_virtual_repositories(id, namespace_id)"
uuid upstream_repository_id FK "NOT NULL, (upstream_repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE NO ACTION"
int position "NOT NULL"
}
maven_virtual_upstream_rules {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid maven_virtual_repository_upstream_id FK "NOT NULL, (maven_virtual_repository_upstream_id, namespace_id) references maven_virtual_repository_upstreams(id, namespace_id)"
smallint rule_type "NOT NULL, 0=allow, 1=deny"
text pattern "NOT NULL, limit 255"
smallint target_field "NOT NULL, 0=group_id, 1=artifact_id, 2=version"
}- maven_virtual_repositories: Maven パッケージの仮想リポジトリです。名前、可視性、クロスフォーマットクエリのために
repository_idを介して親のrepositoriesテーブルを参照します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - maven_virtual_repository_upstreams: 仮想リポジトリとその上流を結合するテーブルです。各仮想リポジトリは上流の順序付きリストを持ちます。各エントリは
upstream_repository_idを介して上流リポジトリを参照し、これはrepositories(id, namespace_id)を指します。複合外部キー(namespace_id, upstream_repository_id)は、上流が同じネームスペース内にあることを強制します。これはレジストリがネームスペースにスコープされること(ADR-001)と一貫しています。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - maven_virtual_upstream_rules: 上流の許可/拒否フィルタールールを定義します。各ルールは、この上流を通じて解決する際にどのアーティファクトが含まれるか除外されるかを制御するために、ワイルドカードパターンと対象フィールドを指定します。パターンは MVP ではワイルドカードのみです。正規表現のサポートは顧客のフィードバックがそれを正当化するまで延期されます(ディスカッション)。
HASH(namespace_id)で 64 パーティションにパーティショニングされます。
インデックス
maven_virtual_repositories:(namespace_id, repository_id)に対するユニークインデックス — 親参照によって仮想リポジトリを検索します。maven_virtual_repository_upstreams:(namespace_id, maven_virtual_repository_id, position)に対するDEFERRABLE INITIALLY DEFERREDユニーク制約 — 仮想リポジトリの順序付き上流を取得します。トランザクション内での並べ替えを可能にするために遅延可能(deferrable)です。(namespace_id, maven_virtual_repository_id, upstream_repository_id)に対するユニークインデックス — 同じ上流が仮想リポジトリに二度追加されるのを防ぎます。(namespace_id, upstream_repository_id)に対するインデックス — 特定のリポジトリを upstream として一覧に含めるすべての仮想リポジトリを検索します。これは、上記のNO ACTIONガードが読み取るものです。どちらのユニークエントリにも、この列順を使用可能な接頭辞として持つものはなく、PostgreSQL は外部キーカラムを自動的にインデックス化しません。maven_virtual_upstream_rules:(namespace_id, maven_virtual_repository_upstream_id)に対するインデックス — 特定の上流のすべてのルールを取得します。
クエリ例
仮想リポジトリを作成する
-- First create the parent repository INSERT INTO repositories (namespace_id, name, format, kind, visibility) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'my-virtual-repo', 1, 1, 1) RETURNING id; -- Link the repository to a repository collection INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>); -- Then create the format-specific record INSERT INTO maven_virtual_repositories (namespace_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>);仮想リポジトリを上流に関連付ける
INSERT INTO maven_virtual_repository_upstreams (namespace_id, maven_virtual_repository_id, upstream_repository_id, position) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0123-7abc-8def-000000000123', '019a1b2c-0789-7abc-8def-000000000789', 1);
NPM リポジトリ
Node パッケージは基本的に .tar.gz ファイルであり、各バージョンが単一のアーカイブです。ただし、Node クライアントはより豊富な機能セットを持ち、例えば私たちが扱う必要のあるディストリビューションタグの使用などがあります。
erDiagram
repositories ||--|| npm_repositories : "has one"
npm_repositories ||--o{ npm_packages : "has many"
npm_packages ||--o{ npm_versions : "has many"
npm_packages ||--o{ npm_tags : "has many"
npm_versions ||--o{ npm_files : "has many"
npm_tags ||--|| npm_versions : "has one"
npm_packages ||--o{ npm_metadata_files : "has many"
npm_files ||--|| blob_storage_attachments : "has one"
npm_metadata_files ||--|| blob_storage_attachments : "has one"
npm_repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
}
npm_packages {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_repository_id FK "NOT NULL, (npm_repository_id, namespace_id) references npm_repositories(id, namespace_id)"
text name "NOT NULL, limit 255"
text scope "nullable, limit 255"
integer versions_count "NOT NULL, DEFAULT 0, buffered counter"
integer tags_count "NOT NULL, DEFAULT 0, buffered counter"
timestamptz last_downloaded_at "nullable, buffered"
timestamptz soft_deleted_at "nullable"
}
npm_versions {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_package_id FK "NOT NULL, (npm_package_id, namespace_id) references npm_packages(id, namespace_id)"
text version "NOT NULL, limit 255"
jsonb package_json "NOT NULL"
bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
timestamptz last_downloaded_at "nullable, buffered"
text gitlab_user_id "nullable, opaque string, limit 255"
text gitlab_project_id "nullable, opaque string, limit 255"
bytea gitlab_git_commit_sha "nullable"
timestamptz soft_deleted_at "nullable"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}
npm_tags {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_package_id FK "NOT NULL, (npm_package_id, namespace_id) references npm_packages(id, namespace_id)"
uuid npm_version_id FK "NOT NULL, (npm_version_id, namespace_id) references npm_versions(id, namespace_id)"
text name "NOT NULL, limit 255"
}
npm_files {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_version_id FK "NOT NULL, (npm_version_id, namespace_id) references npm_versions(id, namespace_id)"
text file_name "NOT NULL, limit 255"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
timestamptz soft_deleted_at "nullable"
}
npm_metadata_files {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_package_id FK "NOT NULL, (npm_package_id, namespace_id) references npm_packages(id, namespace_id)"
smallint kind "NOT NULL, 0=full, 1=dist_tags, 2=abbreviated"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
timestamptz expires_at "NOT NULL"
}- npm_repositories: 複数のパッケージのコンテナです。各リポジトリはオプションのスコープを持つ複数のパッケージをホストできます。名前、可視性、クロスフォーマットクエリのために
repository_idを介して親のrepositoriesテーブルを参照します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_packages: npm パッケージを表します。
nameカラムはスコープを含む完全なパッケージ名(例:@myorg/mypackageまたはlodash)を保存します。versions_countはソフト削除されたものを含むパッケージのnpm_versions行をカウントし、ガベージコレクションが行をハード削除したときにのみデクリメントします。tags_countはそのnpm_tags行をカウントします(npm_tagsにはソフト削除カラムがないため、その問題は発生しません)。両方とも、ADR-004 のパッケージごとのエンティティ数制限(25,000 バージョン、1,000 タグ)を強制するバッファカウンターであり、バッファ書き込み/非同期書き込み を介して維持されます。ソフト削除されたバージョンを含めることはnamespace_statistics.deduplicated_size_bytesの扱いを反映し、不正利用のベクトルを塞ぎます。すなわち、上限からソフト削除された行を除外できる顧客は、繰り返しソフト削除と再公開を行うことで 25,000 バージョンの制限を無期限に下回り続けることができてしまいますが、ソフト削除されたすべての行は依然としてストレージを占有し、復元可能なままです。両方の上限が 32 ビットの上限を十分に下回るため、bigintではなくinteger型です。他の場所の上限のないカウンター(downloads_count、size_bytes)は無制限に増加するためbigintが必要です。last_downloaded_atはパッケージのいずれかのファイルが最後にダウンロードされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。keep_last_downloaded_atライフサイクルルールによって使用されます。npm の単一バージョン非公開化では、最後のアクティブなバージョンがなくなるとこの行をソフト削除するため、ソフト削除されたパッケージは配下のすべてのバージョンがなくなった後も残り得ます。この非対称性が、このテーブルがバージョンレベルのものに頼らず独自の tombstone インデックスを持つ理由です。maven_packagesは同じ形のインデックスを持ち、逆方向から同じ状態に到達します。Maven のパッケージ削除はパッケージ行をマークし、バージョンを生存させるため、親だけに tombstone が付き、子には付きません(以下のインデックス一覧を参照)。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_versions: 埋め込まれた package.json メタデータとともに、npm パッケージの個々のバージョンを保存します。
last_downloaded_atはバージョンのいずれかのファイルが最後にダウンロードされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。keep_last_downloaded_atライフサイクルルールによって使用されます。gitlab_user_id、gitlab_project_id、gitlab_git_commit_shaは、どの GitLab ユーザーがこのバージョンを公開したか、および公開の背後にある CI コンテキスト(プロジェクト、コミット)を記録し、container_manifestsの同等のカラムと同じ形状・根拠によります。created_atはバージョンが最初に公開された時刻を記録し、container_manifestsと同じ公開履歴・時間範囲出所スキャンを支えます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_tags: 特定のパッケージバージョンを指す NPM ディストリビューションタグ(例:
latest、next、beta)を提供します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_files: npm パッケージバージョンのファイルを表します。これらは主に tarball アーカイブです。また、レジストリがパフォーマンスのボトルネックを改善するために使用する補助ファイルである場合もあります。
HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_metadata_files: npm パッケージの事前計算されたメタデータファイルを、
kindごとに 1 つずつ保存します。kindカラムはメタデータのバリアントを区別します。full(0)はすべてのバージョンを含む完全な packument を含み、dist_tags(1)はディストリビューションタグのマッピングのみを含み、abbreviated(2)はリクエストがAccept: application/vnd.npm.install-v1+jsonを持つときに提供されるインストール専用の射影です。適切なファイルは、クライアントのリクエストに基づいて npm メタデータエンドポイントで提供されます。メタデータがパッケージのすべてのバージョンにまたがるため、(npm_versionsではなく)npm_packagesにリンクされます。メタデータファイルはキャッシュ再構築によって生成されます。書き込みは非同期実行を 1 つ enqueue し、新鮮な行がないことを検出した読み取りは、応答前に同じ再構築をリクエスト goroutine 上で実行します。expires_atカラムはキャッシュの鮮度を駆動します。ライター(公開、非推奨化、非公開化、dist-tag の変更)は、データ書き込みと同じトランザクション内で、影響を受けるパッケージのすべての行にexpires_at = NOW()を設定することでキャッシュを強制的に期限切れにします。再構築は、新しく生成された blob で行を upsert する際にexpires_at = NOW() + npm.packument_cache_ttlを設定します。リーダーはexpires_at > NOW()でフィルターし、ミス時には読み取り時 fill にフォールスルーするため、期限切れの行がクライアントに提供されることはありません。このカラムはハード削除の期限ではなく、キャッシュの鮮度シグナルです。強制的な期限切れは blob とアタッチメントをそのまま残すため、それらに対してすでに解決中のレスポンスは、再構築がアタッチメントを入れ替えるまで正常に完了します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。
Maven と同様に、パッケージ名とバージョンはまったく同じ理由で 2 つの異なるテーブルに保存されます。
インデックス
npm_repositories:(namespace_id, repository_id)に対するユニークインデックス — 親リポジトリ参照によって NPM リポジトリを検索します。npm_packages:(namespace_id, npm_repository_id, name) WHERE soft_deleted_at IS NULLに対するユニークインデックス — リポジトリ内で名前によってパッケージを検索します。部分条件により、ソフト削除後に同じ名前のパッケージを再作成できます。(namespace_id, npm_repository_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULLに対するインデックス —keep_last_downloaded_atライフサイクルルールの評価をサポートします。リポジトリ内のすべてのパッケージをスキャンして行ごとにフィルターするのではなく、境界のある範囲スキャンによって期限切れになったパッケージのみを返します。NULLS FIRSTは、一度もダウンロードされていないパッケージを最も古い行とグループ化し、両方が同じ範囲スキャンで返されるようにします。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — ソフト削除されたパッケージを削除時刻順に一覧します。同じ形のバージョンレベルのインデックスとは異なり、主な目的はゴミ箱の一覧表示ではありません。最後のアクティブなバージョンがなくなったときにソフト削除されたパッケージは、その配下のバージョンより長く残ります。そのため、それらのバージョンがパージされると、バージョンレベルのスキャンでもリポジトリの走査でもパッケージ行に到達できず、このインデックスがなければリーパーはテーブルをシーケンシャルスキャンしてその行を探す必要があります。(namespace_id, npm_repository_id, name) WHERE soft_deleted_at IS NOT NULLに対するインデックス — 公開座標にある tombstone 行のversions_countを合計し、ソフト削除をまたぐ公開前のバージョン上限チェックを支えます。パッケージ全体の非公開化ではカウント全体が tombstone 行に移る一方、部分的なユニーク名前インデックスにより再公開時にはゼロの新しいアクティブ行が挿入されるため、上限のリセットを防ぐには tombstone 行を名前で読む必要があります。npm_versions:(namespace_id, npm_package_id, version) WHERE soft_deleted_at IS NULLに対するユニークインデックス — パッケージ内の特定のバージョンを検索します。この部分条件により、ソフト削除後に同じ識別子でバージョンを再作成できます。(namespace_id, npm_package_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULLに対するインデックス —npm_packagesと同じ範囲スキャン戦略を使用し、パッケージのバージョンにスコープされたkeep_last_downloaded_atライフサイクルルールの評価を支えます。(namespace_id, npm_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULLに対するインデックス — ランディングページのリポジトリソートと同様に、バージョン一覧表示でパッケージのバージョンをサイズ順に並べます。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — ソフト削除されたバージョンを削除時刻順に一覧し、npm アーティファクトに対するアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC)に対するインデックス — ネームスペース全体を時系列でスキャンし、公開履歴のページネーションと時間範囲を指定したアーティファクトの来歴クエリを支えます。ソフト削除された公開イベントも監査証跡に表示されるよう、無条件のインデックスとします。npm_tags:(namespace_id, npm_package_id, name)に対するユニークインデックス — パッケージ内で名前によってディストリビューションタグを検索します。(namespace_id, npm_version_id)に対するインデックス — 特定のバージョンを指すすべてのタグを見つけます。npm_files:(namespace_id, npm_version_id, file_name) WHERE soft_deleted_at IS NULLに対するユニークインデックス — ファイル名はバージョン内で一意でなければなりません。部分条件により、ソフト削除後に同じ名前のファイルを再作成できます。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによってファイルを検索します。(namespace_id, blob_sha256)に対するインデックス — 保存された blob の sha256 から、それを参照するすべての npm ファイルへの逆引きで、クロスフォーマットのチェックサム検索を支えます。既存のバージョンをキーとするインデックスは、ダイジェストをキーとするスキャンを直接満たすことはできません。npm_metadata_files:(namespace_id, npm_package_id, kind)に対するユニークインデックス — パッケージごと・kind ごとに 1 つのメタデータファイル。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによってメタデータファイルを検索します。(namespace_id, blob_sha256)に対するインデックス — 保存された blob の sha256 から、それを参照するすべてのメタデータファイルへの逆引きで、npm_filesを反映し、単一の sha256 検索で tarball と packument スタイルのメタデータの両方をカバーします。
クエリ例
特定のリポジトリ ID とパッケージ名のすべてのバージョンを取得する
SELECT nv.* FROM npm_versions nv JOIN npm_packages np ON nv.npm_package_id = np.id AND nv.namespace_id = np.namespace_id WHERE np.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND np.npm_repository_id = '019a1b2c-0123-7abc-8def-000000000123' AND np.name = '@myorg/mypackage' AND np.soft_deleted_at IS NULL AND nv.soft_deleted_at IS NULL;公開パスの制限事前チェックのためにパッケージごとのエンティティ数カウンターを読み取る(アドバイザリ。
npm_versionsとnpm_tagsの部分ユニークインデックスが、レースのない信頼できるガードです)。SELECT versions_count, tags_count FROM npm_packages WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0456-7abc-8def-000000000456' AND soft_deleted_at IS NULL;バージョン ID とファイル名でファイルを取得する
SELECT nf.* FROM npm_files nf WHERE nf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND nf.npm_version_id = '019a1b2c-0456-7abc-8def-000000000456' AND nf.file_name = 'mypackage-1.0.0.tgz' AND nf.soft_deleted_at IS NULL;パッケージの事前計算された完全メタデータファイルを取得する(npm メタデータエンドポイントで提供)
SELECT bsb.object_storage_key, bsb.size FROM npm_metadata_files nmf JOIN blob_storage_blobs bsb ON bsb.namespace_id = nmf.namespace_id AND bsb.sha256 = nmf.blob_sha256 WHERE nmf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND nmf.npm_package_id = 456 AND nmf.kind = 0 AND nmf.expires_at > NOW();読み取りは
expires_at > NOW()でフィルターします。ミス(行がない、またはライターが 強制的に期限切れにしたか TTL が経過したためにexpires_at <= NOW())では、リクエスト goroutine 上で 下記のキャッシュ再構築を実行し、その再構築がコミットした行を提供します。したがってミスは、レスポンスへ直接 レンダリングされる本文ではなく、キャッシュ書き込みと blob の open で終わります。書き込み時に packument キャッシュを強制的に期限切れにする
公開、非推奨化、非公開化、dist-tag の変更は、データ書き込みと同じトランザクション内で、影響を受ける パッケージのすべての kind について
expires_atをNOW()に切り替えることでキャッシュを無効化します。 blob とアタッチメントはそのまま残されるため、すでに進行中のレスポンスは、再構築が アタッチメントを入れ替えるまで既存の blob に対して解決し続けます。UPDATE npm_metadata_files SET expires_at = NOW() WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND npm_package_id = '019a1b2c-0456-7abc-8def-000000000456';初回公開の場合はまだ行が存在しないため、
UPDATEは 0 行に影響します。最初に実行される再構築が、 キャッシュジョブと読み取り時 fill のどちらであっても、キャッシュ行を挿入します。バージョンの公開または非公開化の後にメタデータファイルをアップサートする
キャッシュ再構築は、キャッシュジョブと読み取り時 fill のどちらが実行していても、パッケージの kind ごとにこれを 1 回実行します。古いアタッチメントは、 孤立したアタッチメントが blob のガベージコレクションをブロックするのを防ぐために、 同じトランザクション内で削除しなければなりません(クリーンアップタスク を参照)。
-- The new blob and attachment (id=789) are created earlier in the same transaction. -- The interval below mirrors the configured `npm.packument_cache_ttl` (default 7 days). WITH old AS ( SELECT blob_storage_attachment_id, blob_sha256 FROM npm_metadata_files WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND npm_package_id = '019a1b2c-0456-7abc-8def-000000000456' AND kind = 0 ), upsert AS ( INSERT INTO npm_metadata_files (namespace_id, npm_package_id, kind, blob_storage_attachment_id, blob_sha256, expires_at) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', 0, 789, 'abcd1234...'::bytea, NOW() + interval '7 days') ON CONFLICT (namespace_id, npm_package_id, kind) DO UPDATE SET blob_storage_attachment_id = EXCLUDED.blob_storage_attachment_id, blob_sha256 = EXCLUDED.blob_sha256, expires_at = EXCLUDED.expires_at ) DELETE FROM blob_storage_attachments bsa USING old WHERE bsa.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsa.id = old.blob_storage_attachment_id AND bsa.sha256 = old.blob_sha256;初回挿入時、
oldCTE は行を返さないため、アタッチメントは削除されません。 競合時(更新)には、以前のアタッチメントが削除されます。他のアタッチメントが 参照していない場合、古い blob はガベージコレクションされます(重複排除に対して安全: 各クライアントは独自のアタッチメントを保持するため、1 つを削除しても同じ blob を 共有する他のものには影響しません)。
NPM リモートリポジトリ
erDiagram
repositories ||--|| npm_remote_repositories : "has one"
npm_remote_repositories ||--o{ npm_remote_packages : "has many"
npm_remote_packages ||--o{ npm_remote_versions : "has many"
npm_remote_packages ||--o{ npm_remote_tags : "has many"
npm_remote_packages ||--o{ npm_remote_metadata_files : "has many"
npm_remote_metadata_files ||--o{ npm_remote_tags : "has many"
npm_remote_versions ||--o{ npm_remote_files : "has many"
npm_remote_tags ||--|| npm_remote_versions : "has one"
npm_remote_metadata_files ||--|| blob_storage_attachments : "has one"
npm_remote_files ||--|| blob_storage_attachments : "has one"
npm_remote_repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
text url "NOT NULL, limit 1024"
bytea wrapped_dek "nullable, this row's data-encryption key, wrapped by the namespace key"
uuid ns_key_id FK "nullable, (ns_key_id, namespace_id) references namespace_encryption_keys(id, namespace_id), ON DELETE RESTRICT"
int ns_key_version "nullable, the namespace key version this row was wrapped under"
bytea encrypted_auth_token
smallint cache_validity_hours "NOT NULL, DEFAULT 24"
smallint metadata_cache_validity_hours "NOT NULL, DEFAULT 24"
smallint last_health_status "NOT NULL, DEFAULT 0, 0=unknown, 1=healthy, 2=unhealthy"
timestamptz last_health_checked_at "nullable"
}
npm_remote_packages {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_remote_repository_id FK "NOT NULL, (npm_remote_repository_id, namespace_id) references npm_remote_repositories(id, namespace_id)"
text name "NOT NULL, limit 255"
text scope "nullable, limit 255"
timestamptz last_downloaded_at "nullable, buffered"
timestamptz soft_deleted_at "nullable"
}
npm_remote_versions {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_remote_package_id FK "NOT NULL, (npm_remote_package_id, namespace_id) references npm_remote_packages(id, namespace_id)"
text version "NOT NULL, limit 255"
jsonb package_json "NOT NULL"
bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
timestamptz last_downloaded_at "nullable, buffered"
timestamptz soft_deleted_at "nullable"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
}
npm_remote_tags {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_remote_package_id FK "NOT NULL, (npm_remote_package_id, namespace_id) references npm_remote_packages(id, namespace_id)"
uuid npm_remote_version_id FK "NOT NULL, (npm_remote_version_id, namespace_id) references npm_remote_versions(id, namespace_id)"
uuid npm_remote_metadata_file_id FK "NOT NULL, (npm_remote_metadata_file_id, namespace_id) references npm_remote_metadata_files(id, namespace_id)"
text name "NOT NULL, limit 255"
}
npm_remote_metadata_files {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_remote_package_id FK "NOT NULL, (npm_remote_package_id, namespace_id) references npm_remote_packages(id, namespace_id)"
smallint kind "NOT NULL, 0=full, 1=dist_tags"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
timestamptz upstream_checked_at "NOT NULL, DEFAULT NOW()"
text upstream_etag "nullable, limit 255"
}
npm_remote_files {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid npm_remote_version_id FK "NOT NULL, (npm_remote_version_id, namespace_id) references npm_remote_versions(id, namespace_id)"
text file_name "NOT NULL, limit 255"
bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
timestamptz upstream_checked_at "NOT NULL, DEFAULT NOW()"
text upstream_etag "nullable, limit 255"
timestamptz soft_deleted_at "nullable"
}- npm_remote_repositories: 外部の npm レジストリを表します。URL、認証情報、アーティファクトのキャッシュ TTL(
cache_validity_hours)、およびパッケージメタデータレスポンス用の別個の TTL(metadata_cache_validity_hours)を含みます。ヘルスチェックのステータスは監視のために追跡されます。repository_idを介して親のrepositoriesテーブルを参照します。上流の認証情報は、それを復号するラップ済みデータ暗号化キーとともに暗号文として保存します。カラムの構成と制約については、暗号化キーを参照してください。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_remote_packages: キャッシュされた npm パッケージです。
last_downloaded_atはパッケージのいずれかのキャッシュされたファイルが最後にダウンロードされた時刻を記録し、ホット行の競合を避けるためにバッファ書き込み/非同期書き込みを介して維持されます。keep_last_downloaded_atライフサイクルルールとキャッシュ保持の評価に使用されます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_remote_versions: その
package_jsonメタデータを持つキャッシュされたバージョンです。packument がフェッチされたときに(すべてのバージョンのメタデータを含むため)入力されます。last_downloaded_atはバージョンのいずれかのキャッシュされたファイルが最後にダウンロードされた時刻を記録し、ホット行の競合を避けるためにバッファ書き込み/非同期書き込みを介して維持されます。keep_last_downloaded_atライフサイクルルールとキャッシュ保持の評価に使用されます。created_atはバージョンが最初にキャッシュされた時刻を記録し、npm_versionsを反映してキャッシュ側の公開履歴と出所スキャンを支えます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_remote_tags: キャッシュされた dist-tag からバージョンへのマッピング(例:
latest、next)です。packument から入力されます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_remote_metadata_files: 上流レジストリからキャッシュされた事前計算されたメタデータファイルを、パッケージごと・kind ごとに 1 つずつ保存します。
kindは、すべてのバージョンを含む完全な packument(0)と dist-tags のみのマッピング(1)を区別します。upstream_checked_atはメタデータが上流レジストリに対して最後に検証された時刻を記録し、metadata_cache_validity_hoursと比較して再検証が必要かどうかを判断します。upstream_etagは上流が返した ETag を保存し、変更されていないメタデータの再ダウンロードを回避するための条件付きリクエスト(If-None-Match)を可能にします。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_remote_files: キャッシュされた tarball です。
upstream_checked_atはファイルが上流レジストリに対して最後に検証された時刻を記録し、cache_validity_hoursと比較して再検証が必要かどうかを判断します。upstream_etagは上流が返した ETag を保存し、変更されていない tarball の再ダウンロードを回避するための条件付きリクエスト(If-None-Match)を可能にします。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。
インデックス
npm_remote_repositories:(namespace_id, repository_id)に対するユニークインデックス — 親参照によってリモートリポジトリを検索します。(namespace_id, ns_key_id)に対するインデックス — 暗号化キーの外部キーを支え、キーの行を削除する際にテーブルを走査せず参照元の行を確認できるようにします。npm_remote_packages:(namespace_id, npm_remote_repository_id, name) WHERE soft_deleted_at IS NULLに対するユニークインデックス — 名前によってキャッシュされたパッケージを検索します。部分条件により、ソフト削除後に同じ名前のパッケージを再作成できます。npm_remote_versions:(namespace_id, npm_remote_package_id, version) WHERE soft_deleted_at IS NULLに対するユニークインデックス — パッケージ内のキャッシュされたバージョンを検索します。この部分条件により、ソフト削除後に同じ識別子でバージョンを再作成できます。(namespace_id, npm_remote_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULLに対するインデックス — バージョン一覧表示で、キャッシュされたパッケージのバージョンをサイズ順に並べます。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULLに対するインデックス — ソフト削除されたキャッシュ済みバージョンを削除時刻順に一覧し、キャッシュされた npm アーティファクトに対するアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC)に対するインデックス — ネームスペース全体を時系列でスキャンします。ローカルのnpm_versionsインデックスを反映し、キャッシュ側の公開履歴と来歴を対象にします。ローカルインデックスと同じ監査証跡上の理由から、無条件(soft_deleted_atの述語なし)のインデックスとします。npm_remote_tags:(namespace_id, npm_remote_package_id, name)に対するユニークインデックス — 名前によってディストリビューションタグを検索します。(namespace_id, npm_remote_version_id)に対するインデックス — 特定のバージョンを指すすべてのタグを見つけます。npm_remote_metadata_files:(namespace_id, npm_remote_package_id, kind)に対するユニークインデックス — パッケージごと・kind ごとに 1 つのメタデータファイルを強制します。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによってメタデータファイルを検索します。(namespace_id, blob_sha256)に対するインデックス — 保存された blob の sha256 から、それを参照するすべてのキャッシュされたメタデータファイルへの逆引きで、ホスト型のnpm_metadata_filesインデックスを反映します。npm_remote_files:(namespace_id, npm_remote_version_id, file_name) WHERE soft_deleted_at IS NULLに対するユニークインデックス — ファイル名はバージョン内で一意でなければなりません。部分条件により、ソフト削除後に同じ名前のファイルを再作成できます。(namespace_id, blob_storage_attachment_id)に対するインデックス — ストレージアタッチメントによってファイルを検索します。(namespace_id, blob_sha256)に対するインデックス — 保存された blob の sha256 から、それを参照するすべてのキャッシュされた npm ファイルへの逆引きで、ホスト型のnpm_filesインデックスを反映し、チェックサム検索がキャッシュ側の参照もカバーするようにします。
クエリ例
リモートリポジトリを作成する
-- First create the parent repository INSERT INTO repositories (namespace_id, name, format, kind, visibility) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'npm-registry', 2, 2, 0) RETURNING id; -- Link the repository to a repository collection INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>); -- Then create the format-specific record INSERT INTO npm_remote_repositories (namespace_id, repository_id, url, wrapped_dek, ns_key_id, ns_key_version, encrypted_auth_token) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>, 'https://registry.npmjs.org', $1, $2, $3, $4);パッケージのすべてのキャッシュされたバージョンを取得する(packument レスポンスを提供)
SELECT nrv.version, nrv.package_json FROM npm_remote_versions nrv JOIN npm_remote_packages nrp ON nrv.npm_remote_package_id = nrp.id AND nrv.namespace_id = nrp.namespace_id WHERE nrp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND nrp.npm_remote_repository_id = '019a1b2c-0789-7abc-8def-000000000789' AND nrp.name = '@myorg/mypackage' AND nrp.soft_deleted_at IS NULL AND nrv.soft_deleted_at IS NULL;キャッシュされた tarball をプルする(読み取りパスのショートカット)
SELECT bsb.object_storage_key, bsb.size FROM npm_remote_files nrf JOIN blob_storage_blobs bsb ON bsb.namespace_id = nrf.namespace_id AND bsb.sha256 = nrf.blob_sha256 WHERE nrf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND nrf.npm_remote_version_id = '019a1b2c-0456-7abc-8def-000000000456' AND nrf.file_name = 'mypackage-1.0.0.tgz' AND nrf.soft_deleted_at IS NULL;
NPM 仮想リポジトリ
erDiagram
repositories ||--|| npm_virtual_repositories : "has one"
npm_virtual_repositories ||--o{ npm_virtual_repository_upstreams : "has many"
npm_virtual_repository_upstreams ||--|| repositories : "references upstream"
npm_virtual_repository_upstreams ||--o{ npm_virtual_upstream_rules : "has many"
npm_virtual_repositories {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE CASCADE"
}
npm_virtual_repository_upstreams {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
uuid npm_virtual_repository_id FK "NOT NULL, (npm_virtual_repository_id, namespace_id) references npm_virtual_repositories(id, namespace_id) ON DELETE CASCADE"
uuid upstream_repository_id FK "NOT NULL, (upstream_repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE NO ACTION"
int position "NOT NULL, CHECK position >= 0"
}
npm_virtual_upstream_rules {
uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
uuid npm_virtual_repository_upstream_id FK "NOT NULL, (npm_virtual_repository_upstream_id, namespace_id) references npm_virtual_repository_upstreams(id, namespace_id) ON DELETE CASCADE"
smallint rule_type "NOT NULL, 0=allow, 1=deny, CHECK rule_type IN (0, 1)"
text pattern "NOT NULL, CHECK char_length(pattern) <= 255"
smallint target_field "NOT NULL, 0=full_package_name, 1=scope, 2=version, CHECK target_field IN (0, 1, 2)"
}- npm_virtual_repositories: npm パッケージの仮想リポジトリです。名前、可視性、クロスフォーマットクエリのために
repository_idを介して親のrepositoriesテーブルを参照します。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_virtual_repository_upstreams: 仮想リポジトリとその上流を結合するテーブルです。各仮想リポジトリは上流の順序付きリストを持ちます。各エントリは
upstream_repository_idを介して上流リポジトリを参照し、これはrepositories(id, namespace_id)を指します。複合外部キー(namespace_id, upstream_repository_id)は、上流が同じネームスペース内にあることを強制します。これはレジストリがネームスペースにスコープされること(ADR-001)と一貫しています。HASH(namespace_id)で 64 パーティションにパーティショニングされます。 - npm_virtual_upstream_rules: 上流の許可/拒否フィルタールールを定義します。各ルールは、この上流を通じて解決する際にどのアーティファクトが含まれるか除外されるかを制御するために、ワイルドカードパターンと対象フィールドを指定します。パターンは MVP ではワイルドカードのみです。正規表現のサポートは顧客のフィードバックがそれを正当化するまで延期されます(ディスカッション)。データベースは列挙型とパターン長を
CHECK rule_type IN (0, 1)、CHECK target_field IN (0, 1, 2)、CHECK char_length(pattern) <= 255で強制します。範囲外の識別子は読み取りパスに任せず、書き込み時に拒否されます。パターンの境界は byte ではなく文字数を数えるため、UTF-8 のサイズにかかわらず 255 文字のパターンを受け付けます。HASH(namespace_id)で 64 パーティションにパーティショニングされます。
ここで position は 0 始まりです。CHECK position >= 0 が下限を定め、0 が最優先の位置です。この下限は npm 固有で、リファレンス実装とは異なります。monolith の仮想レジストリは npm を含むすべての出荷済みフォーマットで 1 始まりかつ隙間を詰めます。virtual_registries_packages_npm_registry_upstreams は "position" smallint DEFAULT 1 NOT NULL と CHECK ((1 <= "position") AND ("position" <= 20)) を宣言し、共通の VirtualRegistries::RegistryUpstream は作成時に maximum(:position).to_i + 1 を割り当て、削除時にはより大きいすべての位置をデクリメントします。コンテナテーブルの下限 1 はこの前例と一致します。S32 は、S17 の 1 始まりの連続した番号振り直しと、このドキュメントの position が 1 から始まるクエリ例を根拠にしています。npm の >= 0 と削除による隙間の許容は、S31 がその前例に反して意図的に選択したものです。
2 つの下限は互いに調整されておらず、データベースに存在するのは一方だけです。npm は artifact-registry!2066 で出荷済みですが、コンテナは S32 で指定されているだけで、まだマイグレーションがありません。S32 は、position 値が書き込みサーフェスより下のレイヤーに届かず、解決では一覧を昇順に読み位置をインデックスとして扱わないことを理由に、差異の決着を S17 の管理 API またはこのドキュメントに委ねています。そのため、この段落は分岐を追認せず記録します。#985 で追跡されています。
これら 3 つのテーブルの外部キーには、次の参照アクションがあります。
npm_virtual_repositoriesのrepositoriesを参照する FK(repository_id, namespace_id): フォーマット子テーブルの一般ルールで定められたON DELETE CASCADE。npm_virtual_repositoriesのnamespacesを参照する FKnamespace_id:NO ACTION。npm_virtual_repository_upstreamsのnamespacesを参照する FKnamespace_id:NO ACTION。npm_virtual_repository_upstreamsのnpm_virtual_repositoriesを参照する FK(npm_virtual_repository_id, namespace_id):ON DELETE CASCADE。npm_virtual_repository_upstreamsのrepositoriesを参照する FK(upstream_repository_id, namespace_id):NO ACTION。npm_virtual_upstream_rulesのnamespacesを参照する FKnamespace_id:NO ACTION。npm_virtual_upstream_rulesのnpm_virtual_repository_upstreamsを参照する FK(npm_virtual_repository_upstream_id, namespace_id):ON DELETE CASCADE。
npm_virtual_repository_upstreams の 2 つの外部キーが反対のアクションを取るのは意図的です。npm_virtual_repository_id は親から子へのリンクなので、仮想リポジトリを削除すると upstream の関連付けも削除されます。upstream_repository_id は兄弟参照なので、まだ upstream として一覧に含まれるリポジトリの削除は拒否されます。そこでカスケードすると、一覧に含めるすべての仮想リポジトリが縮小し、空になる可能性もありますが、オペレーターへのシグナルはありません。upstream の削除は意図的な管理操作でなければならず、以下の逆引きインデックスもそのために存在します。
カスケードチェーンは 3 レベルなので、1 つの repositories 行の削除は、npm_virtual_repositories、npm_virtual_repository_upstreams を通じてルールまで到達します。
インデックス
npm_virtual_repositories:(namespace_id, repository_id)に対するユニークインデックス — 親参照によって仮想リポジトリを検索します。npm_virtual_repository_upstreams:(namespace_id, npm_virtual_repository_id, position)に対するDEFERRABLE INITIALLY DEFERREDユニーク制約 — 仮想リポジトリの順序付き上流を取得します。トランザクション内での並べ替えを可能にします。(namespace_id, npm_virtual_repository_id, upstream_repository_id)に対するユニークインデックス — 同じ上流が仮想リポジトリに二度追加されるのを防ぎます。(namespace_id, upstream_repository_id)に対するインデックス — 特定のリポジトリを upstream として一覧に含めるすべての仮想リポジトリを検索します。これは、上記のNO ACTIONガードと関連付けサーフェスが読み取るものです。どちらのユニークエントリにも、この列順を使用可能な接頭辞として持つものはなく、PostgreSQL は外部キーカラムを自動的にインデックス化しません。npm_virtual_upstream_rules:(namespace_id, npm_virtual_repository_upstream_id)に対するインデックス — 特定の上流のすべてのルールを取得します。namespace_idが先頭なので、読み取りは自身のnamespace_id等価条件を運ぶ必要があります。外部キーカラムだけでは使用可能な接頭辞に一致せず、すべてのパーティションを走査します。
順序付き upstream のエントリはユニークインデックスではなくユニーク制約です。遅延は制約の属性なので、ALTER TABLE ... ADD CONSTRAINT ... UNIQUE (...) DEFERRABLE INITIALLY DEFERRED で追加します。PostgreSQL では CREATE UNIQUE INDEX ... DEFERRABLE は構文エラーです。遅延可能なユニーク制約は ON CONFLICT の調停にも使えないため、position の書き込みはこのキーへの upsert ではなく、UPDATE の後に INSERT を行います。
クエリ例
仮想リポジトリを作成する
-- First create the parent repository INSERT INTO repositories (namespace_id, name, format, kind, visibility) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'my-virtual-repo', 2, 1, 1) RETURNING id; -- Link the repository to a repository collection INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>); -- Then create the format-specific record INSERT INTO npm_virtual_repositories (namespace_id, repository_id) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>);仮想リポジトリを上流に関連付ける
INSERT INTO npm_virtual_repository_upstreams (namespace_id, npm_virtual_repository_id, upstream_repository_id, position) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0123-7abc-8def-000000000123', '019a1b2c-0789-7abc-8def-000000000789', 1);
blob ストレージ
blob ストレージのデータ構成は、次の前提の下で行われています。
- blob への一対多の関連付けを扱う必要はありません。これは blob ストレージのクライアント領域で処理されます。したがって、一対一の関連付けのみが必要です。
- 適切な クリーンアップ処理 のために、いくつの blob ストレージクライアントが単一の blob を使用しているか(重複排除)を追跡する必要があります。
- さらに、単一の blob に対する各使用の異なる起源を追跡したい場合があります。
ここで提示するスキーマは、データのストレージ側のみを考慮しています。メトリクスや クリーンアップ などの追加の側面のために補助テーブルが必要になる場合がありますが、これらの部分はまだ評価中のためここでは記述しません。アップロードセッションの追跡については アップロードセッション で記述します。
erDiagram
blob_storage_attachments ||--|| blob_storage_blobs : "has one"
blob_storage_attachments {
bigint id PK "DEFAULT nextval('blob_storage_attachments_id_seq')"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
bytea sha256 PK,FK "NOT NULL, (namespace_id, sha256) references blob_storage_blobs(namespace_id, sha256)"
}
blob_storage_blobs {
bigint id PK "DEFAULT nextval('blob_storage_blobs_id_seq')"
uuid namespace_id PK "NOT NULL, UNIQUE with sha256, no namespaces(id) reference by design"
bytea sha256 PK "NOT NULL, UNIQUE with namespace_id"
text object_storage_key "NOT NULL, limit 1024"
bigint size "NOT NULL"
bytea metadata_sha1 "nullable, CHECK octet_length = 20"
}- blob_storage_attachments: 特定の blob の使用を追跡します。各クライアント(Container、NPM、または Maven リポジトリテーブル)は、blob レコードを使用(作成または再利用)したいたびに、ここにレコードを作成する必要があります。各使用は、ここに単一のレコードを_持たなければなりません_。クライアントは、参照しているアーティファクトレコード(ファイル、blob、キャッシュエントリ)を削除する際に、アタッチメントレコードを削除する責任があります。両方の削除は、孤立したアタッチメントが blob クリーンアップをブロックするのを防ぐために、同じトランザクション内で行わなければなりません。クライアントテーブルから
blob_storage_attachmentsへの外部キーは参照整合性を強制しますが(ダングリング参照を防ぐ)、ON DELETE CASCADEは使用しません。クリーンアップはアプリケーション管理です。例えば、まったく同じファイルを持つ 2 つの Maven パッケージは、それぞれ異なるアタッチメントレコードを参照し、それが同じ blob レコードを参照する必要があります。namespace_idカラムは Cells のシャーディングに必要です。sha256カラムは、パーティションプルーニングされた JOIN を可能にするために、参照されるblob_storage_blobsレコードから伝播されます(パーティショニング戦略 を参照)。主キーは、従来の(id)ではなく(id, namespace_id, sha256)です。sha256が必要なのは、PostgreSQL がハッシュパーティショニングされたテーブルのすべてのユニーク制約にパーティションキーを含めることを強制するためであり、namespace_idが必要なのは、PK をデプロイ間でグローバルに一意に保つためです。ローカルなbigint idは単一の Artifact Registry データベース内でのみ一意なので(ネームスペース ID 型 を参照)、デプロイ間のネームスペースマイグレーション(ADR-022)では、同じ(id, sha256)ペアがターゲットデータベースにすでに存在する可能性があります。UUIDv7 のnamespace_idを PK に追加することで、その衝突を構造的に排除します。クライアントテーブルは、この複合 PK を(namespace_id, blob_storage_attachment_id, blob_sha256)を介して参照します。 - blob_storage_blobs: このテーブルは、オブジェクトストレージ上に存在するすべてのファイルコンテンツを blob として一覧します。オブジェクトストレージキーは専用カラムに完全な形で保存され、blob が使用されるたびに計算されることはありません。
sha256は基本となるコンテンツアドレス可能な識別子であり、常に存在します(NOT NULL)。namespace_idカラムは重複排除を Organization にスコープし、設計上namespaces(id)への参照を持ちません。この行は保存オブジェクトへの唯一のハンドルであるobject_storage_keyを所有し、ADR-025 では garbage collection が行より先にオブジェクトを削除します。そのため、namespacesからのカスケードはオブジェクトがまだ存在する間にハンドルを破壊して漏えいさせ、ブロックする参照は、意図的に延期された garbage collection サイクルの完了まで namespace の削除を止めます。したがって namespace のハード削除後も、garbage collection が回収するまで blob 行は残ります。フォーマット固有のチェックサム(例えば Maven の SHA1 と MD5)はここではなくフォーマット固有のファイルテーブルに保存し、このテーブルをフォーマットに依存しない状態に保ちます。同じ理由から、コンテンツタイプも除外します。これはフォーマットが blob をどのように解釈するかという特性であって blob 自体の特性ではなく、フォーマット固有のテーブルに属します。metadata_sha1カラムは、フォーマットに依存しないというルールに対する意図的かつ限定的な例外です。コミット時に blob に付加される MVP ユーザーメタデータの許可リストにある SHA-1 を反映し、SHA-1 が指定されなかった場合はNULLになります。このカラムをフォーマット固有のテーブルではなくblob_storage_blobsに置くのは、ストレージレイヤーの blob 情報の検索がプッシュとプルのホットパスで 1 回の DB ラウンドトリップとなることが契約上定められているためです。DB のミラーなしでユーザーメタデータを公開すると、ダイジェストごとのオブジェクトストレージ HEAD のファンアウト、または部分的な API 公開が必要になります。同じ値は、コミット時にバックエンドネイティブのx-amz-meta-checksum-sha1/x-goog-meta-checksum-sha1ヘッダーとしてストレージオブジェクトに付加されます。行は不変であるため、DB とストレージオブジェクトのコピーにずれは生じません。将来、許可リストに項目を追加する際は、改訂によって項目ごとの nullable カラムを追加します。完全な根拠については、Artifact Registry S06 ストレージレイヤー仕様を参照してください。主キーが(id, namespace_id, sha256)である理由は、前述のblob_storage_attachmentsと同じです。sha256は PostgreSQL のパーティションキー包含ルールを満たし、UUIDv7 のnamespace_idは PK をデプロイ間でグローバルに一意に保ちます。また、代理bigint idにより、blob ストレージ層全体で行識別子の形を一貫させています。Organization ごとの重複排除は、別個のUNIQUE (namespace_id, sha256)制約によって強制されます。この制約はコンテンツハッシュによる検索インデックスも兼ね、このテーブルを参照するすべての外部キーのターゲットになります。PK を直接参照する FK はありません。(namespace_id, sha256)はすでに行を一意に識別し、UUIDv7 のnamespace_idによってそれ自体がグローバルに一意であるため、呼び出し元は代理idを保持せずに自然キーで JOIN します。
blob ストレージテーブルは、Artifact Registry の外部でも再利用できるように設計されています。これにより、他の機能が同じ重複排除とストレージインフラを活用できます。
すべてのハッシュカラム(digest と sha256、sha1、md5、sha512 — Maven 固有)は、テキストエンコーディングやインラインアルゴリズムプレフィックスを持たない、生のハッシュバイトを保持する bytea として保存されます。コンテナの digest と blob_sha256 カラムは生の 32 バイト SHA-256 を保持し、CHECK octet_length = 32 によって強制されます。OCI のワイヤ形式 sha256:<64 hex chars> はサービスレイヤーで生のバイトへ、また生のバイトから変換されるため、blob_sha256 は blob_storage_blobs.sha256 と直接バイト比較で JOIN します。MVP は sha256 のみをサポートします。sha256 以外の参照はこれらのテーブルに到達する前にサービスレイヤーで拒否されるため、別個の digest_algorithm カラムは保存しません。コンテナ blob の digest と blob_sha256 は同じバイトになるため、(namespace_id, digest) インデックスはコンテンツハッシュの逆引きインデックスも兼ね、ホスト型コンテナテーブルに別個の blob_sha256 インデックスはありません(Container Repositories のインデックス を参照)。将来、sha256 以外のダイジェストアルゴリズムが導入される場合は、2 つのカラムを分離し、再検討します。
アップロードセッション
アップロードセッションは、ADR-008 で記述された 2 フェーズアップロードライフサイクルを通じて、進行中の blob アップロードを追跡します。各セッションは、ネームスペースのストレージパーティション内の uploads/{upload_id} にある一時的なストレージオブジェクトにマッピングされます。セッションは、アップロード API(再開可能なアップロード、同時アップロードの解決)をサポートし、オブジェクトストレージの列挙なしに アップロードパージ を可能にするために、初期スキーマからデータベースで追跡されます(ADR-011)。
erDiagram
namespaces ||--o{ upload_sessions : "has many"
repositories ||--o{ upload_sessions : "has many"
upload_sessions {
bigint id PK "DEFAULT nextval('upload_sessions_id_seq'), part of composite PK (id, namespace_id)"
uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
uuid repository_id FK "NOT NULL, (repository_id, namespace_id) references repositories(id, namespace_id)"
uuid upload_id "NOT NULL"
bigint size_bytes "NOT NULL, DEFAULT 0, bytes uploaded so far"
bytea hash_state "nullable, serialized intermediate SHA-256 hash state"
boolean dirty "NOT NULL, DEFAULT FALSE, concurrent-writer poison bit"
timestamptz created_at "NOT NULL, DEFAULT NOW()"
timestamptz expires_at "NOT NULL"
timestamptz updated_at "NOT NULL, DEFAULT NOW(), chunk-append latency metrics"
}- upload_sessions: 各 blob アップロードが進行中の間、それを追跡します。テーブルはバイナリ存在モデルに従い、コンテナレジストリのパターン を反映します。行が存在すれば、アップロードは進行中であるかクリーンアップが必要です。存在しなければ、アップロードは完了したかパージされました。完了時、ストレージレイヤーは blob をコンテンツアドレス可能なストアに移動し、その後
blob_storage_blobsレコードを作成するのと同じトランザクション内でセッション行を削除します。フォーマット固有の行(blob_storage_attachmentsとフォーマットテーブル)は、その後、呼び出し側のフォーマットサブシステムによって別個のトランザクションで作成されます。これにより、ストレージレイヤーをフォーマット非依存に保ちます。upload_id(UUID)は、一時オブジェクトパス(uploads/{upload_id})で使用されるストレージレベルの識別子です。repository_idはアップロードを開始したリポジトリを記録します。後続のリクエストでは、サーバーは URL 内のリポジトリが session.repository_id と一致することを検証し、upload_id が漏洩した場合のリポジトリ間の再利用を防ぎます。各リクエストの認可は、URL のリポジトリに対してリクエストミドルウェアによって実行され、このカラムには依存しません。複合外部キー(namespace_id, repository_id)は、アップロードがターゲットリポジトリと同じネームスペース内にあることを強制します。size_bytesは一時ストレージに書き込まれたバイト数を追跡します。再開可能なアップロードの場合、各チャンクが到着するにつれて更新され、クライアントにどこから再開するかを伝えるRangeレスポンスヘッダーを生成するために使用されます(OCI Distribution Spec)。モノリシックアップロードの場合、blob データが書き込まれた後に設定されます。created_atはアップロードが開始された時刻を記録します。これは、アップロード期間メトリクス(期間と blob サイズの相関)と、アプリケーションの TTL 設定が引き下げられた場合の遡及的な期限切れ(WHERE created_at < NOW() - :new_ttl)を可能にします。既存のセッションは元の期限を保持するため、expires_atだけではこれをサポートできません。expires_atはセッション期限のタイムスタンプで、作成時にアップロードタイプに基づいてNOW() + :configured_ttlとして計算されます(再開不可能なものは短く、再開可能なアップロードは長く)。期限切れのセッションはアップロードパージの候補です。パージャーは一時ストレージオブジェクトを削除し、行を削除します(ADR-008)。再開可能なアップロードのハッシュ状態は、シリアライズされた中間 SHA-256 状態としてhash_stateカラムに保存されます。単一行のUPDATEは PATCH ごとのオブジェクトストレージのラウンドトリップよりも単純です(ADR-008 を参照)。そのUPDATEは行ロックを取りません。1 つのupload_idに対する同時ライターは、SELECT ... FOR UPDATEロックではなく、size_bytesの compare-and-swap とdirtyポイズンビットによって調整されます(乖離時に終了)。dirtyはそのポイズンビットで、CAS の敗者によって設定され、行の削除によってのみクリアされます。失敗ごとのフローは、Artifact Registry S06 ストレージレイヤー仕様 の「Consistency & Crash-Recovery Model」で定義されています。updated_atはセッションの最終変更時刻を記録し、チャンク追加レイテンシメトリクスと最終アクティビティの可観測性をサポートします。これはアクセスログから導出するのではなく保存されたカラムです。なぜなら、リクエスト時に同期的な再開パスの決定を支えるためです。書き込みコストは無視できます。既存のsize_bytes/hash_stateのUPDATEに相乗りするためです。HASH(namespace_id)で 64 パーティションにパーティショニングされ、スキーマ内の他のすべてのnamespace_idスコープのテーブルと一貫しています。セッションは短命ですが、アップロードパージャーは延期されている(ADR-011)ため、それが出荷されるまで期限切れの行が蓄積され、初日からパーティショニングすることで後のマイグレーションを回避し、repositoriesとのパーティションワイズ JOIN の適格性を保ち、空のパーティションではコストがかかりません。主キーは、従来の(id)ではなく(id, namespace_id)です。PostgreSQL はハッシュパーティショニングされたテーブルのすべてのユニーク制約にパーティションキーを必要とし、この PK のパーティションキーは UUIDv7 のnamespace_idなので、すでにデプロイ間でグローバルに一意です。これは、sha256でパーティショニングし同じ保証のためにnamespace_idを追加するblob_storage_attachmentsとblob_storage_blobsとは異なります。
インデックス
upload_sessions:(namespace_id, upload_id)に対するユニークインデックス — ネームスペース内でアップロード UUID によってセッションを検索します。expires_atに対するインデックス — アップロードパージのために期限切れのセッションを見つけます。(namespace_id, repository_id)に対するインデックス — 特定のリポジトリのすべてのセッションを見つけます。認可チェックとリポジトリ削除時のクリーンアップに使用されます。
クエリ例
アップロードセッションを作成する
INSERT INTO upload_sessions (namespace_id, repository_id, upload_id, expires_at) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NOW() + INTERVAL '1 hour') RETURNING id, upload_id;チャンクアップロード中にセッションを検索する
SELECT * FROM upload_sessions WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND upload_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11';チャンク追加後にセッション状態を更新する(
hash_state+updated_at、size_bytesに対する compare-and-swap)UPDATE upload_sessions SET size_bytes = 1048576, hash_state = 'a1b2c3...'::bytea, updated_at = NOW() WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND upload_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' AND size_bytes = 524288; -- CAS: only persist if no concurrent writer advanced the row; a zero-rowcount result is a conflictアップロードパージのために期限切れのセッションを見つける
SELECT id, namespace_id, upload_id FROM upload_sessions WHERE expires_at < NOW() ORDER BY expires_at LIMIT 100;このクエリはパーティションプルーニングされません。述語が
namespace_idを含まないため、64 パーティションすべてをスキャンします。これはここでは許容できます。パージャーは境界のあるバックグラウンドジョブ(LIMIT 100、expires_atのインデックスに支えられる)であり、ホットパスのクエリではないため、ファンアウトはパフォーマンス上クリティカルではありません。クリーンアップ後にセッションを削除する
DELETE FROM upload_sessions WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = 789;
パーティショニングの不変条件
namespace_id を含むすべてのテーブルはパーティショニングされます。 デフォルトのパーティションキーは HASH(namespace_id) で 64 パーティションです。特定のテーブルは、文書化された理由がある場合に異なるキーを使用することがあります(HASH(sha256) の例外については blob ストレージのパーティショニング戦略 を参照)。namespace_id を含まないテーブルはパーティショニングされません。
このルールは、テーブルごとの判断ではなく、行のプロパティとして述べられています。namespace_id がスキーマの一部であれば、テーブルはパーティショニングされます。「このテーブルは小さい」「このテーブルは親と 1:1 である」「後でパーティショニングを追加できる」といった例外はありません。小さなテーブルも大きなテーブルと同じようにパーティショニングされます。均一性が要点です。低ボリュームのテーブルをパーティショニングするコストは無視できます。64 個のほぼ空の子、計測可能なランタイムオーバーヘッドはありません。一方、後でパーティショニングを_追加する_コストは、本番データが配置された後のテーブルの書き換え、主キーの再構成、カスケードする外部キーの変更によって支配されます。
以下の例外も同様に構造的であり、ここで却下した判断に基づく例外には該当しません。
機械的な帰結
PostgreSQL は、パーティショニングされたテーブルのすべてのユニーク制約にパーティションキーを含めることを要求します。これはスキーマ全体の主キーと外部キーを形作ります。
- 主キー。 すべてのパーティショニングされたテーブルの主キーは
namespace_idを吸収します。(id)は(id, namespace_id)になります。パーティショニングされたテーブルのユニークインデックスは、先頭カラムとしてnamespace_idを含みます。 - パーティショニングされたテーブル間の外部キー。
namespace_idで複合になります。子は(<parent>_id, namespace_id)を介して親を参照し、親の(id, namespace_id)を参照します。このパターンは、repositories、workspaces、フォーマット固有のリポジトリテーブル、中間層テーブル、ファイルテーブル、リモートキャッシュテーブルにわたって均一です。 namespacesへの外部キー。 単一カラムです。namespace_idはnamespaces(id)を参照します。namespacesは主キーが(id)のままである唯一のテーブルです。これはパーティショニングされておらず、独自のnamespace_idを持ちません(それを_定義する_)ため、子テーブルは複合 PK の小細工なしにそれを参照します。
複合外部キーの形状は、ネームスペース境界をスキーマレベルでエンコードします。任意のパーティショニングされたテーブルの行は、外部キーがそれを禁止するため、異なるネームスペースに属する別のパーティショニングされたテーブルの行を参照できません。これは Cells のシャーディングキー(namespace_id)がアプリケーションレベルで引く境界と同じものであり、データベース自体の中で冗長化されています。
例外
2 つの構造的条件のいずれかを満たす場合にのみ、テーブルはパーティショニングされません。
namespace_id を欠くテーブル。 今日の主な例は namespaces 自体です。これは namespace_id が判明する前に slug から解決されるルーティングルートであり、namespace_id を定義するため namespace_id カラムを持ちません。namespace_id を持たない将来のテーブル(例: インスタンス全体の設定、グローバルな cron 状態、デプロイにスコープされたライフサイクルメタデータ)は、このデフォルトを自動的に継承し、パーティショニングされません。
主キー全体が namespace_id であるテーブル。 独立した id カラムを持たないため、ネームスペースごとに 1 行を保持し、2 行目を表現できず、どの外部キーからも参照されないテーブルです。現在、この条件を満たすのは namespace_statistics だけです。今後この条件を適用するテーブルは、主キーが正確に (namespace_id) であり、どこからも参照されないことを独自の説明に明記します。これにより、その主張は単なる表明ではなく監査可能になります。
この 2 つ目の条件は、不変条件が却下する「このテーブルは親と 1:1 である」という例外とは意図的に異なります。それは実装者がどちらとも主張できる関係性に関する判断でしたが、こちらはキー定義と、そのテーブルを参照する制約のカタログ一覧によって確定するスキーマのプロパティです。独自の id を持つテーブルは、どれほど小さくても対象外です。また、id または参照元の外部キーを取得したテーブルは、その時点で対象外になります。不変条件の根拠はいずれも、この形状のテーブルには当てはまりません。パーティショニングは namespace_id にスコープされた読み取りが 64 ではなく 1 つのパーティションに触れるようにするために存在しますが、ここでは主キーが namespace_id そのものなので、すべての読み取りはすでに単一行のキー検索です。また、後からの変換は、両方が成立しなければならない 2 つの理由から低コストです。まず、不変条件が回避するために書かれた主キーの再構成、すなわち (id) から (id, namespace_id) への変更はすでに完了しています。namespace_id だけをキーとするテーブルは、パーティションキーをすべてのユニーク制約に含めるという PostgreSQL の要件を満たすためです。次に、PostgreSQL は通常のテーブルをパーティショニングされたテーブルへ ALTER できないため、変換ではパーティショニングされたテーブルを作成し、その下で既存テーブルと交換します。この処理では、すべての参照元の外部キーを削除して新しいテーブルに対して再作成し、各再作成時に参照元テーブルのスキャンを伴う再検証が必要です。どこからも参照されないテーブルには削除すべき外部キーがありません。そのため、参照元の外部キーがないことは、現在のスキーマに関する単なる観察ではなく条件の一部です。後から参照を取得したテーブルがこの例外にとどまると、その根拠がもはや成り立たなくなるためです。
どちらの例外の述語も構造的です。行内の namespace_id の有無、およびそのカラムがどこからも参照されないテーブルの主キー全体であるかどうかです。いずれも行数、書き込み頻度、現在のアクセスパターンに依存せず、これらはすべてシステムが進化するにつれて変化し得ます。
シングルテナントデプロイ(Dedicated、Self-Managed、単一 Organization の Cells)も例外ではありません。これらは 64 パーティションすべてを保持し、1 つが入力され 63 が空になります。空のパーティションはこの規模では無視できます(それぞれ数 KB のカタログとインデックスのオーバーヘッド)。パーティションプルーニングは影響を受けず、デプロイ間でのスキーマの均一性は、シングルテナントのバリアントを切り出すことよりも価値があります。病的な「1 つのパーティションがすべてを保持する」ケースは、完全なマルチテナント規模での blob_storage_blobs / blob_storage_attachments にのみ適用され、これがそれら 2 つのテーブルが代わりに HASH(sha256) を使用する理由です。blob ストレージのパーティショニング戦略 を参照してください。
blob ストレージのパーティショニング戦略
帰結 で述べたように、blob_storage_blobs と blob_storage_attachments は、すべての Organization にわたってすべてのアーティファクトフォーマットを提供するため、非常に高い行数を蓄積します。意図的なパーティショニング戦略がなければ、これは次の問題につながります。
- テーブルが数十億行に成長するにつれて、インデックスの肥大化とクエリパフォーマンスの低下。
- すべてのアーティファクトタイプを同時にブロックするテーブル全体のロック(例: インデックス作成やスキーママイグレーション中)。
- 高い書き込みレートでの autovacuum の競合。
念頭に置くべき重要な制約: PostgreSQL は、パーティショニングされたテーブルのすべてのユニーク制約にパーティションキーを含めることを要求します。blob_storage_blobs の場合、重複排除制約は UNIQUE (namespace_id, sha256) です。パーティションキーがこれらのカラムのサブセットでない戦略は、その制約に追加のカラムを強制することになり、それは同じ Organization 内の同じ blob が異なるパーティションにまたがって二度保存されるのを防げなくなり、重複排除モデルを完全に損ないます。
以下が候補となる戦略です。
オプション A: sha256 によるハッシュパーティショニング
両方のテーブルを PARTITION BY HASH (sha256) で 64 パーティションにパーティショニングします。
sha256 はコンテンツアドレス可能なダイジェストなので、その値は本質的に均一に分布します。均等なデータ分布のために追加の作業は不要です。これはシングルテナント問題を解決します。シングルテナントデプロイ(Dedicated、Self-Managed、単一 Organization の Cells)は、namespace_id のみを使用すると、すべての行を単一のパーティションに集中させてしまいます。sha256 をパーティションキーとすると、Organization がいくつ存在するかに関係なく、行は 64 パーティションすべてに均等に広がります。
[namespace_id, sha256] に対する既存のユニーク制約はすでに sha256 を含んでいるため、このスキームと互換性があります。パーティションキーが制約の一部であるため、PostgreSQL はハッシュパーティションにまたがって一意性を強制できます。
このアプローチでは、blob_storage_blobs への JOIN が単一のパーティションをターゲットにできるように、sha256 を blob_storage_attachments とフォーマット固有のテーブル(*_files、container_blobs、container_manifests、キャッシュエントリ)に伝播する必要があります。これは、blob 識別子(namespace_id + sha256)が *_files と blob_storage_attachments の両方の行に保存され、単純な bigint 外部キーよりも多くの物理ストレージを使用することを意味します(sha256 は bytea として 32 バイト、bigint は 8 バイト)。しかし、このトレードオフは正当化されます。読み取りパス(アーティファクトプル)、すなわちシステム内で最もホットなクエリは、(namespace_id, sha256) を介して *_files から blob_storage_blobs に直接 JOIN でき、blob_storage_attachments を完全にスキップして 1 つの JOIN を排除します。アタッチメントは、クリーンアップ 中に「この blob はまだ誰かに使用されているか?」に答えるライフサイクルパスのために依然として必要です。
5 つの重要なアクセスパターンは次のように動作します。
| # | 操作 | 頻度 | ヒットするパーティション |
|---|---|---|---|
| AP1 | アーティファクトのプル(namespace_id + sha256 を介した *_files → blob_storage_blobs) | 最高 | 1 |
| AP2 | 孤立チェック(WHERE namespace_id = ? AND sha256 = ?) | 高 | 1 |
| AP3 | 重複排除アップサート(ON CONFLICT (namespace_id, sha256) DO NOTHING) | 中〜高 | 1 |
| AP4 | アタッチメントの CRUD(blob から伝播された namespace_id + sha256) | 中 | 1 |
| AP5 | Organization ごとのストレージ会計(WHERE namespace_id = ?、sha256 なし) | 低 | 全 64(緩和済み) |
ポジティブ:
- テナントの集中に関係なく均一な分布: シングルテナントデプロイは、1 つに集中させる代わりに、データを 64 パーティションすべてに広げます。
- すべての高頻度アクセスパターン(プル、孤立チェック、重複排除アップサート、アタッチメント CRUD)がちょうど 1 つのパーティションにヒットします。
- ユニーク制約
(namespace_id, sha256)はパーティションキーを含みます。重複排除アップサートは単一のパーティションをターゲットにし、外部ロックなしにON CONFLICT DO NOTHINGを介して同時アップロードを解決します。 - 読み取りパス(アーティファクトプル)は
blob_storage_attachmentsの JOIN を完全にスキップし、(namespace_id, sha256)を介して*_filesからblob_storage_blobsに直接進みます。
ネガティブ:
sha256をより多くのテーブルに伝播する必要があります。blob_storage_attachmentsとフォーマット固有のテーブル(*_files、container_blobs、container_manifests、キャッシュエントリ)は、blob_storage_attachment_id外部キーに加えて(namespace_id, sha256)を保持します。これは blob 識別子を行にわたって複製し、行ごとのストレージを増加させます。namespace_idのみ(sha256なし)のクエリはパーティションをプルーニングできず、64 パーティションすべてをスキャンします。主なケースはストレージ会計(Organization ごとに blob サイズを合計する)です。これは、blob の挿入/削除時に遅延インクリメントを介して更新される専用のロールアップテーブルによって緩和されます。これは GitLab で既に確立されているパターンです(例: プロジェクト統計)。ロールアップテーブルがなくても、64 パーティションにわたる並列集約は数秒で完了します。
オプション B: namespace_id によるハッシュパーティショニング
両方のテーブルを PARTITION BY HASH (namespace_id) で固定数のパーティションにパーティショニングします。
すべての一般的なアクセスパターンはすでに WHERE 句に namespace_id を含んでいるため、クエリプランナーはすべての操作に対して単一のパーティションをターゲットにできます。Cells のシャーディングキー(namespace_id)がパーティションキーを兼ね、より広範なアーキテクチャと一貫しています。
[namespace_id, sha256] に対するユニーク制約はすでに namespace_id を含んでいるため、このスキームと変更なしで互換性があります。PostgreSQL はすべてのハッシュパーティションにわたって一意性をグローバルに強制します。
ポジティブ:
- すべての Organization スコープのクエリが単一のパーティションにヒットします。クエリプランナーは他のすべてを自動的にプルーニングします。
- パーティションプルーニングはクリーンアップパスに直接適用されます。
blob_storage_attachmentsの孤立チェック(WHERE namespace_id = ? AND sha256 = ?)は単一のパーティションをターゲットにすることが保証され、検索コストをテーブル全体のボリュームではなくパーティションサイズで境界づけます。 - スキーマ変更とロックは単一のパーティションにスコープされ、他の Organization への影響を軽減します。
- Cells のシャーディングキーと整合します。一般的なアクセスパターンに対してクロスパーティション作業はありません。
[namespace_id, sha256]に対する既存の制約は変更なしで正しく動作します。
ネガティブ:
- Organization のサイズが大きく異なる場合、非常に高い blob 数を持つ Organization が自身のハッシュパーティションを支配し得ます。シングルテナントデプロイ(Dedicated、Self-Managed、単一 Organization の Cells)では、すべての行が単一のパーティションに集中します。VACUUM に数時間かかり、インデックスは数百 GB に達します。
WHERE句からnamespace_idを省略するクエリは、すべてのパーティションをスキャンします。
オプション C: id(主キー)による範囲パーティショニング
両方のテーブルを自動インクリメントする主キーの範囲でパーティショニングします。これは GitLab の既存の テーブルパーティショニングフレームワーク で使用されているアプローチであり、既存のツールによって十分にサポートされています。
ポジティブ:
- パーティションサイズは予測可能に成長します。データが蓄積するにつれて新しいパーティションを簡単に追加できます。
- GitLab の既存のパーティション管理インフラと互換性があります。
ネガティブ:
- 重複排除の一意性を壊します。PostgreSQL は、パーティショニングされたテーブルのすべてのユニーク制約に
idを含めることを要求します。[namespace_id, sha256]にidを追加すると、同じ Organization の同じ sha256 が複数のパーティションに現れ得るようになり、重複排除モデルが完全に壊れます。 - クエリは Organization スコープですが、パーティションは id 範囲ベースなので、すべての Organization スコープのクエリが複数のパーティションにまたがります。
- ロックスコープの削減が Organization 境界と整合しません。
オプション D: created_at による範囲パーティショニング
両方のテーブルを時間範囲(例: 月単位または四半期単位のウィンドウ)でパーティショニングします。
ポジティブ:
- blob がクリーンアップされた後、古いパーティションを簡単にアーカイブまたはドロップできます。
- パーティションは既知の時間ウィンドウに対応し、明確な運用モデルです。
ネガティブ:
- ホットパーティション問題: すべての書き込みが最新のパーティションをターゲットにし、書き込みの競合を集中させます。
- blob は年齢ではなく、すべてのアタッチメントを失ったときに期限切れになります。時間ベースのパーティショニングは実際の blob ライフサイクルと整合しません。
- オプション C と同じユニーク制約の問題:
created_atをユニーク制約に追加する必要があり、クロスパーティション重複排除を壊します。 - アクセスパターンは時間スコープではなく Organization スコープなので、クエリはすべてのパーティションにまたがります。
オプション E: パーティショニングなし
Cells レベルのシャーディング(namespace_id)と標準的なインデックス作成を主要なスケーラビリティメカニズムとして頼ります。パーティショニングは、メトリクスが必要性を示すまで延期されます。
ポジティブ:
- 単純なスキーマと運用: パーティション管理のオーバーヘッドがなく、マイグレーションとスキーマ変更が簡単です。
- 初期段階の規模では十分: 行数が単一の Cell 内で扱いやすいうちはうまく機能します。
ネガティブ:
- Cell 内での無制限の成長: テーブルが成長するにつれて、テーブルレベルのロックがすべての Organization に同時に影響します。
- 適切に設計されたインデックスでさえ、非常に高い行数ではパフォーマンスの圧力に直面します。
決定
sha256 によるハッシュパーティショニング(オプション A)が選択されました。blob_storage_blobs と blob_storage_attachments の両方について。
これは次のことを満たす唯一のオプションです。
- すべての高頻度アクセスパターン(アーティファクトプル、孤立チェック、重複排除アップサート、アタッチメント CRUD)を単一のパーティション内に保ちます。
- テナントの集中に関係なく行を均一に分布させます。これは、
namespace_idベースのパーティショニングがすべての行を 1 つのパーティションに集中させるシングルテナントデプロイ(Dedicated、Self-Managed、単一 Organization の Cells)にとってクリティカルです。 [namespace_id, sha256]に対する既存のユニーク制約と変更なしで互換性があり、ON CONFLICT (namespace_id, sha256) DO NOTHINGを介してレースのない重複排除アップサートを可能にします。
両方のテーブルについて、初期値として 64 パーティションが選択されています。これは、運用オーバーヘッドを管理可能に保ちつつ、十分な分布とロック分離を提供します。
トレードオフは、sha256 を blob_storage_attachments とフォーマット固有のテーブル(*_files、container_blobs、container_manifests、キャッシュエントリ)に伝播する必要があることです。これは blob 識別子(namespace_id + sha256)を行にわたって複製し、bigint 外部キーのみよりも多くの物理ストレージを使用します。利点は、読み取りパス、すなわちシステム内で最もホットなクエリが、(namespace_id, sha256) を介して *_files から blob_storage_blobs に直接 JOIN し、blob_storage_attachments を完全にスキップして 1 つの JOIN を排除することです。アタッチメントは クリーンアップライフサイクルパス のためにのみ残ります。
namespace_id のみ(sha256 なし)のクエリ、例えば Organization レベルのストレージ会計は、パーティションをプルーニングできず、64 パーティションすべてをスキャンします。これは、遅延インクリメントを介して更新される専用のロールアップテーブルによって緩和されます。これは GitLab で既に確立されているパターンです(例: プロジェクト統計)。
フォーマット固有のテーブルのパーティショニング戦略
フォーマット固有のテーブル(ホストされたコンテンツテーブルとそのリモートの対応物)は、パーティショニングの不変条件 によって確立された HASH(namespace_id) デフォルトに従います。各テーブルの箇条書きがそれを明示的に記録します。ホスト型とリモート型は同じアクセス形状を共有するため 1 つの戦略を共有します。すべての主要なアクセスパターンは namespace_id スコープです。テーブルごとの違い(キャッシュ TTL、上流メタデータ)はパーティショニングと直交し、テーブルごとの説明に存在します。
このグループに固有の根拠:
- すべての主要なアクセスパターンが
namespace_idスコープです(リポジトリとアーティファクト座標による検索、パッケージまたはイメージのファイルの一覧表示、上流のキャッシュエントリの一覧表示)。したがって、HASH(namespace_id)はすべての操作に対して単一パーティションのプルーニングを与えます。読み取りパスのショートカット((namespace_id, sha256)を介した*_files→blob_storage_blobs、blob_storage_attachmentsをスキップ)、すなわちシステム内で最もホットなクエリは、このパーティショニングから直接恩恵を受けます。 blob_storage_blobsをHASH(sha256)に駆動するシングルテナント集中の懸念は適用されません。各フォーマット固有のテーブルは 1 つのフォーマット(リモートの場合は 1 つの上流)にスコープされるため、ネームスペースごとのフットプリントは構造的に、blob_storage_blobsが保持するクロスフォーマット集約の一部にすぎません。(namespace_id, blob_sha256)を介したblob_storage_blobsへの JOIN は、namespace_idリテラルでフォーマットテーブルのパーティションをプルーニングします。blob 側が計画時にプルーニングされるのは、クエリが digest を名指す場合だけです。blob_storage_blobs.sha256 = maven_files.blob_sha256のようなカラム間の述語ではsha256がプランナーにとって不明なため、64 個すべての blob パーティションに対するAppendを構築し、実行時に外側の行ごとにプルーニングします。スキャンは実行時に回避されますが、コストは計画時に発生します。
PostgreSQL 17.10 上で、blob_storage_blobs、blob_storage_attachments、maven_files にそれぞれ単一 namespace の 5,000 行を持つ fixture を使用して計測しました。どちらの行も同じ読み取り、すなわち 1 つの maven_files 行とその blob のサイズで、blob 側が参照するテーブルだけが異なります。2 行目はnamespace レベルの調整用 shadow である blob_storage_blobs_by_namespace です。これは PARTITION BY HASH (namespace_id) と PRIMARY KEY (namespace_id, sha256) INCLUDE (size) を持ちます。各値は同一セッションで 5 回繰り返した範囲です。
| blob のサイズを得るために JOIN したテーブル | blob 側のプラン | 計画 | 実行 |
|---|---|---|---|
blob_storage_blobs | 64 パーティションに対する Append、うち 63 は never executed | 3.85〜4.70 ms | 0.45〜0.72 ms |
blob_storage_blobs_by_namespace | 単一パーティションの Index Only Scan、Heap Fetches: 0 | 0.26〜0.37 ms | 0.11〜0.17 ms |
namespace_id リテラルは計画時に shadow をプルーニングしますが、ベーステーブルをプルーニングする digest リテラルはありません。新しい backend でのベーステーブルの最初のプランは計画に 22.07 ms を要し、5,738 個の計画 buffer を読みます。fan-out は fixture の行数ではなく、64 パーティションに対する HASH(sha256) と digest の欠如から生じます。blob_storage_blobs [namespace_id, sha256] のユニーク制約によってこれは変わりません。パーティショニングされたテーブルのインデックスは各パーティションにローカルなので、パーティションを開くコストは下げても Append の大きさは変えません。
パーティション数の根拠
すべての HASH(namespace_id) テーブルは 64 パーティションを使用し、blob_storage_blobs と blob_storage_attachments(HASH(sha256))に選ばれた 64 パーティションと一致します。この数は、既存の Container Registry と Package Registry のデータベースからの本番データに基づいています。
パーティション数は、最大の想定テーブル(container_blobs)によって駆動され、その本番アナログはすでに同等の規模で 64 パーティションを使用しています。他のフォーマット固有のテーブルは大幅に小さく、64 パーティションはそれらすべてにとって余裕があります。
この決定の主要な要因:
- スキュー耐性:
HASH(namespace_id)は均一な分布を保証しません。ネームスペースのサイズは大きく偏っています。少数の大きなネームスペースが不均衡な割合の行を保持します。パーティション数が少ないと、同じパーティションにハッシュされる大きなネームスペースが不均衡を増幅します。64 パーティションでは、最悪のスキューでもパーティションサイズを管理可能に保ちます。 - アンダーパーティショニングの修正は高コスト: 後でパーティション数を変更するには、テーブルの完全な再構築が必要です。小さなテーブルをオーバーパーティショニングするオーバーヘッドは無視できますが、大きなテーブルをアンダーパーティショニングすると実際の運用リスクが生じます。
- パーティションワイズ JOIN: PostgreSQL は、同じパーティションスキーム(同じキー、同じ方法、同じ数)を共有するテーブル間の JOIN を、一致するパーティションを直接 JOIN することで最適化できます。すべての
HASH(namespace_id)テーブルが 64 パーティションを使用するため、この最適化が利用可能です。実際には、クエリはすでにnamespace_id = ?を含むため、プランナーは各側について 1 つのパーティションにプルーニングしますが、パーティションワイズ JOIN は無料の最適化のままです。 - 運用上の一貫性: すべての
namespace_idパーティショニングされたテーブルにわたる単一のパーティション数は、特定のnamespace_idのすべてのテーブルが同じパーティション番号にハッシュされることを意味し、メンテナンススクリプト、監視、一括操作を簡素化します。
どのテーブルがパーティショニングされるかは、ここで列挙されるのではなく パーティショニングの不変条件 によって決定されます。
バッファ書き込みと非同期書き込み
ダウンロードまたはアップロードのリクエストごとに、複数のカラムが更新されます。repositories のカウンターカラム(artifacts_count、downloads_count、size_bytes)、エンティティ数の上限チェックに使用する npm_packages のパッケージごとのカウンター(versions_count、tags_count)、Maven と npm のバージョンテーブル(maven_versions、maven_remote_versions、npm_versions、npm_remote_versions)の size_bytes カウンター、そして container_images、maven_packages、maven_versions、npm_packages、npm_versions の last_downloaded_at タイムスタンプです。これらをリクエストパスで直接書き込むと、同じ行に対する同時リクエストが直列化され(人気のあるパッケージでホット行の競合が発生し)、リクエストのレイテンシがデータベースの書き込みスループットに結び付きます。
これを避けるために、これらのカラムはバッファ書き込み/非同期書き込みを介して維持されます。リクエストハンドラーは高速な中間ストア(例: Redis)に更新を記録し、バックグラウンドプロセスが定期的にバッファされたエントリを行にマージし直します。これは GitLab の ProjectStatistics と同じパターンを再利用します。
この方法で維持されるカラムは、スキーマ図で buffered とフラグ付けされています。
マージのセマンティクス
マージ戦略はカラムの種類に依存します。
- カウンター(
artifacts_count、downloads_count、size_bytes、versions_count、tags_count): バッファされたデルタを既存の値に合計します。すべてのインクリメントを保持しなければなりません。インクリメントを失うと永続的なアンダーカウントが生じます。エンティティ数制限チェック(versions_count、tags_count)については、境界での小さな上限超過は許容されます。制限は(データ整合性ルールではなく)製品の上限であり、ドリフトはバッファウィンドウで境界づけられ、次のフラッシュで再同期されます。重複するバージョン名は、カウンターに関係なく、npm_versionsとnpm_tagsのユニークインデックスによって別個にブロックされます。 - タイムスタンプ(
last_downloaded_at): バッファされた値と既存の値の最大値を取ります(最新が勝つ)。最も新しいダウンロード時刻のみが重要です。中間の値は破棄できます。
両方の戦略は同じバッファリングインフラを共有し、書き込み前にバッファされたエントリがどのように削減されるかだけが異なります。
トレードオフ
- 古さ: バッファされたカラムは、最大 1 つのフラッシュ間隔だけ現実から遅れます。これは現在のコンシューマーにとって許容できます。ライフサイクルルールの評価(
keep_last_downloaded_at)はフラッシュ間隔を十分に上回るスケジュールで実行され、ランディングページのカウンターは短時間の乖離を許容します。これは、自身の書き込みを同期的に観測しなければならない読み取りや、ダウンロードイベントの正確な順序を必要とする決定には_適していません_。 - バッファ損失: フラッシュ前にバッファが失われると、最近の更新が失われます。カウンターについてはこれは永続的なアンダーカウントです。タイムスタンプについては、次のダウンロードが正しい(ただしわずかに遅延した)値を復元します。
ネームスペース ID 型
以下の決定は namespaces.id だけでなく、この ADR が定義するすべてのテーブルの id に適用されます。内部 blob ストレージ層だけが例外です。型選択による影響が最も広い namespaces.id について根拠を論じます。
namespaces.id カラムの型はスキーマ全体にカスケードします。すべてのパーティショニングされたテーブルがシャーディングキーとして namespace_id を保持し、それらのテーブルの本質的にすべての複合主キー、外部キー、複合インデックスがこのカラムを含みます。外部キーと複合インデックスでは先頭にあり、複合主キーでは id の後にあります。機械的な帰結では、(id) が (id, namespace_id) になると述べています。後で型を変更するには、すべてのパーティショニングされたテーブルとすべての物理的な子リレーションにわたる多段階のマイグレーションが必要であり、スキーマが本番データを保持すると実質的に取り消し不可能な決定になります。
選択を駆動する 3 つのプロパティ:
- デプロイモデル間でのグローバルな一意性。 Artifact Registry は、複数の独立したデプロイ(GitLab.com、Dedicated、Self-Managed、Cell ごと、および潜在的には GitLab Rails から独立したスタンドアロン製品)として実行されるように設計されています(ADR-022 を参照)。ローカルなシーケンスから引かれる連続した整数 ID はデプロイ間で衝突し、ネームスペース行が Artifact Registry インスタンス間を移動するあらゆるシナリオ(MVP 後のマイグレーションツール、Cell の統合、デプロイ間の参照)を排除します。
- 運用上のデバッグ容易性。
namespace_id = 42はデプロイ間で曖昧です。同じ整数が、異なる Cell やインストールで無関係なネームスペースを指し得ます。サポートチケット、インシデントの runbook、デプロイ間のログ相関はすべて、識別子が一見して一意であるときに恩恵を受けます。 - ID 生成のための調整依存がない。 デプロイ間で重複しない bigint 範囲を割り当てるには、中央権威(Topology サービスまたは同等のもの)が必要です。UUIDv7 はデータベース上でローカルに、調整なしで生成されます。
オプション
オプション A: UUIDv7
namespaces.id は UUIDv7 値(RFC 9562)が入力される uuid です。スキーマ全体のすべての namespace_id カラムは uuid です。生成は、データベース側(PG18 ネイティブの uuidv7()、または PG13〜17 の pg_uuidv7 拡張)でも、RFC 9562 準拠のライブラリを使用したアプリケーション側でも行えます。カラム型はすべての場合で同じで、パスは後でデータを書き換えることなく変更できます。完全なマトリックスについては下記の決定セクションを参照してください。
ポジティブ:
- すべての Artifact Registry デプロイにわたって構造的にグローバルに一意です。調整も、中央アロケーターも、範囲管理もありません。数千のデプロイが同時に生成しても、衝突は暗号学的に極めて起こりにくいものです。
- 時刻順: 新しい ID は各パーティション内の B-tree の右端に追加されます。credativ による PG18、100 万行の比較 では、UUIDv7 の主キーインデックスは約 90% のリーフ密度(bigint シーケンスも達成するデフォルトの
fillfactor)と約 0% のフラグメンテーションを達成しました。同じワークロードでの UUIDv4 の約 71% のリーフ密度と約 50% のフラグメンテーションと比べてです。 - WAL ボリュームは UUIDv4 よりも bigint にはるかに近いです。UUIDv7 のシーケンシャル挿入の局所性は、ランダムな UUID が被るフルページ書き込みの増幅を回避します。挿入スループットは、現実的な複数カラムスキーマで bigint と数パーセント以内で一致します(kkm-mako、PG18、100 万行・13 カラムの e コマーステーブル: bigint 76.5 秒対 UUIDv7 77.0 秒、Ardent Performance、PG17-dev、10 個の同時クライアントを持つ 2000 万行のテーブル: bigint 3,480 tps 対 UUIDv7 3,420 tps)。むき出しの 2 カラムのトイスキーマでは差がより顕著です。kkm-mako の最小スキーマ は同じ行数で bigint 1.63 秒対 UUIDv7 2.16 秒(約 32% 遅い)を計測しました。これは、より幅広い ID カラムが行のより大きな割合を占めるためです。絶対値はワークロード依存です。
- 埋め込まれたミリ秒タイムスタンプにより、ID は BRIN フレンドリーで、診断のために自明に抽出可能です。
- Artifact Registry が実行され得るすべての PostgreSQL バージョンで利用可能です。PG18 は
uuidv7()をネイティブに出荷します(2025 年 9 月)。PG13〜17 ではpg_uuidv7拡張 が公開ベンチマークによればネイティブに対して 2% 未満のオーバーヘッドでuuid_generate_v7()を提供し、どのバージョンでも RFC 9562 準拠のライブラリでアプリケーション側生成をサポートします。 - デプロイ間のネームスペースの可搬性を構造的に可能にします。MVP 後のマイグレーションツール(ADR-011)、Cell の統合、ADR-022 のスタンドアロン製品パスは、関連するすべての行の
namespace_idを書き換えることなく、Artifact Registry インスタンス間でネームスペース行を移動します。
ネガティブ:
- ストレージ: 値ごとに 16 バイト対 bigint の 8 バイト。
namespace_idはパーティショニングされたテーブルのほぼすべての複合インデックスの先頭カラムなので、この幅広化はすべての物理的な子リレーションにわたって複合的に効きます。Jamauriceholt の PG 15.4 における 2000 万行の外部キーインデックスのベンチマーク は、UUIDv7 で 847 MB 対 BIGSERIAL で 423 MB(約 2 倍)、1 万行の一括挿入で 1,847 バッファ書き込みページ対 847(約 2.2 倍)を計測しました。エントリごとの幅広化はインデックスタプルの約 20 バイト中の約 8 バイト(約 40%)です。観測された合計インデックスサイズは、そのエントリごとの下限から、インデックスがキーか固定オーバーヘッドかの割合に応じて約 2 倍までの範囲です。Artifact Registry のマルチ TB のメタデータ規模では、これは実際のものですが境界のあるコストであり、テーブル全体ではなくnamespace_idを先頭とするインデックスに集中します。 - キー幅に実質的に依存するクエリの読み取りレイテンシは、bigint よりも計測可能に遅くなり得ます。合成的な 500 万ユーザー / 2000 万注文 / 5000 万 audit_log スキーマ(Jamauriceholt) では、1 対多の JOIN が UUIDv7 で BIGSERIAL より約 26 倍遅く、単一行検索が約 15 倍遅く、範囲/ページネーションが約 16 倍遅くなりました。これらの数値は最悪ケースの合成クエリを反映しており、このスキーマに外挿すべきではありません。すべてのホットパスは複合キーに対する単一パーティションの
namespace_id = ?インデックス付き検索です。それらの条件下では、オーバーヘッドは上記のページごとのバイトコストで境界づけられ、クエリ形状のコストに増幅されません。レビュアーがより強力な経験的下限を望む場合、マージ前に PG18 上の代表的な行幅でのパーティションローカルなインデックス付き検索ベンチマークを依頼するのが適切です。 - 時刻順は
HASH(namespace_id)テーブルでのパーティションプルーニングを可能にしません。ハッシングはタイムスタンプ成分に関係なく値をパーティションに散らします。パーティション内の B-tree の局所性は保たれますが、これは bigint シーケンスもより低いストレージコストで提供します。UUIDv7 のパーティションプルーニングの利点はRANGE(uuid)スキームにのみ適用され、ここでは使用されません。 - クライアントライブラリ、管理ツール、API レスポンスは整数ではなく 36 文字の文字列をレンダリングします。軽微ですが広範です。
namespace_idを運ぶあらゆるエンドポイントで JSON レスポンスサイズが増加します。
オプション B: 調整された範囲割り当てを伴う bigint
namespaces.id は bigint DEFAULT nextval('namespaces_id_seq') のままです。各 Artifact Registry デプロイには、Topology サービスによって重複しない bigint 範囲(例: デプロイ X: 1 〜 10^12、デプロイ Y: 10^12+1 〜 2×10^12)がプロビジョニングされます。Artifact Registry はスラッグの取得のためにすでに Topology サービスに依存しています(ADR-022 を参照)。
ポジティブ:
- 現在のドラフトに対してストレージの差分がゼロです。考慮すべきインデックス、WAL、JOIN のコストはありません。
- 既存の依存を再利用します。Topology サービスはスラッグの取得のためにすでに必要です。
- ID 生成はシーケンスの
nextvalのままです。自明に高速で、拡張は不要です。 - Cells 間で調整された bigint シーケンスという GitLab Rails の確立されたパターンと一致します(Cells 開発ガイドライン)。
ネガティブ:
- デプロイ間のネームスペースの可搬性が構造的にサポートされません。Y の割り当て範囲がソース ID を含まない場合、デプロイ X からデプロイ Y へネームスペースを移動するには、依然としてすべての行の
namespace_idを書き換える必要があります。 - 範囲割り当ては、すべての新しい Artifact Registry デプロイにブートストラップステップを追加し、範囲サイズと回収のためのガバナンスモデルを追加します。範囲を重複させてしまう誤割り当ては、早期に検出するのが難しいグローバルな一意性違反です。
- 後でデプロイ間の可搬性をサポートする決定を下すと、この ADR が回避しようとしている bigint から UUID への完全なマイグレーションが必要になります。
オプション C: Snowflake パックされた bigint
アプリケーション側で 64 ビットをビットパックします: デプロイ ID(14 ビット、16K デプロイ)+ タイムスタンプ(41 ビット、エポックから 69 年)+ バックエンドごとのシーケンス(9 ビット、512 ID/ms/バックエンド)。小さなライブラリで Go サービスで生成されます。
ポジティブ:
- bigint に対してストレージの差分がゼロです。同じインデックス、WAL、JOIN プロファイルです。
- 自己識別: デプロイの起源が任意の
namespace_idから抽出可能です。 - UUIDv7 のように時刻順で、同じパーティション内の B-tree 局所性の利点を与えます。
- 拡張依存がありません。ID 生成は数個のビット演算です。
ネガティブ:
- PostgreSQL プリミティブの代わりに Go サービスで保守されるカスタムジェネレーター。すべてのライターは同じライブラリバージョンとクロックソースを使用しなければなりません。
- クロックスキューに敏感: デプロイごとのカウンターはクロックの巻き戻しとバーストトラフィックを生き延びなければなりません。モノトニッククロックの規律と、ミリ秒内のシーケンスカウンターの慎重な扱いが必要です。
- 業界で広く使用されています(Twitter、Discord、Instagram の 41+13+10 バリアント)が、PostgreSQL ネイティブのパターンではありません。ツール、監査可能性、チーム間の親しみやすさは UUID よりも弱いです。
- ビットフィールドの分割は一度限りの設計上の決定です。デプロイビットが少なすぎたり、タイムスタンプ範囲が狭すぎたりすると、後で変更するのが困難です。
- デプロイ間のマイグレーションを解決しません。デプロイ X で生成された ID は X の 14 ビットプレフィックスを永遠に運ぶため、ネームスペースをデプロイ Y へ再配置しても、依然として書き換えか、起源について嘘をつく ID のいずれかを意味します。
決定
namespaces.id にはオプション A(UUIDv7)が選択されました。その結果、スキーマ全体のすべての namespace_id カラムと、API に公開されるすべてのテーブルの id(repositories.id、container_images.id、maven_packages.id など)にも適用されます。これらの id カラムはサーバー側デフォルトを持たない uuid です。アプリケーションレイヤーが INSERT 時に値を供給するため、論理レプリケーションで調整するシーケンスも GENERATED カラムもなく、ネームスペースマイグレーション(ADR-022)に伴うデプロイ間の行再挿入にもシーケンス管理は不要です。内部 blob ストレージ層(blob_storage_attachments、blob_storage_blobs、upload_sessions)は bigint DEFAULT nextval('<table>_id_seq') を維持します。これらのテーブルは API に公開されず、最も大量の行を保持するため、より狭いキーがインデックスサイズを目に見えて削減します。
決定的な要因:
- ネームスペースは可搬性の単位です。 何らかの Artifact Registry 識別子がデプロイ間の移動を生き延びなければならないとすれば、それは
namespace_idです。ネームスペースより下のすべてはそれとともに移動します。ネームスペースより上のすべては不変のスラッグとアンカータプルを通じて表現されます(ADR-022)。 - コストは集中していて境界があります。
namespace_idを 8 バイトから 16 バイトに幅広化することは多くのインデックスの先頭カラムに効きますが、合計ストレージを倍増させません。大きなパーティショニングされたテーブルの行幅は他のカラム(リポジトリ/イメージ/マニフェスト ID、タイムスタンプ、カウンター、32 バイトのbyteaダイジェスト)によって支配されます。予備的なサイジングでは、影響を合計メタデータストレージの数十パーセントに置いており、Artifact Registry のキャパシティ範囲内です。 - 利点はインクリメンタルではなく構造的です。 デプロイ間の移動に触れるすべての MVP 後の機能(ADR-011 のマイグレーションツール、Cell の統合、ADR-022 のスタンドアロン製品パッケージング)は、
namespace_idが構造的にグローバルに一意であるときに意味のある形で簡単になり、アロケーターの不在は調整依存を取り除きます。 - ストレージコストは一度、挿入時に、まだ空のスキーマで支払われます。 オプション B は、デプロイモデルが後でグローバルな一意性を要求する場合、すべてのパーティショニングされたテーブルにわたる取り消し不可能なマイグレーションを必要とします。私たちは、後の無制限なマイグレーションリスクを回避するために、既知の境界のあるコストを今日受け入れます。
- UUIDv7 はホットパスのパフォーマンスプロファイルを保ちます。 単一パーティションの
namespace_id = ?検索は単一パーティションのままです。bigint が提供するパーティション内の B-tree 局所性は、UUIDv7 の時刻順プレフィックスによっても提供されます。失われる唯一のプロパティ(UUID 範囲によるパーティションプルーニング、8 バイトのインデックス先頭カラム)は、HASHパーティショニングに適用されないか、コストが境界づけられています。
実装上の注記:
- 3 つの実行可能な生成パスが存在します。選択はデプロイ時に利用可能な PostgreSQL バージョンに依存し、カラム型とは独立です。
- これらのパス間で後から切り替えるのはメタデータのみ(
ALTER COLUMN SET DEFAULT)であり、すべてのジェネレーターが RFC 9562 準拠の UUIDv7 値を発行する限り、データを書き換えません。これにより、初期パスはスキーマのコミットメントではなくランタイム/運用上の選択になります。 - その準拠はジェネレーターに任せず、データベースが強制します。サーバー側デフォルトを持たない
uuidのidを持つすべてのテーブルには、CHECK ((get_byte(uuid_send(id), 6) >> 4) = 7)というバージョンチェックがあります。uuid_sendは値の 16 個の raw byte を返し、byte 6 の上位 nibble が RFC 9562 のバージョンフィールドです。両方の関数は immutable なので、この式をCHECKに使用できます。UUIDv7 以外を発行するパスの値は、後から発見されるのではなく書き込み時に拒否されます。INSERTがフィールドを割り当てなかった場合の all-zero UUID や、紛れ込んだuuid.New()によるバージョン 4 の値も同様です。 - 残りのテーブルには制限対象の
uuididがないため、チェックを持ちません。blob ストレージ層、idカラムなしでキー付けされる 3 つの Artifact Registry テーブル(repository_collection_repositories、namespace_statistics、(namespace_id, sha256)をキーとするblob_storage_blobs_by_namespaceshadow)、およびキー形状をこのスキーマが設定しないライブラリ所有の schema migration と job queue のテーブルgoose_db_versionとriver_*です。 - すでに行を保持するテーブルへのバージョンチェック追加は、空のテーブルへの追加とは異なります。最初に
ADD CONSTRAINT ... NOT VALID、次に別のVALIDATEを行い、1 つのトランザクションですべてを扱わず、マイグレーションごとに 1 テーブルを扱います。パーティショニングされた親のCHECKは親と 64 個すべてのパーティションでACCESS EXCLUSIVEを取るためです。 - 未解決の問題(GA に近づいたら解決): どの初期パスを取るかは、GA 時点で
.com、Dedicated、Self-Managed にわたって利用可能な PostgreSQL バージョンに依存します。すべてのインストールタイプにわたって PG18 を保証できない場合、アプリケーション側生成が最も安全な暫定的選択です。カラムデフォルトは、PG18 がどこでも下限になったら、ネイティブのuuidv7()に移動できます。 - この ADR のすべての Mermaid 図では、
namespace_idカラムと API に公開されるすべてのテーブルのidをuuidとして示します。blob ストレージ層(blob_storage_attachments、blob_storage_blobs、upload_sessions)だけがbigintのidを維持します。 - UUIDv7 のモノトニック性は、同じミリ秒内の単一バックエンド(データベース側)またはプロセス(アプリケーション側)内で厳格であり、バックエンドやプロセスをまたいでは厳格ではありません。これはインデックスの局所性とデバッグ容易性には十分です。ホットパスのロジックが接続をまたいだ厳格なグローバル順序を想定することはありません。
- スラッグから
namespace_idへの検索キャッシュ(ADR-022 を参照)は影響を受けません。それは不変のスラッグをキーにします。 - パーティショニングされたテーブルで使用する複合主キーパターン(例えば、PostgreSQL のパーティショニングテーブルの制約ルールで必要となる
upload_sessionsの(id, namespace_id))は引き続き維持されます。namespace_idの構成要素はuuidです。idの構成要素は blob ストレージ層を除くすべてのテーブルでuuidであり、blob ストレージ層(例えばupload_sessions)ではbigintです。
パーティションスキーマの構成
パーティショニングされたテーブルごとに 64 個の HASH パーティションがあり、後で中間層テーブルがパーティショニングされるにつれて成長するパーティションセットがあるため、子リレーションは論理テーブルを大きく上回ります。これらの子がどこに存在するか(public で親と並べるか、専用のネームスペースに置くか)は、スキーマの可読性、ツールの整合性、そしてパーティショニングされたテーブルの周りに構築するマイグレーションツールを形作ります。
オプション A: パーティション子のための専用スキーマ
親テーブルは public に存在し、すべてのパーティション子は専用の partitions スキーマに存在します。パーティション DDL は、すべての CREATE TABLE ... PARTITION OF で明示的にパーティションスキーマをターゲットにします。そうでなければ PostgreSQL は子を親のスキーマに配置します。
ポジティブ:
- カタログの可読性:
\dt public.*、information_schema、ER 図、IDE のスキーマビューは、すべてのパーティション子ではなく論理テーブルのみを表示します。スキーマレビュー、オンボーディング、DB コンソール作業は、エンジニアが実際に推論する抽象化で動作します。 - アプリケーションレイヤーは影響を受けません。アプリケーションは
publicの親テーブルを通じてクエリし、partitionsスキーマを参照することはありません。マイグレーションツールのみが、明示的なpartitions.<name>修飾を使用して子パーティションをターゲットにします。 - パーティションライフサイクル操作のためのクリーンなスコープ化: 権限、
pg_dump -n、ロジカルレプリケーションパブリケーション、監視エクスポーターは、テーブル名パターンの代わりに単一のネームスペースをターゲットにします。 - 偶発的なパーティションレベルのクエリを抑制します。特定の子に到達するには
partitions.<name>が必要で、パーティション抽象化をバイパスするのを難しくします。
ネガティブ:
- Postgres のデフォルトが規約に反します。
CREATE TABLE ... PARTITION OF parentは、明示的にオーバーライドされない限り子を親のスキーマに配置するため、強制はデータベース自体ではなくマイグレーションツール、リンター、または CI に存在します。 - パーティショニングヘルパーは子の作成をパーティションスキーマにルーティングしなければならず、サービスのブートストラップはマイグレーション実行前にスキーマとその権限をプロビジョニングしなければなりません(ADR-006)。
- ランタイムの利点はありません。プルーニング、ロック、VACUUM、クエリパフォーマンスは変わりません。このケースは完全に組織的なものです。
オプション B: すべてのテーブルを public に
親とその子パーティションはデフォルトスキーマに一緒に存在します。これは追加設定なしの PostgreSQL のすぐ使える動作です。
ポジティブ:
- 最も単純なブートストラップ: 追加スキーマ、権限の分割、マイグレーションツールのパーティションルーティングヘルパーがありません。ローカル開発、CI、マイグレーションはセットアップなしで動作します。
- Postgres のデフォルトとサードパーティツールの想定(イントロスペクション、ORM、クエリアナライザー)と一致し、ツールごとの設定を回避します。
ネガティブ:
- カタログの煩雑さ: すべてのパーティション子が論理テーブルとネームスペースを共有し、すぐにあらゆる
\dt、information_schemaクエリ、ER 図を支配します。新しいテーブルがパーティショニングされるにつれて問題は複合的になります。 - パーティションライフサイクルツールのためのスキーマレベルのスコープ化がありません。
pg_dump、ロジカルレプリケーション、監視はテーブル名パターン(blob_storage_blobs_*、*_files_*など)として表現しなければなりません。 - パーティションレベルのクエリ(例:
SELECT FROM blob_storage_blobs_37)は通常のテーブル参照と区別がつかず、パーティション抽象化をバイパスするのを容易にします。
決定
オプション A(専用の partitions スキーマ)が選択されました。
決定的な要因は、アプリケーション向けのテーブルとパーティショニングの内部の区別です。論理テーブルは、アプリケーションが読み書きする公開インターフェースです。パーティション子はパーティショニングメカニズムの内部であり、パーティションライフサイクルツールによってのみ触れられるべきです。両方を単一のスキーマに保つことはその境界をぼかします。スキーマのイントロスペクション、権限、運用ツールはすべて、それらを区別するために名前でフィルターしなければなりません。専用の partitions スキーマは、その区別をデータベース自体の中で構造的にします。パーティションライフサイクル操作は 1 つのネームスペースにスコープされ、public を読むものにはアプリケーションに公開される操作対象だけが見えます。
可読性の議論がこの選択を補強します。パーティション子は最初のデプロイから論理テーブルを大きく上回り、より多くのテーブルがパーティショニングされるにつれてギャップが広がるため、単一スキーマのレイアウトは最初のデプロイから不格好で、時間とともに悪化します。ブートストラップコスト(マイグレーションツールのパーティションルーティングヘルパー、起動時のスキーマ作成)は一度限りであり、同じマイグレーション抽象化を採用するすべてのサテライトサービスにわたって償却されます(ADR-006)。
このパターンは規模で検証されています。GitLab Rails はパーティション子を専用の gitlab_partitions_static と gitlab_partitions_dynamic スキーマに整理しています。
パーティション子のみが専用スキーマに移動します。親テーブルと明示的なパーティショニングのないテーブルは public のままです。
クリーンアップタスク
上記のアプローチを理解するには、クリーンアップに関する blob ストレージ部分の課題を理解することが重要です。
一方では、親オブジェクトが破棄される一環として削除される 1 つまたは複数のアタッチメントを持つことができます(パッケージが破棄される、またはクリーンアップポリシーが実行されて数百のファイルを削除する)。
他方では、blob テーブルからレコードを単純に削除することはできません。それらはオブジェクトストレージ上のファイルを参照するためです。したがって、blob レコードを取得し、それを削除し、オブジェクトストレージ上のファイルも削除するクリーンアップタスクが必要です。これはデータベースでは実行できません。バックグラウンドプロセスとして実装されるコールバックが必要です。
blob を破棄のために扱う前に、バックエンドは(重複排除のため)それがもうどの部分にも使用されていないことを確認する必要があります。そこでアタッチメントテーブルが重要な役割を果たします。それは特定の blob の使用を記録します。クリーンアップタスクは、(namespace_id, sha256) ペアがアタッチメントテーブルにまだ存在するかどうかを単純に尋ねるだけで済みます(孤立チェッククエリ を参照)。それが「いいえ」であれば、blob は削除して問題ありません。
このアプローチは、各 blob ストレージクライアントに取り組むエンジニアにとってクリーンアップ契約を単純に保ちます。アーティファクトレコード(単一ファイル、一括破棄、またはクリーンアップポリシーの実行)を削除する際、アプリケーションは対応する blob_storage_attachments レコードも同じトランザクション内で削除しなければなりません。これがクライアントレベルでの唯一のクリーンアップ責任です。オブジェクトストレージとの対話は不要です。その時点から、blob ストレージのバックグラウンドプロセスが引き継ぎます。それは残りのアタッチメントを持たない blob_storage_blobs 行を特定し(孤立チェック)、データベースレコードとオブジェクトストレージファイルの両方を削除します。
アップロードセッションのクリーンアップも同様のパターンに従います。upload_sessions テーブルはバイナリ存在モデルを使用します。行が存在すれば、アップロードは進行中であるかクリーンアップが必要です。したがって、期限切れのセッション(expires_at < NOW() のもの)はパージの候補です。パージャーは一時ストレージオブジェクトを削除し、行を削除します。テーブルは、ストレージ内のオブジェクトを列挙することなく、候補を特定しストレージパス(ネームスペースパーティション下の uploads/{upload_id})を導出するために必要なすべての情報を提供します。アップロードパージの出荷スケジュールについては ADR-011 を参照してください。
この設計図は、クリーンアッププロセスを可能にし得る高レベルのデータベースプリミティブ(アタッチメント追跡、blob ストレージ構成、アップロードセッション追跡)を確立しますが、具体的な実装の詳細(トリガー、バックグラウンドジョブのロジック、パフォーマンス分析)は、後の詳細な仕様作業に委ねられます。
ストレージ使用量の計算
ストレージ使用量は、ネームスペース、リポジトリ、アーティファクトという 3 つのスコープで追跡します。各スコープには、サブミリ秒の読み取りで表示パスを提供する事前計算済みのカウンターと、ドリフトが疑われる場合やオンデマンドの検証が必要な場合にソースデータから正確な値を計算する調整パスがあります。blob ストレージスキーマは、これらの計算と帰属を正確かつ効率的にするように設計されています。
- blob とアタッチメントは Organization にスコープされ、重複排除は Organization _内_でのみ発生します(ADR-002 を参照)。
blob_storage_blobsは Organization ごとに一意の保存済み blob ごとに 1 行を持ちます。オブジェクトストレージ内の各物理オブジェクトは、Organization ごとに一度表現されます。- 物理 blob と
blob_storage_blobsレコードは、すべてのアタッチメントを失ったときに(クリーンアッププロセス を通じて)非同期にクリーンアップされるため、blob_storage_blobsはまだ使用中(または非同期削除を保留中)の blob のみを参照します。結果として、ストレージ使用量クエリはアタッチメント数でフィルターする必要がありません。
したがって、特定の Organization のストレージ使用量を計算することは、blob_storage_blobs に列挙されたその blob のサイズを合計するだけのことです。これはマニフェストごとの container_manifests.size(コンテナリポジトリ を参照)とは異なります。後者は「このマニフェストツリーはどのくらいの大きさか」に答え、マニフェスト間やマニフェストリストの子間で共有される blob を二重カウントすることがあるため、Organization レベルの使用量の代替にはなりません。
別個の ADR が、ストレージ使用量の計算と帰属をより詳細に記述します。この ADR は、それらの計算を容易にするデータベースプリミティブを定義します。
erDiagram
namespaces ||--|| namespace_statistics : "has one"
namespace_statistics {
uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE CASCADE"
bigint deduplicated_size_bytes "NOT NULL, DEFAULT 0, buffered counter"
bigint components_count "NOT NULL, DEFAULT 0, buffered counter"
timestamptz last_reconciled_at "NOT NULL, DEFAULT 'epoch', reconciliation bookkeeping"
}- namespace_statistics: バッファカウンター(非同期フラッシャー)を介して維持される、事前計算されたネームスペースレベルのカウンターを保存します。これは、表示パスと課金システムが読み取るテーブルであり、サブミリ秒のレスポンスを提供します(ベンチマーク表 を参照)。調整メカニズム は、ドリフトが疑われるときにこれらのカウンターを検証・修正するために存在します。構造上、各ネームスペースには 1 行が存在します。テーブル作成時に存在するすべてのネームスペースをシードし、その後
namespacesに挿入されるネームスペースごとにAFTER INSERTトリガーが 1 行を作成するため、読み取りパスは行が存在しない場合とゼロになったカウンターを区別する必要がありません。namespace_id外部キーは、このスキーマ内でreferences namespaces(id)にON DELETE CASCADEを伴う 2 つの外部キーの一方です。もう一方は同じルールの同じ側にあるblob_storage_blobs_by_namespaceshadow テーブルのものであり、トリガーが必要とする参照アクションです。トリガーはすべてのネームスペースに子行があることを保証するため、カスケードがなければ、ネームスペース自身が存在させた行によってネームスペースのハード削除が拒否されます。この行はユーザーデータではなく導出された記録であるため、repositories で述べたルール、すなわち子が純粋な構造であればカスケードし、ユーザーデータであれば拒否するというルールのカスケード側に該当します。主キーは正確に(namespace_id)で、独立したidカラムはなく、このスキーマ内のどの外部キーからも参照されないため、パーティショニングの不変条件の例外にある 2 つ目の条件によりパーティショニングされません。この条件を適用する唯一のテーブルです。deduplicated_size_bytes: ネームスペースが使用する合計ストレージで、blob の重複排除がすでに適用されています(ADR-002 を参照)。このカラムは、将来の生サイズや論理サイズのメトリクスと区別するために、将来の互換性を持たせるべく(size_bytesではなく)このように命名されています。components_count: ネームスペースのホスト型およびリモート型リポジトリに保存されたアーティファクトバージョンの合計数:- Container:
container_manifests+container_remote_manifests。 - Maven:
maven_versions+maven_remote_versions。 - npm:
npm_versions+npm_remote_versions。
ソフト削除された行は、ソフト削除ウィンドウ が期限切れになった後にガベージコレクションがハード削除するまでカウントされ続けます。これは
deduplicated_size_bytesと一致します。これは、ガベージコレクションが基となる blob を回収するまで、ソフト削除されたアーティファクトのバイトを保持します。仮想リポジトリは、独自のバージョンテーブルを持たないため、別途カウントされません。仮想リポジトリは上流の順序付きリストを通じてリクエストを解決し(container_virtual_repository_upstreamsとその Maven および npm の同等のものを参照)、各上流はそれ自体がホスト型またはリモート型のリポジトリであり、そのバージョンは上記のテーブルを介してすでに含まれています。その上で仮想リポジトリをカウントすると、その上流を二重カウントすることになります。これは消費ベースの価格設定とメータリングのためのネームスペースレベルの次元であり、deduplicated_size_bytesを補完します。ストレージ使用量とともにネームスペース概要に表示されます。- Container:
last_reconciled_at: ネームスペースで完全な調整パスが最後に完了した実時間であり、完全に成功した場合にのみ記録されます。これは記録用であり、カウンターでも課金入力でもありません。キャッチアップ候補の選択と古さのスキャンを駆動し、API には決して表示されません。'epoch'デフォルト(1970-01-01)により、新しくシードされた行はテーブル内で最も古い行としてソートされるため、最初に成功したパスで実際の時刻が記録されるまで必ず選択されます。また、このカラムに NULL を保持する行はありません。'-infinity'の方がこの意図を明確に表し、当初はこちらを選択していましたが、読み戻すことができません。time.Timeはこれを保持できないため、このカラムをtime.Timeとして型付けする生成モデルは、デフォルト値を保持している行をスキャンできません。最初のパスが実行されるまではすべての行が該当し、テーブルを読み取るたびにこのカラムを射影から除外するか、無限大をモデル化する型で保持しなければなりません。通常のモデルで印を付けた行を読み取れなくするセンチネルは、記録用カラムには適切なトレードオフではありません。'epoch'を使うと、実際のパスがセンチネル値を記録することは絶対にないという保証は失われますが、これは許容します。パスが 1970-01-01 を記録するにはシステムクロックが数十年単位で誤っている必要があり、このカラムは候補選択だけを駆動する記録用です。課金入力ではなく API にも表示されないため、誤読が起きても調整パスが遅延または繰り返されるだけで、カウンターが破損することはありません。
ネームスペースレベルのストレージ会計の調整
namespace_statistics.deduplicated_size_bytes カウンターとリポジトリレベルの repositories.size_bytes カウンターは、サブミリ秒の読み取りで表示パスを提供します。しかし、2 つの調整シナリオでは、キャッシュされたカウンターではなくソースデータから正確なストレージを計算する必要があります。
- オンデマンドの検証: 顧客が「私の課金は正確か?」と尋ね、私たちはソースデータからネームスペースの正確なストレージを計算する必要があります。これは、すべての 64 個の
sha256パーティションにわたるSUM(size) FROM blob_storage_blobs WHERE namespace_id = ?を意味します。 - ドリフトの修正: 失敗した GC 実行、部分的なフラッシュ、またはその他のイベントがキャッシュされたカウンターを非同期化し、私たちはそれを修正するために正確な値を再計算する必要があります。
blob_storage_blobs は HASH(sha256) でパーティショニングされているため、任意の namespace_id のみのクエリは 64 パーティションすべてにファンアウトします。CloudSQL PostgreSQL 18 インスタンス上の ベンチマーク(シード されたデータセット: 64 個の sha256 パーティションにわたる約 160 万個の blob、Zipf 分布の blob 所有権を持つ 50 万のネームスペース、blob が最も重いネームスペースは 35.3 万個の blob)は、最も重いネームスペースについてベースラインで 78 ms と約 3K+ のバッファヒットを示しています。2 つの加算的な保険ポリシーがこれを改善できます。
オプション A — blob_storage_blobs のカバリングインデックス: 各パーティションの既存の namespace_id インデックスに INCLUDE (size) を追加します。これは 64 パーティションのファンアウトを、最小限またはゼロのヒープフェッチで 64 個のインデックスオンリースキャンに変えます。スペースのオーバーヘッドは無視できます(既存のインデックスのリーフページに size カラムが追加されるだけです)。
オプション B — ネームスペースパーティショニングされたシャドウテーブル: HASH(namespace_id) で 64 パーティションにパーティショニングされ、blob_storage_blobs の AFTER INSERT/DELETE トリガーを介して維持される専用の blob_storage_blobs_by_namespace テーブルです。これは調整クエリを単一パーティションのインデックスオンリースキャンに折りたたみます。スペースのオーバーヘッドは中程度です(blob データの最小サブセット — namespace_id、sha256、size — を 64 個の新しいパーティションとインデックスにわたって複製し、blob 数に比例して線形に成長します)。トレードオフは、すべての blob の INSERT/DELETE での書き込み増幅ですが、調整負荷をメインの blob_storage_blobs テーブル(ホットパス)から遠ざけます。
erDiagram
blob_storage_blobs_by_namespace {
uuid namespace_id PK,FK "NOT NULL, PK with sha256, references namespaces(id) ON DELETE CASCADE"
bytea sha256 PK "NOT NULL, PK with namespace_id"
bigint size "NOT NULL, INCLUDEd in both indexes"
}blob_storage_blobs のトリガーがこのテーブルを維持します。AFTER INSERT は (namespace_id, sha256, size) をシャドウテーブルにコピーし、AFTER DELETE は一致する行を削除します。AFTER UPDATE トリガーは不要です。blob_storage_blobs の行は不変だからです。コンテンツアドレス可能なストレージは、コンテンツへの変更が新しい sha256、したがって新しい行を生成することを意味します(ADR-008 を参照)。主キー (namespace_id, sha256) INCLUDE (size) はパーティションキー(namespace_id)を含まなければならず、blob_storage_blobs のユニークキーを反映します。size も主キーに含まれるため、ダイジェストによる検索では一致する行ごとのヒープフェッチを避け、インデックスオンリーで処理できます。テーブルは他の HASH(namespace_id) テーブルと同じ 64 パーティション数を使用します。(namespace_id) INCLUDE (size) のカバリングインデックスは、ネームスペース全体の合計を求めるインデックスオンリースキャンを可能にします。主キーも namespace_id が先頭にあるため、その合計をインデックスオンリーで処理できます。それでもカバリングインデックスを維持するのは機能上の理由ではなくスキャンコストのためです。カバリングインデックスのエントリには、合計で読み取らない 32 バイトの sha256 が含まれず、幅の広い主キーインデックスの大部分を占めるこのカラムを走査せずに済みます。
namespace_id 外部キーは namespaces(id) を ON DELETE CASCADE で参照します。shadow 行はユーザーデータではなく blob_storage_blobs 行から導出されたコピーなので、namespace_statistics と同じ理由で repositories のルールのカスケード側に置かれます。shadow 行の生存期間は 2 つのうち短い方で制限され、2 つの reaper がそれを強制します。garbage collection が blob_storage_blobs 行を回収すると AFTER DELETE トリガーが削除し、namespace がなくなるとこのカスケードが削除します。shadow は blob_storage_blobs のレプリカではなく、namespace ごとの読み取りを高速化する派生アクセラレーターなので、どちらが先でも正しい動作です。
blob_storage_blobs は意図的に namespaces(id) 参照を持たず、未完の延期ではありません。行は保存オブジェクトへの唯一のハンドルである object_storage_key を所有し、ADR-025 ではオブジェクトを行より先に削除します。そのため、namespaces からのカスケードはオブジェクトが存在する間にハンドルを破壊して漏えいさせます。ブロックする参照も、意図的に延期されレート制限された garbage collection サイクルの完了まで namespace の削除を止めるため適切ではありません。テーブルを作成したマイグレーション 20260612130000_create_blob_storage_blobs.sql は両方の欠けた参照を後続マイグレーションへ延期すると説明していますが、namespaces(id) 側についてはこの決定がそれに優先します。repositories(id, namespace_id) 側は別の問いであり、このセクションでは立場を取りません。
したがって namespace のハード削除は shadow の行を削除し、それがコピーした blob_storage_blobs 行は garbage collection が独自のスケジュールで回収するまで残します。shadow の読者がそのずれを観測することはありません。repositories と blob_storage_attachments は参照アクションなしで namespaces を参照するため、その namespace 配下のすべてのリポジトリとアタッチメントがなくなるまで削除は進まず、調整を駆動する namespace_statistics 行も同じステートメントでカスケードされます。
| アプローチ | タイミング | バッファ | スキャンされたパーティション | 書き込みオーバーヘッド |
|---|---|---|---|---|
namespace_statistics カウンター(表示パス) | 0.013 ms | 1 | 0 | 非同期フラッシャー |
| シャドウテーブル + カバリングインデックス(オプション B) | 29 ms | 1,361 | 1 | トリガー |
| blob のカバリングインデックス(オプション A) | 43 ms | 1,599 | 64 | なし |
| ベースライン(変更なし) | 78 ms | ~3K+ | 64 | なし |
両方のオプションは純粋に加算的であり(blob_storage_blobs 自体への変更なし)、独立して追加または削除できます。相互排他的ではありません。両方とも初期スキーマに含まれます。より多くのカバレッジで始めて、本番メトリクスが不要だと確認したら後でインデックスや補助テーブルを削除する方が簡単です。
ネームスペースレベルのコンポーネント数の調整
namespace_statistics.components_count カウンターは、表示パスとメータリングパイプラインを提供します。ストレージカウンターと同様に、2 つのシナリオでソースデータから正確な値を再計算する必要があります。
- オンデマンドの検証: 顧客(または課金)がコンポーネント数が正確かどうかを尋ね、私たちはそれをソース行から導出する必要があります。
- ドリフトの修正: 失敗したフラッシュ、部分的なバッファ損失、またはバックグラウンドジョブのバグがカウンターを非同期化し、私たちはそれを再計算する必要があります。
調整は、ネームスペースの行にスコープされた 6 つの独立したカウントを合計します。3 つのホスト型(container_manifests、maven_versions、npm_versions)と 3 つのリモート型(container_remote_manifests、maven_remote_versions、npm_remote_versions)です。
再計算された値が components_count が追跡するものと一致するように、ソフト削除された行も含まれます(挿入はインクリメント、ガベージコレクションのハード削除はデクリメント、ソフト削除と復元は no-op)。この括弧内の「garbage collection のハード削除」は、どのパスが実行するかにかかわらず、カウント対象行のあらゆるハード削除を含みます。フォーマット自身の削除は独自のトランザクションで行を削除し、そのようなルートで扱えない行は lifecycle purger が削除します。
SELECT
(SELECT COUNT(*) FROM container_manifests WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM container_remote_manifests WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM maven_versions WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM maven_remote_versions WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM npm_versions WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM npm_remote_versions WHERE namespace_id = $1)
AS components_count;
各サブクエリは単一のソーステーブルでの namespace_id によるカウントであり、soft_deleted_at 述語はありません。そのため、テーブルにまだある行(ライブ + ソフト削除ウィンドウ 内のソフト削除されたもの)が components_count が追跡するものと一致します。6 つのソーステーブルはすべて HASH(namespace_id) でパーティショニングされているため、各サブクエリは単一のパーティションにプルーニングされ、そのパーティションでネームスペースの行をスキャンします。既存の部分ユニークインデックス(WHERE soft_deleted_at IS NULL)はライブ行のみをカバーするため、カウントを直接満たすことはできません。ネームスペースごとのカーディナリティはデータモデルによって境界づけられ(バージョンごとに 1 行、ファイルや blob 参照ごとではない)、調整は頻繁ではない(オンデマンドまたはドリフト修正であり、ホットパスではない)ため、境界のあるスキャンは許容できます。追加の保険ポリシー(カバリングインデックスやシャドウテーブル)は導入されません。本番メトリクスがこれが遅すぎることを示した場合、各ソーステーブルの非部分的な (namespace_id) インデックスが、シャドウテーブルを検討する前の最も安価な次のステップです。
リポジトリレベルのストレージ会計の調整
repositories.size_bytes カウンターはバッファ書き込み/非同期書き込みによって維持され、サブミリ秒の読み取りでランディングページのハイブリッドリストを提供します。ネームスペースレベルのカウンターと同様に、次の 2 つのシナリオではソースデータから正確な値を再計算する必要があります。
- オンデマンドの検証: ユーザーが「このリポジトリは実際にどれだけのストレージを使用しているか?」と尋ね、ソース行から正確な値を導出する必要があります。
- ドリフトの修正: 失敗したフラッシュ、部分的なバッファ損失、またはバックグラウンドジョブのバグによってカウンターの同期がずれ、再計算する必要があります。
各組み合わせが異なるテーブルの連鎖を通じて blob ストレージに到達するため、調整は (repositories.format, repositories.kind) で分岐します。各フォーマットについて、クエリはリポジトリ内のアーティファクトから参照されるすべての blob の sha256 を収集し、リポジトリ内で重複排除するために DISTINCT を適用し、サイズを得るために blob_storage_blobs_by_namespace へ JOIN します。クエリは、中間層のテーブルを各 *_repository_id カラムでフィルタリングします。これらのカラムは repositories.id ではなく、フォーマット固有のスタブテーブル自体の id を参照します。以下のクエリ例では、小さな CTE を介して repositories.id からスタブの id を解決するため、親の識別子を使用して呼び出せます。
- Container:
container_images(container_repository_idでフィルタリング)→container_blobsとcontainer_manifests。両方のテーブルがblob_sha256を保持し、UNION によってレイヤー blob とマニフェストペイロードを対象にします。 - Maven:
maven_packages(maven_repository_idでフィルタリング)→maven_files。単一のファイルテーブルが、パッケージのバージョン固有ファイルとパッケージレベルファイルを対象にします。 - npm:
npm_packages(npm_repository_idでフィルタリング)→npm_versions→npm_filesに加え、npm_metadata_files(バージョンレベルではなくパッケージレベルをキーとする)との UNION を使用します。 - リモート型のバリアント(
kind = remote)は、キャッシュテーブル(*_remote_*)に対して同じ形を使用します。キャッシュもリポジトリのフットプリントの一部です。
SUM(DISTINCT bsb.size) は誤りです。異なる blob が同じ size 値を持つ場合があり、同じ長さの小さなファイルがまとめられてしまいます。調整では、最初に SELECT DISTINCT blob_sha256 を実行し、その後でのみ合計を求めるために blob_storage_blobs_by_namespace へ JOIN しなければなりません。サイズは blob_storage_blobs 自体ではなく、ネームスペースのシャドウテーブルから取得します。このシャドウテーブルは (namespace_id, sha256) INCLUDE (size) をキーとしているため、各ダイジェストは 1 つのパーティション内でインデックスオンリーに解決されます。ここでは namespace_id がリテラルなので単一のパーティションに絞り込まれ、size が主キーに含まれるため、その後のヒープフェッチは発生しません。シャドウテーブルのもう 1 つのインデックスであるカバリングインデックス (namespace_id) INCLUDE (size) は、sha256 で行を特定できないため、この読み取りには使用できません。リポジトリから到達できるすべての sha256 は、そのリポジトリのネームスペースに必ず存在するよう設計されています。以下のクエリ例はすべて、この方法でサイズを解決します。
再計算した値を repositories.size_bytes が追跡する値と一致させるため、ソフト削除された行も含めます。
カウンターはリポジトリ内で重複排除されます(調整の DISTINCT blob_sha256 と一致します)。ある sha256 がリポジトリに初めてアタッチされた場合にのみインクリメントし、その sha256 の最後のアタッチメントがリポジトリからなくなったときにデクリメントします。削除するパスは 2 つあります。フォーマット自身の削除は独自のトランザクションでアタッチメントを削除します。ソフト削除されたアーティファクト配下のアタッチメントはそのルートで扱えないため、lifecycle purger が削除します。どちらも garbage collection を待ちません。garbage collection は blob を回収し、別スコープのカウンターである namespace の deduplicated_size_bytes をデクリメントします。ソフト削除と復元は no-op であり、前述した namespace レベルの動作と一致します。
調整のコストは、リポジトリのアーティファクト数ではなく、namespace がハッシュされるパーティションによって制限されます。フォーマット固有のファイル、blob、manifest テーブル(Container の container_blobs と container_manifests、Maven の maven_files、npm の npm_files と npm_metadata_files、さらにそれぞれの *_remote_* キャッシュバリアント)は HASH(namespace_id) で 64 パーティションに分割されるため、走査の各側は単一パーティションに絞り込まれます。その中でプランナーは sequential path を取ります。リポジトリ述語は blob 保持テーブルより 2 JOIN 上のフォーマット固有 stub テーブルにあり、PostgreSQL はこのパスのテーブル間相関統計を保持しないため、実際には 200 行でも 30,000 行と見積もります。PostgreSQL 17.10 で 11 個の EXPLAIN (ANALYZE, BUFFERS) プランを計測し(artifact-registry!1712)、コンテナ走査でリポジトリを 200 digest に保った結果は次のとおりです。
| 収集テーブルのパーティションに共存するもの | 走査プラン | 時間 |
|---|---|---|
| なし。対象リポジトリの行だけ | nested loop、blob_storage_blobs_by_namespace の Index Only Scan | 0.508 ms |
| 同じ namespace の兄弟リポジトリ、59,800 行 | 収集テーブルと shadow に対する Filter: (namespace_id = ...) 付き Seq Scan | 18.157 ms |
ハッシュパーティションは全 namespace の約 1/64 の行を保持するため、sequential pass は namespace 規模ではなく fleet 規模の量になります。この最後の点は計測ではなく、node type と partition count からの推論です。これらのテーブルのフォーマット固有インデックスは、一様に部分インデックスではありません。npm_metadata_files と npm_remote_metadata_files には soft_deleted_at カラム自体がありません。container_blobs と container_manifests では、このドキュメントが指定するカラムは GA 時にソフト削除とともに導入されるため(ADR-010)、現時点のユニークインデックスは述語を持たず、ソフト削除を含めるこの枠組みはそれらには及びません。npm_files、npm_remote_files、container_remote_blobs、container_remote_manifests は正確に soft_deleted_at IS NULL の部分インデックスです。maven_files と maven_remote_files のユニークインデックスには version ID に関する 2 つ目の連言があり、パッケージレベルのインデックスでは maven_version_id IS NULL、バージョンレベルのインデックスでは IS NOT NULL で、maven_remote_version_id にも同じ組み合わせがあります。したがって、リポジトリ比例のコストは排除されておらず、走査を sequential path に保つのはインデックスの欠如や部分インデックスではなく、行数見積もりです。調整は頻繁ではなく(オンデマンドまたはドリフト修正であり、ホットパスではありません)、パーティションスキャンは許容できます。namespace レベルの調整からの最後の blob_storage_blobs_by_namespace 検索は、計測したすべてのプランで単一パーティションに絞り込まれ、プランナーが走査サイズを正しく見積もる場合は index-only で解決され、同じ過大見積もりではそのパーティションの sequential scan にフォールバックします。
リポジトリレベルの調整のために、追加の保険となる構造(カバリングインデックス、shadow テーブル)は導入しません。namespace レベルの調整ですでに導入する blob_storage_blobs_by_namespace の shadow を再利用し、それ自体には何も追加しません。maven_files を除くこれらすべてのテーブルは、すでに (namespace_id, <its parent id>) で始まる非部分インデックスを持ちます。本番メトリクスで遅すぎることが判明した場合、その 1 つの差を埋めるのが最も安価な次のステップです。artifact-registry#684 は決定ではなく提案として、maven_files (namespace_id, maven_package_id) の非部分インデックスを提案しています。さらに必要な場合は、アタッチ/デタッチ時にフォーマット固有テーブルから維持される、(namespace_id, repository_id, sha256, size) をマッピングするリポジトリパーティショニングの shadow テーブルによって、オプション B の形をより細かいスコープで反映できます。
アーティファクトレベルのストレージ会計
アーティファクトレベルのストレージ使用量は、単一のアーティファクトバージョン(Container マニフェスト、Maven バージョン、npm バージョン)のバイト単位のフットプリントです。アーティファクトごとの UI 表示(例えば「このイメージは 142 MB」「このパッケージバージョンは 4 MB」)を支え、サイズによるフィルタリングまたはソートを行うライフサイクルルールとレポートクエリによって使用されます。
基となるアーティファクトの形が異なるため、会計モデルはフォーマットごとに異なります。
- Container マニフェスト:
container_manifests.sizeはプッシュ時に事前計算され、不変です。マニフェストはコンテンツアドレス可能であるため(ADR-008)、バイトが変更されると、新しいダイジェストと新しいsizeを持つ新しいマニフェストが生成されます。このカラムが信頼できる唯一の情報源であるため、調整は不要です。リモートキャッシュではcontainer_remote_manifests.sizeがこれを反映し、子が遅延キャッシュされるマニフェストリストに対して段階的なセマンティクスを持ちます。 - Maven と npm のバージョン: 各バージョン行は、バージョンのフットプリントについて信頼できる唯一の情報源となる事前計算済みの
size_bytesカラム(maven_versions、maven_remote_versions、npm_versions、npm_remote_versions)を持ちます。container_manifests.sizeとは異なり、プッシュ時に不変の値として設定することはできません。Maven と npm のバージョンはバージョンレベルでコンテンツアドレス可能ではなく、バージョンの存続期間中にファイルを追加または削除できるためです。 そのため、repositories.size_bytesと同様に、バッファ書き込み/非同期書き込みを介してバッファカウンターとして維持します。blob_sha256がバージョンに初めてアタッチされたときにインクリメントし、そのバージョンから当該sha256の最後のアタッチメントがなくなったときにデクリメントします(バージョン内で重複排除し、調整で使用するDISTINCT blob_sha256と一致させます)。削除するパスは 2 つあります。 フォーマット自身のファイル削除は独自のトランザクションで行を削除し、カラムをそこで再計算します。そのルートで扱えないファイルは lifecycle purger が削除します。 どちらも garbage collection を待ちません。 表示パスとバージョン一覧は、サイズでソートまたはフィルタリングする場合も含め、インデックス付きカラムを直接読み取ります。
ソフト削除されたファイルは、ガベージコレクションによってハード削除されるまで、バージョンごとの size_bytes に引き続き寄与します。ソフト削除と復元はカウンターに対して no-op であり、ネームスペースおよびリポジトリのセマンティクスと一致します。
寄与を終わらせるのはハード削除で、実行するパスは 2 つあります。Maven または npm のファイル削除は独自のトランザクションで行を削除し、そこでバージョンの size_bytes を再計算します。ソフト削除されたバージョン配下のファイルはそれらのルートで扱えないため、lifecycle purger が削除します。purger は同じ reap でバージョン行も削除するため、カウンターは再計算されず行とともになくなります。どちらも garbage collection を待ちません。garbage collection は blob を回収し、別スコープのカウンターである deduplicated_size_bytes をデクリメントします。
読み取り時に導出せずカラムを事前計算することで、バージョン一覧をサイズでソートおよびフィルタリングできます。単一バージョンのサイズを導出する処理はいずれの方法でも低コストです(少数のファイルに限定された JOIN)が、サイズがバージョン一覧でソートまたはフィルタリング可能なカラムになると、すべての行で導出する方法はスケールしません。Cloud SQL PostgreSQL 17 インスタンス(large プロファイル、最も深いネームスペースに約 26K の Maven バージョンと約 26K の npm バージョン)で検証しました。
| サイズ上位 50 バージョン | Maven | npm |
|---|---|---|
| 読み取り時に導出(各バージョンの blob を合計) | 58 ms | 29 ms |
事前計算済みの size_bytes カラム + インデックス | 0.06 ms | 0.08 ms |
これは 3 〜 4 桁の差であり、重複排除を行う読み取り時導出のバリアントはさらに低速です(Maven で約 190 ms)。また、バージョン数の増加とともに差は広がります。ランディングページではすでにリポジトリを size_bytes でソートしており、バージョン一覧にも同じ期待が当てはまるため、スコープ間の一貫性を保つためにカラムを事前計算します。
アーティファクトレベルのストレージ会計の調整
container_manifests.size は不変かつコンテンツアドレス可能であるため、調整は不要です。Maven と npm の size_bytes カウンターは、リポジトリレベルおよびネームスペースレベルのカウンターと同様に、失敗したフラッシュ、部分的なバッファ損失、またはバックグラウンドジョブのバグによってドリフトする可能性があります。そのため、オンデマンドの検証またはドリフトの修正のために、ソースデータから正確な値を再計算します。
1 つのバージョンを調整する際は、フォーマット固有のファイルテーブルから、単一の maven_version_id または npm_version_id にスコープして、そのバージョンの重複しない blob_sha256 値を選択し、namespace・リポジトリレベルの走査と同じ shadow である blob_storage_blobs_by_namespace に対して各 digest のサイズを解決します。再計算した値をカウンターが追跡する値と一致させるため、ソフト削除されたファイルも含め、package レベルの行は version ID の等価条件で除外します。
バージョンごとの走査はサイズを読む前に digest を保持するため、digest を事前に名指せない namespace・リポジトリレベルの読み取りを shadow に送るプルーニングの根拠は、このスコープには当てはまりません。digest セットを単一の bytea[] 配列引数(blob_storage_blobs.sha256 = ANY($n))として束縛すると digest の hash partition へ静的にプルーニングできますが、各 surviving partition は自身のインデックスに対して配列全体を評価するため、コストは digest 数ではなく partition 数×digest 数になります。本番カーディナリティでの計測では、PostgreSQL 16〜18、namespace の blob 数 100〜1,000,000、digest 数 1〜512 の全条件で shadow は 0.09〜2.44 ms でした。配列形式が勝るのは digest 数 1、3、5 だけで、15 以上では勝らず、15 で 2〜6 倍、100 で 9〜48 倍遅くなります。Maven で一般的な 4〜15 ファイルではどちらもサブミリ秒なので、shadow は通常のカーディナリティで何も犠牲にせず、15 digest 以上で優位です。
digest を配列として束縛せず、派生テーブルとしてベーステーブルに JOIN するとまったくプルーニングされません。呼び出しごとに 64 パーティションすべてを開き、計測した 123 条件のどれでも shadow に勝りませんでした。Artifact Registry サービスは simple query protocol を使用し、ステートメントを毎回再計画するため、読み取りではなく 64 パーティションに対する計画が走査を支配します。
このスコープで shadow を読むことによって、スキーマがまだ支払っていないコストは生じません。トリガー維持の書き込み増幅は、namespace レベルとリポジトリレベルの走査のために blob_storage_blobs がすでに持つ AFTER INSERT と AFTER DELETE トリガーで blob 書き込みごとに発生するため、バージョンごとの読者は何も追加しません。blob サイズはコンテンツアドレス可能で、内容が変われば異なる sha256 の別行になるため、存在する書き込みパスで size の 2 つ目のコピーが古くなることはありません。訂正は UPDATE ではなく削除と再挿入で、トリガーが伝播します。ただし、この不変性は規約でありスキーマによる強制ではありません。blob_storage_blobs.size の UPDATE を拒否するものはなく、AFTER UPDATE トリガーもないため、直接更新すれば shadow は更新前の値を保持します。このリスクは namespace・リポジトリレベルの走査がすでに持つもので、このスコープ固有ではありません。
バージョンごとのカーディナリティはフォーマットのプロトコルによって制限されるため(通常、Maven は 4〜15 ファイル、npm は 1〜3 ファイル)、再計算はサブミリ秒の単一パーティション読み取りになります。
カラムの導入時に行うバックフィルは、バージョンでグループ化した集合ベースの再計算です。現在の量では低コストです(最も深いネームスペースにある約 26K バージョンを約 284 ms でバックフィルできるため、この規模ではテーブル全体でも数秒です)。事前計算を先送りし、後から本番環境の量に対してバックフィルするよりもはるかに低コストです。今実施することで、3 つすべてのフォーマットにわたってデータモデルと API の一貫性も保たれます。
インデックス
blob_storage_blobs:(namespace_id, sha256)に対するユニークインデックス — 重複排除を強制し、Organization 内で sha256 によって blob の存在を確認します。この制約はパーティションキー(sha256)を含むため、PostgreSQL はすべてのハッシュパーティションにわたって正しく強制します。(namespace_id) INCLUDE (size)のカバリングインデックス — ヒープフェッチなしで ネームスペースレベルのストレージ会計の調整 のためのインデックスオンリースキャンを可能にします。blob_storage_attachments:(namespace_id, sha256)に対するインデックス — blob のコンテンツハッシュが与えられたときにアタッチメントの存在を確認します(クリーンアッププロセス が孤立チェックに使用)。blob_storage_blobs_by_namespace:(namespace_id, sha256) INCLUDE (size)に対する主キー —blob_storage_blobs行との 1:1 対応を強制し、リポジトリレベルおよびアーティファクトレベルの調整で行うダイジェストごとのサイズ検索を、ヒープフェッチなしで解決します。(namespace_id) INCLUDE (size)のカバリングインデックス —sha256を読み取らない ネームスペースレベルのストレージ会計の調整 でネームスペース全体を合計するための、より低コストな単一パーティションのインデックスオンリースキャンです。主キーもこのスキャンに使用できますが、合計では使用しない 32 バイトのキーカラムを含むエントリを走査することになります。namespace_statistics:(namespace_id)に対する主キー — ネームスペースごとに 1 つの統計レコード。このテーブルで必要な一意性は主キーだけで正確に満たされるため、同じ単一カラムに別のユニークインデックスは作成しません。追加しても、それ以上の制約は何も強制されません。(last_reconciled_at, namespace_id)に対するインデックス — 最後に成功した調整パスが最も過去のネームスペースを選択する、キャッチアップの古さのスキャンと、同じ述語によるバックログ数の計算に使用します。どちらもこのテーブルだけを読み取ります。ここに行がないネームスペースを捉えるためにnamespacesを join するスキャンでは、このインデックスをまったく使用できません。結果のIS NULL選言はインデックス条件ではなく join 後のフィルターになるためです。したがって、このインデックスを使用するクエリはnamespace_statisticsに限定されます。このインデックスは、このドキュメントのほぼすべてのほかのインデックスと異なり、意図的にnamespace_idを先頭にしません。スキャンはネームスペース内ではなく、テーブル全体を新しさで並べ替えます。また、このテーブルはパーティショニングされていないため、先頭のnamespace_idはプルーニングにも順序付けにも役立ちません。namespace_idはフィルターではなく同順位を決めるカラムとして後ろに置きます。これによりソートキーが一意になり、スキャンをオフセットではなくキーセットでページングできます。特にカラムの作成直後は、すべての行が同じ'epoch'デフォルトを共有し、タイムスタンプだけでは順序が決まらないため重要です。また、各クエリがインデックスの 2 カラムだけを選択するため、両方のクエリをカバーし、vacuum 済みのテーブルではどちらもインデックスだけから応答できます。
ハッシュパーティショニングされたテーブルでは、インデックスはパーティションごとにローカルです。インデックス操作は単一のパーティションにスコープされ、テーブル全体をロックしません。
blob ストレージのクエリ例
アーティファクトをプルする(読み取りパスのショートカット:
*_files→blob_storage_blobs、アタッチメントをスキップ — 1 パーティション)SELECT bsb.object_storage_key, bsb.size FROM maven_files mf JOIN blob_storage_blobs bsb ON bsb.namespace_id = mf.namespace_id AND bsb.sha256 = mf.blob_sha256 WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.maven_version_id = '019a1b2c-0456-7abc-8def-000000000456' AND mf.file_name = 'myapp-1.0.0.jar' AND mf.soft_deleted_at IS NULL;blob アップロード時の重複排除アップサート(1 パーティション、レースなし)
INSERT INTO blob_storage_blobs (namespace_id, sha256, size, object_storage_key, metadata_sha1) VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'abcd1234efgh5678...'::bytea, 1048576, 'artifact_registry/.../objects/ab/cd/abcd1234efgh5678...', NULL) ON CONFLICT (namespace_id, sha256) DO NOTHING RETURNING id, sha256;Organization 内で sha256 によって blob の存在を確認する(1 パーティション)
SELECT 1 AS one FROM blob_storage_blobs WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND sha256 = 'abcd1234efgh5678...'::bytea LIMIT 1;孤立チェック: この blob はまだいずれかのアタッチメントから参照されているか?(1 パーティション)
SELECT 1 AS one FROM blob_storage_attachments WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND sha256 = 'abcd1234efgh5678...'::bytea LIMIT 1;blob のカバリングインデックス(オプション A)を介したストレージ会計の調整: ソースデータからネームスペースの正確なストレージを計算する(64 パーティション、インデックスオンリースキャン)
SELECT SUM(size) AS total_size_bytes FROM blob_storage_blobs WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8';シャドウテーブル(オプション B)を介したストレージ会計の調整: ネームスペースの正確なストレージを計算する(1 パーティション、インデックスオンリースキャン)
SELECT SUM(size) AS total_size_bytes FROM blob_storage_blobs_by_namespace WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8';表示パス: 事前計算されたネームスペースカウンターを読み取る(単一行検索)
SELECT deduplicated_size_bytes, components_count FROM namespace_statistics WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8';表示パス: 事前計算済みのリポジトリカウンターを読み取る(単一行の検索、ランディングページのハイブリッドリストで使用)
SELECT size_bytes FROM repositories WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0456-7abc-8def-000000000456';リポジトリレベルの調整: 単一リポジトリの正確なストレージをフォーマット別に計算します。各先頭 CTE は、
repositories.idからフォーマット固有のスタブid(container_repositories、maven_repositories、npm_repositories)を解決します。中間層の*_repository_idカラムはrepositories.idではなく、スタブテーブル自体のidを参照します。Container: イメージを走査し、
container_blobsとcontainer_manifestsの blob 参照を UNION し、blob_sha256で重複排除して、ネームスペースのシャドウテーブルを介して合計します。WITH cr AS ( SELECT id FROM container_repositories WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND repository_id = '019a1b2c-0456-7abc-8def-000000000456' ), uniq_blobs AS ( SELECT cb.blob_sha256 FROM container_blobs cb JOIN container_images ci ON ci.namespace_id = cb.namespace_id AND ci.id = cb.container_image_id WHERE ci.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND ci.container_repository_id = (SELECT id FROM cr) UNION SELECT cm.blob_sha256 FROM container_manifests cm JOIN container_images ci ON ci.namespace_id = cm.namespace_id AND ci.id = cm.container_image_id WHERE ci.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND ci.container_repository_id = (SELECT id FROM cr) ) SELECT COALESCE(SUM(bsb.size), 0) AS total_size_bytes FROM uniq_blobs u JOIN blob_storage_blobs_by_namespace bsb ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;Maven: 単一のファイルテーブルが、バージョン固有ファイルとパッケージレベルファイルを対象にします。
WITH mr AS ( SELECT id FROM maven_repositories WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND repository_id = '019a1b2c-0456-7abc-8def-000000000456' ), uniq_blobs AS ( SELECT DISTINCT mf.blob_sha256 FROM maven_files mf JOIN maven_packages mp ON mp.namespace_id = mf.namespace_id AND mp.id = mf.maven_package_id WHERE mp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mp.maven_repository_id = (SELECT id FROM mr) ) SELECT COALESCE(SUM(bsb.size), 0) AS total_size_bytes FROM uniq_blobs u JOIN blob_storage_blobs_by_namespace bsb ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;npm: パッケージ → バージョン → ファイルを走査し、パッケージレベルのメタデータファイルを UNION して、
blob_sha256で重複排除します。WITH nr AS ( SELECT id FROM npm_repositories WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND repository_id = '019a1b2c-0456-7abc-8def-000000000456' ), uniq_blobs AS ( SELECT nf.blob_sha256 FROM npm_files nf JOIN npm_versions nv ON nv.namespace_id = nf.namespace_id AND nv.id = nf.npm_version_id JOIN npm_packages np ON np.namespace_id = nv.namespace_id AND np.id = nv.npm_package_id WHERE np.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND np.npm_repository_id = (SELECT id FROM nr) UNION SELECT nmf.blob_sha256 FROM npm_metadata_files nmf JOIN npm_packages np ON np.namespace_id = nmf.namespace_id AND np.id = nmf.npm_package_id WHERE np.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND np.npm_repository_id = (SELECT id FROM nr) ) SELECT COALESCE(SUM(bsb.size), 0) AS total_size_bytes FROM uniq_blobs u JOIN blob_storage_blobs_by_namespace bsb ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;
アーティファクトレベル: 事前計算済みの Container マニフェストサイズを読み取る(不変、単一行の検索)
SELECT size FROM container_manifests WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0789-7abc-8def-000000000789';アーティファクトレベル: 事前計算済みの Maven バージョンサイズを読み取る(信頼できる唯一の情報源、単一行の検索)
SELECT size_bytes FROM maven_versions WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0456-7abc-8def-000000000456';アーティファクトレベル: バージョン一覧表示のために、Maven パッケージのバージョンをサイズ順に一覧する(
(namespace_id, maven_package_id, size_bytes DESC)インデックスを使用。npm も同様)SELECT id, version, size_bytes FROM maven_versions WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND maven_package_id = '019a1b2c-0123-7abc-8def-000000000123' AND soft_deleted_at IS NULL ORDER BY size_bytes DESC LIMIT 50;アーティファクトレベルの調整: カウンターを検証または修正するために、ソースデータから Maven バージョンの正確なサイズを再計算する(通常 4〜15 ファイル、カーディナリティは限定的)。ステップ 1 で
maven_filesからバージョンの重複しない digest を収集し、ステップ 2 でblob_storage_blobs_by_namespaceからサイズを合計します。リテラルのnamespace_idが 1 パーティションにプルーニングし、各 digest はその中で index-only に解決されます。-- Step 1: the version's distinct digests (keyset-paged on blob_sha256) SELECT DISTINCT mf.blob_sha256 FROM maven_files mf WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.maven_version_id = '019a1b2c-0456-7abc-8def-000000000456'; -- Step 2: the digests from step 1, resolved against the namespace shadow SELECT COALESCE(SUM(bsb.size), 0) AS bytes FROM blob_storage_blobs_by_namespace bsb WHERE bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = ANY($1::bytea[]);アーティファクトレベル: 事前計算済みの npm バージョンサイズを読み取る(信頼できる唯一の情報源、単一行の検索)
SELECT size_bytes FROM npm_versions WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0456-7abc-8def-000000000456';アーティファクトレベルの調整: ソースデータから npm バージョンの正確なサイズを再計算する(通常、バージョンごとに 1〜3 ファイル。
npm_metadata_filesはパッケージレベルをキーとし、単一バージョンのフットプリントには含まれない)。blob_storage_blobs_by_namespaceから、バージョンの重複しない digest を JOIN してサイズを合計します。これは調整セクションで説明した読み取りです。WITH uniq_blobs AS ( SELECT DISTINCT nf.blob_sha256 FROM npm_files nf WHERE nf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND nf.npm_version_id = '019a1b2c-0456-7abc-8def-000000000456' ) SELECT COALESCE(SUM(bsb.size), 0) AS bytes FROM uniq_blobs u JOIN blob_storage_blobs_by_namespace bsb ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;
帰結
ポジティブ
各アーティファクトフォーマットに合わせたデータ構成: 各アーティファクトフォーマットに専用のテーブルを使用することで、テーブル構成に最大の柔軟性が得られます。フォーマットプロトコルが要求する任意の数の追加カラムを持つことができます。すでに専用のテーブルを使用しているため、追加の補助テーブルは不要です。
各フォーマットのデータテーブルは関連する使用パターンを持つ: 各フォーマット専用のテーブルは、REST および GraphQL API と関連するアーティファクト管理クライアントから使用パターンを受け取ります。これにより、他のフォーマットの使用パターンからの分離が提供されます。
フォーマット関連データのパフォーマンスの分離: 特定のアーティファクトフォーマットのテーブルでのパフォーマンスのボトルネックは、他のフォーマットに即時の影響を与えません。
透過的なオブジェクトストレージのクリーンアップ: オブジェクトストレージのクリーンアップタスク が blob ストレージ ドメインに集約されているため、親ドメイン(この場合は各フォーマット固有のドメイン)はこの部分を扱う必要がありません。さらに、このクリーンアップは削除操作がどのように発生したか(単一要素の破棄、一括破棄、選択された要素のセットに対して破棄を実行するバックグラウンドクリーンアップポリシー)に影響されません。
blob ストレージの分離が再利用性を提供する: blob ストレージテーブルは、ここで記述する Artifact Registry 機能に縛られません。したがって、この部分は他の領域でのファイルアップロードのニーズに再利用できます。
効率的なストレージ会計: Organization スコープの重複排除と Organization ごとの重複排除された blob レコードにより、ストレージ使用量クエリは単純かつ効率的です。注意:
sha256ベースのパーティショニングでは、Organization レベルの集約は 64 パーティションすべてをスキャンします。これは、遅延インクリメントを介して更新される専用のロールアップテーブルによって緩和されます(パーティショニング戦略 を参照)。統一されたクロスフォーマットの一覧表示: 親の
repositoriesテーブルは、ネームスペース内のすべてのフォーマットと種類(ホスト型、仮想型、リモート型)にわたるすべてのリポジトリを一覧表示する単一のソースを提供し、複数のテーブルにわたるUNION ALLなしにランディングページのハイブリッドリストを支えます。スタンドアロンのリモートリポジトリが共有を可能にする: 独自のライフサイクルを持つスタンドアロンエンティティとしてのリモートリポジトリは、複数の仮想リポジトリ間で共有でき、設定とキャッシュエントリの重複を減らします。
ネガティブ
クロスフォーマットの詳細クエリは依然として JOIN を必要とする: 親の
repositoriesテーブルはランディングページの一覧表示ユースケースを解決しますが、フォーマット固有の詳細(例: コンテナイメージ、Maven パッケージ)へのアクセスには依然としてフォーマット固有のテーブルへの JOIN が必要です。blob ストレージのための集約されたテーブル: これは 2 つの欠点をもたらします。第一に、これらのテーブルには非常に大量の行が存在します。この状況に対処するには慎重なテーブル設計が必要です。第二に、これらのテーブルの問題(テーブル全体のロックなど)は、すべてのアーティファクトタイプに潜在的に影響します。
リポジトリごとのストレージ帰属は JOIN を必要とする: リポジトリレベルでの正確なストレージ使用量の帰属は、フォーマット固有のテーブルから
blob_storage_attachmentsを通じてblob_storage_blobsへの JOIN を通じて導出されます。これは blob ストレージを汎用的かつ重複排除された状態に保ちますが、非正規化されたリポジトリごとのカウンターと比べていくらかの複雑さを追加します。2 ステップのリポジトリ作成: リポジトリの作成には、親の
repositoriesテーブルとフォーマット固有のテーブルの両方への挿入が必要です。これは単一テーブルへの挿入と比べてトランザクションの複雑さを追加します。
代替案
共通データの集約
ここでの異なるアプローチは、アーティファクトフォーマット領域のすべての共通データを共通の集約されたテーブルに保存することです。
これは、複数のソースを一緒に JOIN することなくそれらのクエリに答えられるため、混在するアーティファクトフォーマットのデータアクセスに非常に役立ちます。
このアプローチは Package Registry 機能 ですでに使用されており、この執筆時点で、それらの共通テーブルは予想どおり大量の行を持つだけでなく、大量の特化したインデックスも持っています。これらのインデックスのそれぞれは、アーティファクトフォーマットに固有のアクセスパターンをサポートします。インデックスの量が今日かなり多いため、新しいインデックスを追加すること(例えば Package Registry 機能に新しいフォーマットサポートが追加される場合)は、より多くの精査や反対さえも受けることになります。
さらに、各アーティファクトフォーマットには保存する必要のある固有のデータがあります(例: 正規化されたパッケージ名)。この固有のデータは、一部の行だけが使用するカラムを作ることになるため、共通テーブルには保存できません。これはいくつかの補助テーブルの作成につながります。これらの補助テーブルは、特定のアーティファクトタイプのアクセスパターンに必要な JOIN の量を増加させます。
repositories 親テーブルの導入は、このアプローチの限定版を採用しています。一覧表示とフィルタリングに必要なクロスフォーマットメタデータ(名前、可視性、フォーマット、種類、カウンター)のみが集約されます。フォーマット固有のデータは専用のテーブルに残り、上記で記述したインデックスの増殖と補助テーブルの問題を回避します。
参考文献
- ADR-001: アンカーポイントとしての Organization - レジストリが Organization にアンカーする理由
- ADR-002: ストレージ重複排除のスコープ - 重複排除スコープに関する詳細な決定
- Package Registry の共通テーブルの分解 - 共通のアーティファクト関連データを中央テーブルに保存する際に直面する問題の詳細。
a1f3c26a)