我们为什么选 Rust 重写核心服务?

两年多来,Kraken 的 Core Backend 团队一直在用 Rust 来对原本使用 PHP 编写的服务进行现代化改造,同时还在用 Rust 开发新产品、扩展功能集合并支持不断增长的加密货币交易活动。

重写核心服务

针对一个问题从头开始构建一个解决方案往往会给我们带来另一个问题。当原来的开发人员没有参与新解决方案的设计和实现时,这种情况尤其常见。还有一些情况下,新的方案理论上更好用,但是做起来费的时间太久,拖慢了系统响应需求的进程。虽然我们可以设法避免这些常见的陷阱,但不管怎样我们在重写之前都要三思而后行。

2011 年 Kraken 成立时,PHP 提供了一个兼顾执行安全性、速度和生产力的选项。彼时我们用 PHP 构建了那么多功能,实在令人印象深刻。但多年以来,Kraken 取得了长足发展,而 PHP 代码库开始变得难以扩展,很难共享知识并安全地做出较大的更改。这些核心服务处理的是分布式数据存储、加密和信息安全方面的事宜,这类技能组合在 PHP 开发人员中并不常见,他们通常更专注于在现有的 Web 和电商框架上构建内容。

总体而言,Kraken 已进入了爆发式增长阶段,代码库和工具都需要跟上脚步。考虑到这一点,动态类型的编程语言非常适合早期的构建阶段,但随着代码库的扩张和工程师人数的增加,代码维护起来愈加困难。强类型提供了保证(和格式化的文档),从而加快了开发速度,让单个代码库可以支持更多开发人员。

我们重写核心服务的主要目标是:

  1. 尽可能保持系统安全性
  2. 即使系统变得越来越大,也让系统更易维护、更加健壮
  3. 获得更好的性能

早在 2018 年初,我们就已经意识到,继续使用 PHP 并不是实现这些目标的最佳长期解决方案。

为什么选择 Rust?

2018 年初,Kraken 已经有了用 Go 和 C++编写的生产服务。尽管 Rust 提供了出色的性能、安全性和现代语言结构,但将其作为重写核心服务的语言选项还是一种赌注。

Kraken 非常注重安全性。因此,我们不想让 C++代码参与用户输入。即使是世界上最好的 C++团队(如构建 Windows™或 Chrome™的团队),做出来的代码中也有约 70%的 CVE 来自于内存安全性问题——诸如释放后使用、缓冲区溢出、两次释放等,这可能会导致内存访问控制和特权升级攻击。可是在 Java、Go 或 Rust 等语言中,这些漏洞是被彻底堵死的。

尽管 Go 可以抵御这类漏洞,但它不提供诸如泛型或求和类型之类的现代编程特性,结果会导致数据建模或重复问题。Kotlin 提供了一个更复杂的类型系统,并且像 Go 一样,它简化了异步编程,但是带有一个承载诸多遗产的 Java 生态系统。

再来看 Rust。它的可靠性和性能让它在加密货币和区块链项目中取得了成功。一些 Kraken 工程师开始拿它做实验,并视其为构建可以长期满足 Kraken 后端需求的系统的一种选项:性能匹敌 C++、现代语言构造有助于准确地建模业务逻辑和错误用例、对异步编程有着一流支持、编译时线程安全,还有充满活力的生态系统。Rust 的价值主张和社区取得的成功促使 Kraken 在 2018 年中开始用 Rust 来重写核心服务。

两年后

Core Backend 团队成绩斐然,如今同时负责现代化的 Rust 核心服务和仍在重写中的旧版 PHP 服务。同时,其他一些团队已经成功应用了 Rust:Kraken 的期货团队加入了我们的行列,他们独立地将所有后端堆栈迁移到了 Rust 上;Cryptowatch 选择了 Rust 用于桌面应用程序;Kraken 将冷存储系统迁移到 Rust;Kraken Digital Asset Bank 也在用 Rust 构建。这种语言本身也有了显著改进,让异步网络服务编写起来更容易了。

