bigywater.tech
Git · 30

reftable 后端:--ref-format=reftable 的目录结构

reftable 是 git 新的引用存储格式:不再一个 ref 一个文件,而是一组有序的表。这篇创建 reftable 仓库,看目录结构、每次更新产生的表文件与自动合并、表的头尾字节、5000 个 tag 时的体积和耗时,以及与 files 后端互相迁移。

135,479 vs 290,103
5000 个 tag:reftable 与 packed-refs 的字节数
1 个表
一次事务写入 5000 个 ref 产生的表数量;逐个更新 200 次后也只有 4 个表
68 字节
reftable 表文件尾部的长度,含 CRC-32,可以逐字节校验

目录结构

实测 · 本机执行输出
  $ cat .git/HEAD; echo "---"; git config --local --list | grep -E "refstorage|formatversion"; echo "---"; ls -l .git/refs | awk 'NR>1 {print $5, $9}'; cat .git/refs/heads; echo; echo "---"; ls -l .git/reftable | awk 'NR>1 {print $5, $9}'; cat .git/reftable/tables.list
  ref: refs/heads/.invalid
  ---
  extensions.refstorage=reftable
  core.repositoryformatversion=1
  ---
  41 heads
  this repository uses the reftable format
  
  ---
  124 0x000000000001-0x000000000001-0b921ef5.ref
  43 tables.list
  0x000000000001-0x000000000001-0b921ef5.ref
  (git 自己仍能正常工作:)
  $ git symbolic-ref HEAD; git show-ref 2>&1 | head -2; echo "(新仓库没有任何 ref,show-ref 无输出)"
  refs/heads/main
  (新仓库没有任何 ref,show-ref 无输出)

reftable 仓库的 .git/HEAD 是占位值 ref: refs/heads/.invalid,.git/refs/heads 是一个说明文件而不是目录——用来挡住不认识 reftable 的旧版 git。配置里 extensions.refstorage=reftable,版本升到 1。真正的 ref(和 reflog)存在 .git/reftable/ 下。

追加与合并

实测 · 本机执行输出
  提交 1 之后:
  $ ls .git/reftable | grep -v tables.list
  0x000000000001-0x000000000002-40602fbf.ref
  git branch b2 之后:1 个表  1-3(355B)
  git branch b3 之后:1 个表  1-4(388B)
  git branch b4 之后:1 个表  1-5(419B)
  git branch b5 之后:2 个表  1-5(419B)  6-6(255B)
  git branch b6 之后:1 个表  1-7(481B)
  git branch b7 之后:2 个表  1-7(481B)  8-8(255B)
  git branch b8 之后:1 个表  1-9(543B)

每次更新写一个新表,文件名里是它覆盖的更新序号范围。git 自动把相邻、大小相近的表合并,所以表的数量保持很小,不像松散 ref 那样随 ref 数线性增加文件。

表的字节

实测 · 本机执行输出
  (先 git pack-refs --all 合并成 1 个表)
  表文件 0x000000000001-0x000000000009-bd6423a5.ref,543 字节
  0000  52 45 46 54 01 00 10 00 00 00 00 00 00 00 00 01  REFT............
  0010  00 00 00 00 00 00 00 09 72 00 01 0a 00 23 48 45  ........r....#HE
  …(共 543 字节)
  头部:魔数 b'REFT'  版本 1  块大小 4096  最小更新序号 1  最大更新序号 9
  尾部(最后 68 字节):魔数 b'REFT'  与头部相同: True  ref_index_offset=0  obj_offset(含长度字段)=0  obj_index_offset=0  log_offset=266  log_index_offset=0
  CRC-32(尾部前 64 字节)= e12b8a26;文件里记的 e12b8a26;一致: True
  表里有多少个 ref(用 git 数): 8

表以 24 字节头开头:魔数 REFT、版本、块大小、最小与最大更新序号;末尾 68 字节是头的副本加各区偏移(ref 索引、对象反查、reflog 等)与 CRC-32。小表里没有的区域偏移为 0;reflog 也存在表里。

体积与速度

实测 · 本机执行输出
  后端                       文件数        占用字节    git tag -l    rev-parse 单个
  files(松散)               5001     205,041       91.1 ms         11.0 ms
  files(packed-refs)         1     290,103       14.6 ms         10.6 ms
  reftable                   1     135,479       14.8 ms         11.0 ms

5000 个 tag 时,reftable 只有 packed-refs 的一半左右(ref 名前缀压缩);列举所有 ref 与 packed-refs 一样快,远快于松散文件;只查一个 ref 三者没有差别。

原子写入

实测 · 本机执行输出
  一次 update-ref --stdin 事务写入 5000 个:表的个数 = 1
  逐个 git tag(第 N 个之后的表数):1→2  2→2  3→1  4→2  5→2  8→1  16→2  32→2  64→2  100→3  150→2  200→4

一次事务里的所有更新属于同一个更新序号,只写一个表;逐个更新则产生许多小表,靠合并控制数量(200 次后只有 4 个)。

迁移

实测 · 本机执行输出
  $ git refs migrate --ref-format=reftable; cat .git/HEAD; ls .git | grep -E "^(logs|packed-refs|reftable)$"; git for-each-ref --format="%(refname)"; git config --local --list | grep -E "refstorage|formatversion"
  ref: refs/heads/.invalid
  reftable
  refs/heads/b1
  refs/heads/main
  refs/tags/t1
  core.repositoryformatversion=1
  extensions.refstorage=reftable
  $ git refs migrate --ref-format=files; cat .git/HEAD; ls .git | grep -E "^(logs|packed-refs|reftable)$"; git for-each-ref --format="%(refname)"; git config --local --list | grep -E "refstorage|formatversion" || echo "(没有 refstorage 了;formatversion 回到 0)"
  ref: refs/heads/main
  logs
  packed-refs
  refs/heads/b1
  refs/heads/main
  refs/tags/t1
  core.repositoryformatversion=0

git refs migrate --ref-format=reftable|files 在两种后端之间转换,ref 内容保持不变。reftable 没有 logs/ 与 packed-refs,迁回 files 后它们重新出现。

分步演示

reftable
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。表文件头尾按字节解析(解析器在实验脚本内),CRC-32 校验通过;“占用字节”是文件大小之和,不含文件系统块对齐;5000 个 tag 用 git update-ref --stdin 创建;耗时 5 次中位数
    • 没有验证:表内部的 ref 块 / 对象块 / reflog 块的记录编码(前缀压缩、变长整数),只解析了头尾;多个并发写入者之间的锁(tables.list.lock);在没有 reftable 支持的 git 版本上打开这样的仓库的表现(只看到了 refs/heads 说明文件);reftable 的 reflog 过期;更大规模(十万以上 ref)的性能
    京ICP备2026067502号-1京公网安备11010502063487号