Rust 2026-10-15·Ievgenii Svyryd

基准测试第 2 轮:Rust、Go 与各 Rust 框架的性能剖析

Go 与七个 Rust 应用(axum、actix-web、Rocket、Loco、Cot,以及运行在 axum 和 actix 上的 Rustango)在相同规则下测试,每次只运行被测应用,并附上 CPU、系统调用和网络协议剖析,展示每个应用把时间花在哪里。

基准测试第 2 轮:Rust、Go 与各 Rust 框架的性能剖析

这是公平基准测试的第 2 轮。第 2 轮只涵盖编译型应用:Go 和七个 Rust 应用。它新增了三个 Rust 框架(Rocket、Loco、Cot),以及展示每个应用时间花在哪里的 CPU、系统调用和网络协议剖析。

测量于 2026 年 10 月 14 日,机器为 Apple M3 Pro(Docker 虚拟机,12 个 CPU、16 GB)。

  • 441/s Go 在缓存首页上的垃圾回收次数
  • 3 vs 9 每个详情页的 Postgres 往返次数:Go 对比带获取 ping 的 sqlx
  • +45% Rustango 运行于 actix,首页,每个 worker 一个连接池分片
  • 6.3–22.8 MiB 负载下内存,全部八个应用
  • 0 144 次运行中的错误或非 200 响应

测试环境

与第 1 轮相同的博客:10 位作者、30 个标签、1 000 篇文章、2 600 条文章-标签关联和 10 000 条评论。六个路由:首页(最新 20 篇文章)、包含全部评论的文章详情页、标签页、从 Redis 页面缓存读取的首页和详情页,以及 /compute(计算小于 20 000 的素数之和,不访问数据库)。

八个应用:

  • Go:标准库 net/http、pgx v5、go-redis
  • axum + sqlx 和 actix + sqlx:原生 sqlx,使用与 Go 相同的 SQL 文本,Tera 模板
  • Rocket:rocket_db_pools(sqlx)和 rocket_dyn_templates(Tera)
  • Rustango,分别运行在 axum 和 actix-web 上:Rustango ORM 及其页面缓存
  • Loco:SeaORM、Tera
  • Cot:Cot 自带 ORM、askama 模板

规则:

  • 应用获得 4 个 CPU(绑定到 0–3 号核心)和 2 GB 内存,使用 4 个 worker。PostgreSQL 16 和 Redis 7 运行在 4–7 号核心,负载生成器 oha 运行在 8–11 号核心。
  • 只运行被测应用,其他应用均停止。切换应用之间,Postgres 连接数会回落到基线。
  • 连接池:最多 16 个连接,最少 4 个。
  • 施加负载之前,每个应用的每个页面都会与 Go 输出的 HTML 对比。只要不一致,测试即终止。
  • 不启用压缩。先预热 5 s,然后每个端点在 50 个连接下运行三次、每次 20 s;取中位数。

测试装置

DOCKER NETWORK · NO COMPRESSION oha load generator 8 9 10 11 CORES 8–11 App under test 4 CPUs · 2 GB · 4 workers 0 1 2 3 CORES 0–3 · PINNED Databases Postgres · MySQL · Redis 4 5 6 7 CORES 4–7 HTTP 50 conns SQL · cache 16 conns ✓ HTML parity vs Go, before any load PER ENDPOINT 5 s run 1 · 20 s run 2 · 20 s run 3 · 20 s WARM-UP → MEDIAN OF 3 · HTTP 200 ONLY DOCKER NETWORK oha load generator CORES 8–11 8 9 10 11 HTTP 50 conns ✓ HTML parity vs Go App under test 4 CPUs · 2 GB · 4 workers CORES 0–3 0 1 2 3 SQL · cache · 16 conns Databases PostgreSQL · MySQL · Redis CORES 4–7 4 5 6 7 PER ENDPOINT 20 s 20 s 20 s 5 S WARM-UP → MEDIAN OF 3

