胡萝卜烤地瓜
胡萝卜烤地瓜
发布于 2026-07-25 / 3 阅读
0
0

NO.A10 给每次构建记本明白账-Build Registry

⚠️ 法律与用途声明:本文讨论构建产物与缓存的可观测性设计,基于开源工具(Conan 2 / GitHub Actions),属于经验分享。所有企业名、项目名、内部服务与字段命名均已脱敏,代码示例中的标识符均为占位。读者须遵守所在地区法律法规。


一、构建跑了一堆,谁记得住

CI 跑久了,构建记录会堆成山。某天线上出了问题,你想回头查"上周那个包,是哪次构建发的、用的什么工具链、缓存在不在",结果发现——GitHub 上的 run 列表翻不到底,本地的构建目录早被清理脚本删了,当时的构建参数谁也记不清。最后全靠聊天记录和记忆拼凑,还拼不准。

这种痛,根子上的问题是:构建发生的时候,没人给它记一笔账。构建日志在 CI 平台上,缓存状态在磁盘上,发布记录在某个人的脑子里,三者各管各的,谁也对不上谁。

Build Registry 这个东西,就是给每次构建记一本明白账——构建一发生就登记,后续发布、缓存治理的每一次状态变化,都往这本账上回写。这一篇就讲这本账记什么、怎么翻、怎么对账。

二、这本账记了什么

每次构建完成,登记表就多一条记录。这条记录不只是一句"构建成功",而是把这次构建的身份、结果、生命周期状态都钉在一起(脱敏后的字段全貌):

{
  "run_key": "<run_id>-attempt-<n>",
  "repo": "<owner>/<repo>",
  "branch": "<分支>",
  "target_key": "<目标>-<工具链>",
  "toolchain": "<工具链版本>",
  "status": "success",
  "kind": "build",
  "promote_status": "pending",
  "cache_status": "active",
  "cache_updated_at": "<时间>",
  "approver": null,
  "timestamp": "<时间>"
}

几个字段拎出来说一下为什么记:

  • run_key:这次构建的物理实例身份(NO.A3 讲过,含 attempt,re-run 不混淆)。
  • target_keytoolchain:这次构建编的是哪个目标、用了哪个工具链版本。出问题时这两个字段最常被查。
  • statuskind:构建本身成没成功、这条记录是构建还是晋升(promote 也会留痕)。
  • promote_status:它放行没(NO.A9 讲的门禁钥匙)。
  • cache_statuscache_updated_at:这次构建的重缓存在磁盘上还在不在、最后一次变更是什么时候。
  • approver:谁放的行。没人放就是 null。

注意这些字段不是各自为政的孤岛,它们被一条记录串成一个完整的"生命周期档案":从构建发生、到放行发布、到缓存被清理,整个过程在一条记录里看得清清楚楚。

三、翻账:按需筛选

账记下来了,还得能翻。几百上千条记录堆在那,不能让人一页页翻。登记表支持按多个维度筛选(伪代码示意):

# 翻"某仓库的失败构建"——排查问题常用
build_registry.query(
    repo="<owner>/<repo>",
    status="failed",
)

# 翻"已发布但缓存已被清理的"——看哪些包的真源在、缓存没了
build_registry.query(
    promote_status="promoted",
    cache_status="pruned",
)

# 翻"某目标最近的成功构建"——找可复现的那次
build_registry.query(
    target_key="<目标>",
    status="success",
    order_by="timestamp",
    limit=5,
)

多维筛选的价值是:不同角色关心不同切片。排查的人想看失败记录,发版的人想看待确认的候选,治理的人想看哪些缓存可以回收。同一本账,每个人按自己的维度翻,各取所需。这比把所有记录铺成一个长列表、让人肉眼扫强太多。

筛选维度还有一个常被忽视的细节:时间统一显示成一个时区(我们用本地时区)。别小看这条——构建平台、CI 日志、本地机器的时区经常不一致,记录里时间一会儿是 UTC 一会儿是本地,对账时能把人绕晕。账本里统一成一个时区,省心。

四、cache_status 三态:这条记录的现况

构建记录里有个特别实用的字段 cache_status,它回答一个高频问题:这次构建的重缓存,现在还在不在磁盘上? 它有三个状态:

  • active:构建目录还在,重缓存也还在。需要的话能直接复现这次构建的现场。
  • pruned:构建目录还在,但重缓存(包缓存、编译缓存)已经被治理脚本清掉了。审计元数据还在,但想重新构建得从真源重拉。
  • missing:登记表里有这条记录,但构建目录已经整个没了(可能是手动删的,或被更激进的清理收掉了)。