总的来说,我们一直很忙:Core Backend 团队的 Rust git 存储库保存了约 500000 行代码,比 PHP 更多,尽管许多特性仍是在 PHP 中实现的。部分原因是我们用 Rust 编写了更多的基础代码、测试和全新的特性,另一个因素是 PHP 与其他动态类型化的编程语言一样,不需要类型化结构定义(包括错误),而 Rust 代码中这种定义占据了很大一部分。在 PHP 中没有那些显式结构,这让重写过程几乎成了一次逆向工程的演练。

从策略上讲,我们决定在 Rust 中重写完全相同的功能:由于所有 PHP 服务都是无状态的,因此可以轻松地将逻辑(逐个端点地)移植到 Rust。这样一来,新招募的团队就可以获取更多有关底层系统的知识,并可以进行增量部署或轻松回滚。我们已经构建了一个全面的集成测试套件,PHP 和 Rust 服务都需要通过它的测试以确保行为是相似的。将功能移植到 Rust 后,可以更轻松、更安全地扩展。

尽管性能提升不是重写的主要目标,但我们很高兴看到 Rust 提供了开箱即用的惊人速度。我们 Tokio 驱动的 RPC 服务器并未做过特别优化(尽管我们通常对内存使用模式非常谨慎),结果每个实例可以支持 150k 请求/秒的吞吐量,同时将 p99.9 延迟保持在 3ms 以下。系统的运行速度取决于最慢的部分,虽然我们的 PHP 核心服务不是 Kraken 的唯一瓶颈,但它们的 IO 性能要比 Rust 的低一些,并且对负载更敏感。在将整个端到端路径迁移到 Rust 并消除瓶颈之后,我们的客户应该能看到巨大的性能提升。同时,我们会将端点迁移至 Rust、重新设计数据库和扩展服务,尽一切努力来提高性能和可靠性。

这是将一个端点移植到 Rust 时响应时间的变化

用于应用程序服务的 Rust

Rust 通常被宣传为一种出色的系统编程语言,非常适合底层任务、命令行实用程序和网络服务(例如负载均衡器)。许多人认为 Rust 的复杂性对于一般的业务逻辑来说是很大的劣势,Rust 的就业市场也太小了,以至于公司很难使用这种语言来完成诸如构建用户管理系统或 REST API 之类的常见任务。

Rust 非常适合系统编程,但我们也一直用它来做一些通常用更高级别语言(例如 Java、Ruby 或 TypeScript)实现的应用程序服务。正确性在 Kraken 中绝对至关重要,而 Rust 的现代语言结构让我们更容易编写正确而健壮的代码。Rust 缺少垃圾收集的特性在编写不需要“关心”内存管理的通用逻辑时往往被认为是一种劣势,但在实践中这并不是问题,因为我们正在构建的是无状态服务,而存储循环数据从来都不是问题。

但 Rust 需要精确度,我想说的是这是这种语言最大的好处:它的显式性(受其强大的类型系统支持)带来了容易审查且运行时可靠的表达性代码。在这方面,我认为 Rust 与 Java 和其他同类语言比起来既有更低级别的优势,也有更高级别的好处。Core Backend 团队还开发了其他一些技术服务,例如负载均衡器或服务监视流,它们需要良好的性能,而且使用 Rust 让我们不必在系统和应用程序逻辑语言之间来回切换,还可以重用库和模式,实践中这非常方便。