与第 1 轮相同的测试装置。第 2 轮只使用 PostgreSQL 和 Redis(不使用 MySQL)。

版本

  • Go 1.25.14、pgx v5.7.2、go-redis v9.7.0
  • rustc 1.99.0;axum 0.8.9、actix-web 4.16.0、tokio 1.53、sqlx 0.8.6、Tera 1.20.1
  • Rocket 0.5.1(sqlx 0.7.4、hyper 0.14)
  • Loco 1.2.0(SeaORM 2.0.4、sqlx 0.9.0)
  • Cot 0.7.0(sqlx 0.9.0、askama 0.16.1)
  • Rustango:无获取 ping,页面缓存存储原始字节,每个请求使用一个池连接(发现 5)

无法拉平的设置

设置应用原因
连接池每次借出连接都会 ping 一次 Postgres(sqlx test_before_acquire)Rocket, Loco, Cot没有可关闭它的设置。axum + sqlx、actix + sqlx 和 Rustango 中已关闭。
连接池为 10 个连接而非 16 个CotCot 没有连接池设置。
每个 worker 一个连接池分片(16 个连接中的 4 个),每个 worker 一个 Redis 连接actix + sqlx, Rustango · actix在 actix-web 上共享连接池更慢(见发现 6)。
sqlx 0.7.4 和 hyper 0.14Rocket由 Rocket 0.5.1 锁定。
首页、详情页和标签页分别需要 5、5 和 7 次查询,而不是 3、3 和 4 次Cot它的查询 API 无法表达 Go 的查询(见发现 9)。
使用 askama 而非 TeraCotCot 的模板引擎。

结果

每秒请求数,越高越好。

未缓存页面

相同 HTML,50 个连接,每个请求都访问 PostgreSQL

req/s · higher is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
首页
Go 8 741 req/s
axum + sqlx 14 140 req/s
actix + sqlx 14 408 req/s
Rocket 13 316 req/s
Rustango · axum 14 686 req/s
Rustango · actix 17 401 req/sfastest
Loco 10 290 req/s
Cot 2 018 req/s
详情页
Go 22 607 req/s
axum + sqlx 22 842 req/s
actix + sqlx 21 630 req/s
Rocket 24 561 req/s
Rustango · axum 25 788 req/s
Rustango · actix 27 292 req/sfastest
Loco 15 635 req/s
Cot 9 282 req/s
标签页
Go 6 386 req/s
axum + sqlx 6 467 req/s
actix + sqlx 6 628 req/s
Rocket 6 836 req/s
Rustango · axum 7 090 req/sfastest
Rustango · actix 7 056 req/s
Loco 5 604 req/s
Cot 3 065 req/s

50 个连接下三次 20 s 运行的中位数。测试时只运行被测应用。0 个非 200 响应,0 个错误。

缓存页面

从 Redis 页面缓存提供,不执行 SQL

req/s · higher is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
首页,缓存
Go 91 839 req/s
axum + sqlx 198 669 req/s
actix + sqlx 226 887 req/s
Rocket 138 224 req/s
Rustango · axum 174 501 req/s
Rustango · actix 231 975 req/sfastest
Loco 153 243 req/s
Cot 190 245 req/s
详情页,缓存
Go 141 574 req/s
axum + sqlx 214 871 req/s
actix + sqlx 240 161 req/s
Rocket 155 691 req/s
Rustango · axum 192 897 req/s
Rustango · actix 242 609 req/sfastest
Loco 170 158 req/s
Cot 203 119 req/s

50 个连接下三次 20 s 运行的中位数。测试时只运行被测应用。0 个非 200 响应,0 个错误。

计算

小于 20 000 的素数,不访问数据库

req/s · higher is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Go 16 580 req/sfastest
axum + sqlx 14 963 req/s
actix + sqlx 14 578 req/s
Rocket 14 696 req/s
Rustango · axum 14 799 req/s
Rustango · actix 14 710 req/s
Loco 14 184 req/s
Cot 15 330 req/s

