요약.
rdc repo diff는git status --short문법(A/M/D/R)으로 두 포크된 저장소 간의 파일 수준 차이를 보여주며, 어느 쪽도 복호화하지 않습니다.
- 두 LUKS 이미지 파일을 FIEMAP ioctl로 블록 수준에서 비교합니다. 이는 익스텐트 맵 메타데이터만 읽습니다. 키가 로드되지 않고 평문이 읽히지 않습니다.
- aes-xts는 길이를 보존하며 각 512바이트 섹터를 독립적으로 암호화합니다. 따라서 변경된 평문 섹터는 동일한 오프셋에서 변경된 암호문 섹터입니다(16 MiB LUKS 데이터 오프셋만큼 이동됨). 오프셋을 빼고, ext4 익스텐트 맵을 통해 장치 범위를 파일 이름에 매핑하면 파일 목록을 얻을 수 있습니다.
- 비용은 저장소 크기가 아닌 변경된 블록 수를 추적합니다. 1 GB 포크와 100 GB 포크는 동일한 밀리초에 diff됩니다. 비교가 메타데이터 전용이기 때문입니다.
그러므로 Rediacc에서 포크는 저장소의 LUKS 이미지에 대한 cp --reflink=always입니다. 즉각적이며 크기를 신경 쓰지 않습니다. 100 GB 저장소는 1 GB 저장소만큼 빠르게 포크됩니다. 마케팅처럼 들릴 수 있지만, 이것은 reflink가 작동하는 방식입니다. btrfs가 익스텐트 맵을 복사하고 그 아래의 블록을 공유합니다. 저희는 이것을 적극 활용합니다. 포크는 테스트 샌드박스, 일회용 브랜치, 작업이 끝나면 버리는 스테이징 복사본입니다.
저희에게 없었던 것은 명확한 다음 질문에 대한 저렴한 답이었습니다. 이 포크가 실제로 무엇을 변경했는가. 단순한 방법: 포크를 마운트하고 LUKS 컨테이너를 잠금 해제하고 내부 ext4를 탐색하고 모든 파일을 부모와 해시 비교하는 것. 이는 읽기와 복호화 모두에서 저장소 크기에 따라 확장됩니다. 키가 diff 경로에서 라이브 상태여야 합니다. 그리고 스토리지 레이어가 이미 무료로 알고 있는 한 가지를 버립니다. 어떤 블록이 분기되었는지를. rdc repo diff는 다른 경로를 택합니다. 변경된 블록에 따라 확장됩니다. 키를 로드하지 않습니다. 두 암호화된 이미지를 diff하여 파일 목록을 얻습니다.
diff하는 스택
디스크에서 “두 저장소”가 무엇을 의미하는지 정확히 설명하겠습니다. 전체 트릭이 여기에 달려 있습니다. 아래에서 위로: SSD, 호스트 스토리지, btrfs 풀. 그 위에 저장소당 하나의 LUKS2 이미지 파일. 잠금을 해제하면 dm-crypt 장치가 생깁니다. 그 안에 컨테이너가 사용하는 ext4 파일시스템이 있습니다. 하나의 저장소는 btrfs 풀의 하나의 파일입니다.
포크는 그 파일의 reflink입니다. 포크 직후 두 이미지 파일은 바이트 단위로 동일합니다. 모든 물리적 블록을 공유합니다. 부모와 포크는 데이터의 두 복사본이 아닙니다. 동일한 블록을 가리키는 두 익스텐트 맵입니다. 포크 내에서 쓰기를 하면 스토리지 레이어가 변경된 영역에 대해 새 블록을 할당합니다. 해당 포크의 익스텐트 맵만 다시 작성됩니다. 부모의 블록은 변하지 않습니다.
따라서 “두 저장소 diff”는 “대부분의 익스텐트를 공유하는 두 파일 diff”로 줄어듭니다. 커널은 이미 이것에 답할 수 있습니다. 두 파일 중 단 한 바이트도 읽을 필요가 없습니다.
FIEMAP: 읽지 않고 무엇이 변경되었는지 커널에 묻기
FIEMAP ioctl은 파일의 익스텐트 맵을 반환합니다. (논리적 오프셋, 물리적 오프셋, 길이) 튜플의 목록입니다. 각 튜플은 파일의 한 부분이 디스크에서 어디에 있는지 나타냅니다. 순수한 파일시스템 메타데이터입니다. 파일 데이터를 읽지 않습니다. 암호화된 이미지의 경우 키가 필요 없습니다. 암호문은 커널이 해석할 필요가 없는 바이트입니다.
두 익스텐트 맵을 diff합니다. 두 포크가 동일한 물리적 블록을 가리키는 논리적 범위는 공유됩니다. 공유는 동일함을 의미합니다. 문자 그대로 장치에서 동일한 블록이기 때문입니다. 포크가 자체 개인 블록을 가지는 범위가 쓰기입니다. 변경된 블록입니다. 스토리지 레이어가 어쨌든 유지하는 메타데이터에서 얻었습니다.
비용 스토리가 여기에서 나옵니다. FIEMAP 비교는 익스텐트 레코드를 읽지 데이터를 읽지 않습니다. 작업이 얼마나 많은 익스텐트가 변경되었는지에 따라 확장됩니다. 저장소 크기에 따라서가 아닙니다. 1 GB 포크와 100 GB 포크는 동일한 짧은 개인 익스텐트 목록을 반환합니다. 동일한 파일을 변경했다면 같은 밀리초입니다. 솔직한 주의사항: 익스텐트 탐색 시간은 크기가 아닌 이미지 단편화에 따라 확장됩니다. 무거운 무작위 쓰기 아래 copy-on-write 이미지는 익스텐트가 쌓입니다. 측정한 가장 단편화된 프로덕션 이미지에서 전체 filefrag 탐색은 3.19초가 걸렸습니다. 단편화 벤치마크 포스트를 참조하세요. 그것이 메타데이터 측면의 상한선입니다. 백그라운드 스캔이지 데이터 읽기가 아닙니다.
두 암호화 레이어를 통해 변경된 블록에서 파일 이름으로
암호화된 이미지의 변경된 바이트 범위 목록은 아직 유용하지 않습니다. 범위는 암호문의 위치입니다. 원하는 이름은 두 레이어 위, 내부 ext4에 있습니다. 그 사이의 연결은 복호화가 아닌 주소 산술입니다.
LUKS는 aes-xts로 암호화합니다. 길이를 보존하며 각 512바이트 섹터를 독립적으로 암호화합니다. 변경된 평문 섹터는 동일한 오프셋에서 변경된 암호문 섹터를 생성합니다. 유일한 이동은 LUKS 데이터 오프셋입니다. 암호화된 페이로드 앞의 16 MiB 헤더와 키슬롯입니다. 각 변경된 이미지 범위에서 해당 오프셋을 뺍니다. 이제 dm-crypt 장치의 일치하는 범위를 얻었습니다. 내부 ext4가 있는 블록 장치입니다. 키를 사용하지 않았습니다. 뺄셈입니다.
이제 장치 범위를 파일에 매핑합니다. ext4도 inode당 익스텐트 맵을 유지합니다. 동일한 (논리적, 물리적, 길이) 구조입니다. 마운트된 내부 파일시스템에서 FIEMAP을 통해 접근합니다. inode를 한 번 탐색하여 블록-to-파일 인덱스를 구축합니다. 그런 다음 해당 인덱스에서 변경된 각 장치 범위를 조회합니다. inode 1234의 데이터 익스텐트와 겹치는 범위는 해당 inode의 경로에 속합니다. 그것이 변경된 파일입니다.
이것이 절대 하지 않는 것을 명확히 말씀드리겠습니다. 변경된 이미지에서 평문을 파생하지 않습니다. 알려진 오프셋에서 파일시스템 구조를 읽습니다. 이것을 암호화된 측면과 복호화된 측면 모두에서 수행합니다. 그런 다음 주소로 두 가지를 결합합니다. 블록 필터는 어떤 장치 영역이 이동했는지 알려줍니다. ext4 익스텐트 맵은 어떤 파일이 각 영역을 소유하는지 알려줍니다. 어떤 단계도 변경된 블록의 내용을 검사하여 변경되었다고 결정하지 않습니다.
추가, 삭제, 이름 변경: inode 신원 탐색
수정은 블록 비교에서 직접 나옵니다. 추가, 삭제, 이름 변경은 하나의 관찰이 더 필요합니다. reflink가 이것을 무료로 제공합니다. 포크는 inode 번호를 보존합니다. 전체 이미지를 reflink하면 무언가가 분기되기 전에 전체 내부 파일시스템을 바이트 단위로 복제합니다. 따라서 부모에 존재했던 inode는 포크에서 동일한 번호를 갖습니다.
이로써 신원은 집합 비교가 됩니다. 다른 경로를 가진 양쪽의 inode는 이름 변경입니다. 새 쪽에만 있는 inode는 추가입니다. 이전 쪽에만 있는 inode는 삭제입니다. 이름 변경은 장치 익스텐트 겹침으로 확인됩니다. 이름이 변경된 파일의 데이터 블록은 두 포크 모두에서 동일한 장치 오프셋에 있습니다. 두 포크는 하나의 좌표계를 공유합니다. 이 겹침은 또한 관련 없는 데이터에 대해 inode 번호가 재사용되는 경우를 배제합니다. 순수한 이름 변경은 파일의 데이터 블록이 변경되지 않은 상태로 나타납니다. 디렉토리 항목만 이동했습니다.
다음은 기본 이름-상태 형식으로 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 저장소에서 수정된 파일 하나. 파일 데이터를 읽지 않는 블록 비교에서 보고됩니다. 잠금 해제된 것은 없습니다.
기본값은 정확성을 위해 한 가지를 더 합니다. 블록 필터는 상위 집합입니다. btrfs 익스텐트는 실제로 변경된 바이트보다 더 많은 영역을 커버할 수 있습니다. 따라서 한 파일에 대한 쓰기가 익스텐트를 공유하는 이웃을 표시할 수 있습니다. 변경되지 않은 파일을 보고하지 않으려면 기본값이 블록으로 표시된 각 후보를 확인합니다. 양쪽에서 해당 파일만 해시합니다. 저장소가 아닌 후보를 해시합니다. 따라서 확인 비용은 여전히 변경 집합을 추적합니다. --fast는 블록 필터를 신뢰하고 확인을 건너뜁니다. 답을 빠르게 원하고 가끔 거짓 양성을 허용할 때 사용하세요.
AI 에이전트에 왜 이것이 필요한가
이 명령이 존재하는 이유는 에이전트 워크플로입니다. 에이전트가 프로덕션을 포크하고 변경을 실행한 후 실제로 무엇을 건드렸는지 깔끔하게 보고할 방법이 없는 것을 계속 봤습니다. AI 에이전트는 프로덕션을 즉시 포크할 수 있습니다. 격리된 포크 내에서 위험한 변경을 실행합니다. 그런 다음 어떤 것이든 다시 승격하기 전에 정확히 무엇을 건드렸는지 알아야 합니다. 포크는 브랜치입니다. Diff는 리뷰입니다.
에이전트는 이름-상태를 읽지 않고 --json을 읽습니다:
$ rdc repo diff --name prod:experiment --json -m hostinger
구조화된 출력은 에이전트에게 정확한 변경 집합을 제공합니다. 수정, 생성, 삭제된 경로들. --stat을 사용하면 파일당 변경 크기(바이트와 블록). diff를 본 후 승격하는 에이전트는 프로덕션 근처에 놔둘 수 있는 에이전트입니다. 폭발 반경은 검사 가능하지, 단언이 아닙니다. 다른 모드들은 동일한 리뷰 루프를 지원합니다. --name-only는 순수 경로 목록. --content <path>는 한 파일의 통합 텍스트 diff(텍스트만; 이진 파일은 Binary files differ 보고). --stat은 에이전트가 무엇이 얼마나 변경되었는지 알아야 할 때.
DR 테스트에 왜 이것이 필요한가
동일한 기능이 위험 없이 물어보기 어려웠던 DR 질문에 답합니다. 프로덕션을 포크합니다. 백업을 포크에 복원합니다. 포크를 프로덕션에 diff합니다. diff는 복원이 예상한 파일 집합을 재현했는지 알려줍니다. 프로덕션을 중단하지 않고 이 작업을 합니다. diff 경로에서 아무것도 복호화하지 않습니다.
이것은 일정에 따라 실행할 수 있는 리허설입니다. 복원은 격리된 포크에 이루어집니다. diff는 git 문법으로 델타를 보고합니다. 깨끗한 리허설: 변경된 집합이 백업이 포함해야 하는 것과 일치합니다. 라이브 프로덕션에 대한 복구를 검증하고 있습니다. 복사본을 만드는 데도 버리는 데도 비용이 들지 않습니다.
솔직한 한계
콘텐츠 diff는 텍스트만입니다. --content는 텍스트 파일의 통합 diff를 생성합니다. 나머지는 git이 하는 것처럼 Binary files differ를 보고합니다. 암호화 후 압축된 blob의 줄 단위 diff는 의미가 없습니다.
관련된 포크를 diff하지, 임의의 저장소는 아닙니다. 전체 메커니즘은 공유 좌표계에 달려 있습니다. 공유 익스텐트가 동일성을 증명합니다. 보존된 inode 번호가 신원을 고정합니다. 공통 데이터 오프셋이 이것을 묶습니다. 공통 조상에서 포크된 적 없는 두 저장소는 이 중 어느 것도 공유하지 않습니다. 저렴한 diff가 없습니다. 이것은 버그가 아닌 기능입니다. 관련 없는 히스토리 간의 git diff가 의미 없는 것과 같은 방식입니다.
이름 변경 감지는 inode 기반입니다. 파일시스템이 실제로 이름 변경으로 기록하는 이름 변경에 대해 정확합니다. 동일한 내용을 새 이름으로 삭제-후-생성하는 경우? inode 테이블에 대한 두 가지 작업입니다. 따라서 이름 변경이 아닌 하나의 삭제와 하나의 추가로 보고됩니다. git의 내용 유사성 휴리스틱은 그것을 이름 변경으로 부를 것입니다. inode 탐색은 그렇지 않습니다. 그것이 파일시스템이 한 것에 대한 올바른 답입니다. 인간이 의도한 것에 대한 답이 아니더라도.
그리고 메타데이터 탐색은 단편화에 따라 확장됩니다. 심하게 단편화된 이미지에서 익스텐트 열거는 밀리초가 아닌 초입니다. 여전히 저장소 크기와 무관합니다. 여전히 모든 데이터 읽기가 없습니다. 그러나 가장 단편화된 이미지에서 문자 그대로 즉각적이지는 않습니다.
결론
rdc repo diff는 암호화된 실행 중인 인프라에 버전 관리 인체공학을 적용합니다. 인터페이스는 의도적으로 git입니다. A/M/D/R, 통합 diff, --stat. 새로 배울 것이 없습니다. git status --short를 읽을 수 있다면 두 LUKS 이미지 간의 diff를 읽을 수 있습니다. 그 아래의 엔지니어링이 가치 있는 부분입니다. 두 가지 거부에 해당합니다. 절대 복호화하지 않습니다. aes-xts는 블록 수준 FIEMAP 비교가 주소로 변경된 모든 섹터를 찾을 수 있게 합니다. 그리고 변경되지 않은 데이터에 비용을 지불하지 않습니다. 스토리지 레이어가 이미 어떤 블록이 분기되었는지 기록했습니다. 포크는 브랜치입니다. Diff는 리뷰입니다. 리뷰는 변경 비용을 치르지, 저장소 무게가 아닙니다.