随着团队和代码库的成长,有效审查代码的能力变得至关重要。Rust 可以让行为清晰且隔离地表现出来,这意味着我们无需过多考虑系统的其他部分——只研究当前函数往往就足够了。在审查代码时,我们会看到一个 diff(更改的行和周围的上下文),虽然可能需要更多时间来深入研究更改,但更快的审核可以让开发人员迅速获得反馈,这是很好的驱动力。在 Rust 中可以肯定的是,编译后的更改不会出现数据争用(并发错误的主要来源之一)和内存安全问题(我们的大多数代码都用的是 safe Rust)。我可以很容易地发现可能导致问题的函数(当没有其他选择时,Rust 会中止执行)、发现无用的内存副本,并收集开发人员的意图。Rust 的 linter、Clippy 有助于统一代码样式,带来了更符合习惯、更一致的代码库。最近两年来我审查了成千上万的合并请求,Rust 为我带来了比其他主流编程语言都更高的信心。

Rust 是一种大型而复杂的语言,开发人员很容易在细节上迷失方向。还好我们没必要为了保持效率而了解所有的细节。根据我们的经验,Rust 是一种非常有生产力的语言:它具有出色的工具链,可以迫使我们彻底建模问题、节省宝贵的调试时间、解决潜在的生产问题,并且非常便于代码重用(这是生产力的倍增器)。

最后,我觉得有必要澄清“fighting the borrow checker”这种说法,它把 Rust 编译器说成了一种怪物:以我的经验,这种情况主要发生在初学者中,另外就是很少一部分人试图对代码进行细节优化或探索极限情况时容易碰到它。大多数有经验的 Rust 开发人员很清楚怎样建模代码可以避免在编译器上浪费时间处理各种问题,并且一眼就能发现那些反模式,就像大多数人都知道如何正确地驾驶汽车来避免事故一样。

建立一个 Rust 团队

在这两年中,我们已经构建了现代化的 Kraken 后端技术栈的基础、将现有功能重写为 Rust、构建了新的 Rust 服务和功能,还组建了一支由 30 多名工程师组成的 Core Backend 团队。一些开发人员一开始应聘的是 PHP 开发,但加入团队后学会了 Rust。值得一提的是,Kraken 是一家全球化的远程优先公司,Core Backend 团队的工程师来自 15 个国家,工作地点分布在 12 国境内。

Rust 吸引了很多热情的开发人员,他们通常对系统编程、分布式系统或加密技术感兴趣。我们目前的 Core Backend 工程师中有很大一部分是 Rust 爱好者,他们通过各种 Rust 在线资源发现了我们的招聘机会,包括 Reddit 和 This Week In Rust(它们多次推荐了我们的招聘信息,谢谢!)。因为这些社区,人们很久以前就知道 Kraken 在招聘 Rust 开发人员了。

我们的 Core Backend 团队成员需要在竞争激烈的市场中应对具有挑战性的技术和业务问题,在世界各地进行远程工作,而我们提供了与地理位置无关的高额基本薪酬和慷慨的期权。此外,团队成员几乎所有时间都在编写 Rust。这两年来应聘的众多候选人很满意这样的条件,使我们得以组建世界一流的工程团队。

我们一开始也接纳对 Rust 感兴趣但缺乏上手经验的开发人员,但我们很快意识到这并不总是可行的,并且学习曲线因人而异。有趣的是,Rust 吸引了来自各种语言的开发人员,这些语言差异巨大,有的是静态类型,有的是动态类型。很难说来自哪些背景的人们学习起来会更困难,但有些人在几周内就可以入门,而另一些在几个月后仍然难以上手。习惯依赖文档并且对语义有系统理解的人们最有可能快速成长。像许多快速发展的公司一样,我们需要新员工迅速投入实际问题的工作中。因此,我们要求应聘者提供可证明的 Rust 经验,并且需要通过测试,检验他们是否全面了解 Rust 的类型系统以及标准库和常见板条箱的实用知识。

我相信使用 Rust 可以帮助人们成为更好的开发人员,因为它推动人们重视简洁的设计和精确度。但光是了解 Rust 并不能让我们成为出色的工程师。我们看到许多候选人对 Rust 都很满意,但他们在构建后端系统方面经验有限。我们雇用了许多有着巨大潜力的初级开发人员,因为在组建团队时,平衡是成功的关键。经验丰富的开发人员往往是出色的导师:他们通常拥有简化事物的智慧,知道不应该过于信任自己,也明白该如何最大限度地发挥自身对业务线的影响。