50 个连接下三次 20 s 运行的中位数。测试时只运行被测应用。0 个非 200 响应,0 个错误。

延迟,p50 / p99,单位 ms:

应用首页详情页标签页首页,缓存详情页,缓存计算
Go5.50 / 9.961.91 / 5.027.61 / 11.720.35 / 2.730.24 / 2.052.37 / 10.75
axum + sqlx3.49 / 4.712.14 / 3.607.42 / 13.890.24 / 0.530.22 / 0.513.35 / 6.07
actix + sqlx3.32 / 6.382.21 / 4.307.23 / 13.230.20 / 0.500.19 / 0.453.41 / 4.97
Rocket3.66 / 5.831.97 / 3.587.14 / 11.550.34 / 0.700.30 / 0.673.40 / 6.14
Rustango · axum3.28 / 5.651.83 / 3.556.90 / 11.180.27 / 0.590.25 / 0.523.38 / 6.09
Rustango · actix2.80 / 4.511.77 / 3.376.91 / 11.530.20 / 0.480.19 / 0.453.41 / 3.96
Loco4.62 / 10.842.99 / 6.348.67 / 14.520.30 / 0.790.28 / 0.623.48 / 6.90
Cot23.35 / 50.365.28 / 8.0215.57 / 27.060.25 / 0.500.23 / 0.523.27 / 5.85

内存

负载下的内存

未缓存首页上应用容器的常驻内存

MiB · lower is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Go 22.8 MiB
axum + sqlx 7.6 MiB
actix + sqlx 7.2 MiB
Rocket 8.7 MiB
Rustango · axum 6.9 MiB
Rustango · actix 6.3 MiBlowest
Loco 12.0 MiB
Cot 22.6 MiB

docker stats,取首页第一次运行期间六个采样中 CPU 最高的那一个。

Go 和 Cot 用得最多。Cot 的首页每次请求都会加载全部 1 000 篇文章及其正文(发现 9)。

发现

每条发现都说明测量了什么、为什么会这样,以及如何证明。剖析数据来自同一测试装置上单次 25 s 的运行,每次一个应用,因此其 req/s 与上面的公平测试略有不同。凡是原因未经证实的,都会注明。

1. 计算:Go 快 8–17%

现象。Go 在 /compute 上达到 16 580 req/s;Rust 应用为 14 184–15 330。

原因。两个编译后的循环都是 8 条指令,含一条 64 位 sdiv 和一次除零检查。差别在于 LLVM 选择的指令顺序。究竟是 CPU 的哪个微架构细节让这种顺序更慢,尚未证实:Docker 虚拟机没有硬件计数器。

证明方法。单独运行这个循环,单核,不经过 HTTP:

素数循环,单次调用

单核,不经过 HTTP

微秒 · lower is better
  • Go
  • Rust
Go 194 微秒lowest
Rust 224 微秒
Rust,采用 Go 的指令顺序 194 微秒lowest

采用与 Go 完全相同指令顺序的 Rust,运行时间与 Go 相同。每个请求的其余部分在 Rust 中更便宜:8 µs,而 Go 为 14 µs。

延迟。Go 的 p50 更低,但 p99 更高。每个应用的平均延迟都等于 50 / req/s,因此平均值跟随吞吐量;差别在于分布。Go 不按顺序处理请求:p10 0.56 ms,p99 10.75 ms。Rust 应用几乎按先进先出处理:axum + sqlx p10 1.85 ms,p99 6.07 ms。Go 的 p99 为何接近 10 ms,尚未证实。

2. 缓存页面:Go 的垃圾回收器

现象。在缓存首页上,Go 达到 91 839 req/s;axum + sqlx 为 198 669。

原因。Go 在这个路由上每秒执行 441 次垃圾回收,stop-the-world 停顿合计约占 10% 的时间。其余差距是每个请求的 CPU:Go 为 27.2 µs,axum + sqlx 为 14.6 µs。

