TL;DR.
rdc repo diffはgit status --shortの文法(A/M/D/R)で、フォークされた2つのリポジトリ間のファイルレベルの差分を表示します。そして、どちらも復号しません。
- 2つのLUKSイメージファイルをブロックレベルで比較するために、エクステントマップのメタデータのみを読み取るFIEMAPアイオクトルを使用します。鍵は読み込まれず、平文も読み取られません。
- aes-xtsは長さ保存型であり、各512バイトのセクターを独立して暗号化します。そのため、変更された平文セクターは同じオフセット(16 MiB LUKSデータオフセットでシフト)の変更された暗号文セクターになります。オフセットを引いて、デバイス範囲をext4エクステントマップ経由でファイル名にマッピングすれば、ファイルリストが得られます。
- コストは変更ブロック数に比例し、リポジトリサイズには依存しません。1 GBのフォークと100 GBのフォークの比較は同じミリ秒数で完了します。なぜなら、比較はメタデータのみだからです。
つまり、RediaccのフォークはリポジトリのLUKSイメージの cp --reflink=always です。瞬時に行え、サイズを気にしません。100 GBのリポジトリは1 GBのリポジトリと同じ速さでフォークされます。マーケティング的に聞こえるかもしれませんが、これはリフリンクの仕組みそのものです。btrfsはエクステントマップをコピーし、下層のブロックを共有します。私たちはこれを積極的に活用しています。フォークはテストのサンドボックス、使い捨てのブランチ、作業が終われば捨てるステージングコピーです。
しかし、次の当然の疑問「このフォークは実際に何を変えたのか」に対して安価に答える手段がありませんでした。単純なルートは、フォークをマウントし、LUKSコンテナをアンロックし、内部のext4を探索し、すべてのファイルを親と照合してハッシュを取ることです。これはリポジトリサイズに応じて読み取りと復号の両方でスケールします。diff経路上でキーを必要とします。そして、ストレージ層がすでに無償で知っている情報、すなわちどのブロックが分岐したか、を捨て去ります。rdc repo diff は別のルートを取ります。変更ブロック数でスケールします。鍵を読み込みません。2つの暗号化イメージを比較することでファイルリストを得ます。
比較するスタック
「2つのリポジトリ」がディスク上で何を意味するかを正確に説明します。すべてのトリックはここにあります。下から順に:SSD、ホストストレージ、btrfsプール。その上に、リポジトリごとに1つのLUKS2イメージファイル。それをアンロックするとdm-cryptデバイスが得られます。その中にコンテナが使うext4ファイルシステムがあります。1つのリポジトリはbtrfsプール上の1つのファイルです。
フォークはそのファイルのリフリンクです。フォーク直後、2つのイメージファイルはバイト単位で同一です。すべての物理ブロックを共有しています。親とフォークはデータの2つのコピーではありません。同じブロックを指す2つのエクステントマップです。フォーク内で書き込みを行うと、ストレージ層は変更された領域のために新しいブロックを割り当てます。そのフォークのエクステントマップのみが書き換えられます。親のブロックはそのまま残ります。
「2つのリポジトリを比較する」は「ほとんどのエクステントを共有する2つのファイルを比較する」に還元されます。カーネルはすでにそれに答えられます。どちらのファイルの1バイトも読む必要はありません。
FIEMAP:読まずにカーネルに何が変わったかを尋ねる
FIEMAPアイオクトルはファイルのエクステントマップを返します。(論理オフセット、物理オフセット、長さ)のタプルのリストです。各タプルは、ファイルの一部がディスク上のどこにあるかを示します。純粋なファイルシステムメタデータです。ファイルデータは読みません。暗号化イメージに対して鍵は不要です。暗号文はカーネルが解釈する必要のないバイト列です。
2つのエクステントマップを比較します。両方のフォークが同じ物理ブロックを指している論理範囲は共有されています。共有とは同一を意味します。文字通り同じブロックがデバイス上にあるからです。フォークが自分のプライベートブロックを持つ範囲が書き込みです。それらが変更されたブロックです。ストレージ層が常に保持するメタデータからそれらを得ました。
コストの話がここから来ます。FIEMAP比較はエクステントレコードを読み、データを読みません。その作業はいくつのエクステントが変わったかでスケールし、リポジトリサイズではありません。1 GBのフォークと100 GBのフォークは同じ短いプライベートエクステントのリストを返します。同じファイルを変更した場合、ミリ秒は同じです。正直な注意点:エクステント走査時間はサイズではなくイメージの断片化でスケールします。コピーオンライトイメージは激しいランダム書き込みの下でエクステントを積み上げます。私が計測した最も断片化した本番イメージでの完全な filefrag 走査は3.19秒かかりました。断片化ベンチマーク投稿を参照してください。これがメタデータ側の上限です。バックグラウンドスキャンであり、データ読み取りではありません。
変更されたブロックから、2つの暗号化レイヤーを経由してファイル名へ
暗号化イメージ内の変更されたバイト範囲のリストはまだ役に立ちません。その範囲は暗号文の位置です。欲しい名前は2レイヤー上、内部ext4にあります。それらの橋渡しは復号ではなくアドレス演算です。
LUKSはaes-xtsで暗号化します。これは長さ保存型であり、各512バイトのセクターを独立して暗号化します。変更された平文セクターは同じオフセットで変更された暗号文セクターを生成します。唯一のシフトはLUKSデータオフセットです。これが暗号化されたペイロードの前にある16 MiBのヘッダーとキースロットです。各変更されたイメージ範囲からそのオフセットを引きます。これでdm-cryptデバイス上の対応する範囲が得られます。それが内部ext4が存在するブロックデバイスです。鍵は使いませんでした。引き算です。
次に、デバイス範囲をファイルにマッピングします。ext4もiノードごとにエクステントマップを持ちます。同じ(論理、物理、長さ)構造です。マウントされた内部ファイルシステム上のFIEMAPを通じてアクセスします。iノードを一度走査してブロック対ファイルのインデックスを構築します。次に各変更されたデバイス範囲をそのインデックスで調べます。iノード1234のデータエクステントと重なる範囲はそのiノードのパスに属します。そのパスが変更されたファイルです。
これが決して行わないことを明記します。変更されたイメージから平文を導出することはしません。既知のオフセットでファイルシステム構造を読みます。これを暗号化された側と復号された側の両方で行います。次にアドレスで2つを結合します。ブロックフィルターはどのデバイス領域が移動したかを示します。ext4エクステントマップはどのファイルが各領域を所有するかを示します。どちらのステップも変更されたブロックの内容を調べてそれが変更されたと判断しません。
追加、削除、リネーム:iノードアイデンティティ走査
変更はブロック比較から直接わかります。追加、削除、リネームにはもう一つの観察が必要です。リフリンクがそれを無料で提供してくれます。フォークはiノード番号を保持します。イメージ全体をリフリンクすることで、何も分岐する前に内部ファイルシステム全体がバイト単位でクローンされます。つまり、親に存在していたiノードはフォークでも同じ番号を持ちます。
これによりアイデンティティは集合比較になります。両側に異なるパスで存在するiノードはリネームです。新しい側にのみ存在するiノードは追加です。古い側にのみ存在するiノードは削除です。リネームはデバイスエクステントの重なりによって確認されます。リネームされたファイルのデータブロックは両方のフォークで同じデバイスオフセットにあります。2つのフォークは1つの座標系を共有します。この重なりはiノード番号が無関係なデータに再利用されることも除外します。純粋なリネームは、ファイルのデータブロックが変更されていない状態で現れます。移動したのはディレクトリエントリのみです。
これがデフォルトのname-status形式で、git status --short と同じA/M/D/Rの文法です:
$ rdc repo diff --name test-1gb:fork1 -m hostinger
M hello.txt
1 file changed: 0 added, 1 modified, 0 deleted, 0 renamed
1 GBのリポジトリ内の1つの変更されたファイル。ファイルデータを読まないブロック比較から報告されました。何もアンロックされていません。
デフォルトは正確性のためにもう一つのことを行います。ブロックフィルターはスーパーセットです。btrfsのエクステントは実際に変更されたバイトより多くをカバーできます。そのため、あるファイルへの書き込みがエクステントを共有する隣のファイルにフラグを立てることがあります。変更されていないファイルを報告するのを避けるため、デフォルトはブロックでフラグされた各候補を確認します。そのファイルのみを両側でハッシュします。候補をハッシュするのであり、リポジトリではありません。したがって確認コストは変更セットを追跡します。--fast はブロックフィルターを信頼して確認をスキップします。高速な答えが欲しく、まれな誤検知を許容できる場合に使用します。
AIエージェントがこれを必要とする理由
このコマンドが存在する理由はエージェントワークフローです。エージェントが本番をフォークし、変更を実行し、その後実際に何に触れたかを報告するクリーンな方法がないのを見続けていました。AIエージェントは本番を瞬時にフォークできます。分離されたフォーク内でリスクのある変更を実行します。次に、何かを本番に昇格させる前に、正確に何に触れたかを知る必要があります。フォークはブランチです。Diffはレビューです。
エージェントはname-statusを読まず、--json を読みます:
$ rdc repo diff --name prod:experiment --json -m hostinger
構造化された出力はエージェントに正確な変更セットを提供します。変更、作成、削除されたパスが何か。--stat があれば、ファイルごとの変更サイズをバイトとブロックで。昇格させる前に差分を見られるエージェントは、本番の近くに置けるものです。爆発半径は検査可能であり、主張されるものではありません。他のモードも同じレビューループを提供します。--name-only でパスのみのリスト。--content <path> で1つのファイルの統合テキスト差分(テキストのみ。バイナリファイルは Binary files differ と報告)。--stat でエージェントが何がどれだけ変わったかを知る必要がある場合。
DRテストがこれを必要とする理由
同じプリミティブが、リスクなしに尋ねるのがかつて難しかったDRの問いに答えます。本番をフォークします。バックアップをフォークにリストアします。フォークを本番と比較します。差分はリストアが期待していたファイルセットを再現したかどうかを教えてくれます。本番を停止せずに行えます。そして差分経路で何も復号しません。
これはスケジュールで実行できるリハーサルです。リストアは分離されたフォークに着地します。差分がgit文法でデルタを報告します。クリーンなリハーサルとは、変更セットがバックアップに含まれるはずのものと一致することです。ライブ本番に対してリカバリを検証しています。コピーは作成も廃棄もタダです。
正直な限界
コンテンツの差分はテキストのみです。--content はテキストファイルの統合差分を生成します。それ以外のすべてに対して、gitと同様に Binary files differ と報告します。暗号化されてから圧縮されたblobの行指向の差分はノイズです。
関連するフォーク間で差分を取るのであり、任意のリポジトリ間ではありません。メカニズム全体が共有座標系の上に成り立っています。共有エクステントが同一性を証明します。保持されたiノード番号がアイデンティティを固定します。共通のデータオフセットがそれらをつなぎます。共通の祖先からフォークされたことがない2つのリポジトリはそれらを何も共有しません。それらの間に安価な差分はありません。これはバグではなく機能です。無関係なヒストリ間の git diff が意味をなさないのと同様です。
リネーム検出はiノードベースです。ファイルシステムが実際にリネームとして記録したリネームについては正確です。新しい名前で同一内容を削除してから作成した場合はどうでしょうか。iノードテーブルに対して2つの操作です。したがって、1つの削除と1つの追加として報告され、リネームではありません。gitのコンテンツ類似ヒューリスティックはそれをリネームと呼ぶでしょう。iノード走査はそう呼びません。それがファイルシステムが行ったことについての正しい答えです。人間が意図したことについての答えでなくても。
そしてメタデータ走査は断片化でスケールします。激しく断片化したイメージでは、エクステントの列挙はミリ秒ではなく秒です。それでもリポジトリサイズには依存しません。それでもデータ読み取りから解放されています。ただし、最も断片化したイメージでは文字通り瞬時ではありません。
まとめ
rdc repo diff は暗号化された実行中のインフラにバージョン管理のエルゴノミクスをもたらします。インターフェースは意図的にgitです。A/M/D/R、統合差分、--stat。新しく学ぶことはありません。git status --short を読めるなら、2つのLUKSイメージ間の差分を読めます。下に隠れたエンジニアリングが価値のある部分です。それは2つの拒否に帰着します。復号しません。aes-xtsにより、ブロックレベルのFIEMAP比較がアドレスですべての変更されたセクターを特定できます。そして変更されなかったデータに対して代金を払いません。ストレージ層がどのブロックが分岐したかをすでに記録しています。フォークはブランチです。Diffはレビューです。レビューは変更のコストで済み、リポジトリの重さではありません。