ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

告别 Go,拥抱 Rust

告别 Go,拥抱 Rust

Go 运行时里一个写死的120秒计时器,如何让生产监控出现周期性尖峰?Discord、Cloudflare 和 Vercel,又为何在部分核心服务中转向 Rust?

打开一个高吞吐 Go 服务的 Grafana 仪表盘,如果它在内存里保存着数 GB 状态,你可能会看到一条锯齿状曲线。

前119秒,一切平稳。CPU 使用率维持在20%左右,响应时间不到两毫秒。可到了第120秒,曲线突然冲高:延迟涨到原来的十倍,CPU 核心满载。两秒之后,尖峰消失,系统重新恢复平静,等待下一个两分钟。

没有定时任务启动,没有数据库备份,也没人发布新代码。

Discord 的工程师第一次在 Read States 服务中遇到这种诡异尖峰时,几乎把基础设施翻了个遍。问题不在 AWS 网络,也不在数据库集群。最后,他们在 Go 自身的运行时代码中找到了线索,具体位于src/runtime/proc.go:

// forcegcperiod 是两次 GC 周期之间的最大时间间隔,单位为纳秒。varforcegcperiodint64=2*60*1e9

两分钟。直接写在运行时里。

过去十年,Go,也就是 Golang,一直是云基础设施领域的宠儿。Rob Pike、Ken Thompson 和 Robert Griesemer 在 Google 创造了它,随后,Docker、Kubernetes、Terraform 和 CockroachDB 等项目相继出现。它的吸引力很直接:编译足够快,goroutine 让并发编程变得容易,性能又明显超过 Python、Ruby 和 Node.js。

如今,在软件工程最关注性能的那一端,一种分工正在形成。Go 依然适合云服务集成和 CRUD API,但构建代理、存储层、实时消息系统及构建工具的团队,开始在部分性能关键路径中,用 Rust 替换 Go。

一个很香的 AI 平台:GPT-5.6 低倍率且不降智 和 Claude Code 4.8 只要 0.25倍率,包含 image-2生图。重点是 首字请求都在 5s 内。入口:https://ai.aiyuhub.com

为什么 Go 最先胜出:应届毕业生理念

要理解企业为什么迁出 Go,得先看看2007年 Rob Pike 为什么会这样设计它。

当时,Google 面临的是一个具体问题:数百万行复杂的 C++,漫长的编译等待,以及不断加入的新工程师。庞大的代码库,需要一种更容易理解、也更不容易犯错的开发方式。

Rob Pike 在演讲中对此相当坦率。Go 的目标并不是挑战计算机科学的理论极限,而是让普通开发者也能迅速掌握语言,在大型仓库里开始工作,并交付可运行的网络服务,同时减少手动内存管理带来的问题。

为了实现这一点,Go 的设计者作出了两项重要取舍:

  • 简化类型系统和语言机制,不提供宏,也不让开发者直接管理常规内存释放。
  • 在编译后的程序中,加入并发垃圾回收器,以及管理 goroutine 的调度器。

对于大量后端微服务,这样的取舍非常划算。解析一份 JSON,查询一次 PostgreSQL,再返回 HTTP 200,主要时间本来就花在等待外部系统上。如果数据库查询需要五毫秒,一次几百微秒的短暂停顿,未必会成为主要问题。

但当你的服务本身就是数据库、缓存或者代理时,情况就不同了。

120秒陷阱:Discord 的临界点

看看 Discord 的 Read States 服务。每次打开 Discord,它都要告诉客户端:哪些频道已经读过,还有多少未读提醒需要显示。面对海量用户持续打开频道、发送消息,如果每次读状态更新都访问磁盘数据库,存储层很难承受。

因此,Discord 在 Go 服务里放了一份内存中的最近最少使用缓存,也就是 LRU 缓存,保存数千万条用户状态记录,再每隔30秒,把修改过的状态批量写入 Cassandra。

按理说,这应该非常快。实际运行中,它却每隔两分钟就出现一次严重的 p99 延迟尖峰。

问题出在缓存布局与垃圾回收之间的相互作用:

通常,Go 会根据GOGC等参数动态决定何时启动垃圾回收。默认配置下,目标堆大小大致围绕上一轮存活堆的增长比例计算。Discord 的工程师在启动时预分配了一大块 LRU 缓存,并重复利用其中的结构体,让堆大小保持稳定。他们希望借此减少 GC 的触发频率。

但运行时还有另一套机制。

当长时间没有发生 GC 时,内部的forcegcperiod机制会介入,触发垃圾回收。那份大型 LRU 缓存里包含大量需要追踪的指针,回收器仍然必须扫描相关对象,确认它们的存活关系。即使最后几乎没有内存可以释放,扫描本身也有成本。

工程师尝试调整GOGC,却没能解决根本问题。缓存变小,未命中次数增加,后端读取变慢;缓存变大,GC 需要处理的对象又更多,周期性扫描的负担更重。