证明方法。调高 GOGC 可消除大部分差距:

Go,缓存首页,调优 GC

之前:默认 GOGC=100。剖析运行。

beforeafter
GOGC=400, req/s 97 289 req/s 144 083 req/s +48%
GC 几乎关闭,req/s 97 289 req/s 159 734 req/s +64%
GC 几乎关闭,p99 2.48 ms 0.90 ms −64%

剩余 CPU 的去向,按缓存首页上每个请求计:

Goaxum + sqlx
应用 CPU27.2 µs14.6 µs
内核写路径10.6 µs6.9 µs
系统调用10.7(6.6 read,2.9 write)4.8(2.0 recvfrom,1.4 writev)
Redis CPU6.8 µs2.5 µs
Redis 流量go-redis:每个请求一次 GET一个连接,多个请求的 GET 合并在同一个 pipeline 中

Go:每个缓存请求的 CPU

总计 27.2 µs

  • 39%内核写路径,10.6 µs
  • 61%其余全部,16.6 µs

axum + sqlx:每个缓存请求的 CPU

总计 14.6 µs

  • 47%内核写路径,6.9 µs
  • 53%其余全部,7.7 µs

3. 详情页:Postgres 往返次数

现象。详情页:Go 22 607,axum + sqlx 22 842,Rocket 24 561,Rustango · axum 25 788 req/s。它们发送的都是同样的三条查询。

原因。与 Postgres 的往返次数不同。pgx 每条查询一次往返。sqlx 在从连接池取出连接时多一次(获取 ping),归还时再多一次。axum + sqlx 每条查询取一次连接,所以每条查询都要付出这笔开销;Rocket 和 Rustango 每个页面只取一次连接,所以只付一次。每多一次往返,约消耗 10 µs 的 Postgres CPU。

证明方法。在负载期间抓取 Postgres 协议流量,统计每个页面的 ReadyForQuery 消息数:

每个详情页的 Postgres 往返次数

协议抓包,同样的三条查询;ping = sqlx 的获取 ping

次往返 · lower is better
  • Go
  • Rustango
  • Rust
Go · pgx 3 次往返lowest
Rustango 4 次往返
Rocket 5 次往返
axum + sqlx,关闭 ping 6 次往返
axum + sqlx,开启 ping 9 次往返

首页的次数与此相同。

axum + sqlx:关闭获取 ping

剖析运行

beforeafter
详情页,req/s 18 222 req/s 23 472 req/s +29%
详情页,Postgres CPU 166.6 µs/req 134.3 µs/req −19%
首页,req/s 12 140 req/s 14 419 req/s +19%
首页,Postgres CPU 237.9 µs/req 208.5 µs/req −12%

少三次往返,每个页面节省 29–32 µs 的 Postgres CPU:每次约 10 µs。

Go 的往返次数最少,但在这里并不是最快的:在这个路由上,它的垃圾回收器约有 9% 的时间让它停下。

4. 首页:GC 与模板渲染

现象。首页:Go 8 741 req/s,axum + sqlx 14 140。

原因。两个已测量的原因。一是 Go 的垃圾回收器(在这个路由上停顿合计约占 8% 的时间),二是模板渲染:Go 的 html/template 使用反射,每个请求耗费 167 µs;Rust 中的 serde + Tera 耗费 72 µs。

证明方法。

Go,未缓存首页,调优 GC

之前:默认 GOGC=100。剖析运行。

beforeafter
GOGC=400, req/s 9 359 req/s 12 848 req/s +37%
GC 几乎关闭,req/s 9 359 req/s 13 967 req/s +49%

模板渲染,首页

每个请求的 CPU,来自 CPU 剖析

微秒 · lower is better
  • Go
  • Rust
Go · html/template 167 微秒
Rust · serde + Tera 72 微秒lowest

