一开始,我是做Go的语言开发,做Go的RPC框架,但当时我们遇到了很多Go语言的一些问题。
在Go里面,你想要深度优化是非常非常困难的一件事情,因为当我们体量变得越来越大的时候,深度优化是越来越重要了。但是如果在Go里面,想要去做一些深度优化,你经常会发现在跟runtime以及编译器做很多斗争,需要用一些很hack的办法去绕过它的一些限制。
Go里面的工具链和包管理相对来说不太成熟,如果有用过我们开源的Kitex框架的同学可能会非常了解。举个例子,比如在Go里面,想调一个gRPC的服务,或调一个Thrift的服务,需要调一个需要生成代码的服务,我需要先在开发的时候把代码生成好,要用一个命令行工具生成完之后,把生成代码一并地给提交到版本管理里面。直白来说,这是个很蠢的做法,像C++、Java,还有Python也许都会采用一些其他的方案,但是Go就必须这么操作,因为它编译器就没有这个能力,没有办法在编译时去生成这个东西。还有一点,比如我在编译时也许可以调个脚本去生成,但问题在于本地没有这个文件,代码生成、代码补全提示都没有,IDE会直接给你里面所有的就下划线飘红,这是个体验很差的事情。
Go里面的抽象能力是比较弱的,它没有零成本抽象这么个概念。
新版本代码
旧版本代码
大家可以看到这个是新的代码生成出来的汇编,在Go里面它的汇编生成或编译器,应该说非常非常不智能,它没有做一些像代码位置上的调整,或者没有做这种代码的指令的重排,就导致了比如我们刚才看到的error的错误信息,他直接把所有的错误信息或这些字符串全部插入到了正常的流程当中。带来了什么问题?带来了我们的L1的cachemiss的大量提高,因为L1的cache是会很大程度上影响到我们的执行性能、运行性能,所以就导致了性能的下降。
如何解决这个问题?你们可能会认为,编译器的问题是不是无法解决。后来我们用了个非常hack的办法,既然编译器不会去做代码重排,我们就只能自己做。我们自己把所有的错误都定义到了正常return语句的后面,在出错的时候用Goto跳转到后面,跳转到label上。大家可能在写代码或者学的时候都会听说过说Goto要慎用,尽量别用。但是在这种场景下,我们只能这么做。这个时候Goto语句被直接编译的时候,生成一条汇编的jmp指令,最后测出来的性能比之前的旧的版本即直接returnerror还要好。因为它的cache的miss直接从大概之前的2.4降到了现在的1.8,又提升了很多。
这个是一个很有意思的例子,也体现出了在Go上面我们想要做深度优化其实非常的困难。
还有一个就是在Go里面其实没有零成本抽象概念。零成本抽象的意思就是,如果我们没有用到的东西,是不需要为之付出代价的;而如果我们用到的东西,不管是编译器、标准库还是第三方库的提供者,都应该是做到最好的,不可能做得更好。可能写C++,还有写Rust的同学比较了解这个概念,但是在Go里面是没有的。
而为什么说Go里面没有零成本抽象概念,举个例子,我们做Thrift的编解码抽象,那么Apache官方的Thrift,是支持很多种不同的Transport和Protocol组合。底层传输层,还有上层的序列化层,它其实是有很多不同的协议的,比如transport层,有个叫framed的transport,有个叫buffered的transport,甚至还有一些像memory直接在内存里面。除了transport还有protocol,现在基本上是两种:Binary和Compact。大家知道,它有多种不同的组合。官方实现它为了去支持多种不同的这样的组合,用了Go里面的interface去做了抽象,但是我们后面把抽象给去掉了,我们直接依赖了一个具体的protocol的实现,也就是依赖了一个具体的struct。
那么为什么我们不用它的抽象?因为抽象是有代价的。代价就是在Go里面,它的interface是动态分发的,即运行时通过类型的元数据和指针,去动态调用所需接口,它可能会造成多一次的内存寻址。
但这并非最主要的,最主要是它会影响到inline。而且在Go里面没有提供一种零成本抽象的方案,它不像Rust里面有一个boxdyn,与interface很像。还有一种是静态编译式的,做静态分发,在编译的时候直接把类型给单例化出来了,这就是一个零成本的,但是Go里面没有。
还有一个项目其实是非常有意思的,我们CloudWeGo社区开源了一个叫做Sonic的项目,应该是在Go里面最快的JSON的序列化、反序列化的这么一个项目。这个项目为什么快?因为它的秘诀就是世界上最快的Go代码,不要用Go写,直接用汇编和C写就完事了。大家可以看Github上面代码语言的统计,实际上Go占27.1%。
大家会发现其实所有的Go的代码里面,基本上也是通过Go去生成汇编。所以这就是我们的结论,世界上最快的Go代码,不要用Go写,用汇编写就完事了。
但尽管JSON库用了非常多的黑科技去优化,但是我们可以看到,绿色的这一条是Rust里面比较经典的,叫serde库。serdeJSON这个库就是benchmark的结果,我们做benchmark发现它还是比不过这个rust的库。所以我们后面下定决心,想要去研究一下Rust方向,去尝试一下落地。
讲到Rust这个方向的语言,肯定要了解一下它的历史。Rust一开始是由一个名叫Graydon的人开发出来的,他是一个Mozilla的职业的编程语言的工程师。Mozilla当时想要去实现一个叫Servo的一个引擎,觉得这个语言很有价值,决定去使用它去赞助了这个语言。
在2015年的时候,Rust发布了1.0的版本。1.0版本其实就代表了一个稳定性的承诺。在2018年发布了1.31版本,1.31版本代表的是生产力。在edition2018时候引入了Asyncawait异步的Rust,在现在来看,我给他的评价是未来可期。
在2024的规划里面,因为其实大家也都听说过Rust的学习曲线比较陡峭,目前Rust官方其实也已经了解到了一些特别是async的Rust存在一些使用上的问题,并且也非常重视这个事情。所以它2024年的目标主要就是为了让Rust更加好用,更加易用,并且能够落地更多的项目。
Rust在我们看来有三大优势:性能、安全、协作。性能和安全可能大家都会比较了解,或者会听得比较多一些。
比如这是一个debian搞的benchmarkgame的一个结果,我选的是一个纯计算的case的结果。在里面可以看到,Rust语言其实是遥遥领先其他几个语言,特别是Go的,大概有4倍的提升。
有同学会问,为什么Rust会比C和C++性能还好,其实这也是因为Rust它对于程序员的一个要求,因为它的代码的限制更加严格,这就直接导致了编译器可以做更加激进的一些优化。所以它的性能在有部分时候是可以超过C和C++的。
这个结论可能大家听起来觉得有点云里雾里,其实它的一个推论更加重要,就是一切的内存和并发安全问题都是unsafe代码导致的。这就直接表示,如果在线上一个服务出现了coredump,或者出现一些内存安全并发安全的问题,不用去看safe代码,直接看unsafe代码就好。因为Rust里面unsafe代码是非常少量的,它不像C和C++,可以说全都是unsafe,如果去找可能不知道要找到何年何月。但是在Rust里面,只要去看变更中新增加了哪些unsafe的代码,这些代码肯定就是问题的源泉,这就是Rust的安全性所带来的好处。
我认为Rust非常适合协作,是因为它确实是一门真正工程实践出来的语言。它有非常智能的编译器,有完善的文档,有非常齐全的工具链,以及成熟的包管理。而且最重要的一点,你可以完全信任别人的代码,这个是在C和C++甚至在Go里面都做不到的。
在StackOverflow上,每年都会有开发者调研。Rust已经连续七年成为最受欢迎的语言,而且可以看到它离第二的差距挺明显。
我也简单介绍一下在业界上有哪些应用案例,因为一个语言除了在社区应用的比较广之外,被企业接受也是一个很重要的指标。首先在Meta(Facebook)接受,它已经是一个后端正式的支持语言。在我们公司字节跳动,在很多场景上也已经用到了Rust,特别是飞书。如果有用过飞书的企业可以了解一下,飞书所有的逻辑全都是Rust编写的,在Google、蚂蚁金服还有下面有很多的企业。
Rust的应用在最近也是越来越广。还有一个很重量级的项目叫做RustforLinux,这个是Linux内核至今为止,唯一接受的除了C以外语言,应该是相当重量级的一个代表。
我再简单做一个C和C++和Go的对比。我觉得学习难度上C++和Rust都是高,性能上C++和Rust也都是高。但是安全性上面,Rust其实是完爆这两个其他语言的,特别是在协作上。正如之前提到过的,对于C++来说没有一个原生的包管理工具,同时它也没有办法让你去信任别人的代码。使用成本上面,我综合认为C++使用成本比较高,Go和Rust使用成本都是中等。为什么说Rust使用成本是中等?因为使用成本不仅仅是开发的时候所要付出的成本,它还涉及到一个服务要上线之后,要让它进入到稳定状态中间的debug所需要的成本,以及你如果出事故所带来的一些损失,这些都是要考量在内的。综合下来,我认为Rust它的使用成本是中等。
这里又有一个问题了,为什么Rust这么好?它和C++是同一个level的语言,为什么C++做不到这么好?当然,其实我们都说软件工程里面没有银弹,这是因为C++的历史包袱确实太多了。Rust胜在没有历史包袱,所以它设计的时候就不像C++必须要兼容那种旧的使用模式。不能说更新个C++21,旧的代码全都编译不过,那么大家谁愿意做呢。Rust其实有点占了这方面优势。
接下来我为大家介绍一下,我们字节跳动做了些什么。这是一个很悲伤的故事、很悲伤的数字。在我们开始做的时候,其实公司内的生态是0,服务端什么都没有,什么都要自己开始建。
基础库大概是像日志、监控、链路追踪、mysql、redis、动态配置、mq这些属于我们认为非必须的、非常重要的一些基础库。这些可能是需要推广方,比如我们团队是作为推广方,去把这些全部建设起来。
接下来剩下一些非必须的基础库,可能是某一些业务单独的库,就可以发动群众的力量,因为它只要最基础的这些东西。比如它能够完成一个CRUD的基础服务,剩下的东西可以一边在开发业务过程当中,一边自己去写。
我们也准备了3个开发的框架:
第一个,基于Axum的Web框架。Axum算是tokio现在比较火的一个官方的HTTP的Web框架。
第二个,RPC框架,支持了GRPC和thrift,叫做Volo。已经开源在CloudWeGo的组织下面了,如果之后有RPC的需求可以直接来使用这个框架。第三个,异步的运行时的Monoio框架。这个主要是考虑到提供给一些性能非常关键的业务以及基础设施,就是基础架构的服务去使用。它的好处在于它采用ThreadPerCore模型,这样就可以解决Tokio的很多问题,比如它的future必须加Sync的一个问题。因为threadpercore的情况之下,它能保证一个task一定在一个线程中被运行,这样很多时候就不需要send加sync的约束,可以直接用TLS(threadlocalstorage)或者其他的这些技术,以及一些无锁的技术去编程,这可以很大程度上提高性能。第二个就是它采用了Linux最新发布的io_uring技术去做IO层,如果有对于性能要求非常高的同学可以去了解一下。
我们当时毕竟是吃螃蟹,肯定也遇到了一些问题。我们主要遇到开源库的bug,以及开源库不完全满足需求的问题。比如最近我们就遇到了一个业务,它在使用的是Snappy库,做压缩和解压缩的库。他发现压缩和解压缩的writer他是不能复用的,我们后面就自己给他提了PR去支持上了。
所以在吃螃蟹这个阶段,可能需要能够自己去解决问题,要去提PR,以及要自己Fork一些开源库出来去用的。这个要做好心理准备,因为这是我们真正实践上遇到的问题。还有一个可能跟技术没有特别强关联的问题:我们发现有很多一线的同学其实特别喜欢Rust、特别想用Rust。正如刚才提到,Rust是stuckworkflow上最受开发者喜爱的语言榜榜首,已经连续第7年。但是很多的leader管理者会担心,我们团队只有一个人会Rust,如果这个人他转岗了或离职了,这个服务是不是就没有办法维护了?这可能是很多的leader会担心的问题。
这时其实就需要我们作为推广方去介入,并且去帮助他去开发一些项目。这个时候可能不会让他去开发一个业务的服务,因为业务服务毕竟有时需要整组一起讨论、一起拍板之后才能选择某个技术站,但是个人的项目是可以用的。
很多对Rust非常感兴趣的的工程师,只是缺少有一个人带头,或者缺少一个契机。最重要的一点就是要寻找一些典型的业务去共同地开发、去获得收益。在我们看来,这些典型的业务有一个特征,就是proxy的业务,它是一个代理类的业务,重计算,但是逻辑是比较简单的,因此虽然我们在推动落地时需要一些前期投入,但是是值得的。
但是如果用我们的采用了GAT加TAIT这两个特性之后,代码量只需要这么多,这其实是一个非常明显的对比。其实我们很早就开始用了。
所以在实际推广的时候,可以去考虑把这些有用的特性都给打开。特别是有一个特性叫asyncfnintrait,在trait里面可以定义异步函数了。这个特性已经在nightly上达到了MVP,虽然存在一些问题,但起码是可用的状态了。
接下来为大家介绍一下我们的落地的一些成果。首先是有一个proxy类的业务,它的CPU的占用从大概630%降低到了380%,几乎是提升了一倍。第二个就是它的memory,也就是它的内存占用大概从9GB降到了2GB。然后它的P99和AVG也有非常大量的提升了。
还有某一个比较重要的线上业务,它的提升量也是非常明显。举个例子,它的成本是降低了50%。大家就会问:这些数据能证明什么、代表什么?我光知道好,CPU降低了,memory降低了,ABG降低了,延迟降低了,它能说明什么?我们来简单地算一下。
这是某云的一个价格截图,64核128G的机器,一个月的价格是6262元。因为某云买5年可以打3折,所以我就按照最优惠的方案给大家算。28000一年,相当于算下来是437元/CPU*年。业务使用了1万个核心,我们刚才计算下来成本减去了48.97%,我就按50%算,节省5000核,相当于一年节省的数量就是200多万。这就是一年省下来成本。
我们又有同学会说了,你没把开发的人力算上。我再把开发的人力给算上,已经是一个非常非常高的,几乎不可能达到的一个开发的成本了。因为我们服务大概就花了大概两三个月,写完就重构完了,我就算6个月,开发的加办公的成本,我也选了一个非常非常高的值,其实应该是达不到这个值的。假如他是120万,他第一年的净收益就是接近100万,再往后每一年都是纯收益了,已经没有成本了。当然实际上它的收益应该是远不止这个值的,因为CPU成本是没有这么便宜的。有做过成本核算、效益核算的同学,可能会比较了解一些它的成本的值。一个经验值大概是在1000一核,可以按照这个值去计算。因为除了CPU单纯的CPU价格之外,还有网络的成本,比如机房运维人员的成本,其实成本是远不止437一个CPU的。还有一点,如果AVG,也就是latency延迟有降低的情况之下,其实是能够带动业务的增长的。
Rust现在的现状:好用。它确实挺好用,它的功能很多,很符合人体工程学,但是它还不够好用。
其次,它的抽象能力、表达能力是挺强的,但是它的高阶抽象还有些问题,特别是写Rust。可能用过HRTB的同学会知道,HRTB配合GAT或和TAIT使用的时候会有些坑,但是使用场景实在太高阶,基本上业务开发是用不到的,所以也可以接受。第三,Rust的异步生态现在较为完善,但是它和同步的生态存在一些割裂。比如一个函数Fn,它不能同时是异步和同步的,异步版本和同步版本必须写两个不同的函数。
当然,其实Rust官方也已经感受到了这些问题,都在解决,特别在2024的roadmap里面都有提到这些问题。像Niko也在尝试一个新的方案,它希望一个函数可以同时是async以及sync的两个版本。如果调用方是async,它就会调一个async的,在编译时生成一个async的实现。他会做这样的一件事情。
还有一些比较好的消息是开发者非常喜爱,用户的忠诚度是非常高的。我相信写出来的同学应该不会再考虑转到转回到其他语言去了,起码我是不会再回去写go了。开源项目也是爆炸性的增长,特别是这两年,可以明显感觉到越来越多的开源项目采用Rust。不管是新增项目还是重构的项目,有越来越多的公司接受开始使用Rust。
Rust其实它的应用的方向非常非常的广,我大概简单列了一下,包括像RustforLinux其实已经相当于补足了。Rust应用的最后一块拼图,也就是OS层和嵌入式层,都是可以使用Rust去写的。还有一个最近很有名很火的一个方向,就是WebAssembly,基本上Rust也属于是第一梯队的,就是最佳的语言了。
目前面临的一些哪些挑战,第一个是Rust的职位不够多,人才不够多,其实是一个相互的关系,如果人才多起来,职位也会多。如果职位多,人才也会多。所以这可能需要的不仅是某一家公司去投入做什么,而是希望所有喜爱Rust的程序员,共同地一起去把整个生态建设起来。第二是Rust在中国的名声不够响,虽然现在已经在慢慢地提升当中了,但是它和Go确实还有一些的名气上的差距,通过培训班的数量就能看得出来。
第三是Rust缺少像K8S一样的杀手机应用。这也是大家一直提到的一点,下面是Rust官方做的调研的一个图,有22.51%的人认为Rust是forthemajorityofmycoding,他们大部分的主要代码都是用Rust写的。有17%的人说是自己所有使用的语言中之一,有18%的人说他只是偶尔去使用。但是这张图其实就说明了Rust语言它是否真的能够产生价值。
再下面这张图是Rust官方来问,你觉得Rust语言有没有帮助你真的去实现一些什么东西?有80%的人认为Rusthashelpedusachieveourgoal,Rust已经帮助我们达到了我们的目标。但是有一个坏消息是47%的人认为用Rust是challenge的,是有挑战性的。后面有70%。有80%的人认为Rust是值得我们的付出的,以及有90%的人认为我们在未来还愿意去使用Rust。