作者|吴迪本文整理自字节跳动基础设施服务框架高级研发工程师吴迪在CloudWeGo开源一周年技术沙龙活动中分享的演讲。技术沙龙的主题是《字节高性能开源微服务框架:CloudWeGo》。本文将从以下三个方面介绍CloudWeGo国内首个开源RustRPC框架Volo:CloudWeGo为何选择Rust语言进行探索;为什么它创建了RPC框架Volo;如何选择Rust语言和Go语言。1。CloudWeGo选择Rust语言探索的原因CloudWeGo官方宣布,新一代RustRPC框架Volo开源!很多朋友会有疑问,为什么CloudWeGo会选择Rust作为探索语言呢?本文首先介绍原因。Volo开源官宣:https://mp.weixin.qq.com/s/XcceLyKxWOVtoMIJBuwXWQ1.1Go代价深度优化难。Volo早期团队成员来自Kitex项目(CloudWeGo开源的Golang微服务RPC框架)。当时我们投入了大量的时间和精力对Kitex等相关基础库进行性能优化,但最后发现在Go中很难做到深度优化。我们只能做一些算法层面和实现层面的优化。如果我们想继续优化其他层面,比如指令层面的优化,是很难以低成本实现的。在大多数情况下,许多优化都是在与运行时和编译器作斗争。工具链和包管理不够成熟。比如在使用Kitex框架时,需要使用相应的Kitex工具生成代码,才能正常编译使用。虽然这种情况在Frugal工具成熟后可能会有所改善,但是如果更新了IDL,仍然需要使用Kitex重新生成相应的结构。这个问题不是Kitex的问题,而是Go语言本身的问题。Go语言在编译时不提供类似的能力。抽象能力弱Go语言的抽象能力比较弱,Go语言中的抽象不是零成本抽象,而是有代价的抽象。那么我们应该如何理解使用Go语言的三个成本呢?下面具体分析。1.1.1深度优化难点如图所示,这是一个简单的Kitex项目生成的代码示例。这两段代码的目的是在解析错误时,向上层返回一些信息。Kitex新版代码公开后,业务组的同学反映,他们的在线序列化和反序列化性能相差20%。经过调查,我们发现了这个变化。新版Kitex代码旧版Kitex代码此更改旨在为客户提供更多错误上下文信息。但是它会带来什么问题呢?如下图所示,它直接一对一生成汇编代码到主进程中,也就是说Go语言编译器会逐行翻译,不会重新排列。那么这会带来什么问题呢?由于我们主进程中的代码比正常进程有所增加,所以我们重点关注L1-icache-load-misses这一行。新版本的代码比旧版本的代码在L1指令缓存级别有更多的缓存未命中。高出20%,这就是我们的代码效率降低20%的原因。那么我们如何解决这个问题呢?我们的解决方案如下图所示。在err!=nil的情况下,直接手动添加一个goto语句,将所有的错误处理代码放在函数的末尾,也就是return之后。这相当于在编译器没有实现指令重排的情况下手动进行指令重排。最终优化的效果非常明显。可以看出,与前一个相比,缓存未命中减少了25%。以上例子只是使用Go语言进行深度优化时遇到的一些难点。在抽象能力上,使用Go语言也会遇到一些困难。1.1.2零成本抽象什么是零成本抽象?用过C++和Rust的同学可能对这个概念不陌生。零成本抽象是指我们不需要为未使用的功能支付编译和运行成本,即用户不需要为未使用的东西付费。相应的,如果用户对已经用过的东西没有进一步优化的余地,因为它已经默认提供了最佳实践。总结如下:不用的不用付费;你不能用你使用的东西做得更好。那么为什么Go语言中没有零成本的抽象呢?以Thriftcodec为例,我们首先使用了ApacheThrift,它抽象出TProtocolInterface和TTransportInterface以支持多种不同的Protocol和Transport组合,但是Kitex直接依赖于具体的BinaryProtocal实现(struct)。试想一下,ApacheThrift这样做的成本是多少?这就是Go中Interface带来的代价。Go中的接口是动态分布的,即在运行时通过类型元数据和指针动态调用需要的方法,运行时会多做一次内存寻址。但这还不是最关键的,最重要的是它会让编译器无法内联,无法做很多优化。一般比较注重性能的语言都会同时提供静态分布和动态分布抽象能力,而Go语言只提供Interface动态分布能力,可以理解为Go语言中的抽象和性能。这就是Go语言的抽象能力比较弱的原因。1.2SonicSonic是CloudWeGo开源的一个JSON库,已经被很多CloudWeGo用户使用。最初,这个库的组件如下图所示,其中2/3的代码由Assembly组装而成。Sonic库中仅有的27%的Go源码如下图所示。虽然算作Go代码,但实际上是汇编代码。所以我们可以得出结论,世界上最快的Go语言程序很可能是用汇编代码编写的。1.3性能最好的GoJSON库虽然Sonic使用了各种黑科技,甚至2/3的代码都是手动微调的汇编代码,但Sonic的整体性能还是不如Rust最全能的SerdeJSON库。如图所示,绿色直方图代表SerdeJSON库,蓝色直方图代表Sonic库。根据这个Benchmark,即使与C和C++库相比,Rust语言编写的库在各方面的综合性能也是最好的。试想一下,有多少Go组件能够接受如此大量的人为输入进行深度优化?这只是一个例子。事实上,我们之前在Kitex的很多优化,也是要和编译器和运行时做斗争的。所以我们意识到用Go语言做深度优化是非常困难的。1.4关于Rust我们为什么选择Rust作为语言?在回答这个问题之前,我们首先要了解语言。那么先介绍一下Rust语言的发展历史。1.4.1Rust的历史Rust语言是由GraydonHoare开发的。他是Mozilla的编程语言工程师,专门为该语言开发编译器和工具集。当时Mozilla想要开发Servo引擎,想要在拥有高性能的同时保证安全,所以选择了Rust语言。从2010年到2015年,Rust有GC。后来社区一致表示支持Rust一定要有高性能,所以GC被禁止了。2015年,Rust发布了1.0版本,也正式宣告了Rust的稳定性。Rust以三年为单位进行社区规划和迭代。从2015年到2018年,Rust实现了生产力的承诺,即它的工具文档和编译器变得更智能,对开发人员更友好。从2018年到2021年,Rust将对异步生态系统做出更多改进。之前的Rust是没有异步生态的,但是从2018年开始,正式引入了异步功能。1.4.2Rust2024从2021年到2024年,Rust有一个名为ScalingEnpowerment的2024计划。之所以取这个名字,是因为Rust有一个目标——“让每个人都能构建可靠、高效的软件”。Rust最受大家关注,也经常被大家诟病的,就是Rust的整个学习曲线非常陡峭,所以在这个计划中写了“Flattenthelearningcurve”。1.4.3Rust的三大优势2022年,众多开源项目呈现爆发式增长。在我们了解了Rust语言之后,我们发现它具有三个非常重要的优势:第一是高性能;二是安全性强;三是轻松协作。所以我们想尝试在服务端使用Rust语言来开发微服务,从而解决我们面临的一些性能问题。性能许多用户对性能有很高的要求,想知道Rust的性能如何。下图是各种语言的benchmark对比结果。可以看出Rust的性能非常好,远超Go语言,甚至优于C++。当然,我们要强调的是,这个Benchmark要求所有语言必须使用相同的算法,不做额外的优化。毕竟如果都是用汇编代码写的话,每种语言的性能都差不多。但是在真正的开发过程中,有多少代码能够经过如此多的人工精细优化呢?另外,可能有人会质疑Rust的性能比C和C++好。其实这是因为Rust对程序员的输入有更严格的要求,所以编译器可以做进一步的优化。安全性因为有大量关于Rust语言安全性的材料可用,所以我不会详细介绍。只声明一个重要的结论:Rust1.0之后,非Unsafe代码不可能出现内存安全问题。这个结论是有数学证明的,所以是非常可靠的。我们应该如何理解这个结论呢?你可以从它的推理入手,即:所有的内存/并发安全问题都是由不安全的代码引起的。也就是如果有安全问题,我们可以把调查限制在一个很小的范围内。因为毕竟绝大多数的Rust语言代码都是SafeRust,而不是UnsafeRust。协作Rust是一种真正通过工程实践形成的语言。它拥有非常智能的编译器、完备的文档、集群化的工具链和成熟的包管理,因此Rust非常适合协作。我们在使用的时候可以专注于逻辑功能的实现,不用担心内存安全和并发安全等问题。还有一个很重要的一点就是可以限制别人的代码,因为如果别人的代码有内存安全问题或者并发问题安全问题,将无法编译。所以在做CodeReview的时候,我们只需要关注逻辑功能的正确性即可,因为只要提交的代码能够编译提交,就不用担心安全问题。这虽然是Rust语言的优势,但也给用户带来了一些不便。我们经常听说Rust开发者辛苦,也是因为编译。1.4.4Rust的影响如下图所示。Rust连续七年位居StackOverflow最受开发者欢迎的编程语言榜首。此外,还有一个非常重量级的项目叫做“RustforLinux”,除了C语言之外,Rust是迄今为止Linux内核唯一接受的语言。这些成绩足以说明Rust在开源界的份量和影响力。2.创建RPC框架的原因Volo阐述了CloudWeGo选择Rust语言的原因以及Rust的优势。我还将解释创建Volo框架的原因以及Volo的特点。2.1生态状况Volo框架的产生与当时的生态状况有关。我们调查了整个社区的生态,发现没有可用的AsyncThrift实现。即使是社区最成熟的Tonic框架,其服务治理功能也比较弱,可用性不够强。更重要的是,在当时的Rust语言社区中,还没有基于GenericAssociatedType(GAT,Rust语言最新的重量级特性)和TypeAliasImplTrait(TAIT,另一个重量级特性)的简单易用的框架).抽象的。2.2Easeofuse为什么要分别解释GAT和TAIT的两个特点?据Rust官方团队介绍,这是自Rust1.0以来语言层面和TypeSystem层面的最大变化。例如,下图是一个现有的社区解决方案。代码是在不使用GAT和TAIT超时中间件的情况下编写的。我们可以发现,如果要保证性能不丢失,需要写很多代码。在Volo框架中,因为使用了GAT和TAIT这两个特性,所以代码的写法如下图所示。我们可以很明显的对比代码量和易用性之间的差距是非常明显的。Rust以难以学习和使用而闻名。我们希望尽可能降低用户使用Volo框架和Rust语言编写微服务的难度,为用户提供最符合人体工学和直观的编码体验。因此,我们将框架的易用性作为重要目标之一。只有每个人都真正使用Volo,Volo才能体现它的价值。因此,Volo框架基于GAT和TAIT特性,大大提高了用户编写中间件的便利性。此外,我们提供了Volo命令行工具来生成默认布局,Volo命令行工具提供了管理IDL的能力,这在业界是首创的。我们还提供了过程宏等功能,可以进一步降低Services的编写难度。当然,还有很多其他的精心设计。例如,许多API都尽可能以最符合人体工学的方式给出,以避免误用。2.3可扩展性基于服务的抽象受益于Rust强大的表达和抽象能力。开发者可以使用非常灵活的Service抽象,以统一的形式处理RPC元信息请求和响应,如服务发现、负载均衡等,服务管理功能就是直接实现Service。基于RPC元信息的控制另外,在我们的框架设计中,所有的框架行为都是由RPC元信息控制的。因此,我们只要修改Service中的RPC元信息,就可以直接控制框架的行为来实现需要的功能。下图是Volo内置负载均衡中间件实现中最关键的部分,也就是红色线框圈出的代码。只需将LoadBalance选择的地址放入RPC元信息中即可,其他代码可以直接忽略。2.4性能如果过多地谈论框架的性能比较,很容易引起战争。不过基于Rust语言的性能优势和CloudWeGo团队对极致性能的追求,我们可以期待Volo的性能也非常高。跨语言比较Volo和Kitex是不公平的,但是由于很多用户关注的是性能数据,为了让用户对Volo框架的性能有一个大概的了解,我们只给出比较简单的性能数据。在与Kitex相同的测试条件下(极限4C),Volo极限QPS为35W。同时,我们正在内部验证基于Monoio(CloudWeGo开源的RustAsyncRuntime)的版本,极限QPS可达44W。当然,还有很多其他的性能指标,比如响应时间,也会极大地影响用户体验。所以除了Benchmark,我们还挑选了两家从Go迁移到Volo框架的业务来呈现真实的业务落地收益。业务A(代理类)。一个企业有很多IO。迁移到Volo框架后,各方面的数据如下:CPUUsage630%->380%MEM9GB->2GBP99150-200ms->20-35msAVG4-5ms->1.5ms可以看出,无论of无论是CPU、内存还是延迟指标,都有非常明显的提升。下图中间的红线代表Volo上线的时间,即红线左边的部分是Go的指标,红线右边的部分是Rust的指标.通过左右两边的对比,我们可以更直观的看出Volo框架给业务A带来的好处。业务B(有很多业务逻辑)。业务B是计算密集型业务。使用Volo框架后CPU为400%->130%。因此,在计算密集型业务中,CPU的提升更为明显。2.5相关生态随着Volo框架的开源,所有的开源生态加起来如下:Volo是RPC框架的名称,包括Volo-Thrift和Volo-gRPC。Volo-rs组织:Volo的相关生态。Pilota:Volo使用的Thrift和Protobuf编译器和编解码器的纯Rust实现(不依赖于protoc)。Motore:Volo是参考Tower设计的,使用了GAT和TAIT的中间件抽象层。Metainfo:Volo用于透传元信息的组件,定义了一套透传元信息的标准。全景图如下:2.6仓库地址以下是所有相关生态的仓库地址。欢迎大家提交Issue或PR共同打造Volo!Volo:https://github.com/cloudwego/voloVolo-rs:https://github.com/volo-rsPilota:https://github.com/cloudwego/pilotaMotore:https://github.com/cloudwego/motoreMetainfo:https://github.com/cloudwego/metainfo3。Rust语言和Go语言如何选择在了解了Volo框架后,对于Rust语言和Go语言如何选择,我有一些主观的建议和想法。3.1与C++和Go的比较如果Go的服务要用另一种语言重写,Rust语言和C++目前是比较可选的,所以我将这三种语言进行比较,以提供面临选择编程语言的用户一些参考。在学习难度上,Rust语言和C++的学习难度比较高,而Go语言的学习难度比较低。在性能方面,Rust语言和C++的性能比较高。我给Go语言的性能评价为中等。毕竟与Python等服务相比,Go语言还是要强很多。在安全性方面,C++的安全性比较低,Go语言的安全性中等,Rust语言的安全性比较高。因为Go语言虽然可以通过GC来防止一些内存安全问题,但是并不能防止像DataRace这样的并发安全问题,而且很多时候这样的问题其实是很难排查的。生锈是可防可控的,应尽量预防。只要有内存安全问题或者并发安全问题,就不能编译成功。在协作方面,Rust语言的协作程度较高,Go语言与C++??的协作程度中等。首先,C++没有官方的包管理工具,必须依赖第三方社区提供的包管理工具,但不同项目使用的包管理工具可能不同,这对用户来说很不方便;其次,当开发者能够保证自己的代码没有bug并且符合最佳实践时,难免会与一些第三方库和老社区的一流库重叠,造成混用;最后,如果涉及到需要团队合作开发的大型项目,我们不能保证团队中其他人写的代码不会出现内存安全问题。至于Go语言,它的编译时和工具链能力都比较弱,所以也被评为中等。在功能和使用成本方面,用户应该都有很好的了解,在此不再赘述。在使用成本方面,我给C++的使用成本评价为高,Go语言和Rust语言的使用成本中等。C++业务上线后经常出问题,出现问题难以排查是很常见的。使用Go语言做一些通用的编程是可以的,但是一旦涉及到定制化的需求,实现起来就有一定的困难。比如需要根据不同的平台系统做系统级的编程,用Go语言就很麻烦。.语言只是一个工具,我们还是需要根据不同的场景选择更合适的语言。那么为什么使用Go语言和Rust语言的成本是中等的呢?因为我们不能只关注写代码的效率,还要考虑运维和Debug的成本。Go语言也可能会产生panic,我们内部经常会出现一些并发问题,需要不断检查。Rust语言预付了这部分成本。相对于其他语言框架上线后测试保证稳定性,我们把这部分时间和精力花在开发期,也避免了上线意外带来的损失。所以我将Go和Rust评为中等使用成本。3.2Rust&Go如果分别比较Rust语言和Go语言,应该如何解读?这是一个非常经典的问题。可以尝试从以下四个方面考虑:合作关系、取长补短。我们团队认为两者其实不是对立关系,而是合作关系,相互学习。说到底,语言只是一种工具,很多时候我们只是需要一个更得心应手的工具。(性能>>开发效率)||(安全>>开发效率)->Rust对于需要极致性能、繁重计算的应用,以及需要稳定性并能接受开发速度有一定损失的应用,推荐使用Rust。Rust在这类应用中可以发挥极致性能优化和安全性的优势。迭代速度要求高->Go对性能不敏感的应用,IO密集型应用,以及需要快速开发和快速迭代而不是稳定性的应用,推荐使用Go语言。将Rust用于此类应用程序不会带来明显的好处。考虑团队的技术储备和人才储备当然还有一个很重要的考虑,就是团队现有的技术栈,也就是技术储备和人才储备。4.总结希望以上内容可以让大家对Volo及相关生态有一个初步的了解。目前,Volo还处于早期开发阶段。欢迎有兴趣的同学加入我们,共同建设CloudWeGo和Rust开源社区。我们真诚期待更多开发者的加入,也期待Volo能够帮助越来越多的企业快速构建云原生架构。
