一个徽章坏了,顺带扯出了 2.3 万个 feature

2023 年 10 月中旬,crates.io 团队收到一封用户反馈:他们维护的某个 crate,放在 README 里的 shields.io 徽章不显示了。

这件事乍看毫不起眼。shields.io 徽章时不时出问题很正常,原因五花八门。但这次的问题链,一旦顺着拉下去,牵出的东西让整个 crates.io 团队都没想到。

本文基于 Rust 官方博客 2023 年 10 月 26 日发布的《A tale of broken badges and 23,000 features》整理撰写,作者为 Tobias Bieniek,代表 crates.io 团队。

原文地址


一个徽章,一条 20 MB 的 API 响应

这位用户在提 Issue 时已经自己做了初步定位,发现问题的根源在 shields.iocrates.io 发出的那个 API 请求:

这个 crate 大量使用了 feature flag,导致 API 响应的体积极度膨胀。

具体来说,shields.io 调用的是 crates.io/api/v1/crates/{crate_name} 接口------这个接口会一次性返回该 crate 所有已发布版本的完整元数据,包括每个版本的全部 feature 列表。

问题是,这个 crate 叫做 icondata,它的 API 响应已经超过了 20 MBshields.io 拿到这个响应之后直接超时,徽章自然也就坏了。

20 MB 是什么概念?这个 crate 当时一共才发布了 9 个版本


9 个版本,为什么会有 20 MB?

答案是:这个 crate 有将近 2.3 万个 Cargo feature。

icondata 是一个为 Rust 网页应用提供 SVG 图标的库。它把每一个图标都对应设计成一个独立的 Cargo feature,这样用户在编译时只需要启用自己用到的图标,最终打包出的 WebAssembly 产物就不会把成千上万个用不到的图标都塞进去。

从 crate 作者的角度看,这个设计完全合理。SVG 图标库本来就有成千上万个图标,按需引入是最自然的做法,Cargo 的 feature 机制也正是为此而生。cargo 不会报错,crates.io 也不会给出任何警告。

但没有人告诉这个 crate 的作者:2.3 万个 feature,已经把 crates.io 的某些内部机制压垮了。


问题一:API 响应永远不分页

crates.io/api/v1/crates/{name} 接口有一个长期的设计问题:它返回所有版本的完整数据,没有分页

对于只有几个版本、每个版本 feature 寥寥无几的普通 crate,这不是问题。但 icondata 有 2.3 万个 feature,哪怕只有 9 个版本,每次 API 调用都需要把 9 × 2.3 万条 feature 数据一并吐出来,响应体积随之爆炸。

团队知道分页是正确的方向,但这是一个破坏性变更------已有无数工具和集成依赖当前的响应结构。这个问题被搁置了很久,直到这次事件才让团队意识到必须提上日程。


问题二:索引文件也在膨胀

crates.io 的稀疏索引(sparse index)是另一个受害者。

Cargo 在解析依赖时,需要从索引中获取每个 crate 的元数据。索引文件中存储的内容包括该 crate 每个版本的依赖和 feature 列表。对于普通 crate,这个文件很小,Cargo 可以快速拉取和解析。

icondata 的索引文件因为包含 2.3 万个 feature 的重复记录,体积同样极为庞大。每次有用户或 CI 环境需要解析这个 crate 的依赖时,都需要下载和处理这个异常大的文件。


问题三:数据库查询承压

crates.io 的后端使用 PostgreSQL 存储 crate 的元数据。feature 数据以关联记录的形式存储在数据库表中,查询某个 crate 的完整信息时,需要通过 JOIN 把这些记录拼回来。

2.3 万条 feature 记录的存在,让原本为普通规模设计的查询开始出现性能问题。这类查询对绝大多数 crate 可以在毫秒级完成,但遇到 icondata 这种异常情况,耗时会急剧上升。


团队的应对

面对这次事件,crates.io 团队采取了几项措施。

立即生效的限制:新发布的 crate 版本,feature 数量上限为 300 个。 这一限制已记录在 Cargo 的官方文档中。对于有特殊需求的 crate,团队表示会逐案评估是否给予豁免。

这个数字的选择有一定的工程判断成分:300 个 feature 对绝大多数用例来说绰绰有余,同时也能将 API 响应体积和数据库查询压力控制在可接受的范围内。

API 分页的问题,团队明确表示将被列为近期必须解决的工作,这次事件提供了足够的推动力。


一个值得深想的细节

这件事有一个值得单独拿出来说的侧面:整个过程中,icondata 的作者没有做任何"错误"的事。

用 feature 来控制编译粒度,是 Rust 生态里成熟的做法。cargo 文档里有这个模式,众多知名 crate 也都在用。没有任何工具链工具会在你的 feature 数量超过某个阈值时发出警告,crates.io 在接受发布时也不会提示任何异常。

问题出在基础设施的隐性假设上:API 的设计者从未预料到会有 crate 拥有数万个 feature;数据库查询的设计者也没有为这种极端情况预留余量。这类问题不到被触发,几乎不可能在事前被发现。

对于写基础设施代码的人来说,这是一个标准的"边界条件"教训:系统在正常范围内运行良好,在正常范围之外,假设就开始逐一失效。


如果你现在就想用 v0 只包含需要的图标......

icondata 最终将自己拆分为多个子 crate,每个子 crate 对应一个图标库(如 Bootstrap Icons、Remix Icons 等),每个子 crate 各自包含该库的图标 feature。这样每个 crate 的 feature 数量就控制在了合理范围内,同时保留了按需引入的设计意图。

这是一个在遭遇系统限制之后的工程权衡:不改变设计理念,但改变实现的边界划分。


小结

一个坏掉的 shields.io 徽章,揭开了 crates.io 在超出预期规模时的多个薄弱点:无分页的 API、膨胀的索引文件、未能为极端情况设计的数据库查询。

团队的修复是务实的:先设一个上限防止同样问题再次发生,再逐步处理更深层的架构问题。现在,Cargo 的文档里白纸黑字写着:每个 crate 最多 300 个 feature,特殊情况逐案审批。

这条规则的背后,是 2.3 万个 SVG 图标和一个坏掉的版本徽章。


相关推荐
heimeiyingwang2 小时前
【架构实战】Docker容器网络模型详解
网络·docker·架构
tumeng07112 小时前
Spring详解
java·后端·spring
羑悻的小杀马特2 小时前
Pinecone向量数据库深度解析:从核心架构到LangChain集成实战
数据库·架构·langchain·pinecone
jwt7939279372 小时前
Spring之DataSource配置
java·后端·spring
黑牛儿2 小时前
Swoole协程 vs Go协程:PHP开发者一看就懂的实战对比
后端·golang·php·swoole
Rust研习社2 小时前
深入理解 Rust 裸指针:内存操作的双刃剑
开发语言·后端·rust
j_xxx404_2 小时前
深入理解Linux底层存储:从物理磁盘架构到文件系统(inode/Block)原理
linux·运维·服务器·后端
一几文2 小时前
软考高级系统架构师25年下半年案例分析真题回顾带解析1,质量属性+质量属性场景+AES-256加密算法
架构·系统架构·软考高级·软考·aes·考证·质量属性
小江的记录本3 小时前
【网络安全】《网络安全与数据安全核心知识体系》(包括数据脱敏、数据加密、隐私合规、等保2.0)
java·网络·后端·python·算法·安全·web安全