考虑到这种语言可以解决 C++、Java 或 Go 的许多痛点,我希望有更多经验丰富的开发人员进入 Rust 的世界。我之前从事 Java 开发工作有十多年的经验,向来对过度炒作的新技术保持合理的怀疑态度。但现在我并不想回去用一种素质不及 Rust 的语言——特别是 Rust 让我专注于手头的模块,而无需不断考虑许多隐式不变式,例如某个代码段是否是从另一个线程调用的,而我需要让它保持线程安全之类的问题。

我们希望随着越来越多的公司(从 Discord 和 Deliveroo 到亚马逊和微软)在 Rust 上加大筹码,我们可以帮助业界发出一个信号,告诉大家 Rust 的工作机会有很多,并且花时间学习这种语言不会浪费精力。许多经验丰富的开发人员更愿意留在他们擅长的技术栈中,但是有些人可能还是喜欢尝试摆脱自己的舒适区并挑战自我。

Rust 很伟大,但不是完美的!

Rust 让我们能够构建许多运行良好的高性能生产代码。我们有一个庞大的团队,成员分别负责后端中各种差异巨大的部分。大部分代码都非常健壮:我们还没有经历过 Rust 核心服务的崩溃或恐慌(大致相当于运行时异常)。

总体而言,我可以说我们只遇到过业务逻辑问题、配置错误问题,并且遇到了一个一般性的性能问题,其与在 musl libc 上运行的,具有特定内核配置的 Tokio 相关,不过我们用 perf 工具定位后就轻松修复了。

尽管这种语言的确很棒,但也有一些众所周知的局限困扰着我们。

  • 理想情况下,每个易错函数将具有自己的错误枚举来精确捕获其错误并处理,但实际上它过于冗长,结果导致了不太精确的错误特征(trait)或每个模块使用一个枚举。Rust 语言在这方面可以做得更好:有一些倡议和宏对此做了探索。
  • 在设计库箱时,缺乏 specialization 和通用关联类型(generic associated types,GAT)可能会带来很大限制。

我们一直在搭配使用 async Rust 与 Future 组合器,并在 async/await 支持进入 nightly 版本后立刻开始使用它。这项功能非常棒,使我们能够使用 Tokio 构建大规模的并发应用程序。我们并不需要花费太多时间来让我们的服务器处理超过 10K 的并发连接或实现背压。但它还是有一些改进余地:

  • 与 Rust 的大多数部分不同,async 函数看起来有点像无害的常规函数,但它们可能不会完全执行(更确切地说,它们返回的 Future 可能不会被轮询完成)。处理清理逻辑时要格外注意,因为逻辑本身目前不能异步。支持异步 drop 的提案有望提供一个解决方案。如何让这种问题更清晰可见仍然悬而未决;可以让属性清楚地表明 Future 可以安全地取消,否则就让一个 lint 警告提醒我们吗?
  • 尽管现在的情况有所好转,但异步框架的分裂严重损害了生态系统。如果 Rust 能够提供一种构造,允许任务调度子系统被抽象出来而无需额外开销,那会是很大的改进。这样人们就可以选择自己喜欢的执行器,并将任务执行器传递给库,或者自行驱动 Future。
  • 静态初始化任务执行器的当前设计使开发人员简单地拉出一个依赖项就能错误地运行多个执行器。线程局部变量的普遍使用加大了调试的困难。
  • 如果能够在特征中设计异步函数而无需装箱,并能引用结果类型,肯定会成为一项重大的性能改进。
  • 我们也希望看到围绕 io-uring 的工作带来巨大的性能改进,同时不至于造成生态系统的进一步分裂。

