Hello! It is I, Lewis, with my first Tangled post. At Tangled my job involves a lot of Research & Development, by which I mean fucking around and finding out. Let me tell you about one such finding out, which turned into this new Tangled API.

Bobbin 是一个只读、仅 API 的 AppView ,专为 Tangled 打造。它通过 XRPC 提供 sh.tangled.* lexicons 的读取端点, 让客户端无需逐一请求 N 个 PDSes 即可渲染 git 仓库、issue、pull request、 评论、star 和 follow。它没有永久存储,所有数据仅存在于内存中, 每次重启时都会从上游重新回填。

I, along with many others, find the term "AppView" to be too abstract or perhaps even unfortunately-named. In atproto, datasets are distributed across Personal Data Servers (PDSes) that users each own, so an AppView is a single API through which one can query a dataset, in contrast to all downstream clients each maintaining a connection with every PDS. An AppView is one of the many programs in use to reduce the strain on PDSes, so that a PDS is relatively cheap and resource-efficient to run.

以下是我认为 Tangled AppView 存在的一些问题,这些问题也解释了为什么我最终开发了 Bobbin:

  1. Tangled 的 AppView 没有 API,它通过 SSR 同时充当 Web 客户端。
  2. 它在回填 atproto 数据方面不够灵活。
  3. 其底层架构天生是为“自己就是数据源”的 Web 程序设计的。
  4. 由于以上原因,以及存在一些私有数据(如 email 到用户 DID 的映射),Tangled 只能运行一个 AppView, 这导致访问延迟大致取决于与斯德哥尔摩服务器的距离。
  5. 它在内存危机期间非常耗费 RAM。

把以上所有问题的反面拼在一起,就是我理想中的 AppView:仅提供 API、易于回填、易于分布式部署、节省 RAM。

为什么要“仅 API”?一旦可以通过通用的 XRPC API 获取 Tangled 数据, 就能在其上构建更多程序,例如替代前端、第三方客户端、个人仪表盘, 我自己则是为了打造一个好用的 CLI(这也属于替代前端的范畴)。 现有的 Tangled AppView 自己就是 Web 客户端,没有任何可扩展的接口, 除非你是个抓取高手,否则必须自己想办法从遍布的 PDSes 中抓取 Tangled 数据集。

我在思考如何打造这样一个程序时,也在担心状态存储的问题——我曾在半夜惊醒,担心 Tranquil PDS 自研的嵌入式数据库会以某种神秘方式出错。 如何让新程序完全不写入任何持久化、可能出错的状态? 此外,AppView 位于 PDSes 下游,保存的状态越多,与真实情况背离的风险就越大。 只要漏掉一条记录,AppView 就会严重失真,因此最重要的是让 AppView 能随时与真实数据重新对齐。

作为一个简单的人,我认为让一个只负责缓存和提供记录的程序完全不碰磁盘是最好的办法。 我希望它在每次重启时都从头回填所有数据。这样一来,与现实的对齐只需一次重启就能自动完成! 这可行吗?如果每次回填都需要一整天,那部署岂不是会变得很慢? 到底值不值得?

我最初的测试结果令人振奋:用 Hydrant 作为聚合器, 我写了一个小服务器,实际在约 15 分钟内就回填了 Tangled 全部 atproto 数据集。 我把可接受的回填时间定在 5 分钟以内,以便在蓝绿部署中能在合理预热时间内切换实例。

Hydrant is an aggregator for arbitrary atproto datasets that works by tailing the live firehose, backfilling every repo on the network by pulling their CARs, and serving it back over a websocket. All Bobbin has to do is open a connection, ask for cursor 0, and (re)build its index off the resulting barrage.

Massive gratitude for Dawn for having made Hydrant! Then for hearing my struggles and going and optimizing it also for fun!

达到 5 分钟后,我又把目标推进到 90 秒以内。目前的实际表现取决于上游 Hydrant 实例的热度, 最佳情况下约 30 秒,最差情况下(Hydrant 也从 PDSes 重新拉取记录)可达 20 分钟。

