Scale-Up vs Scale-Out:为什么每种语言都有其胜出之处

发布: (2026年4月18日 GMT+8 08:18)
15 分钟阅读
原文: Dev.to

Source: Dev.to

Introduction

我曾与一个团队合作,将一个关键服务从 Go 重写为 Rust,理由是“性能”。六个月后,服务速度提升了 30 %,但团队却非常痛苦,功能交付速度降到了爬行状态。与此同时,仍在使用 Go 的竞争团队已经发布了四个新功能。

我们最终做了事后分析。该服务在一台 4 核机器上大约每秒处理 2,000 次请求,CPU 利用率只有约 20 %。Rust 带来的额外速度并没有带来任何实际收益——瓶颈在于下游数据库的延迟。我们付出的代价是,在编写不安全代码、与借用检查器斗争以及帮助团队克服学习曲线的过程中,错失了所有未能发布的功能。

这件事让我领悟到一个我本该更早明白的问题:你到底在扩展什么,语言能为你提供哪种类型的扩展?

TL;DR

语言基准测试只针对一个维度进行优化:每请求性能。真实系统有多个维度——吞吐量、延迟、并发性、开发者速度、运维复杂度、内存效率。Rust、Go、Java、Python 并不是在争夺“最快”。它们是针对不同扩展假设的不同答案。根据适配度而非排行榜来选择。

可伸缩策略

垂直扩展(Scale‑up)

  • 让一台机器做更多。
  • 更快的 CPU、更多的 RAM、专用硬件,降低每次操作的成本。

横向扩展(Scale‑out)

  • 增加更多机器。
  • 更便宜的通用硬件、更高的并发,大量工作并行运行。

这些不仅是基础设施决策。它们体现在你选择的语言和生态系统中。针对垂直扩展优化的语言(Rust、C++)有不同的优先级,而针对横向扩展优化的语言(Go、Elixir)或既不针对扩展而是针对开发者效率的语言(Python、Ruby)亦是如此。

语言适配图

混合坐标轴是导致混乱的主要原因。

  • “Rust 比 Go 更快” 在每次操作的微基准测试中成立,但如果你的工作负载是 I/O‑受限的服务间流量,这一点并不重要。
  • “Python 很慢” 在计算密集型循环中成立,但对一个 500 QPS、95 % 时间在等待 PostgreSQL 的 API 来说并不相关。

面向纵向扩展的语言

当每台机器的吞吐量成为瓶颈且你能够承担工程成本时,这类语言占据主导。问题的范围比很多人声称的要窄,但确实存在:

  • 高频交易引擎——微秒级别至关重要,GC 暂停不可接受,每一条缓存线都很重要。
  • 推理引擎——llm.cppvllmmistral.rs。内存布局、SIMD、自定义内核。
  • 数据库和存储引擎——ScyllaDB、TiKV、Foundation 内部实现。必须永久运行且不能泄漏的状态机。
  • 网络数据平面——Cloudflare 的 Pingora、边缘代理。
  • 游戏引擎、音视频编码、嵌入式系统。

模式: 单箱长期高负载运行。内存安全重要,因为错误会随时间累积。性能重要,因为每核吞吐量是关键乘积。

成本: 每一次提交都更慢。重构代价高。上手时间以月计,而非周。编译时间本来就如此。只要服务存在,你就每天都要为此付出代价。

面向横向扩展的语言(Go)

Go 命中一个特定的甜点:廉价并发、可预测的性能、快速交付的代码以及易于招聘。它是一种横向扩展语言。

  • 每核数千个 goroutine,2 KB 栈,用户态上下文切换。“再多一个等待者”的成本几乎为零。
  • 标准库覆盖约 80 % 的后端工作——HTTP 服务器、JSON、SQL、加密。
  • 编译速度足够快,保持在开发流中;迭代循环的感觉类似动态语言。
  • 极简主义非常激进。一个人可以在一个周末读完整套语言文档。新员工在几天内即可产出。

失效之处: 每次操作的性能。Go 的 GC 还算可以,但并非无感。零拷贝的通用代码比 Rust 更难编写。类型系统并不能阻止 Rust 能防止的整类错误。

Go 的赌注是:你最可能遇到的问题是“我需要用两倍的代码处理十倍的并发工作”,而不是“我需要让这个循环快 5 %”。对大多数后端服务而言,这个赌注是正确的。

面向 JVM 的语言(Java / Kotlin)

当工作负载是横向扩展且需要 Go 所不具备的运行时灵活性时,JVM 是你的选择:

  • 成熟的 JIT 能在热路径上进行超越 AOT 的优化。
  • 丰富的分析与监控工具(JFR、async‑profiler、flight recorder)让部署后的调优成为可能。
  • 经过 25 年沉淀的库生态,几乎可以找到任何需求的成熟库。
  • 在其上使用 Kotlin 能提供现代语法和协程,同时仍然保持在同一生态系统内。

失效之处: 启动时间、内存开销、运维复杂度(GC 调优是一项真实工作)、偶尔出现的特定版本怪癖。相较于 Go,招聘难度在许多市场更大。

Java 的赌注是:“十年后你仍在运行这项服务,并且希望在那一天能够调优其运行时”。对拥有深厚基础设施的大型企业而言,这个赌注是值得的。对刚起步、仅交付前三个服务的创业公司来说,这些开销并不划算。

面向团队的语言(Python / Ruby)