这三个状态的意义在于:它把"账本上的记录"和"磁盘上的实物"对应起来。你看到一条 promoted 记录,cache_status 是 pruned,你立刻就知道——这个包的真源在 Artifactory 里好好的(A8),但它当时的构建现场已经被清理了,要复现得从真源重建。信息完整,不用猜。

五、对账:账本和实物对不上时怎么办

账记得再勤,也会有对不上的时候。比如有人手动删了某个构建目录,但登记表里那条记录还写着 active;或者治理脚本中途出错,缓存删了一半,状态没回写成功。这时候需要一道对账机制。

对账的逻辑很直白(伪代码):

def sync_cache_status(record):
    run_dir = run_directory_of(record.run_key)
    if not exists(run_dir):
        return "missing"              # 目录都没了
    elif has_heavy_cache(run_dir):    # 目录在、且重缓存还在
        return "active"
    else:                             # 目录在、但重缓存已被清
        return "pruned"

# 扫一遍登记表,把每条记录的 cache_status 修正成实际值
for record in build_registry.all():
    actual = sync_cache_status(record)
    if actual != record.cache_status:
        build_registry.update(record.run_key, cache_status=actual)

对账不是日常动作,是兜底动作——平时靠治理脚本自动回写状态,只有怀疑账本和实物脱节时(比如手动删过东西、或者脚本出过错),才跑一次对账把状态修正回来。对账本身不删任何文件,只改账本上的字段,安全。

这个机制的本质是:承认账本会漂移,所以留一个主动校正的口子。比起假装账本永远准、结果关键时刻发现对不上,主动给一个对账工具,反而让人踏实。

六、可观测是治理的前提

把前几篇串起来看,你会发现 Build Registry 这本账,是整套缓存治理能跑起来的前提。

NO.A3 讲缓存治理——定时清理、敢删重缓存。敢删的底气,除了"真源在"(A8),还有一条就是"删完之后账本会如实反映"。清理脚本删完一个 run 的重缓存,会回写那条记录的 cache_status 为 pruned:

# 治理脚本删完重缓存后,回写账本
build_registry.update(run_key, cache_status="pruned")

这样,清理就不再是"删完拉倒、没人知道删了啥"的黑箱。每一条被清理的记录,都在登记表里如实变成了 pruned。谁要看治理效果,翻账本就知道哪些被清了、哪些还在、哪些早就没了。

这就是"可观测闭环":构建一发生就登记(register),治理脚本删缓存后回写状态(write-back),需要时对账校正(reconcile),任何时候翻账本都能看清全貌(observe)。没有这个闭环,缓存治理就是个不敢碰的黑箱;有了它,治理才成了可查、可控、可对账的日常

七、几条攒下来的判断

构建发生时就记账。别等出事才翻日志。构建一完成就登记,把身份、结果、状态钉成一条记录,这是可观测的起点。

一条记录串起完整生命周期。构建、发布、缓存清理的状态变化,都回写到同一条记录上。一条记录看完一件事的全程,不用跨系统拼。

筛选要按角色给维度。不同人关心不同切片,多维筛选让同一本账服务多个角色。

cache_status 三态把账本和实物对应。active/pruned/missing,让记录不只是历史档案,还反映当前现况。

承认账本会漂移,留对账口子。主动给校正工具,比假装账本永远准更可信。

可观测是治理的前提。敢删缓存的底气,一半来自真源在(A8),一半来自删完账本如实反映(这本账)。

这套东西上线之后,最直观的变化是:再没人问"那次构建怎么样了"——因为答案都在账本里,自己翻就有。最好的可观测,是让人不需要问别人,自己就能查到

到这里,平台系列的制品与可观测部分就讲完了。下一篇,我们进入一个更具体的工程难题:板测三键模型——上板测试的时候,怎么用 run_id / attempt / run_key 三个键,把构建产物和板上的测试结果对上号,不让 re-run 把旧结果覆盖了。


参考链接:

[1] GitHub Actions:查看 workflow run 与 run attempt https://docs.github.com/en/actions/how-tos/manage-workflow-runs

[2] Conan 2:list 命令(查询仓库内容) https://docs.conan.io/2/reference/commands/list.html

[3] 可观测性(Observability)概念 https://opentelemetry.io/docs/concepts/observability-primer/

[4] 事件溯源与状态回放(状态机记账思路) https://martinfowler.com/eaaDev/EventSourcing.html

[5] 系统台账与 reconciliation 模式 https://kubernetes.io/docs/concepts/using-kubernetes-object-management/

[6] Conan 2:cache 生命周期与清理 https://docs.conan.io/2/reference/commands/cache.html


评论