维护者常看一个开源项目的 star 历史曲线,来判断某次发布、某个事件是否改变了项目走向。但你有没有想过,这条曲线在 GitHub API 里原本是怎么来的?过去,工具要重建这条曲线,得去分页地拉取所有 stargazer 记录——包括每个当前 star 了仓库的用户名和 star 时间,然后只留时间戳、丢掉身份信息。这个设计随着项目变火,两头都出了问题:一方面身份信息可被批量获取,诱导了垃圾邮件和钓鱼;另一方面,高 star 仓库的一次分页请求,数据库可能要为它处理成千上万条记录。

GitHub 这次的做法很直接:不再返回“人”,改返回“数量”。但有意思的是,仅仅减少返回的数据量,并没有让查询变得更便宜。真正贵的是数据库要处理的量,而不是返回的量。于是他们重新设计了一个端点:一次请求就能拿到最多 30 周的每日 star 数,而且完全不带任何用户身份。怎么做到便宜的?关键是在计数之前先按时间过滤 star 记录,让数据库只处理必要的那部分数据,而不是全量扫描。

先说说原设计为什么贵。老的 `GET /repos/{owner}/{repo}/stargazers` 端点回答的是“谁 star 了这个仓库”,它返回分页的用户列表;如果指定了自定义 media type,每条还会带上 star 创建时间。这个模型是为“当前 star 人群”建的,不是历史事件日志:有人取消 star,那条记录就被删了。当工具把它当时间序列用,重建增长曲线就得取出每个当前 star 的身份、处理每一条,每页最多 100 人,于是工作量随仓库热度线性增长。拿 `react/react` 举例,约 25.1 万 stars,要拉完整曲线大约需要 2511 次请求,很多工具因此撞上速率限制,只能抽样读。

更坑的是分页实现里有个 bug:每次请求本来只要一页(最多 100 人),但为了算总页数,数据库还得把仓库当前所有 star 都数一遍。在高 star 仓库上,这等于每次请求都要数好几万条。在 GitHub 限制访问前的那一周,这类 count 查询里约有 3.7 万次慢查询被记录,平均每次耗时近 3 秒,数据库保护机制还杀掉了超过 1000 次,直接导致部分请求超时。下图展示的就是这些 count 操作的实际代价:

后来 GitHub 先收紧了权限,把 stargazer 和 watcher 列表的访问限制到仓库管理员和协作者,无效调用方会收到空响应或 403。这立刻砍掉了那些暗地里靠返回身份算历史的工具,但也让 Star History 这类正经工具“断粮”了。好在社区反馈及时,GitHub 的 Community 团队牵线,产品工程部门直接介入,和 Star History 的维护者(Bytebase 联创 Tianzhou Chen)一起设计了专门接口。用维护者的话说,一个棘手局面最终变成了建设性合作。

新端点到底怎么做?核心就是“先过滤再计数”。它不再返回用户列表,而是直接按天聚合并返回 star 数增量。这样数据库只需处理与时间范围相关的 star 记录,而不是全表扫描。对维护者来说,曲线有了,身份没了,安全风险和性能负担同时降下来。按文中的数据,权限收紧后,stargazer 请求量下降了约 60%,数据库花在 count 查询上的总时间减少了约 80%——这还是在新端点上线前。

这件事的启发不止于 GitHub。任何想对外提供“历史趋势”类数据的 API,都可以想想:是不是非要暴露底层实体列表?聚合结果往往比原始明细更小巧、更安全,也更符合“查询意图”。但在设计时,性能瓶颈往往不在响应体大小,而在数据库需要处理的数据量——提前按维度(比如时间)过滤,远比事后截断输出有效。另外,当 API 变更破坏既有工具时,主动和社区共建、设计专用端点,比单纯“给个替代方案”更能解决问题。

如果你也在做类似的数据开放服务,这条经验可以借鉴:先把访问场景理清,是“谁动了我的仓库”还是“项目热度怎么变”?前者必须带身份,后者只需要数字。用聚合接口承载后者,把身份留在权限内,你既能保性能,又能避风险。唯一要注意的是,聚合接口的粒度要匹配真实需求,比如 30 周日频对大多数“看趋势”场景已经够用,太细的粒度反而增加存储和查询压力。

[图片说明]:文中图1是 GitHub 在限制访问前后,stargazer 请求中 count 查询的耗时与次数对比图,直观展示了几万次慢查询和超时带来的数据库压力。

阅读原文 → 返回 AI 技术文档

内容与图片版权归原作者所有 · 原文: https://github.blog/engineering/architecture-optimization/from-2500-requests-to-24-rebuilding-star-history-without-the-stargazers/