根据剖析,axum + sqlx 在首页上的 CPU 去向(各部分有重叠:内存分配发生在 sqlx 和 Tera 内部):

  • sqlx:每个请求约 112 µs,其中约 25 µs 是连接归还连接池时的 ping
  • glibc malloc:约 56 µs
  • 内核 sendto:38 µs

5. Rustango:修复带来了什么变化

现象。修复之前对 Rustango 的剖析发现了四项开销。其中三项已修复:

修复前的开销CPU 占比修复
每条查询都从连接池借一次连接19–35%每个请求使用一个池连接:详情页 +26%
每次借出连接都 ping Postgres8–16%默认关闭 ping
CachePageLayer 以 JSON + base64 存储页面每次缓存命中 ~12–20%页面以原始字节存储
每次请求都重新构建并编译 queryset~2%尚未修复

Rustango · axum,修复前 → 修复后

剖析运行,每次一个应用

beforeafter
首页,req/s 11 215 req/s 15 849 req/s +41%
详情页,req/s 17 267 req/s 28 097 req/s +63%
首页,应用 CPU 286.6 µs/req 220.8 µs/req −23%
详情页,应用 CPU 164.7 µs/req 110.4 µs/req −33%

在公平测试中,运行在 actix-web 和 axum 上的 Rustango 是全部三个未缓存页面上最快的两个应用。

待解决。在缓存首页上,Rustango · axum 为 174 501 req/s,而 axum + sqlx 为 198 669。这一差距尚未剖析。

6. actix-web:共享连接池很慢

现象。在第 1 轮中,actix-web 应用在未缓存页面上落后于 axum,且只用了 400% CPU 预算中的不到 300%。

原因。只有一个共享连接池时,每条查询都会唤醒另一个运行时的线程。给每个 actix worker 分配自己的连接池分片(16 个连接中的 4 个)和自己的 Redis 连接即可解决。其机制——套接字由打开它的运行时驱动——属于推断;效果则是实测的。

证明方法。相同应用,共享连接池对比每个 worker 一个分片:

共享连接池 → 每个 worker 一个连接池分片

剖析运行

beforeafter
actix + sqlx
首页 11 094 req/s 15 108 req/s +36%
详情页 18 080 req/s 23 330 req/s +29%
首页,缓存 192 604 req/s 238 884 req/s +24%
Rustango · actix
首页 12 052 req/s 17 454 req/s +45%
详情页 23 032 req/s 28 693 req/s +25%
首页,缓存 190 889 req/s 237 962 req/s +25%
每个请求的上下文切换次数共享连接池每个 worker 一个池
actix + sqlx,首页4.041.45
actix + sqlx,详情页3.571.64
Rustango · actix,首页2.700.81
Rustango · actix,详情页2.341.03

每个请求的上下文切换次数约为原来的 1/2.5。增加 worker 没有帮助:8 个 worker 配共享连接池,在 actix + sqlx 首页上达到 11 904 req/s(4 个 worker、共享池:11 094)。

7. Rocket:数据库页面表现强劲,缓存页面最慢

现象。Rocket:详情页 24 561 req/s,领先 Go 和原生 sqlx 应用;缓存首页 138 224,是 Rust 中最慢的结果。

原因。在数据库页面上,Rocket 每个页面只取一次连接,进行 5 次往返(发现 3)。在缓存页面上,它每个请求消耗 24.1 µs CPU,而 axum + sqlx 为 14.6 µs;每个请求 8.1 次系统调用,对方为 4.8 次。剖析显示多了一次 writev(每个请求 2.24 次对 1.36 次),且 malloc 存在争用;futex 调用为每个请求 2.88 次对 0.91 次。

证明方法。在缓存首页上进行 CPU 计量、系统调用计数和 CPU 剖析。

8. Loco

现象。Loco:首页 10 290,详情页 15 635,缓存首页 153 243 req/s。

