语言注册表天生就不稳定Language Registries Are Unstable by Default
Nesbitt提出一个尖锐观点:编程语言注册表本质上就是不可靠的。他以apt install -t unstable命令作比喻,说明注册表版本管理存在根本缺陷。作者主张开发者不应依赖注册表的稳定性,而应采用确定性构建系统。这种立场挑战了当前包管理依赖的传统做法。
Andrew Nesbitt
从结构上看,在公共注册表中运行 pip install requests 或 npm install react 与在 Debian sid 上运行 apt install -t unstable 是相同的操作,但没有人会这样描述它。我说的“unstable”并不是指 buggy(有缺陷),而是特指自 20 世纪 90 年代末以来 Debian 所采用的概念:一个由软件包组成的池子,每当维护者上传新版本时,新包就会立即进入该池中,无需经过推广关卡、无需满足最低驻留时间,也不存在从上传到用户机器之间的任何质量门槛。
在语言注册表中,任何经过身份验证的发布者都可以在任何时候推送任意版本,索引会在几秒内更新,而任何未特意配置为其他方式的解析器都会在下次运行时开始选择最新版本。从维护者按下回车键到企业 CI 流水线执行结果之间,除了消费者自己设置的配置外,没有任何中间环节,而大多数消费者并没有进行任何设置。
Debian sid 的存在是基于这样一个明确前提:你不会将生产环境指向它。Debian 安装后的默认 sources.list 指向 stable(稳定版),其中的软件包已经过数月的测试,而这些软件包只有在经过不稳定分支中无发布关键错误的强制阶段后,才能进入测试阶段。Fedora 有 updates-testing,需通过 karma 投票才能推广;Ubuntu 有 -proposed 存储库,需通过 autopkgtest 才能解锁;Arch 有 core-testing 和 extra-testing 仓库,大多数用户从未启用;FreeBSD 则同时提供 ports 树的 quarterly 和 latest 分支。
我能想到的所有系统包管理器都会向用户提供一个稳定性通道的选择,并默认使用保守选项,而前沿通道则是用户明知风险后主动选择的。语言包管理器只提供一个通道,其行为却像前沿通道一样,墙上没有开关可以切换到另一种模式。
我们已经远远超出了将 event-stream、Shai-Hulud、xz 以及当前这一系列自我传播的 GitHub Actions 蠕虫视为一连串不幸的孤立事件的时代。如今几乎每天都有某家安全厂商发布恶意软件分析报告,GitHub Advisory Database 添加恶意软件条目的速度比我阅读的速度还快,我已经厌倦了每次看到这些事件都被当作攻击者在一个原本健全的系统中发现了意外漏洞来对待。
一个接受来自数万松散验证发布者的上传,并在几分钟内将最新上传作为默认解析目标的注册表,必然会以某种背景频率向用户分发恶意软件,因为这就是不稳定池的用途所在。我们将这个池直接连接到了生产环境,且没有推广步骤,考虑到该设计正是发行版明确标记为“自行承担风险”的分支,我对这种反复的惊讶感到难以理解,甚至比事件本身更令人费解。
集成问题
发行版之所以拥有稳定性通道,是因为发行版承担了集成问题的责任:成千上万软件包必须协同启动一个可用的操作系统,因此用户上游的某个角色必须在任何东西发布之前确保 glibc、systemd、Python 和 GNOME 都能彼此协调。发布团队是结构性必需的,一旦有了发布团队,就自然会出现推广关卡,而一旦有了推广关卡,频道也就随之产生了。
语言注册中心早期采取相反的做法,将集成问题推给了每个消费者的 lockfile。从来就没有一个专门的实体来负责确认 requests 2.32.0、urllib3 2.2.0 和 certifi 2024.2.2 是否真的能协同工作,因此这个问题每天在数千个 CI 流水线中被重复解答了数千次,而不是在注册中心只问一次。由于没有上游的责任方负责集成,也就没有人自然适合去运行一个发布准入检查,而注册中心本身也普遍不愿承担这个角色,将自己定位为中立管道而非需要发布治理层级的治理者。
词汇上的差异加剧了这一问题,因为“channel”是分发和工具链领域的术语(rustup 有 stable、beta 和 nightly;conda 有 defaults 和 conda-forge;snap 暴露 tracks,flatpak 暴露 branches;Nix 有 nixos-25.11 和 nixos-unstable),而语言注册中心谈论的是 indexes、dist-tags 和 pre-release markers,这些都没有“选择你想要多少风险”的相同含义。从 Debian 或 Fedora 打包走过来的开发者一眼就能看出 npm 缺少这个模式,但那些从 package.json 开始学习依赖管理的人,却找不到那个缺失事物的名称,因此它不会被视为一种缺失。
少数语言生态系统构建了部分等效机制,其中最强大的是 Stackage——一个基于 Hackage 并模仿发行版发布流程的精选 LTS 和 nightly 快照集合。它的存在是因为 Haskell 的类型系统使得跨包不兼容足够痛苦,以至于精选工作被推到了上游而非停留在每个人的 cabal.project 中。它已成为 stack 超过十年的默认解析目标,这证明了一个语言社区可以在不 fork 注册中心的前提下,在开放注册中心之上运行一个精选通道。Conda 将 channels 视为一等公民,允许你像 apt sources.list 一样组合 defaults、conda-forge 和领域特定的 channels,尽管这更多是关于主题领域精选而非稳定性分层的体现。
rustup 的 stable/beta/nightly 应用于编译器而非 crates,npm 的 dist-tags(latest、next、beta、canary)由发布者控制、自愿且按包而非全注册中心范围设置。PyPI 依赖 PEP 440 预发布标记,pip 默认跳过除非你传入 --pre,TestPyPI 是发布演练空间而非 staging 通道,其余(RubyGems、Maven Central、NuGet、Hex、Go、CPAN)在注册中心层面根本没有任何机制。
紧迫性异议
对发布与安装之间任何延迟的主要反对意见是:安全修复不能等待——维护者发布 CVE 补丁后,消费者需要立即在生产环境中使用,而不是等待七天后进入“暂缓区”,而阻止修复的冷却期会让所有人多暴露在风险中一周。攻击者知道这一点并利用同样的反射机制,这就是为什么 npnjs.com 钓鱼活动以伪造的 npm 支持邮件形式出现,为什么 xz 后门被快速推向发行版发布,以及为什么在事件线程中“立即更新”在传输上与下一次攻击的交付机制无法区分。没有发布准入检查的索引无法区分真正的紧急补丁和伪造的补丁,因为它对两者都应用零审查。
自稳定版本发布以来,发行版一直为安全更新设有独立的安全通道。Debian 的安全归档和 Fedora 的稳定更新仓库绕过了常规测试迁移流程,因此修复可以在几小时内到达用户手中,但上传仍需经过安全团队审核,而非由持有凭证者按下发布键后立即生效。该团队还可以完全不依赖上游直接发布补丁版本,而语言生态系统大多无法做到这一点:对于 npm 或 PyPI 而言,修复的唯一交付路径是上游维护者在唯一的无审查索引上发布新版本,这也是为何紧迫感如此不容商量的原因之一。
在过去一年的冷却期后,你可以观察到整个生态系统正逐步重建 Debian 的推广模型,而无需使用其任何术语——pnpm、Yarn、Bun、npm、uv 和 pip 都在彼此相隔数月的时间内相继推出了“不选择超过 N 天的新版本”这一机制。它们实际上都是对 Debian 最小不稳定停留时间规则的消费者端重新实现,并在每个记得开启的项目边缘进行配置。
gem.coop 的冷却端点是目前最接近真正第二通道的方案,而非客户端的近似模拟。它是一个独立的源 URL,提供 RubyGems 目录中延迟 48 小时发布的最新版本,项目只需修改 Gemfile 中的一行即可加入,无需升级 Bundler 或学习新标志,且所有历史版本的 Bundler 都已支持它,因为从客户端角度看它只是一个 gem 源。这本质上就是 sources.list 模型:稳定性策略体现在你指向哪个索引,而非客户端配置中,判断哪些内容足够稳定以供发布的工作也仅由上游完成一次,而非在每个消费者的配置中重复实现。
更重型的方案中,每个带有 dev → staging → prod 推广管道的 Artifactory 或 Nexus 部署都在企业内部重建了 Debian 式安全口袋模型,包含阻止制品通过扫描和隔离期的策略关卡。Python 侧的 devpi、Go 的 Athens、npm 的各种 verdaccio 配置以及轻量级缓存代理等工具也在私有环境中吸收相同需求,这也是为何对注册表层功能的需求始终分散的原因:每个需要稳定通道的大型组织都会内部构建一个,结果从未形成对上游注册表的共享需求。
我并非呼吁制定新标准或设计巧妙的解析器 hack,因为四分之一世纪前的人们早已完成这项设计工作,至今仍在生产环境中运行。Debian 的测试迁移规则、Fedora 的 Bodhi karma 系统和 FreeBSD 的季度分支都是公开代码库中经过实战检验的推广函数文档,而语言包管理侧却不断重新发明其中的各个片段,给每个片段起个新名字,并作为新型供应链特性发布。
冷却标志是最小的通道,即一个基于时间的推广标准,在边缘应用,而六种工具在同一年汇聚于同一功能,相当于六次独立重新发现 Britney 配置中的一行代码。PyPI 或 npm 的注册表级别稳定通道会是什么样子,以及谁来运行推广函数,是我想另起一篇单独讨论的问题,这次将从发行版发布团队的文档出发,而不是从零开始的白板。
我此刻想要的只是对我们已有事物的诚实标注。如果 npm 或 PyPI 明天提供两个索引,并以 Debian 描述 sid 的方式描述其中一个——作为每分钟都在变化的开发 staging area,由那些接受自己将是第一个遇到任何问题的人来指向它——我认为大多数团队不会故意将生产构建指向它。如今每个生产构建都精确地指向那个目标,不是因为有人将其与替代方案权衡过,而是因为从来就没有替代方案出现在菜单上,而相当一部分“供应链安全”工作,正是行业慢慢意识到它从未被真正问过之后才开始的。
需要完整排版与评论请前往来源站点阅读。