Yes, you too can spin these up & order the entirety of Tangled in 20 minutes or less, or you get a refund. (Of €0.)

我遇到了一个小难题:回填尚未完成时,程序应该提供什么服务? 我认为不应该在聚合或关联数据可能不准确的情况下对外提供服务。 那是否要让程序在最长 20 分钟的回填期间完全不响应请求? 我本可以这样做,但最终选择了在回填期间通过 Slingshot 进行点查,至少能立即返回准确数据,即使程序自身尚未完全接收到这些数据。

Slingshot is a wonderful edge cache for atproto records & identities. Bobbin asks it for single record and identity lookups, so that a freshly-born Bobbin can answer individual queries accurately before its own index has warmed up. Slingshot is part of the microcosm project, a bundle of community-run atproto infrastructure. Thanks Fig for making it!

这个逐渐演变为 Bobbin 的程序最终能将 Tangled 的全部 atproto 数据集保存在约 200MB 内存中, 经过一些压缩后降至 100MB,还有进一步压缩的空间。 你可能会感叹“整个平台社交层的数据居然这么小!”,但请记住: 从今天起,这个数据集只会越来越大,节省空间至关重要。

Bobbin 在实时查询时会使用 Slingshot 作为记录/身份解析器,用于解析它尚未在 RAM 中或尚未从 Hydrant 收到的数据。 我非常欣赏 Fig 的工作,但依赖 Slingshot 而非自己实现查询只是为了节省时间; Bobbin 未来最大的架构改进之一,就是内联这部分逻辑,让 Bobbin 不再需要请求 Slingshot, 而是找到更快的方式获取相同数据。

有人可能会想把 Hydrant 的嵌入式实例直接塞进 AppView 程序里,让服务特定 atproto 数据集的程序只是 Hydrant 数据的薄包装。 但我希望能轻松启动成百上千个 Bobbin 实例。如果每个 Bobbin 都运行一个 Hydrant,不断 ping PDSes,会非常浪费。 一个 Hydrant 实例可以毫不费力地服务数百个 Bobbin。

由于目前没有针对 Knot 相关数据的 Hydrant/relay 等价物,Bobbin 会将 Knot 相关的请求直接代理到指定的 knot。 我认为需要一个 knotstream 聚合器,或许只需给 Hydrant 加一个插件,让它同时提供这两种服务。 我个人认为 Bobbin 和 Knot Mirror 应该合并成一个服务,但这只是我的看法。

有了“无状态”AppView,我们就不必担心数据库及其各种问题:迁移、数据与“真实”数据集略有偏差时的生产环境修复等。 每次想改动或新增功能时,只需重启 Bobbin 即可完成。另一个好处是 Bobbin 非常适合“云原生部署”(用企业的话来说)。 Tangled 自己的旗舰实例就运行在 Cloudflare Containers 上。

话虽如此,我并不完全排斥 Bobbin 接触磁盘。一种可爱的改进是每小时把 RAM 中的数据快照一次, 启动时只回填差异部分;但目前我认为还有继续走无磁盘路径的空间——Bobbin 在分配 200MB 内存时就能健康运行。 此外,我最初“节省 RAM”的要求目前依然满足,但一旦内存占用过高,Bobbin 就会在运行时动态将部分数据卸载到磁盘。 你可能会问,我如何定义“过高”?我看自己的内心,它告诉我 1GB 是浪费,10GB 是不可接受的。 Bobbin 还要服务用户,每个 API 请求都会在存储 Tangled 数据集之外额外增加内存压力。

以上就是 Bobbin 的全部故事!现在我们有了一个有趣的、无磁盘存储、回填时间低于 90 秒、 能作为小引擎提供 API 请求的 AppView。借助它,任何人都能更轻松地基于 Tangled 数据集构建程序。

目前有一个 Bobbin 实例运行在 api.tangled.org, 希望你也能轻松托管自己的实例。Bobbin 的源代码位于 我们的单体仓库