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

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

1重写核心服务

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

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

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

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

尽可能保持系统安全性

即使系统变得越来越大,也让系统更易维护、更加健壮

获得更好的性能

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

2为什么选择Rust?

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

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

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

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

3两年后

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

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

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

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

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

4用于应用程序服务的Rust

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

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

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

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

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

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




转载请注明:http://www.aierlanlan.com/rzfs/4173.html