这些语言既不针对纵向扩展,也不针对横向扩展,而是优化团队本身。

  • 编写快、阅读快、调试快。
  • 拥有庞大的数据、机器学习、脚本、DSL 库。
  • 对计算机科学学生、数据科学家、分析师的上手门槛低。
  • 从原型到生产的路径比任何其他语言都短。

失效之处: 每核吞吐量、并发(GIL 真实存在)、内存使用。Python 和 Ruby 并不是构建 100 K QPS 服务的语言。

但许多真实公司并不需要 100 K QPS 的服务。他们需要的是让功能跑起来,面向用户上线,并快速迭代。如果你当前面临的问题是“我们需要…”。

to ship the next feature this week,” Python might be the right answer even if a Rust version would technically run faster.

Python’s bet: throughput isn’t the constraint yet. Time‑to‑shipped‑feature is. For most companies most of the time, that’s correct.

Source: … (保持原样,不翻译)

决策框架

  • “我可以在周五之前交付一个功能并让它上线”往往胜过“这个服务快了 2 倍”。请对其进行衡量。如果你当前的技术栈需要两天的流程才能部署一行代码的改动,那么瓶颈不在吞吐量,而在交付速度。
  • Scale‑up 在运营成本上比 scale‑out 更低。只需一台机器、一个进程、一套日志。Scale‑out 虽然提供更好的冗余,却会带来分布式系统的问题——一致性、顺序、部分故障、混沌工程。如果你的团队只有三个人,管理一个 20 节点集群的运维复杂度可能会消耗的时间比语言选择节省的时间更多。
  • 在云规模下,内存成本高昂。一个占用 2 GB 的 Rust 服务相较于需要 8 GB 的 Java 服务,每实例可节省 4 倍。乘以成千上万的实例后,“每次操作的性能”不再是关键指标——每 GB 的成本开始变得重要。
  • 在你的市场中人才池最深的语言,通常是新系统的最佳选择,其他条件相同的情况下。边际的技术提升不足以抵消六个月的招聘周期。
  • 有些语言上手快(Go、Python),且具备深度的长尾。另一些语言上手慢(Rust、Haskell),只有在经过一定的学习曲线后才能发挥生产力。对于长期维护的系统,资深团队可以接受陡峭的学习曲线;而对于快速迭代的团队,陡峭的学习曲线成本高昂。

真实世界的演进

  1. 从小做起 – 选择 Python 或 Ruby,构建产品并投入生产。十名员工,一个代码库,节奏快速。
  2. 扩展 – 单体系统出现裂痕。一些服务用 Go 重写,以获得并发性和运维简易性。少数性能关键的服务用 Rust 编写。数据基础设施运行在 JVM 上(Kafka、Spark、Flink)。一些内部工具仍保留 Python,因为团队熟悉且有效。
  3. 成熟 – 五年后,技术栈已多语言化。没有人后悔。后悔的只有那六个月试图让单语言栈超出舒适区的经历——Python 团队不断推动“再多点异步”,Rust 团队在本可以用 Go 编写的代码上与借用检查器搏斗,或者 Java 团队解释为何堆栈跟踪长达 400 行。

模式:选择适合服务的语言,而不是让服务去适配语言。

当有人提议“我们用 X 来构建这个新东西”时,我会问:

  1. 预期的流量特征是什么?每个请求的工作负载形态如何?
  2. 这是受限于垂直扩展(单机吞吐)还是水平扩展(并发工作)?
  3. 谁来编写这部分代码?我们需要他们多快达到生产力?
  4. 谁来运维?他们对工具的熟悉程度如何?
  5. 是否需要与现有生态系统交互(JVM 数据平台、Rust 安全基础设施)?
  6. 预期的寿命有多长?

对这些问题的回答通常会让我在约 80% 的系统中选择三种语言之一:Go、Rust,或(针对数据相关工作)JVM 上的 Kotlin。Python 仍然用于工具和胶水代码。其他语言则视具体情境而定。

结论

基准测试并没有帮助。每操作的微基准只是在回答没人真正关心的问题。正确的问题是:哪些维度对这个系统重要,哪种语言的优势与这些维度相匹配。

我仍然看到工程师在争论 Rust 或 Go 哪个“更好”。两者都是优秀的语言。对于它们未被设计来解决的问题,它们都是不合适的选择。真正有意义的问题是,你在为哪种规模付费——而诚实的答案几乎总是两者的混合,并且会随时间演进。

我开头提到的 Rust 重写并不是因为 Rust 是一种糟糕的语言而做出的错误决定。错误的原因是我们并不是受限于横向扩展(scale‑up),而是受限于下游数据库(downstream‑database)。没有任何语言能够解决这个瓶颈。

了解你正在购买的规模,并有目的地去购买它。

Further Reading

  • 为什么 Go 能处理数百万连接:用户空间上下文切换,解释 — Go 的规模化决策背后的设计。
  • Go 的并发关注结构,而非速度 — 你实际从 Go 中获得的东西,以及没有得到的东西。
  • NATS vs Kafka vs MQTT:同一类别,完全不同的工作 — 将相同的适配 vs 基准思考应用于消息传递。
0 浏览
Back to Blog

相关文章

阅读更多 »

地球日的活力

我构建的 History 按日历天在浏览器中保存;每个部分旁边的照片是真实的捆绑图像。可选的 Gemini API 路由可以添加温暖的教练……