于是,他们用 Rust 重写了服务。

Rust 不依赖追踪式垃圾回收器,而是通过所有权和借用规则,在编译期约束内存使用。对象退出生命周期时,可以直接执行相应的释放逻辑,不需要另起一轮全堆扫描来判断它是否仍然存活。

即便还没有完成优化,最初那份较为粗糙的 Rust 实现,就已经在各项指标上超过了经过大量调优的 Go 版本。上线之后,两分钟一次的延迟尖峰消失了,平均响应时间也从毫秒级降到了微秒级。

不只是 Discord:来自 Cloudflare、Dropbox 和 Vercel 的实例

如果只有 Discord 一个案例,或许还能把它归结为特殊工作负载。但过去几年,多个系统工程团队都记录过在关键组件中采用 Rust 的原因:

  • **Dropbox 的存储引擎 Magic Pocket:**Dropbox 离开 AWS S3、构建自己的 EB 级存储系统时,早期存储节点软件使用过 Go。随着部署规模扩大,运行时和垃圾回收带来的内存成本变得重要。当你需要采购成千上万台物理存储服务器时,每台机器多消耗的内存,最终都会变成可观的硬件费用。Rust 提供的内存控制能力,因此成为核心存储组件的重要考量。
  • **Cloudflare 的边缘代理:**Cloudflare 很早就大规模采用 Go,但在 Oxy 等新一代代理基础设施中选择了 Rust。全球边缘节点需要处理庞大流量,团队尤其在意可预测的尾延迟,以及对内存布局的细致控制。突发负载之下,这些要求会让运行时开销变得更加敏感。
  • **Vercel 的 Turborepo 迁移:**Vercel 收购的高速构建系统 Turborepo,最初使用 Go 开发,后来逐步迁移到 Rust。考虑因素不只是执行速度,还包括 Go 通过 cgo 调用原生库的成本、跨平台实现,以及与 SWC、Turbopack 等 Rust 工具体系共享代码的便利性。

硬件问题:逃逸分析与指针追逐

为什么在某些工作负载下,精心设计的 Rust 程序,能够比对应的 Go 实现消耗更少的内存和 CPU?

一个重要因素,是 Go 编译器替开发者处理的逃逸分析。

在 Go 中,变量最终放在栈上还是堆上,通常由编译器决定。如果编译器无法证明对象可以安全地留在栈上,它就可能被分配到堆中。通过接口传递、跨 goroutine 使用,或者保存到更长生命周期的数据结构里,都可能影响这一判断。

当大量对象进入堆中,又采用指针密集的组织方式时,有两类成本值得关注:

  • **分散的内存布局:**例如,[]*User保存的是一组指针,实际User对象可能散落在不同地址。遍历时,CPU 需要沿着指针继续读取数据,更容易遇到缓存未命中。接口值通常也包含类型信息与数据指针,具体开销取决于使用方式。
  • **GC 写屏障:**在并发垃圾回收期间,某些指针写入需要配合写屏障,帮助回收器正确追踪对象关系。这些指令不会直接出现在业务代码中,却仍然需要 CPU 执行。

Rust 则允许开发者更直接地控制数据布局。例如,Vec<User>可以把结构体连续放在一块内存里,减少元素之间的指针跳转,也不需要为追踪式 GC 执行写屏障。当然,Go 同样可以使用[]User这样的连续布局;真正影响表现的,是数据结构、分配方式,以及运行时机制的共同作用。

Go 仍然在哪些方面胜出,以及何时切换

这是不是意味着 Go 正在走向衰落?远没有。

如果团队只是从 PostgreSQL 取几行数据,再向 Stripe 发送 webhook,却为了一个普通业务 CRUD API 全面切换 Rust,很可能得不偿失。借用检查器、生命周期,以及复杂异步场景中的固定与投影机制,都会增加学习和实现成本,而业务收益未必能测量出来。

如果瓶颈是等待第三方 REST API,或者一条没有建好索引的 SQL,换成 Rust 也解决不了根本问题。Go 编译快、语法简洁,go test、pprof等工具也很成熟,依然非常适合应用后端、CLI 工具和 Kubernetes Operator。

不过,在控制平面与数据平面之间,越来越多团队开始作出不同选择:

  • **控制平面使用 Go:**API、编排逻辑、部署工具、认证服务和业务流程。这些地方通常更看重开发效率,而不是从每个 CPU 核心里挤出最后一点性能。
  • **数据平面考虑 Rust:**存储引擎、高并发网络代理、内存缓存、音视频处理流水线,以及在内存里长期保存数百万对象、对尾延迟非常敏感的服务。

Rob Pike 为行业带来了一门让数百万开发者更容易编写并发云程序的语言。这本身就是巨大的成功。

只是,语法上的简单,不代表整套系统没有代价。到了足够大的规模,垃圾回收的成本,终究需要有人结账。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进