原因。SeaORM 发送的查询数与 Go 相同。在缓存页面上,Loco 每个请求消耗 21.0 µs:比 axum + sqlx 在 malloc 中多 2.4 µs,在中间件中多 1.2 µs。在数据库页面上,获取 ping 无法关闭(发现 3 展示了它在 sqlx 中的开销)。

证明方法。在缓存首页上进行 CPU 计量和 CPU 剖析。Loco 的数据库页面没有剖析,因此 ping 能解释多少差距尚未证实。

9. Cot:查询 API 决定了上限

现象。Cot:首页 2 018,详情页 9 282,标签页 3 065 req/s,但缓存首页达到 190 245。

原因。Cot 的查询 API 只有 filter、limit 和 offset:没有 ORDER BY、JOIN、IN 或 COUNT。为了显示最新的 20 篇文章,首页要加载全部 1 000 篇文章及其正文。页面需要 5、5 和 7 次查询,而不是 3、3 和 4 次。负载下内存为 22.6 MiB。缓存页面不经过 ORM,速度很快:每个请求 16.4 µs CPU。

证明方法。查询次数来自代码,内存来自公平测试,CPU 计量来自缓存首页。

10. 内存

见上方的内存图表。六个 Rust 应用在负载下使用 6.3–12.0 MiB。Go 使用 22.8 MiB。Cot 使用 22.6 MiB;它的首页每次请求都加载所有文章(发现 9)。

11. 共同开销

有两项开销出现在每个 Rust 应用中,且不属于任何框架:

  • Tera 渲染:在未缓存页面上约占 18–20% 的 CPU
  • glibc malloc:约 12%

换用更快的分配器(如 mimalloc 或 jemalloc)可能会降低第二项。这一点未经测试。

局限

  • 只有一台机器:Apple M3 Pro。其他 CPU 上结果的排序可能不同,尤其是计算(发现 1)。
  • 以读取为主的负载:没有写入,没有事务。
  • 应用运行在 macOS 上的 Docker 虚拟机中,而不是裸机 Linux。虚拟机没有硬件性能计数器。

结论:为什么 actix-web 上的 Rustango 快了这么多

Rustango · actix,首页

三次运行:第 1 轮、一次剖析运行,以及本轮的公平测试

req/s · higher is better
第 1 轮(0.60.6) 6 341 req/s
修复后,共享连接池 12 052 req/s
修复后,每个 worker 一个池 17 401 req/sfastest

两处改动,分别在两个地方。

1. Rustango 的修复。这些修复对 axum 和 actix-web 都有帮助:

  • 每一列按其类型只解码一次。之前解码占用 46% 的 CPU。
  • 每次从连接池借出连接前不再 ping:每条查询少一次 Postgres 往返。
  • 每个请求使用一个池连接:一个页面的所有查询共用一个连接,而不是每条查询各借一次、归还时各 ping 一次。

2. actix-web 的配置。axum 运行一个共享的多线程 Tokio 运行时。actix-web 为每个 worker 运行一个单线程运行时。当所有 worker 共享一个连接池时,连接的套接字始终由打开它的运行时驱动,因此当另一个 worker 使用该连接时,每次读写都会唤醒那个线程。于是应用的上下文切换多了 2.5×,4 个 CPU 只用了约 280%。每个 worker 一个连接池分片(16 个连接中的 4 个)和一个 Redis 连接,就消除了这些跨线程跳转:

  • Rustango · actix:首页 12 052 → 17 454,详情页 23 032 → 28 693,缓存首页 190 889 → 237 962 req/s
  • actix + sqlx:首页 11 094 → 15 108 req/s
  • 8 个 worker 配共享连接池并无帮助:actix + sqlx 首页 11 904 req/s

提速和上下文切换的减少都是实测的。套接字归属是最好的解释,但并未直接证明。axum 没有这个问题,因为它的所有任务都运行在同一个共享运行时上。

实践建议:在 actix-web 上,应在每个 worker 内部创建数据库连接池,而不是为所有 worker 只创建一次。

联系我 →