在工具链方面,Cargo 和 Rustup 大大简化了设置和编译项目的工作。RustAnalyzer 带来了显著的改进,并提供了很棒的 IDE 体验。编译时间总体变短了:想要更短也是可以的,但考虑到增量构建和 sccache,现在这样也不错了。优化构建确实速度很慢,但总体来说为了性能和安全性这是很小的代价。具有许可支持的私有 Cargo 注册表显然会助力 Rust 的企业应用。我们一直在使用 git 依赖项,但缺乏语义版本控制的支持让更新过程变得很痛苦。市面上有一些开源的 Cargo 注册表可用,但 Cargo 本身不支持访问令牌或凭证。我们很乐意赞助这项工作。

Kraken 热爱 Rust

总的来说,Rust 非常成熟,并且它的大多数痛点在其他主流语言也多多少少会存在。Rust 让代码重用起来非常轻松,并使我们能够在不牺牲性能的情况下安全地应对快速变化的大型代码库。

对我们而言,使用 Rust 不再是一项实验或赌注。这是我们正在构建的可靠技术,并且 Core Backend 团队正在寻找熟练的工程师。不得不提的是我们团队在 Rust 之外的价值观:Core Backend 团队利用 Rust 的工程价值和受 Netflix 影响的高性能团队文化,进一步扩展了 Kraken 的文化,以及对我们使命的承诺。我们相信超越代码的工程文化,相信自主权。我们拒绝傲慢自大。我们不断相互学习。不在一个办公室工作让我们面临更大的挑战,而积极进取的人们可以成为优秀的工程师,能够自我驱动、技术精湛,能够在需求和技术解决方案之间架起桥梁,从而自主前行。我们既看重天赋也看重努力,同时在工作与生活之间保持了良好的平衡,维持健康的体魄。我们关心自己在构建的事物,并全力帮助我们的队友取得成功。我们意识到完美是出色成果的敌人(众所周知,Rust 开发人员都是完美主义者!😉)。最后,我们相信团队会不断自我完善:如果技术或组织方面出现问题,我们会解决它们。

我们的 Core Backend 团队目前在招募中级和高级后端工程师以及站点可靠性工程师,这些人员可帮助支持和改善我们的运营、工具链和 CI。我们还在聘请测试工程师来帮助我们使用 Rust 和 Cucumber 测试 API。

Kraken 的其他团队也一直在寻找优秀的工程师,他们选择 Rust 作为构建强大和快速响应系统的首选工具:

  • KrakenDigitalAssetBank 是一家特殊用途的托管机构,使用 Rust 建立现代银行和支付系统,正在寻找高级工程师;
  • Kraken 期货团队两年来一直使用 Rust 作为主要语言来构建衍生品交易服务,他们以前用的是 Java 和 Kotlin,正在寻找后端工程师;
  • 我们的交易技术团队在建立现货交易的同时,还使用 C++和 Rust 构建大量服务,并正在招聘后端工程师;
  • Cryptowatch 构建了一个轻量级的桌面交易应用程序,他们也在雇用 RustGUI 开发人员。
  • 请务必检查我们的其他开放职位!

最后,我们想帮助 Rust 成长。我们已经通过 Kraken Grants 计划赞助了一些开源工作(例如 iced GUI 框架)。我们很乐意赞助 Rust 项目或相关关键项目的个人贡献者。如果你正在为 Rust 生态系统做出重要贡献并需要资金,请与我们联系!同时,RustAnalyzer 团队所做的出色工作给我们留下了深刻的印象,这些工作直接让整个社区受益,我们将为该项目捐款 5 万欧元!

原文链接:

https://blog.kraken.com/post/7964/oxidizing-kraken-improving-kraken-infrastructure-using-rust/?fileGuid=C3XtwPgtGkgvxpKj

本文文字及图片出自 InfoQ

你也许感兴趣的:

共有 1 条讨论

  1. admin  这篇文章, 并对这篇文章的反应是俺的神呀赞一个

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注