原文:Who Should Pay For Source Code Availability? | Loris Cro's Blog
我有一个项目(Zine,一个静态网站生成器),它有11个依赖项,托管在GitHub、Codeberg和自托管的Forgejo实例上。每当GitHub或Codeberg宕机时,我的项目的新构建就会失败。
虽然自托管的Forgejo实例的正常运行时间(Uptime)明显优于GitHub,但它们最终永久宕机并彻底破坏我的项目的风险更高。
解决这个问题最直接、最实用的方法是对所有内容进行分支(Fork)或打包依赖(Vendor)。
分支意味着将所有依赖项移动到与Zine相同的托管平台上。如果你能克隆主项目,你也能够克隆它的所有依赖项。
打包依赖意味着更进一步:将所有依赖项的源代码提交到你自己的仓库中,这样克隆项目这个动作实际上就会获取到所有东西。
如果你非常关心实用性和极致的韧性(Resilience),那么你可能会更喜欢打包依赖而不是分支。
分支要求你对整个依赖树进行分支(即包括你的间接依赖),并且还要修改任何非叶子依赖项,使其指向你自己的分支。相比之下,对于Zig工具链的用户来说,打包依赖极其简单。
对于不太了解的人来说,Zig 0.17.0-dev最近改变了依赖缓存的工作方式:全局缓存现在将包存储为压缩包,每个项目都有一个本地的 zig-pkg/ 目录,里面包含它所使用的依赖项的解压文件。如果这是你的目标,这会让打包依赖变得微不足道:只需将你的 zig-pkg/ 目录纳入版本控制即可。(否则,建议将其添加到 .gitignore 中。)
如果你计划对依赖项进行修改,并且关心如何更容易地将它们合并回上游(Upstream),那么分支可能比打包依赖更可取。即使你不打算发送拉取请求(PR),在一个分支中挑选(cherry-pick)提交也比从外部项目的打包依赖目录中这样做要容易得多。
分支/打包依赖确实有用,这一点毫无疑问。但这真的是我们能做到的最好方案吗?
拥有集中式包索引(如 npmjs.com 或 crates.io)的语言不必担心这些问题,因为它们本质上将分支策略作为内置功能(所有源代码都被“分支”到由集中式包索引托管的副本中),但这这种方法确实让我很不满意,因为它不必要地将解决方案与包管理绑定在一起。我们的目标应该是不论何时都能可靠地获取源代码,而不仅仅是从包管理器获取时。
由于GitHub的不稳定性,这些话题最近变得更加切合实际,但现实中,问题并不是“我们如何让源代码托管变得可靠?”。如果我们真的想的话,我们确实知道如何让代码托管变得可靠。问题其实是:它应该花费多少成本?谁来支付这笔费用?
正如我们行业中经常出现的情况一样,我们从来都不愿面对这个问题,我们一直乐于吸附大科技公司的血。但和往常一样,这个问题最终必须要给出答案,而当这一刻到来时,我们很快就会把矛头指向公司,带着正义的愤慨大喊“恩施化”(Enshittification)!
是的,GitHub毫无疑问正在走向恩施化,就像Discord以及之前的许多平台一样;但我们期望继续免费消耗资源多久?GitHub的现状只是向我们展示了一个从一开始就破碎的系统的裂痕。
我不认为大公司没有责任,我确信在许多未写明的运营手册中,混淆成本公式都有其专门的章节,但应该更清楚这一点的不是公司。是我们。是我们这种刻意的无知造成了一种扭曲,迫使公司要么接受它,要么利用它来保持竞争力。
那么,有没有一种方法可以可靠地提供源代码,同时又能平衡成本公式呢?
我认为,自托管一个代码托管平台(Forge)并不足以平衡这个方程,因为它把使源代码可用的全部成本推给了该软件的创作者,但我们已经确定,此类代码的消费者同样对它能够可靠获取感兴趣,这就是导致分支和打包依赖的原因。
从某种意义上说,分支和打包依赖是分摊保持源代码可用成本的一种方法,但它们都是低效的,因为被分支/打包依赖的代码无法被轻易发现,也无法自动作为镜像使用。
如果有人在你的项目之外对你的依赖项感兴趣,他们的包管理器将无法自动从你的分支/打包依赖副本中获取它。当然,你可能对承担这个成本不感兴趣,但就目前的情况来看,这其实也不是什么选择。
如果我们能做得更好,那就太酷了,因为这可以帮助我们降低为每个人保持源代码可靠可用的整体成本。
Radicle 网络
Radicle 是一个点对点(p2p)网络,其中的节点(Peers)会播种(Seed)他们感兴趣的代码库(Repositories)。
某些节点可能对提高网络的韧性感兴趣,因此会想要重新播种所有内容(或接近所有内容),而另一些节点可能只对确保一组特定项目保证可用感兴趣,从而将他们的播种策略严格限制为仅允许相关代码库。
Radicle 以非常有趣的方式支持问题追踪(Issues)和补丁(Patches,即 GitHub 术语中的 Pull Requests):这里没有集中式(或联盟式)的 Web 服务器来托管这些内容,因为它们作为 Git 对象直接存储在 Git 仓库本身中(启用了 CRDT),当你向网络广播你的更改时(具体来说就是运行 git push),它们可以被重新同步。
为了帮助你更好地理解此功能的工作原理,让我们以一个具体的例子来关注 Issues。
假设我有兴趣将 Radicle 用于 Zine。一旦我将我的仓库发布到 Radicle 网络,我就会想要设置我自己的全天候(always-on)节点,以保证 Zine 始终可用,并促进开发。其他一些节点也可能会决定播种 Zine,但由于 Zine 仍然是一个小项目,我目前不打算指望这一点。
当 Alice 想要提出一个 Issue 时,她会通过在她自己播种的 Zine 副本中打开它来做到这一点(为简单起见,这里再次使用 GitHub 术语称之为她的“分支”)。此时,她会将她的更改发布到 Radicle 网络,任何对这些更改感兴趣的节点都会获取它们。
我的全天候节点被配置为播种所有 Zine 的副本,因此它保证会获取 Alice 的更改,即使 Alice 的个人节点离线,也能使它们保持可用。
此时,当我从网络拉取更改时,我会看到 Alice 的新 Issue,并且我能够使用 Radicle 的 CLI 工具或桌面应用程序对其实施评论、打标签、指派或关闭,这同样适用于 Bob 创建的假设性 Patch。
有兴趣跟进的旁观者将能够通过 Web 界面查看所有源代码、Patches 和 Issues,就像 heartwood 仓库上的这个示例一样。不过 Web UI 是只读的,因为所有更改都必须通过 Radicle 网络提交,鉴于 AI 机器人和爬虫的崛起,这是一个非常方便的权衡。
要了解 Radicle 在实践中是如何工作的,还有一些其他的事情需要学习,所以我邀请大家查看官方文档。
使用 Radicle 实现可靠的源代码可用性
在 Radicle 上发布 Zine 时,我也会有兴趣从我的全天候节点播种我的依赖项,以便保证 Zine 的用户也能可靠地获取成功构建 Zine 所需的依赖项。
我们几乎回到了分支/打包依赖的状况,但有一个关键区别:播种我的依赖项可以使它们被网络发现!
当角色互换时也是如此:当有人依赖 Zine 时,他们会有兴趣播种它,从而使 Zine 的源代码更加可靠地可用,这对我和他们都有好处。此外,根据他们如何微调其播种策略,Zine 的用户不仅可以选择播种我的权威副本,还可以选择播种 Zine 的(全部或部分)“分支”。通过播种 Zine 的“分支”,他们还可以播种 Issues 和 Patches,从而帮助 Zine 上的协作更具韧性。
我发现这种模式有助于以高度契合每个人利益的方式保持成本公式的平衡,同时在成本效益上也明显高于“自托管 forge + 分支/打包依赖”的设置——后者基本上浪费了大量的冗余。
当我深入了解 Radicle 时,正是在这个时候,我认为如果 Zig 包管理器直接支持 Radicle 会很酷,但事实证明这并不像我最初想象的那么简单……不过,其中涉及的摩擦力也出人意料地与相关各方的利益高度一致!
依赖 Radicle 仓库
在 Radicle 中,每个节点都有一个加密身份(DID),用于在网络中标识节点并宣称对代码库的所有权。
当你将代码库发布到 Radicle 时,rad CLI 工具会向代码库添加一个身份文档,将代码库与所有者的 DID 绑定,该文档的哈希值就成为代码库 ID(RID)。
网络也将使用此信息来区分代码库的权威版本与其他人的“分支”,这就是 Radicle 工具如何保证当你从网络克隆代码库时,能获得一个你可以信任的副本。
当你想要从 Radicle 网络克隆代码库时,你会运行类似这样的命令:
$ rad clone rad:z3WukSjzicL8WaZHFALbBwb2r8W52
上面示例中传递给 clone 的参数是一个包含代码库 ID 的 rad URI,它明确不是一个 URL,因为它没有表达应该从哪里获取代码库;这正是关键所在。Radicle CLI 工具将与 Radicle network 交互,从碰巧拥有该数据的任何连接对端(Peer)获取此代码库。
如果我们考虑 build.zig.zon 文件的两个假设变体:
// 真实语法
.dependencies = .{
.zine = .{
.url = "git+https://github.com/kristoff-it/zine",
.hash = "...",
},
},
// 对 Radicle 的假设支持
.dependencies = .{
.zine = .{
.rad = "z3WukSjzicL8WaZHFALbBwb2r8W52",
.hash = "...",
},
},
你可以看到关键的区别在于:第一个指定了位置,而第二个没有。
在第一种情况下,依赖项是脆弱的:每当 GitHub 宕机时,你就无法获取它;而在第二种情况下,只要至少有一个活跃的 Radicle 节点播种了 Zine 仓库,你就能获取它。
这将是一个巨大的升级,但有一个问题:为了能够与 Radicle 网络对话,你必须是网络中的一个对端(Peer),这意味着如果你不是 Radicle 用户,你将无法成功运行 zig build(具体来说,你需要运行一个 Zig 可以与之对话的本地节点)。
这是个大问题!
Zig 旨在成为“零依赖”软件,我们希望尽可能多的 Zig 项目只需安装了 Zig 并运行 zig build 就能构建。情况并非总是如此,因为有些项目会有系统依赖,但除此之外,它不应该对任何非必需品产生硬依赖。
此时我开始琢磨,有没有办法在不成为对端的情况下查询网络?直到我意识到发现并不是问题,而且我没有在考虑成本公式!
Radicle 网络中的一个节点会有兴趣承担作为网络“长期记忆”一部分的成本,这确实意味着与其他 Radicle 节点进行数据的输入/输出传输,但这并不意味着该节点愿意承担未经身份验证的客户端以无限制的速率请求获取该节点播种的一个或多个仓库副本的成本。
如前所述,Radicle 节点具有身份,网络中的其他节点可以决定禁止表现不良的节点,并总体上跟踪它们的行为。另一方面,zig build 客户端本质上是未经身份验证的(就此而言,git 客户端也是如此)。在你的日常生活中,这两者可能没有太大区别,但试想一下,某人配置错误的 CI 无缘无故地不断猛烈轰炸你的节点。
如果考虑我自己的用例,我会愿意承担这部分外部流量,以便为非 Radicle 用户提供访问 Zine 的权限,但我可以理解其他节点(可能播种了 Zine)不愿意这样做。
幸运的是,这个概念在 Radicle 中也得到了很好的建模。
我之前在谈到 Issues 和 Patches 时提到,旁观者可以使用 Web UI 来跟进开发,但这有点过于简化了。默认情况下,Radicle 节点不运行任何 HTTP 服务。这是一个完全可选的功能,你可以根据自己的兴趣决定是否启用。
如果你确实启用了 HTTP 服务,那么 HTTP 客户端将能够访问你的节点,并使用 Web UI 或 Git over HTTP 来克隆该节点播种的代码库,而不需要 Radicle 身份。
就我而言,我希望在我的全天候节点上启用 HTTP 访问,并允许未经身份验证的 HTTP 客户端根据需要下载 Zine(及其依赖项)。任何其他决定播种 Zine(同时还启用 HTTP 接口)的节点也将有效地提供相同的服务。
这意味着,虽然指向启用 HTTP 的 Radicle 节点的 URL 不如 RID 那样具有韧性,但它们仍然等于或优于指向自托管代码托管平台的链接,特别是因为这样的节点有很多!
例如,heartwood 可以通过以下任何一个主机的 HTTP 获取(写本文时它有超过250个播种节点):
https://radicle.network/nodes/seed.radicle.dev/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
https://radicle.network/nodes/index.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
https://app.radicle.at/nodes/seed.radicle.at/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
https://radicle.defelo.de/nodes/radicle.defelo.de/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
https://radicle.network/nodes/rad.hardenedbsd.org/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
https://radicle.jarg.io/nodes/radicle.jarg.io/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
请注意,Web 界面有时是相同的,但每个链接引用的是一个不同的主机,该主机将用于“从 git 克隆”按钮。
为了让 Zig 包管理器能够利用 Radicle,我们实际需要的全部就是简单地实现 #14291(支持镜像)。
正如我所暗示的,我仍在学习 Radicle 的工作原理,所以我不知道这是否已经在大力推进、计划中还是不在范围内,但根据我目前的理解,我认为如果 rad CLI 工具有一个将 RID 解析为一组 HTTP URL 的命令会很好,这样你就可以直接将它们粘贴到启用了镜像的 build.zig.zon 文件中。
未来,探索在 build.zig.zon 中指定 RID 的方法可能是值得的,这样它就可以被可选地用于发现新的公共 HTTP 主机,或者(同样是可选地)在本地节点可用时连接到 Radicle 网络(即用户是 Radicle 网络的一部分)。
Codeberg 和 Tangled 怎么样?
当人们谈论离开 GitHub 时,这两个平台经常被提及。让我们看看它们与 Radicle 相比如何。
Codeberg
Codeberg 是一个德国非营利组织,运行 Forgejo 的公共部署,而 Forgejo 可以合理地被描述为 GitHub 的克隆。这对采用很有好处,因为 GitHub 用户很容易将他们的工作流程迁移到 Codeberg.
运行 Codeberg 和维护 Forgejo 的人们(其中有一些重叠)对 Zig 非常好。Zig 如今是 Codeberg 上托管的最大的项目之一,也是星标数最多的项目。由于我们的规模,我们以新的和“激动人心”的方式给 Codeberg 的基础设施施加了压力(例如收到波涛汹涌的垃圾信息)。
在这个过程中,维护人员帮助我们在 CI 需求上解除阻塞,并且还实现了一些让我们在 Codeberg 上的生活更轻松的体验优化功能。
另一方面,我们的贡献者最近在尝试向 Codeberg 上的 Zig 贡献代码时开始遇到问题。
这主要表现为无法对 Zig 进行分支(操作超时),这导致我们建议贡献者使用 AGit 工作流,因为它允许你在不需要先进行分支的情况下创建 PR。
我们认为这个问题与以下事实有关:在 Forgejo 中,分支在服务器端存储为原始仓库的一个完全独立的克隆,复制了每个 Git 文件(这些文件最初是硬链接的,但任何一侧的更改都会导致实际复制),而不是将分支实现为存储在单个仓库中的一组命名空间分支。
一方面,这种设计选择使得 Andrew 难以向 Linux 内核虚假提交伪造的 Rootkit,但另一方面,它使得 Forgejo 比使用 Git 命名空间存储同一仓库的“分支”的 Radicle 更消耗资源。
与此有些关联的是,Andrew 最近在他的“分布式系统(Systems Distributed)”演讲中(录音尚未发布)指出,在设计协作方式时,Codeberg 不应该需要模仿 GitHub。
GitHub 出于商业原因以集中方式托管 Issues 和 PR:供应商锁定(Vendor lock-in)。但 Codeberg 没有这套相同的激励机制,因此它完全有自由将平台设计得更高效、更偏向本地优先(Local-first)。Codeberg 上的某些讨论也暗示了一个事实:至少有一些 Forgejo 贡献者同意摆脱某些 GitHub 主义会很好。
最后,让我们从保持源代码可靠可用和平衡成本公式的角度来看看 Codeberg。
在可用性方面,Codeberg 并不完美。该平台曾遭受 DDoS 攻击,有过计划外的停机,并且作为其维护流程的一部分,它也有定期计划内的停机。这都在预期参数之内。Codeberg 是一个集中式平台,无论如何,预期的停机是不可避免的。
使集中式平台保持高可用是困难且昂贵的,Codeberg 旨在可靠性曲线上达到一个成本效益更高的点(在收益递减的平稳期之前),这是合理的。
从成本公式的角度来看,Codeberg 是一个依赖捐赠的非营利组织。它免费提供服务,但这种安排当然有其局限性。
一个例子是最近禁止 LLM(大语言模型)项目的政策变更,其理由的一部分是其沉重的成本往往与实际使用不成比例:“我们都在为饥饿的 LLM 买单”。
同样的政策变更还包括禁止与加密货币相关的项目。在第二个例子中,提出的动机似乎更多是出于伦理考虑,而不是资源消耗,尽管这两者是密不可分的。
有些人将这一决定描述为不公平或短视的,但它实际上是成本公式如何保持平衡的非常自然的后果:Codeberg 从捐赠中获得资金,而这些捐赠者也(粗略地说)是非营利组织的投票成员,他们根据自己的利益和信仰分配资金是再自然不过的事情了。
这是 Radicle 模型表现出色的另一个地方:每个节点根据其自身的策略决定要播种哪些仓库。因此,如果你是一个不喜欢加密货币的人,你可以选择永远不播种任何这些仓库。以中心为 LLM 的项目也是如此。
总而言之,我很高兴 Codeberg 存在。我希望它能追求更多创新的本地优先设计,但除此之外,我很高兴它向世界展示了你无需依赖大科技公司就能构建一个能够为数百万人服务的平台。
从我的角度来看,禁止 Codeberg 上的加密货币项目是其运作方式的不幸后果,我很高兴 Radicle 给每个人公平发布自己项目的机会,即使我自己不打算播种任何加密货币项目。
Tangled
当谈论具有去中心化设计的新代码托管平台时,Tangled 经常与 Radicle 一起被提及。
Tangled 构建于 atproto 之上,这与驱动 Bluesky 的系统是同一个。在 atproto 中,所有用户都有一个 DID(类似于 Radicle)和一个用于存储其生成的构件的个人数据服务器(PDS)。
使用 Bluesky 的 atproto 用户会将其在 Bluesky 上的操作(发帖、点赞、转推)对应的条目(本质上是 JSON 文件)存储在他们的 PDS 中。如果同一用户还使用 Tangled 等其他服务,那么他们也会在其 PDS 中存储与其在那里的活动相对应的条目(例如 Issue 评论)。
像 Bluesky 和 Tangled 这样的应用程序随后将收集这些条目并以聚合的方式展示它们,为你提供集中式系统的用户体验,同时将每个单独的 PDS 作为事实来源(Source of truth)。
作为构建分布式应用程序的通用系统,atproto 非常有趣,但在我看来,在保持源代码高度可用这一具体任务上,Tangled 相比 Radicle 还是有所欠缺(老实说,这很可能甚至不是 atproto 的目标)。
一个问题是 Tangled 似乎不专注于本地优先的工作流,这意味着它没有下载 Issues 以便在离线时查看和管理它们的方法。鉴于 atproto 的工作原理,这绝对是可以实现的,尽管它不如 Radicle 的解决方案优雅——在 Radicle 中,Issues 和 PR 直接存储在仓库本身中,这意味着拉取一次就足以同时获取所有内容。
Radicle 更适合的另一个更大原因是,Tangled 是联邦式(Federated)的,而 Radicle 是 P2P 的。这两种方法之间的区别有时很难看出,但有一个方面让它变得非常明显:看 Issues。
在 Radicle 上,对于一个仓库的“Issues”长什么样,没有权威的统一视图。这一切都取决于你的播种策略。如果你决定播种所有“分支”,你将看到网络上存在的所有 Issues,但你也可以选择更严格的策略,只播种你关注的少数作者的“分支”。在第二种情况下,你不必担心垃圾信息,因为即使某个地方的恶意用户正在创建垃圾 Issues,它们也永远不会到达你这里(如果你选择“播种全部”策略,你仍然可以封禁他们)。
最重要的是,这是每个节点自己做出的选择。每个 Radicle 节点用户都可以自己决定他们想看什么。
在 atproto 上,部署在其上运行的任何应用程序的独立 AppView(Web 界面)应该总是可行的,但托管在其上的许多应用程序的吸引力之一在于,用户不会被迫面对网络去中心化的现实。
例如,人们会去 bsky.com 查看 skeet(天呐,讨厌这个名字),我个人甚至从未见过该界面的替代部署的链接。就 Tangled 而言,我的理解是,即使你部署了自己的实例,大多数人仍然会通过 tangled.org 发现并与 Zig 仓库交互,这意味着我们必须像在 GitHub 和 Codeberg 上一样完全相同地策划我们的 Issues 页面。
最后一个主要问题与分支有关。Tangled 中的分支与 GitHub 上的分支相同:一个无法自动用作镜像的独立副本(既因为没有自动发现,也因为没有强制执行可用于区分官方提交与对分支所做更改的密码签名)。
话虽如此,我相信 Tangled 理论上可以实现这样一个系统,但据我所知,正如似乎没有人对本地优先工作流感兴趣一样,似乎也没有人对利用冗余来实现更高源代码可用性感兴趣。
考虑到这一点,让我们来看看成本公式:谁来支付保持 Tangled 运行的费用?
答案目前是:VC(风险投资)和其他一群投资者。
这本身不是问题,但我们本质上回到了原点(平方的 GitHub)。Tangled 必须提供免费服务才能与其他平台竞争,最终它必须定义自己的商业模式才能维持下去。
作为代码托管平台,Tangled 有一条合理的变现途径(私有仓库、CI、企业功能),但这并不能改变它最终必须面对标题中的那个问题的事实,这不可避免地会导致某种形式的恩施化。
支持 Tangled 的一点是,用户可以自托管自己的 Knot(一个讲 atproto 的 Git 服务器,本质上就是用于代码的、特定于 Tangled 的 PDS),这可以将一些成本转移回给用户,但据我所知,在 atproto 生态系统中,人们在想要更好地控制个人数据时才会自托管 PDS(和 Knot),而不是将其作为分摊成本的一种方式。一个说明问题的例子是,当你同时注册 Bluesky 和 Tangled 时,系统会向你提供免费使用它们的 PDS(和 Knot)的机会,这也是绝大多数最终用户的做法。
此外,Tangled 利用了部分 atproto 基础设施来保持运行。我前面提到过 PDS 和 AppView,但在它们之间还有中继器(Relays),它们本质上是 PDS 数据聚合器,被 AppView 用于更高效地从数量可能很庞大的单个 PDS 中获取更新的数据。
所有这一切都是有成本的,而消耗资源的人并不是付钱的人。
总的来说,我认为人们应该对“X 但去中心化”保持怀疑。去中心化注定是问题和低效的根源,只有通过良好、创新的设计,它才能回报足够的价值来弥补额外的复杂性。
就 Tangled 而言,我的印象是,整个去中心化的设置并没有提供足够的价值来证明所有额外的机器开销是合理的,尽管我总的来说觉得 atproto 很有趣。
坦率地说,对我来说 Tangled 看起来就像是一个加了更多步骤的 GitHub,这使得它不如 Codeberg 有趣——Codeberg 在经济学方面进行了创新,而且由于它是一个集中式服务,运行成本也更低。相比之下,Tangled 需要大量的机械装置来为你提供来自分布式系统的数据的聚合视图,而且由于已经在压垮 GitHub 的充满垃圾信息的重 LLM 项目,这带来了不可忽视的额外成本。
我怀疑 Tangled 最终会不会禁止重 LLM 项目,那么谁来为所有这些买单呢?
关于集中式包注册中心
我们这篇文章快要结束了,但首先我想花点时间谈谈包索引/注册中心(Package indexes/registries)。
包注册中心存在的一个主要原因是保证包的可靠可用性。大多数包“索引”不仅进行索引,还托管和分发包,这是有成本的。机器和带宽无疑是一个不小的成本,但另一个主要成本是拥有能够迅速扑灭火灾以保证良好运行时间的运维人员。
那么,谁来支付保持包注册中心运行的费用?
答案因平台而异,但至少在很大程度上是大科技公司,要么直接资助,要么通过赞助运行包注册中心的组织来资助。当大科技公司支付的费用不足以维持运行包注册中心的全部成本时,个别开发者有时会自愿奉献时间。你可能还记得 Rust Foundation 不久前通过付费给 Ferrous Systems 担任 Crates.io 的夜间监控,把某些人从必须全天候待命的志愿者的苦海中拯救出来(💀)。
使集中式系统保持高可用并不便宜,我们在 Zig 软件基金会(ZSF)对此有着非常深刻的理解,这就是为什么我们明确避免设计具有此类要求的系统。例如,https://ziglang.org(官方网站)没有保证运行时间,也没有 ZSF 员工或承包商为其全天候待命。
这对预构建的 Zig 二进制文件的可用性意味着什么?很高兴你问了!
在 Zig 网站从 AWS 迁移到自托管之后,如果网站宕机而你的 CI 尝试获取 Zig,构建任务就会失败。这是一种具有成本效益的方法,但不是最好的最终用户体验。一段时间后,多亏了 Matthew Lugg 所做的工作,我们宣布了社区镜像,其中我们定义了如何实现和公告预编译 Zig 编译器 tarball 的镜像托管。
今天,所有可用于获取 Zig 编译器副本的主要工具都将通过利用我们的社区镜像来做到这一点,从大多数人用来在 CI 中获取 Zig 的 GitHub/Forgejo Action —— mlugg/setup-zig 开始。
这个系统的一个有趣细节是,镜像可以选择从其他镜像获取缺失的 tarball,而不仅仅是从官方 Zig website。这让我想起了什么 :^)
你将很难找到一个使用 mlugg/setup-zig 却因为在获取 Zig 时遇到瞬时网络错误而失败的 CI 任务。我们拥有极高的可用性,但不需要任何人为此支付溢价。
好了,回到包注册中心。鉴于我们在本文中学到的知识,以及 Zig 社区镜像的有效性所体现的,我认为得出以下结论是合理的:与分布式镜像的冗余能够以极低的成本提供极高的可用性相比,集中式包注册中心是对资源的低效利用。
对我的推理路线的一个可能反驳是,一些包注册中心并不真正提供源代码,而是提供预构建的二进制文件,例如 PyPI。我已经在“Python 包索引应该去掉辅助轮(The Python Package Index Should Get Rid Of Its Training Wheels)”中触及了这个主题,简而言之,我坚信让索引将二进制文件视为事实来源是一个错误,相反,应该为其包要求一个可复现的构建过程(Reproducible build process),并将二进制构件视为可以尽力缓存(即可以随意丢弃和重新创建)的输出,以加速构建。
另一个有趣的案例研究是 Google 的 Go 模块代理(Go Module Proxy)。Go 编程语言使用完整的 URL 来定义导入,并且在很长一段时间里,它只会从提供的 URL 获取每个包。2019 年,Google 推出了 Go 模块代理,作为一种保证 Go 包高度可用(不会因为主机宕机而导致 CI 运行失败)且不可变(例如保证随时间推移未被篡改)的方法。
Google Go 模块代理本质上是一个带有独特权衡集合的集中式包注册中心。它不是让包作者直接将他们的包上传到其中,而是用作所有 Go 包管理器客户端流量的代理,并缓存以这种方式发现的所有包。
这种方法带来了一些额外的要求,例如 Google 必须能够证明他们没有篡改数据,或者代理有时必须保留原作者删除的包的副本(以免破坏构建),而有时又必须不长期存储某个包(出于许可原因)。有关详细信息,请参阅 Go Module Proxy 常见问题解答。
这种设计的另一个问题是让代理发现何时发布了包的新版本,这涉及一堆轮询,有时会对托管多个仓库的平台造成灾难性的后果。
运行 proxy.golang.org 要花多少钱?我不知道,但我怀疑它不便宜。
Go 开发者很幸运,他们可以依靠 Google 来保持其开源基础设施的运行。在历史上,其他生态系统不得不更多地担心这个话题。请参阅“我在 Python 软件基金会董事会任职期间学到的东西(Things I’ve learned serving on the board of the Python Software Foundation)”中的这段引文(重点是我加的):
“PyPI 的数字是惊人的。今天有 570,000 个项目,由 12,035,133 个文件组成,每天提供 19 亿次下载(这个数字来自 PyPI Stats)。这些下载的带宽是由 Fastly 捐赠的,Fastly 是 PSF 的远见赞助商,最近签署了一项为期五年的协议以继续这项服务。”
(这件大事在当时备受关注——在该协议之前,人们曾担心如果 Fastly 决定结束这种赞助会发生什么。)
最近,PSF、Rust 基金会和其他一些开源组织签署了一份关于开源包索引的可持续管理的联合声明,本质上是要求公司帮助支付保持这些服务运行的费用。
不过,我对这一点有争议的看法:我认为我们应该停止期望大科技公司为我们的基础设施买单,相反,我们应该对其进行设计,使其运行成本更低,同时保持高可用性,届时应该要求公司为其消耗的内容付费,而不需要支付太多超出此范围的费用。
在我看来,集中式包索引作为一个概念并没有随着时间推移而变得更好,大多数生态系统如果能积极主动地将其基础设施现代化,将会受益匪浅。我怀疑任何参与开源组织的人是否真的喜欢乞求大科技公司施舍更多的免费额度。
就 Zig 而言,甚至在了解 Radicle 之前,Andrew 就已经构想了一个去中心化的解决方案,用于分摊保持源代码可用性的成本。
结论
我对 Radicle 感到兴奋,但在我对它充满信心之前,我还有很多东西要学。
虽然在文章前面我把它表述为一个假设,但我实际上已经终止了我的 GitHub 订阅,并在 radicle.garden 上获得了托管的 Radicle 节点,我在那里播种了 Zine 和其他一些项目。
我的下一个目标是将 Awebo(alpha 阶段的可自托管 Discord 替代品)的开发暂时转移到 Radicle 上作为一个实验,以便获得更多第一手经验。
此时此刻,我无法确定 Radicle 是否会适合我,或者它总体上是否会成功,但我有一件事可以肯定:在我提到的或我所知道的所有其他替代方案中,没有一个能在追求高可用性时,其成本效益甚至能 remotely(哪怕稍微)接近 Radicle。
我确信 Radicle 中有很多大大小小的事情要么缺失,要么可以改进,但当谈到宏大的想法时,我简直无法想象还有什么比使用 P2P 网络来实现我们都依赖的源代码的“长期记忆”更好的安排了。
最后,我知道至少有三个不同的代码托管平台项目是用 Zig 编写的。
Radicle 是该领域的竞争者,但我确实相信,也存在一些解决方案的空间,它们带来了一些本地优先的优势,而不需要选择加入 P2P 网络的额外复杂性。我希望我的文章能够帮助传播对问题空间的认识,并激发每个人思考不同本地优先解决方案之间的互操作性。
但最重要的是,我希望我已经清楚地表明了牢记你所设计的任何事物的成本公式是多么重要。
学习低级编程语言最好的事情之一就是获得对手动内存管理的访问权限,这可以引导你设计出比相信无限内存错觉时所能设计出的质量更高的软件。
意识到系统中涉及的成本,以及不同的各方最终将如何承担这些成本,是另一项约束,它将为设计出质量呈几何级数提升的新架构打开大门。
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:
- 供稿,分享自己使用 Zig 的心得
- 改进 ZigCC 组织下的开源项目
- 加入微信群、QQ 群、QQ 频道、Telegram 群组、Google Groups 与更多 Zig 爱好者交流
我有一个项目(Zine,一个静态网站生成器),它有11个依赖项,托管在GitHub、Codeberg和自托管的Forgejo实例上。每当GitHub或Codeberg宕机时,我的项目的新构建就会失败。
虽然自托管的Forgejo实例的正常运行时间(Uptime)明显优于GitHub,但它们最终永久宕机并彻底破坏我的项目的风险更高。
解决这个问题最直接、最实用的方法是对所有内容进行分支(Fork)或打包依赖(Vendor)。
分支意味着将所有依赖项移动到与Zine相同的托管平台上。如果你能克隆主项目,你也能够克隆它的所有依赖项。
打包依赖意味着更进一步:将所有依赖项的源代码提交到你自己的仓库中,这样克隆项目这个动作实际上就会获取到所有东西。
如果你非常关心实用性和极致的韧性(Resilience),那么你可能会更喜欢打包依赖而不是分支。
分支要求你对整个依赖树进行分支(即包括你的间接依赖),并且还要修改任何非叶子依赖项,使其指向你自己的分支。相比之下,对于Zig工具链的用户来说,打包依赖极其简单。
对于不太了解的人来说,Zig 0.17.0-dev最近改变了依赖缓存的工作方式:全局缓存现在将包存储为压缩包,每个项目都有一个本地的
zig-pkg/目录,里面包含它所使用的依赖项的解压文件。如果这是你的目标,这会让打包依赖变得微不足道:只需将你的zig-pkg/目录纳入版本控制即可。(否则,建议将其添加到.gitignore中。)如果你计划对依赖项进行修改,并且关心如何更容易地将它们合并回上游(Upstream),那么分支可能比打包依赖更可取。即使你不打算发送拉取请求(PR),在一个分支中挑选(cherry-pick)提交也比从外部项目的打包依赖目录中这样做要容易得多。
分支/打包依赖确实有用,这一点毫无疑问。但这真的是我们能做到的最好方案吗?
拥有集中式包索引(如
npmjs.com或crates.io)的语言不必担心这些问题,因为它们本质上将分支策略作为内置功能(所有源代码都被“分支”到由集中式包索引托管的副本中),但这这种方法确实让我很不满意,因为它不必要地将解决方案与包管理绑定在一起。我们的目标应该是不论何时都能可靠地获取源代码,而不仅仅是从包管理器获取时。由于GitHub的不稳定性,这些话题最近变得更加切合实际,但现实中,问题并不是“我们如何让源代码托管变得可靠?”。如果我们真的想的话,我们确实知道如何让代码托管变得可靠。问题其实是:它应该花费多少成本?谁来支付这笔费用?
正如我们行业中经常出现的情况一样,我们从来都不愿面对这个问题,我们一直乐于吸附大科技公司的血。但和往常一样,这个问题最终必须要给出答案,而当这一刻到来时,我们很快就会把矛头指向公司,带着正义的愤慨大喊“恩施化”(Enshittification)!
是的,GitHub毫无疑问正在走向恩施化,就像Discord以及之前的许多平台一样;但我们期望继续免费消耗资源多久?GitHub的现状只是向我们展示了一个从一开始就破碎的系统的裂痕。
我不认为大公司没有责任,我确信在许多未写明的运营手册中,混淆成本公式都有其专门的章节,但应该更清楚这一点的不是公司。是我们。是我们这种刻意的无知造成了一种扭曲,迫使公司要么接受它,要么利用它来保持竞争力。
那么,有没有一种方法可以可靠地提供源代码,同时又能平衡成本公式呢?
我认为,自托管一个代码托管平台(Forge)并不足以平衡这个方程,因为它把使源代码可用的全部成本推给了该软件的创作者,但我们已经确定,此类代码的消费者同样对它能够可靠获取感兴趣,这就是导致分支和打包依赖的原因。
从某种意义上说,分支和打包依赖是分摊保持源代码可用成本的一种方法,但它们都是低效的,因为被分支/打包依赖的代码无法被轻易发现,也无法自动作为镜像使用。
如果有人在你的项目之外对你的依赖项感兴趣,他们的包管理器将无法自动从你的分支/打包依赖副本中获取它。当然,你可能对承担这个成本不感兴趣,但就目前的情况来看,这其实也不是什么选择。
如果我们能做得更好,那就太酷了,因为这可以帮助我们降低为每个人保持源代码可靠可用的整体成本。
Radicle 网络
Radicle 是一个点对点(p2p)网络,其中的节点(Peers)会播种(Seed)他们感兴趣的代码库(Repositories)。
某些节点可能对提高网络的韧性感兴趣,因此会想要重新播种所有内容(或接近所有内容),而另一些节点可能只对确保一组特定项目保证可用感兴趣,从而将他们的播种策略严格限制为仅允许相关代码库。
Radicle 以非常有趣的方式支持问题追踪(Issues)和补丁(Patches,即 GitHub 术语中的 Pull Requests):这里没有集中式(或联盟式)的 Web 服务器来托管这些内容,因为它们作为 Git 对象直接存储在 Git 仓库本身中(启用了 CRDT),当你向网络广播你的更改时(具体来说就是运行
git push),它们可以被重新同步。为了帮助你更好地理解此功能的工作原理,让我们以一个具体的例子来关注 Issues。
假设我有兴趣将 Radicle 用于 Zine。一旦我将我的仓库发布到 Radicle 网络,我就会想要设置我自己的全天候(always-on)节点,以保证 Zine 始终可用,并促进开发。其他一些节点也可能会决定播种 Zine,但由于 Zine 仍然是一个小项目,我目前不打算指望这一点。
当 Alice 想要提出一个 Issue 时,她会通过在她自己播种的 Zine 副本中打开它来做到这一点(为简单起见,这里再次使用 GitHub 术语称之为她的“分支”)。此时,她会将她的更改发布到 Radicle 网络,任何对这些更改感兴趣的节点都会获取它们。
我的全天候节点被配置为播种所有 Zine 的副本,因此它保证会获取 Alice 的更改,即使 Alice 的个人节点离线,也能使它们保持可用。
此时,当我从网络拉取更改时,我会看到 Alice 的新 Issue,并且我能够使用 Radicle 的 CLI 工具或桌面应用程序对其实施评论、打标签、指派或关闭,这同样适用于 Bob 创建的假设性 Patch。
有兴趣跟进的旁观者将能够通过 Web 界面查看所有源代码、Patches 和 Issues,就像
heartwood仓库上的这个示例一样。不过 Web UI 是只读的,因为所有更改都必须通过 Radicle 网络提交,鉴于 AI 机器人和爬虫的崛起,这是一个非常方便的权衡。要了解 Radicle 在实践中是如何工作的,还有一些其他的事情需要学习,所以我邀请大家查看官方文档。
使用 Radicle 实现可靠的源代码可用性
在 Radicle 上发布 Zine 时,我也会有兴趣从我的全天候节点播种我的依赖项,以便保证 Zine 的用户也能可靠地获取成功构建 Zine 所需的依赖项。
我们几乎回到了分支/打包依赖的状况,但有一个关键区别:播种我的依赖项可以使它们被网络发现!
当角色互换时也是如此:当有人依赖 Zine 时,他们会有兴趣播种它,从而使 Zine 的源代码更加可靠地可用,这对我和他们都有好处。此外,根据他们如何微调其播种策略,Zine 的用户不仅可以选择播种我的权威副本,还可以选择播种 Zine 的(全部或部分)“分支”。通过播种 Zine 的“分支”,他们还可以播种 Issues 和 Patches,从而帮助 Zine 上的协作更具韧性。
我发现这种模式有助于以高度契合每个人利益的方式保持成本公式的平衡,同时在成本效益上也明显高于“自托管 forge + 分支/打包依赖”的设置——后者基本上浪费了大量的冗余。
当我深入了解 Radicle 时,正是在这个时候,我认为如果 Zig 包管理器直接支持 Radicle 会很酷,但事实证明这并不像我最初想象的那么简单……不过,其中涉及的摩擦力也出人意料地与相关各方的利益高度一致!
依赖 Radicle 仓库
在 Radicle 中,每个节点都有一个加密身份(DID),用于在网络中标识节点并宣称对代码库的所有权。
当你将代码库发布到 Radicle 时,
radCLI 工具会向代码库添加一个身份文档,将代码库与所有者的 DID 绑定,该文档的哈希值就成为代码库 ID(RID)。网络也将使用此信息来区分代码库的权威版本与其他人的“分支”,这就是 Radicle 工具如何保证当你从网络克隆代码库时,能获得一个你可以信任的副本。
当你想要从 Radicle 网络克隆代码库时,你会运行类似这样的命令:
上面示例中传递给
clone的参数是一个包含代码库 ID 的 rad URI,它明确不是一个 URL,因为它没有表达应该从哪里获取代码库;这正是关键所在。Radicle CLI 工具将与 Radicle network 交互,从碰巧拥有该数据的任何连接对端(Peer)获取此代码库。如果我们考虑
build.zig.zon文件的两个假设变体:你可以看到关键的区别在于:第一个指定了位置,而第二个没有。
在第一种情况下,依赖项是脆弱的:每当 GitHub 宕机时,你就无法获取它;而在第二种情况下,只要至少有一个活跃的 Radicle 节点播种了 Zine 仓库,你就能获取它。
这将是一个巨大的升级,但有一个问题:为了能够与 Radicle 网络对话,你必须是网络中的一个对端(Peer),这意味着如果你不是 Radicle 用户,你将无法成功运行
zig build(具体来说,你需要运行一个 Zig 可以与之对话的本地节点)。这是个大问题!
Zig 旨在成为“零依赖”软件,我们希望尽可能多的 Zig 项目只需安装了 Zig 并运行
zig build就能构建。情况并非总是如此,因为有些项目会有系统依赖,但除此之外,它不应该对任何非必需品产生硬依赖。此时我开始琢磨,有没有办法在不成为对端的情况下查询网络?直到我意识到发现并不是问题,而且我没有在考虑成本公式!
Radicle 网络中的一个节点会有兴趣承担作为网络“长期记忆”一部分的成本,这确实意味着与其他 Radicle 节点进行数据的输入/输出传输,但这并不意味着该节点愿意承担未经身份验证的客户端以无限制的速率请求获取该节点播种的一个或多个仓库副本的成本。
如前所述,Radicle 节点具有身份,网络中的其他节点可以决定禁止表现不良的节点,并总体上跟踪它们的行为。另一方面,
zig build客户端本质上是未经身份验证的(就此而言,git 客户端也是如此)。在你的日常生活中,这两者可能没有太大区别,但试想一下,某人配置错误的 CI 无缘无故地不断猛烈轰炸你的节点。如果考虑我自己的用例,我会愿意承担这部分外部流量,以便为非 Radicle 用户提供访问 Zine 的权限,但我可以理解其他节点(可能播种了 Zine)不愿意这样做。
幸运的是,这个概念在 Radicle 中也得到了很好的建模。
我之前在谈到 Issues 和 Patches 时提到,旁观者可以使用 Web UI 来跟进开发,但这有点过于简化了。默认情况下,Radicle 节点不运行任何 HTTP 服务。这是一个完全可选的功能,你可以根据自己的兴趣决定是否启用。
如果你确实启用了 HTTP 服务,那么 HTTP 客户端将能够访问你的节点,并使用 Web UI 或 Git over HTTP 来克隆该节点播种的代码库,而不需要 Radicle 身份。
就我而言,我希望在我的全天候节点上启用 HTTP 访问,并允许未经身份验证的 HTTP 客户端根据需要下载 Zine(及其依赖项)。任何其他决定播种 Zine(同时还启用 HTTP 接口)的节点也将有效地提供相同的服务。
这意味着,虽然指向启用 HTTP 的 Radicle 节点的 URL 不如 RID 那样具有韧性,但它们仍然等于或优于指向自托管代码托管平台的链接,特别是因为这样的节点有很多!
例如,
heartwood可以通过以下任何一个主机的 HTTP 获取(写本文时它有超过250个播种节点):https://radicle.network/nodes/seed.radicle.dev/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5https://radicle.network/nodes/index.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5https://app.radicle.at/nodes/seed.radicle.at/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5https://radicle.defelo.de/nodes/radicle.defelo.de/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5https://radicle.network/nodes/rad.hardenedbsd.org/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5https://radicle.jarg.io/nodes/radicle.jarg.io/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5请注意,Web 界面有时是相同的,但每个链接引用的是一个不同的主机,该主机将用于“从 git 克隆”按钮。
为了让 Zig 包管理器能够利用 Radicle,我们实际需要的全部就是简单地实现 #14291(支持镜像)。
正如我所暗示的,我仍在学习 Radicle 的工作原理,所以我不知道这是否已经在大力推进、计划中还是不在范围内,但根据我目前的理解,我认为如果
radCLI 工具有一个将 RID 解析为一组 HTTP URL 的命令会很好,这样你就可以直接将它们粘贴到启用了镜像的build.zig.zon文件中。未来,探索在
build.zig.zon中指定 RID 的方法可能是值得的,这样它就可以被可选地用于发现新的公共 HTTP 主机,或者(同样是可选地)在本地节点可用时连接到 Radicle 网络(即用户是 Radicle 网络的一部分)。Codeberg 和 Tangled 怎么样?
当人们谈论离开 GitHub 时,这两个平台经常被提及。让我们看看它们与 Radicle 相比如何。
Codeberg
Codeberg 是一个德国非营利组织,运行 Forgejo 的公共部署,而 Forgejo 可以合理地被描述为 GitHub 的克隆。这对采用很有好处,因为 GitHub 用户很容易将他们的工作流程迁移到 Codeberg.
运行 Codeberg 和维护 Forgejo 的人们(其中有一些重叠)对 Zig 非常好。Zig 如今是 Codeberg 上托管的最大的项目之一,也是星标数最多的项目。由于我们的规模,我们以新的和“激动人心”的方式给 Codeberg 的基础设施施加了压力(例如收到波涛汹涌的垃圾信息)。
在这个过程中,维护人员帮助我们在 CI 需求上解除阻塞,并且还实现了一些让我们在 Codeberg 上的生活更轻松的体验优化功能。
另一方面,我们的贡献者最近在尝试向 Codeberg 上的 Zig 贡献代码时开始遇到问题。
这主要表现为无法对 Zig 进行分支(操作超时),这导致我们建议贡献者使用 AGit 工作流,因为它允许你在不需要先进行分支的情况下创建 PR。
我们认为这个问题与以下事实有关:在 Forgejo 中,分支在服务器端存储为原始仓库的一个完全独立的克隆,复制了每个 Git 文件(这些文件最初是硬链接的,但任何一侧的更改都会导致实际复制),而不是将分支实现为存储在单个仓库中的一组命名空间分支。
一方面,这种设计选择使得 Andrew 难以向 Linux 内核虚假提交伪造的 Rootkit,但另一方面,它使得 Forgejo 比使用 Git 命名空间存储同一仓库的“分支”的 Radicle 更消耗资源。
与此有些关联的是,Andrew 最近在他的“分布式系统(Systems Distributed)”演讲中(录音尚未发布)指出,在设计协作方式时,Codeberg 不应该需要模仿 GitHub。
GitHub 出于商业原因以集中方式托管 Issues 和 PR:供应商锁定(Vendor lock-in)。但 Codeberg 没有这套相同的激励机制,因此它完全有自由将平台设计得更高效、更偏向本地优先(Local-first)。Codeberg 上的某些讨论也暗示了一个事实:至少有一些 Forgejo 贡献者同意摆脱某些 GitHub 主义会很好。
最后,让我们从保持源代码可靠可用和平衡成本公式的角度来看看 Codeberg。
在可用性方面,Codeberg 并不完美。该平台曾遭受 DDoS 攻击,有过计划外的停机,并且作为其维护流程的一部分,它也有定期计划内的停机。这都在预期参数之内。Codeberg 是一个集中式平台,无论如何,预期的停机是不可避免的。
使集中式平台保持高可用是困难且昂贵的,Codeberg 旨在可靠性曲线上达到一个成本效益更高的点(在收益递减的平稳期之前),这是合理的。
从成本公式的角度来看,Codeberg 是一个依赖捐赠的非营利组织。它免费提供服务,但这种安排当然有其局限性。
一个例子是最近禁止 LLM(大语言模型)项目的政策变更,其理由的一部分是其沉重的成本往往与实际使用不成比例:“我们都在为饥饿的 LLM 买单”。
同样的政策变更还包括禁止与加密货币相关的项目。在第二个例子中,提出的动机似乎更多是出于伦理考虑,而不是资源消耗,尽管这两者是密不可分的。
有些人将这一决定描述为不公平或短视的,但它实际上是成本公式如何保持平衡的非常自然的后果:Codeberg 从捐赠中获得资金,而这些捐赠者也(粗略地说)是非营利组织的投票成员,他们根据自己的利益和信仰分配资金是再自然不过的事情了。
这是 Radicle 模型表现出色的另一个地方:每个节点根据其自身的策略决定要播种哪些仓库。因此,如果你是一个不喜欢加密货币的人,你可以选择永远不播种任何这些仓库。以中心为 LLM 的项目也是如此。
总而言之,我很高兴 Codeberg 存在。我希望它能追求更多创新的本地优先设计,但除此之外,我很高兴它向世界展示了你无需依赖大科技公司就能构建一个能够为数百万人服务的平台。
从我的角度来看,禁止 Codeberg 上的加密货币项目是其运作方式的不幸后果,我很高兴 Radicle 给每个人公平发布自己项目的机会,即使我自己不打算播种任何加密货币项目。
Tangled
当谈论具有去中心化设计的新代码托管平台时,Tangled 经常与 Radicle 一起被提及。
Tangled 构建于 atproto 之上,这与驱动 Bluesky 的系统是同一个。在 atproto 中,所有用户都有一个 DID(类似于 Radicle)和一个用于存储其生成的构件的个人数据服务器(PDS)。
使用 Bluesky 的 atproto 用户会将其在 Bluesky 上的操作(发帖、点赞、转推)对应的条目(本质上是 JSON 文件)存储在他们的 PDS 中。如果同一用户还使用 Tangled 等其他服务,那么他们也会在其 PDS 中存储与其在那里的活动相对应的条目(例如 Issue 评论)。
像 Bluesky 和 Tangled 这样的应用程序随后将收集这些条目并以聚合的方式展示它们,为你提供集中式系统的用户体验,同时将每个单独的 PDS 作为事实来源(Source of truth)。
作为构建分布式应用程序的通用系统,atproto 非常有趣,但在我看来,在保持源代码高度可用这一具体任务上,Tangled 相比 Radicle 还是有所欠缺(老实说,这很可能甚至不是 atproto 的目标)。
一个问题是 Tangled 似乎不专注于本地优先的工作流,这意味着它没有下载 Issues 以便在离线时查看和管理它们的方法。鉴于 atproto 的工作原理,这绝对是可以实现的,尽管它不如 Radicle 的解决方案优雅——在 Radicle 中,Issues 和 PR 直接存储在仓库本身中,这意味着拉取一次就足以同时获取所有内容。
Radicle 更适合的另一个更大原因是,Tangled 是联邦式(Federated)的,而 Radicle 是 P2P 的。这两种方法之间的区别有时很难看出,但有一个方面让它变得非常明显:看 Issues。
在 Radicle 上,对于一个仓库的“Issues”长什么样,没有权威的统一视图。这一切都取决于你的播种策略。如果你决定播种所有“分支”,你将看到网络上存在的所有 Issues,但你也可以选择更严格的策略,只播种你关注的少数作者的“分支”。在第二种情况下,你不必担心垃圾信息,因为即使某个地方的恶意用户正在创建垃圾 Issues,它们也永远不会到达你这里(如果你选择“播种全部”策略,你仍然可以封禁他们)。
最重要的是,这是每个节点自己做出的选择。每个 Radicle 节点用户都可以自己决定他们想看什么。
在 atproto 上,部署在其上运行的任何应用程序的独立 AppView(Web 界面)应该总是可行的,但托管在其上的许多应用程序的吸引力之一在于,用户不会被迫面对网络去中心化的现实。
例如,人们会去
bsky.com查看 skeet(天呐,讨厌这个名字),我个人甚至从未见过该界面的替代部署的链接。就 Tangled 而言,我的理解是,即使你部署了自己的实例,大多数人仍然会通过tangled.org发现并与 Zig 仓库交互,这意味着我们必须像在 GitHub 和 Codeberg 上一样完全相同地策划我们的 Issues 页面。最后一个主要问题与分支有关。Tangled 中的分支与 GitHub 上的分支相同:一个无法自动用作镜像的独立副本(既因为没有自动发现,也因为没有强制执行可用于区分官方提交与对分支所做更改的密码签名)。
话虽如此,我相信 Tangled 理论上可以实现这样一个系统,但据我所知,正如似乎没有人对本地优先工作流感兴趣一样,似乎也没有人对利用冗余来实现更高源代码可用性感兴趣。
考虑到这一点,让我们来看看成本公式:谁来支付保持 Tangled 运行的费用?
答案目前是:VC(风险投资)和其他一群投资者。
这本身不是问题,但我们本质上回到了原点(平方的 GitHub)。Tangled 必须提供免费服务才能与其他平台竞争,最终它必须定义自己的商业模式才能维持下去。
作为代码托管平台,Tangled 有一条合理的变现途径(私有仓库、CI、企业功能),但这并不能改变它最终必须面对标题中的那个问题的事实,这不可避免地会导致某种形式的恩施化。
支持 Tangled 的一点是,用户可以自托管自己的 Knot(一个讲 atproto 的 Git 服务器,本质上就是用于代码的、特定于 Tangled 的 PDS),这可以将一些成本转移回给用户,但据我所知,在 atproto 生态系统中,人们在想要更好地控制个人数据时才会自托管 PDS(和 Knot),而不是将其作为分摊成本的一种方式。一个说明问题的例子是,当你同时注册 Bluesky 和 Tangled 时,系统会向你提供免费使用它们的 PDS(和 Knot)的机会,这也是绝大多数最终用户的做法。
此外,Tangled 利用了部分 atproto 基础设施来保持运行。我前面提到过 PDS 和 AppView,但在它们之间还有中继器(Relays),它们本质上是 PDS 数据聚合器,被 AppView 用于更高效地从数量可能很庞大的单个 PDS 中获取更新的数据。
所有这一切都是有成本的,而消耗资源的人并不是付钱的人。
总的来说,我认为人们应该对“X 但去中心化”保持怀疑。去中心化注定是问题和低效的根源,只有通过良好、创新的设计,它才能回报足够的价值来弥补额外的复杂性。
就 Tangled 而言,我的印象是,整个去中心化的设置并没有提供足够的价值来证明所有额外的机器开销是合理的,尽管我总的来说觉得 atproto 很有趣。
坦率地说,对我来说 Tangled 看起来就像是一个加了更多步骤的 GitHub,这使得它不如 Codeberg 有趣——Codeberg 在经济学方面进行了创新,而且由于它是一个集中式服务,运行成本也更低。相比之下,Tangled 需要大量的机械装置来为你提供来自分布式系统的数据的聚合视图,而且由于已经在压垮 GitHub 的充满垃圾信息的重 LLM 项目,这带来了不可忽视的额外成本。
我怀疑 Tangled 最终会不会禁止重 LLM 项目,那么谁来为所有这些买单呢?
关于集中式包注册中心
我们这篇文章快要结束了,但首先我想花点时间谈谈包索引/注册中心(Package indexes/registries)。
包注册中心存在的一个主要原因是保证包的可靠可用性。大多数包“索引”不仅进行索引,还托管和分发包,这是有成本的。机器和带宽无疑是一个不小的成本,但另一个主要成本是拥有能够迅速扑灭火灾以保证良好运行时间的运维人员。
那么,谁来支付保持包注册中心运行的费用?
答案因平台而异,但至少在很大程度上是大科技公司,要么直接资助,要么通过赞助运行包注册中心的组织来资助。当大科技公司支付的费用不足以维持运行包注册中心的全部成本时,个别开发者有时会自愿奉献时间。你可能还记得 Rust Foundation 不久前通过付费给 Ferrous Systems 担任 Crates.io 的夜间监控,把某些人从必须全天候待命的志愿者的苦海中拯救出来(💀)。
使集中式系统保持高可用并不便宜,我们在 Zig 软件基金会(ZSF)对此有着非常深刻的理解,这就是为什么我们明确避免设计具有此类要求的系统。例如,
https://ziglang.org(官方网站)没有保证运行时间,也没有 ZSF 员工或承包商为其全天候待命。这对预构建的 Zig 二进制文件的可用性意味着什么?很高兴你问了!
在 Zig 网站从 AWS 迁移到自托管之后,如果网站宕机而你的 CI 尝试获取 Zig,构建任务就会失败。这是一种具有成本效益的方法,但不是最好的最终用户体验。一段时间后,多亏了 Matthew Lugg 所做的工作,我们宣布了社区镜像,其中我们定义了如何实现和公告预编译 Zig 编译器 tarball 的镜像托管。
今天,所有可用于获取 Zig 编译器副本的主要工具都将通过利用我们的社区镜像来做到这一点,从大多数人用来在 CI 中获取 Zig 的 GitHub/Forgejo Action ——
mlugg/setup-zig开始。这个系统的一个有趣细节是,镜像可以选择从其他镜像获取缺失的 tarball,而不仅仅是从官方 Zig website。这让我想起了什么 :^)
你将很难找到一个使用
mlugg/setup-zig却因为在获取 Zig 时遇到瞬时网络错误而失败的 CI 任务。我们拥有极高的可用性,但不需要任何人为此支付溢价。好了,回到包注册中心。鉴于我们在本文中学到的知识,以及 Zig 社区镜像的有效性所体现的,我认为得出以下结论是合理的:与分布式镜像的冗余能够以极低的成本提供极高的可用性相比,集中式包注册中心是对资源的低效利用。
对我的推理路线的一个可能反驳是,一些包注册中心并不真正提供源代码,而是提供预构建的二进制文件,例如 PyPI。我已经在“Python 包索引应该去掉辅助轮(The Python Package Index Should Get Rid Of Its Training Wheels)”中触及了这个主题,简而言之,我坚信让索引将二进制文件视为事实来源是一个错误,相反,应该为其包要求一个可复现的构建过程(Reproducible build process),并将二进制构件视为可以尽力缓存(即可以随意丢弃和重新创建)的输出,以加速构建。
另一个有趣的案例研究是 Google 的 Go 模块代理(Go Module Proxy)。Go 编程语言使用完整的 URL 来定义导入,并且在很长一段时间里,它只会从提供的 URL 获取每个包。2019 年,Google 推出了 Go 模块代理,作为一种保证 Go 包高度可用(不会因为主机宕机而导致 CI 运行失败)且不可变(例如保证随时间推移未被篡改)的方法。
Google Go 模块代理本质上是一个带有独特权衡集合的集中式包注册中心。它不是让包作者直接将他们的包上传到其中,而是用作所有 Go 包管理器客户端流量的代理,并缓存以这种方式发现的所有包。
这种方法带来了一些额外的要求,例如 Google 必须能够证明他们没有篡改数据,或者代理有时必须保留原作者删除的包的副本(以免破坏构建),而有时又必须不长期存储某个包(出于许可原因)。有关详细信息,请参阅 Go Module Proxy 常见问题解答。
这种设计的另一个问题是让代理发现何时发布了包的新版本,这涉及一堆轮询,有时会对托管多个仓库的平台造成灾难性的后果。
运行
proxy.golang.org要花多少钱?我不知道,但我怀疑它不便宜。Go 开发者很幸运,他们可以依靠 Google 来保持其开源基础设施的运行。在历史上,其他生态系统不得不更多地担心这个话题。请参阅“我在 Python 软件基金会董事会任职期间学到的东西(Things I’ve learned serving on the board of the Python Software Foundation)”中的这段引文(重点是我加的):
最近,PSF、Rust 基金会和其他一些开源组织签署了一份关于开源包索引的可持续管理的联合声明,本质上是要求公司帮助支付保持这些服务运行的费用。
不过,我对这一点有争议的看法:我认为我们应该停止期望大科技公司为我们的基础设施买单,相反,我们应该对其进行设计,使其运行成本更低,同时保持高可用性,届时应该要求公司为其消耗的内容付费,而不需要支付太多超出此范围的费用。
在我看来,集中式包索引作为一个概念并没有随着时间推移而变得更好,大多数生态系统如果能积极主动地将其基础设施现代化,将会受益匪浅。我怀疑任何参与开源组织的人是否真的喜欢乞求大科技公司施舍更多的免费额度。
就 Zig 而言,甚至在了解 Radicle 之前,Andrew 就已经构想了一个去中心化的解决方案,用于分摊保持源代码可用性的成本。
结论
我对 Radicle 感到兴奋,但在我对它充满信心之前,我还有很多东西要学。
虽然在文章前面我把它表述为一个假设,但我实际上已经终止了我的 GitHub 订阅,并在
radicle.garden上获得了托管的 Radicle 节点,我在那里播种了 Zine 和其他一些项目。我的下一个目标是将 Awebo(alpha 阶段的可自托管 Discord 替代品)的开发暂时转移到 Radicle 上作为一个实验,以便获得更多第一手经验。
此时此刻,我无法确定 Radicle 是否会适合我,或者它总体上是否会成功,但我有一件事可以肯定:在我提到的或我所知道的所有其他替代方案中,没有一个能在追求高可用性时,其成本效益甚至能 remotely(哪怕稍微)接近 Radicle。
我确信 Radicle 中有很多大大小小的事情要么缺失,要么可以改进,但当谈到宏大的想法时,我简直无法想象还有什么比使用 P2P 网络来实现我们都依赖的源代码的“长期记忆”更好的安排了。
最后,我知道至少有三个不同的代码托管平台项目是用 Zig 编写的。
Radicle 是该领域的竞争者,但我确实相信,也存在一些解决方案的空间,它们带来了一些本地优先的优势,而不需要选择加入 P2P 网络的额外复杂性。我希望我的文章能够帮助传播对问题空间的认识,并激发每个人思考不同本地优先解决方案之间的互操作性。
但最重要的是,我希望我已经清楚地表明了牢记你所设计的任何事物的成本公式是多么重要。
学习低级编程语言最好的事情之一就是获得对手动内存管理的访问权限,这可以引导你设计出比相信无限内存错觉时所能设计出的质量更高的软件。
意识到系统中涉及的成本,以及不同的各方最终将如何承担这些成本,是另一项约束,它将为设计出质量呈几何级数提升的新架构打开大门。
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来: