工程回顾:2025 年版
Everett Pompeii
在开发像 Bencher 这样的新技术时,存在一种根本的张力,即希望选择乏味的技术和打破平均值之间的平衡。在当下,这种拉锯战中确切的位置可能很难判断。每三年,Rust 编程语言会推出一个新的Rust 版本。我认为这是一个不错的节奏。时间足够长,可以取得真正的进展,但也足够短,以免偏离太远。随着 Bencher 在今年春天迎来三周年,我认为这是一个很好的时机,可以停下来反思所有让我们走到今天的工程决策。
在这篇文章中,我将回顾过去三年中 Bencher 在何处使用了它的“创新代币”。Bencher 是一个开源的持续基准测试工具套件。我将从 Bencher 架构 的前端开始,一直深入到底层。在这个过程中每一站,我都会讨论我们是如何走到这里的,并对每个工程决策的结果做出二分判断。
前端
前端库
作为一个正在康复中的 C++ 开发者,我非常喜欢 Rust。如果我能按照自己的想法去做,我会使用全栈 Rust 来编写 Bencher。深入挖掘 Bencher 仓库,你会看到我正尝试这样做。我尝试了 Yew、Seed 和 Sycamore。尽管这些可能在某些项目中表现出色,但有一个主要的症结让我无法克服:JavaScript 互操作性。
虽然通过 Rust 可以从 WASM 中进行 JS 互操作,但这并不容易。我想要 Bencher 拥有高度交互的图表,这意味着需要使用像 D3 这样的库,也就意味着要进行 JS 互操作。
所以,如果我必须使用 JavaScript,我应该选择哪个库呢?
回到我试验的那些 Rust crates,Yew 是 React Hooks 的 Rust 类比。我过去曾使用 React Hooks 创建并部署了一个前端,所以对这个框架最为了解。然而,我发现 React Hooks 的生命周期非常复杂,并且充满了陷阱和奇怪的边缘案例。
我非常喜欢 函数式反应式编程 (FRP) 的核心原则。这引导我尝试了 Elm 及其 Rust 类比 Seed。不幸的是,使用 Elm 也面临与使用 Rust 同样的问题。Elm 需要自己的 JavaScript 互操作。我也发现 Elm 架构 对我来说有点过于限制。
在所有我尝试的 Rust 框架中,我最喜欢 Sycamore。Sycamore 受到 Solid 的启发。我了解到 Solid 的更多信息后,越发喜欢它。与 React 不同,Solid 不使用 虚拟 DOM。相反,它编译成传统的 JavaScript。这使得它更快、更小、更易于使用。Solid 由少数强大的原语组成,允许细粒度的反应性。当用户界面中的某些内容更新时,只有依赖它的代码才会重新运行。在过去的三年中,我发现与 Solid 一起工作是一种享受。
技术 结论 Yew ❌ Seed ❌ Sycamore ❌ Elm ❌ SolidJS ✅
前端框架
Solid 本身只是一个库。为了构建一个现代化的前端,我需要使用一个功能齐全的 Web 应用框架。为了保持简单,我把所有的筹码都押在了 Solid 上,最初使用了 SolidStart。那时,SolidStart 只支持单页面应用(SPA)。
SPA 对于开始开发来说是可以的。但是,最终我需要开始关注像 SEO 这样的问题。我开始撰写更多的Bencher 文档。我还计划了网站的学习部分。这意味着我需要客户端渲染(CSR)和静态网站生成(SSG)。SolidStart 非常年轻,我无法让它满足我的所有需求。
在了解 Astro 并尝试之后,我决定将整个 Bencher 前端从 SolidStart 转到 Astro。这样做有两个缺点。最明显的是涉及的工作量。说实话,也不是太糟糕。Astro 有它的岛屿架构和一流的Solid 集成。我还能从 Solid Router 中获取我需要的很多逻辑,它非常顺利地工作了。
至今仍在存在的一个妥协是,Bencher 从单页面应用变成了多页面应用。在控制台中点击的大多数地方都会导致整页重新渲染。在我第一次进行切换时,Astro 承诺了视图过渡。我尝试过,但它们有些问题。我仍然需要回到这一点。
在此期间,SolidStart 似乎已经赶上了一些。他们现在同时支持 CSR 和 SSG。不过,我还没有检查它们是否可以像我需要的那样在同一个站点上同时工作。时过境迁。
技术 结论 SolidStart ❌ Astro ✅
前端语言
Astro具有内置的TypeScript支持。在从SolidStart过渡到Astro的过程中,我也开始从JavaScript转向TypeScript。Bencher的TypeScript配置被设置为Astro的strictest
设置。然而,Astro在构建过程中不进行类型检查。在撰写本文时,Bencher仍有604
个类型错误。这些类型错误在编辑代码时更多用作提示,但它们不会(目前还不会)阻止构建(见GitHub问题557)。
我还添加了Typeshare,以同步Bencher的Rust数据类型与TypeScript前端。这对开发Bencher控制台非常有帮助。此外,所有针对用户名、电子邮件等的字段验证器都通过WASM在Rust代码和TypeScript前端之间共享详见WASM。让WASM在SolidStart和Astro中都能正常工作有点麻烦。前端中最大的一类错误是调用WASM函数但WASM模块尚未加载的地方。我已经找到了修复它的方法,但有时还是会忘记,然后再次出现。
自动从Rust代码生成共享的类型和验证器使得与前端的接口变得更加容易。它们都在CI中进行检查,所以永远不会不同步。我所要做的就是确保HTTP请求格式正确,然后一切就会正常运作。这使得无法使用全栈Rust的痛苦稍微减轻了一些。
技术 结论 Rust ❌ JavaScript ❌ TypeScript ✅ Typeshare ✅ WASM ✅
前端托管
我最初决定全面投入使用 Solid,很大程度上受到Netlify 聘请 Solid 的创建者全职开发的影响。你看,Netlify 最大的竞争对手是 Vercel。Vercel 创建并维护 Next.js。我猜 Netlify 希望 Solid 成为他们的 Next.js。因此,我认为没有比在 Netlify 上托管一个 SolidStart 站点更好的地方了。
默认情况下,Netlify 会尝试让你使用他们的构建系统。使用 Netlify 的构建系统使得原子部署变得非常困难。即使后端流水线失败,Netlify 仍然会发布前端。这非常糟糕!这导致我选择在与后端相同的 CI/CD 环境 中构建前端,然后只使用他们的 CLI 将最新版本上传到 Netlify。当我从 SolidStart 迁移到 Astro 时,我能够保持相同的 CI/CD 设置。Astro 提供了一个官方的 Netlify 集成。
Bencher 在 Netlify 的免费套餐下坚持了相当长的一段时间。不过,随着 Bencher 的受欢迎程度增加,我们开始超过一些免费套餐限制。我曾考虑将 Astro 站点迁移到 sst
on AWS。然而,到目前为止,节省的成本似乎不值得付出这一努力。
技术 结论 Netlify 构建 ❌ Netlify 部署 ✅
后端
后端语言
Rust.
技术 结论 Rust ✅
HTTP 服务器框架
我选择 Rust HTTP 服务器框架时,首要考虑之一是内置的 OpenAPI 规范 支持。出于同样的原因,我投入精力设置 前端的 Typeshare 和 WASM,我希望能够从该规范中自动生成 API 文档和客户端。对我来说,重要的是这种功能是内置的,而不是第三方的附加组件。为使自动化真正有价值,它必须几乎 100% 的时间都能正常工作。这意味着维护和兼容性负担需要由核心框架工程师自己承担。否则,你将不可避免地会陷入边缘案例的困境。
另一个主要考虑因素是被遗弃的风险。有几个曾经很有前途的 Rust HTTP 框架现在几乎被遗弃了。我发现唯一一个具有内置 OpenAPI 规范支持并且我愿意赌一把的框架是 Dropshot。Dropshot 是由 Oxide Computer 创建并仍在维护的。
到目前为止,我只有一个主要的 Dropshot 问题。当 API 服务器产生错误时,由于缺少响应头,导致前端出现 CORS 失败。这意味着 Web 前端无法向用户显示非常有帮助的错误信息。我没有致力于向上游提交一个修复,而是把精力放在使 Bencher 更易于使用和更加直观。但是结果表明,解决方案 不到100行代码。我真是自讨苦吃!
顺便提一下,axum
框架 在我开始进行 Bencher 时还没有发布。如果它当时已经存在,我可能会试着将它与许多第三方 OpenAPI 附加组件之一配对,虽然我知道这不是个好主意。幸运的是,axum
当时还没有来诱惑我。Dropshot 是一个很好的选择。有关这一点的更多信息,请参见 API 客户端 部分。
技术 结论 Dropshot ✅
数据库
我尽量让 Bencher 尽可能简单。第一版的 Bencher 通过 URL 查询参数获取所有内容,包括基准测试结果。我很快了解到所有浏览器都有 URL 长度限制。这确实合理。
接下来,我考虑将基准测试结果存储在 git
中,并仅生成静态 HTML 文件,其中包含图表和结果。但是,这种方法有两个主要缺点。首先,git clone
时间对于重度用户最终会变得无法忍受。其次,所有历史数据都必须存在于 HTML 文件中,导致重度用户的初次加载时间非常长。一个开发工具应该热爱它的重度用户,而不是惩罚他们。
结果发现,我的问题有解决方案。它就叫做数据库。
那么为什么不直接引入 Postgres 然后就了事呢?嗯,我确实希望人们能够 自托管 Bencher。我能够简化架构,就可以让别人更轻松(和便宜)地自托管。由于前端和后端分离,我已经需要两个容器。我能避免第三个吗?没错!
在使用 Bencher 之前,我只用过 SQLite 作为测试数据库。开发者体验非常棒,但我从未考虑过在生产环境中运行它。然后我发现了 Litestream。Litestream 是一个 SQLite 的灾难恢复工具。它在后台运行,并持续将更改复制到 S3 或您选择的任何其他数据存储。这使得它既易于使用又非常经济实惠,因为 S3 不收写入费用。对于一个小型实例,每天只需花费几分钱。
当我第一次遇到 Litestream 时,它还承诺即将推出实时读取副本。然而,这 从未实现。建议的替代方案是同一开发者的继承项目,称为 LiteFS。然而,LiteFS 有很大的缺陷。如果所有副本都中断,它不提供内置的灾难恢复。为了拥有多个副本,您必须在应用程序逻辑中引入是否为读者或写者的概念。而绝对的障碍是它需要一直运行一个 Consul 实例来管理副本。使用 SQLite 的整个重点就是避免再添加一个服务。谢天谢地,我也没有尝试将 LiteFS 用于 Bencher Cloud,因为 LiteFS Cloud 在发布一年后关闭,而 LiteFS 现在几乎已经停滞。
目前,部署之间的小停机时间是通过 Bencher CLI 处理的。将来,我计划使用 Kamal 进行零停机部署。随着 Rails 8.0 默认使用 Kamal 和 SQLite,我对 Kamal 和 Litestream 的结合相当有信心。
技术 结论 URL 查询参数 ❌ git + HTML ❌ SQLite ✅ Litestream ✅ LiteFS ❌
数据库驱动程序
越接近数据库,我就越希望使用强类型的东西。在前端可以稍微随意一些。如果出现问题,下一次推送到生产环境一切都会恢复正常。但是如果数据库损坏,修复起来就麻烦多了。考虑到这一点,我选择使用 Diesel。
Diesel 是一个用于 Rust 的强类型对象关系映射(ORM)和查询构建器。它在编译时检查所有数据库交互,防止运行时错误。这种编译时检查也使得 Diesel 针对 SQL 提供了零开销的抽象。除了在通过性能调优使事情 加速 1200 倍时出现的小 bug之外,使用 Diesel 时没有出现运行时 SQL 错误。
🐰 有趣的事实:Diesel 使用 Bencher 进行 持续基准测试!
技术 结论 Diesel ✅
后端托管
就像我选择 Netlify 作为我的前端托管是因为我使用 Solid, 我选择了 Fly.io 作为我的后端托管是因为我使用 Litestream。 Fly.io 刚刚聘请了 Litestream 的创建者进行全职开发。 正如上面提到的,Litestream 的工作最终被 LiteFS 吞并, 而 LiteFS 现在已经停止。 所以事情并没有按照我预期的发展。
将来当我切换到 Kamal 时,我也会迁移离开 Fly.io。 Fly.io 曾经历过几次重大故障,每次都使 Bencher 崩溃长达半天。 但最大的问题是使用 Litestream 带来的阻抗不匹配。
每次我登录到 Fly.io 仪表板时,我都会看到这个警告:
ℹ 您的应用程序正在一台机器上运行
扩展并在更多机器上运行您的应用程序,以确保高可用性,只需一个命令:
查看文档获取有关扩展的更多详细信息。
但使用 Litestream,你还是不能有多于一台的机器! 你们从未兑现你们承诺的读取复制功能!
所以,是的,这一切有点讽刺和令人沮丧。 有一次,我研究了 libSQL 和 Turso。 然而,libSQL 需要一个特殊的后台服务器进行复制, 这使得它无法与 Diesel 一起使用。 无论如何,看起来我躲过了另一个生命周期结束的关闭。 我对 Turso 用 Limbo,他们用 Rust 重写的 SQLite 非常感兴趣。 但我不会很快做出这种切换。 下一步是一个不错、无聊且稳定的运行 Kamal 的虚拟机。
用于 Litestream 复制的 AWS S3 后端运行得非常完美。 即使 Litestream 和 Fly.io 周围发生了意外, 我仍然认为使用 Litestream 与 Bencher 是正确的选择。 我开始遇到一些 Bencher Cloud 的扩展问题, 但这些都是好问题。
技术 结论 Fly.io ❌ AWS S3 ✅
命令行界面 (CLI)
CLI 库
在构建 Rust CLI 时,Clap 算是事实上的标准。 所以当我第一次公开演示 Bencher 时,想象一下我的震惊, 创作者本人 Ed Page 竟然在场!🤩
随着时间的推移,我不断发现 Clap 的更多有用功能。
有点尴尬,但我最近才发现 default_value
选项。
所有这些功能确实有助于减少我需要在 bencher
CLI 中维护的代码量。
🐰 趣闻: Clap 使用 Bencher 来 跟踪二进制文件大小!
技术 判定 Clap ✅
API 客户端
选择 Dropshot 作为 Bencher 的 HTTP 服务器框架 的一个主要因素 是其内置生成 OpenAPI 规范 的能力。 我希望有一天可以从规范中自动生成一个 API 客户端。 大约一年之后,Dropshot 的创建者实现了这一目标:Progenitor。
Progenitor 就像是 Dropshot 的阴阳互补。 使用 Dropshot 的 OpenAPI 规范,Progenitor 可以生成一个 Rust API 客户端, 支持位置模式:
或者生成器模式:
个人而言,我更喜欢后者,
所以 Bencher 也是这么用的。
Progenitor 还可以生成一个完整的 Clap CLI 来与 API 交互。
然而,我没有使用它。
我需要对某些事情有更严格的控制,
尤其对于类似 bencher run
的命令。
我发现生成的类型唯一显著的缺点是,
由于 JSON Schema 的限制,当需要能够区分缺失的 item
键和具有 null
值的 item
键时,不能简单地使用 Option<Option<Item>>
。
使用 double_option
可以解决这个问题,
但在 JSON Schema 的层面上看起来一切都相同。
使用 flattened 或 untagged 的内部结构枚举
与 Dropshot 结合得不太好。
我发现的唯一解决方案是使用 顶级、无标签的枚举。
不过,目前整个 API 中只有两个这样的字段,
所以问题不大。
技术 结论 Progenitor ✅
开发
开发者环境
当我开始在 Bencher 工作时,人们呼吁终结 localhost。 我早已不再需要一个新的开发笔记本电脑, 所以我决定尝试一个云开发环境。 当时 GitHub Workspaces 并未一般性地向我的用例开放, 所以我选择了 Gitpod。
这个实验持续了大约六个月。 我的结论是:云开发环境不适用于副项目。 你想跳进去并快速完成五分钟的工作? 不行!你要坐在那里,等待你的开发环境第 1000 次重新初始化。 哦,你有一个整个周末下午真正冲刺工作的时间? 不行!你的开发环境会随机停止工作,而你正在使用它。再一次,再一次,再一次。
这些问题是我作为付费用户遇到的。 每月 25 美元,我可以每五年获得一台配置更优的新 M1 MacBook Pro。 当 Gitpod 宣布他们将定价模式从固定费率改为基于使用量时, 我干脆让他们取消了我的计划,并前往 apple.com。
也许这都是 Gitpod 现已放弃的决定使用 Kubernetes 的问题。
但我并不急于再次在 Bencher 上尝试另一种云开发环境。
最终我将 Gitpod 配置移植到一个开发容器上,
以便让贡献者更容易入门。
但对于我而言,我还是选择坚持使用 localhost
。
技术 结论 Gitpod ❌ M1 MacBook Pro ✅
持续集成
Bencher 是 开源的。作为一个现代开源项目,你几乎必须在 GitHub 上。这意味着对于持续集成(CI),最简单的路径是 GitHub Actions。多年来,我开始厌恶基于 YAML 的 CI DSLs。它们每一个都有自己的怪癖,而对于像 GitHub 这样的大公司,得到一个 ⚠️ 图标而不是 ❌ 图标可能要拖延多年。
这促使我尝试 Dagger。当时,你只能通过这个叫做 CUE 的生僻语言来使用 Dagger。我试过了。真的试过了。差不多花了整个周末。也许如果当时有 ChatGPT,我可能能挺过去。但不只是我。Dagger 最终完全放弃了 CUE,转而采用更合理的 SDK。不过到那时,对我来说已经太晚了。
被 Dagger 打败后,我接受了我的 YAML CI DSL 命运,Bencher 现在使用 GitHub Actions。事实上,我甚至还创建了一个 Bencher CLI 的 GitHub Action。成为你希望在世界上看到的变化问题。
技术 结论 Dagger ❌ GitHub Actions ⚠️
结论
构建 Bencher 的过程中,我学到了很多关于每个工程决策所带来的权衡。有些选择如果可以重来,我会做出不同的决定,但这也是件好事。这意味着我在这个过程中学到了一些东西。总体上,我对 Bencher 目前的状况感到非常满意。Bencher 从我笔记本中的一个草图发展成为一个功能齐全的产品,拥有不断增长的用户群体、充满活力的社区,以及付费客户。我期待着接下来三年中我们会走到哪里!
技术栈 组件 技术 结论 前端 前端库 Yew ❌ Seed ❌ Sycamore ❌ Elm ❌ SolidJS ✅ 前端语言 Rust ❌ JavaScript ❌ TypeScript ✅ Typeshare ✅ WASM ✅ 前端托管 Netlify Builds ❌ Netlify Deploys ✅ 后端 后端语言 Rust ✅ HTTP 服务器框架 Dropshot ✅ 数据库 URL 查询参数 ❌ git + HTML ❌ SQLite ✅ Litestream ✅ LiteFS ❌ 数据库驱动 Diesel ✅ 后端托管 Fly.io ❌ AWS S3 ✅ CLI CLI 库 Clap ✅ API 客户端 Progenitor ✅ 开发 开发者环境 Gitpod ❌ M1 MacBook Pro ✅ 持续集成 Dagger ❌ GitHub Actions ⚠️
Bencher: 持续性能基准测试
Bencher是一套持续型的性能基准测试工具。 你是否曾经因为性能回归影响到了你的用户? Bencher可以防止这种情况的发生。 Bencher让你有能力在性能回归进入生产环境 之前 就进行检测和预防。
- 运行: 使用你喜爱的基准测试工具在本地或CI中执行你的基准测试。
bencher
CLI简单地包装了你现有的基准测验设备并存储其结果。 - 追踪: 追踪你的基准测试结果的趋势。根据源分支、测试床和度量,使用Bencher web控制台来监视、查询和绘制结果图表。
- 捕获: 在CI中捕获性能回归。Bencher使用最先进的、可定制的分析技术在它们进入生产环境之前就检测到性能回归。
基于防止功能回归的原因,在CI中运行单元测试,我们也应该使用Bencher在CI中运行基准测试以防止性能回归。性能问题就是错误!
开始在CI中捕捉性能回归 — 免费试用Bencher Cloud。