43

Java,Go和Rust间的比较

 3 years ago
source link: http://dockone.io/article/10579
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文对Java、Go和Rust之间的对比并非完全是基准测试,更多的是比较输出的可执行文件大小、内存使用情况、CPU使用率、运行时要求,当然会有一个小基准测试用于获取RPS数据,使得更容易理解这些数值。

为了尝试更合理比较这三者,我在这次比较中分别用每种语言写了个Web服务。该Web服务非常简单,提供了3个REST端点。

atUZLGwQsAn9kIb.png

三个Web服务的存储库托管在 GitHub 上。

制品大小

介绍下我是如何构建二进制文件的。在Java示例中,我使用 maven-shade-plugin 插件并使用 mvn package 命令,Go则使用 go build 命令,最后是Rust则使用 cargo build --release

FR9Z2JpDNfkxL58.png

制品编译的大小也取决于所选的库/依赖项,因此,如果它们膨胀了,那么编译的程序也会是同样的结果。在此处特定情况下,对于我所选择的库,上图显示是程序编译的大小。

在下面单独的部分中,我将构建所有三个程序并打包成Docker镜像,并列出它们的大小,以及显示每种语言所需的运行时开销。更多细节如下。

内存使用情况

空闲状态

IP5TQWK2O8ie7sX.png

什么?在空闲运行时显示内存占用的Go和Rust版本的条形图在哪?好吧,它们是有的,只是Java在JVM启动程序,处于空闲时,什么都不做的情况下,就消耗了高达160MB的内存。在Go的情况下,程序使用了0.86 MB,在Rust的情况下使用0.36 MB。这是一个非常大的区别!在这里,Java比Go和Rust对应的程序多用了两个数量级的内存,只是空跑内存什么也不做。这是对资源的巨大浪费。

提供REST请求

我们使用 wrk 来请求API,并观察内存和CPU使用情况,以及三个版本的程序的每个端点在我的机器上的每秒请求数。

wrk -t2 -c400 -d30s http://127.0.0.1:8080/hello 

wrk -t2 -c400 -d30s http://127.0.0.1:8080/greeting/Jane

wrk -t2 -c400 -d30s http://127.0.0.1:8080/fibonacci/35

以上 wrk 命令表示,使用两个线程(用于wrk),并在池中保持400个开启的连接,并反复调用GET端点,持续30秒。此处我只使用两个线程,是因为 wrk 跟被测试的程序都运行在同一台机器上,因此我不想它们在可用资源,尤其是CPU上相互竞争。

每个Web服务都单独测试,且每次运行测试都会重启Web服务。

以下是每个版本的程序三次运行中的最佳结果。

/hello

该端点返回一个“Hello, World!”的消息。它分配字符串 "Hello, World!",并将其序列化,以JSON格式返回。

ymkGQEJNbjnS6gY.png6IXsYy2UeGJjtxP.pngR394VA1Dgk6BJuT.png
/greeting/{name}

该端点接受段路径参数 {name} ,然后将字符串"Hello,{name}"格式化,序列化并返回以JSON格式的问候信息。

pYfNrQZCuaT3mMX.pngXgrnNVPMyh36zpC.pngUTesGtCvOWAw1xl.png
/fibonacci/{number}

该端点接受段路径参数 {number} 并以JSON格式序列化返回输入的数字和斐波那契数。

对于这个特定的端点,我选择用递归的形式来实现它。我知道,毫无疑问,迭代实现可以获得更好的性能结果,而且出于生产目的,应该选择迭代形式,但是在生产代码中,有些情况下必须使用递归(非特指专用于计算斐波那契数)。因此我想让这个实现与CPU堆栈分配密切相关。

Sl9MP5GDtTWKcx4.pngqNuHkMQIG1rpSmA.pngMZKRnxyXHlrgSkP.png

/fibonacci 端点测试中,Java实现是唯一一个出现150次请求超时的, wrk 输出如下所示:

lOYiezoUcEkMmjy.pngt3G5dUZiOxqsNDP.png

运行时大小

为了模仿真实世界的云原生应用,并消除"它在我的机器上正常!"这种情况,我为这三个应用分别创建了一个docker镜像。

Docker源文件包含在存储库中相应程序的文件夹下。

我使用 openjdk:8-jre-alpine 作为Java应用的基础运行时镜像,它是已知的最小的镜像之一。然而这带来了一些需要注意的事项,可能适用,也可能不适用于你的应用。主要是alpine镜像在处理环境变量名方面不符合posix标准,所以你不能在docker文件中使用带 . (点)的ENV(也不是什么大事),另一个是alpine Linux镜像是用musl libc而不是glibc编译的,这意味着如果你的应用程序依赖于需要glibc存在的东西,它就无法工作。就我而言,alpine很好用。

至于Go和Rust版本的应用,我使用了静态编译,这意味着它们在运行时镜像中不需要任意libc(glibc、musl等等),也意味着它们不需要一个带OS的基础镜像来运行。所以我使用了 scratch 镜像,这是一个no-op(无操作?)镜像,它托管编译后的可执行文件,零开销。

我使用的docker镜像命名约定是 {lang}/webservice 。Java、Go和Rust版本的应用程序的镜像大小分别是113MB、8.68MB和4.24 MB。

w3JGLCIjlpEgubf.png

结论

tQUCnF1jyE7d5ZB.png

在得出任何结论之前,我想指出这三种语言之间的关系(或者说缺乏关系)。Java和Go都是垃圾收集型语言,然而,Java是提前编译(AOT)为在JVM上运行的字节码。当Java应用程序启动时,会调用Just-In-Time(JIT)编译器来优化字节码,随时随地将其编译成本地代码,以提高应用程序的性能。

Go和Rust都是提前编译成原生代码,在运行时不会发生进一步的优化。

Java和Go都是垃圾收集类型语言,存在STW的副作用。意味着每当垃圾回收器运行的时候,它就会停止应用程序,进行垃圾回收,当垃圾回收结束后再从之前的状态中恢复。大部分垃圾回收器需要停止程序,但是也有一些实现不需要这样子。

当Java在90年代诞生时,它最大的卖点之一就是“一次编写,随处运行”。在当时这是非常棒的,因为当时市场上还没有很多虚拟化解决方案。如今,大多数CPU都支持虚拟化,在代码可以在任何地方(无论在任何受支持的平台上)运行的前提下,使用一种语言进行开发的诱惑就消失了。Docker和其他解决方案提供了廉价的虚拟化。

在整个测试过程中,Java版本的应用比Go或Rust对应的应用消耗了更多的内存,在数量级上,前两次测试中,Java使用的内存大约多出8000%。这意味着对于现实世界的应用来说,Java应用的运营成本更高。

在前两项测试中,Go应用程序的CPU使用量比Java少了20%左右,而服务的请求却多了38%。另一方面,Rust版本的CPU使用量比Go少57%,而服务的请求量却多13%。

第三个测试在设计上就是CPU密集型的,我希望尽可能地利用CPU。Go和Rust都比Java多利用了1%的CPU。而且我想如果wrk不是在同一台机器上运行的话,这三个版本的CPU都会达到100%的上限。在内存方面,Java比Go和Rust多利用了2000%以上的内存。Java比Go多服务20%左右的请求,而Rust比Java多服务15%左右的请求。

在写这篇文章的时候,Java编程语言已经存在了近三十年,这使得在市场上找到Java开发者相对容易一些。另一方面,Go和Rust都是相对较新的语言,所以相对于Java来说,自然而然的数量或开发人员就少了。尽管如此,Go和Rust都得到了很多关注,许多开发人员在新项目中采用了它们,并且有许多在生产环境中运行的项目使用Go和Rust,因为简单地说,它们在资源需求方面比Java更高效。(也可能是因为它们是比较新的酷炫语言)

我在写这篇文章的程序时,我学会了Go和Rust。就我而言,Go的学习曲线很短,因为它是一门比较容易上手的语言,而且语法相对于其他语言来说也很小。我只花了几天时间就用Go写好了程序。关于Go,有一点需要注意的是编译速度,我不得不承认,与Java/C/C++/Rust等其他语言相比,Go的编译速度极快。Rust版本的程序我花了一周左右的时间才完成,我不得不说,大部分时间都花在了借用检查器(borrow checker)上。Rust有严格的所有权规则,但一旦掌握了Rust中所有权和借用的概念,编译器的错误信息就会突然变得更有意义。Rust编译器之所以在违反借用检查规则时对你“谆谆教诲”(无情报错),是因为编译器希望在编译时证明分配的内存的生存期和所有权。通过这样做,它保证了程序的安全性(例如:没有悬空指针,除非使用了不安全的代码逃逸), 并且在编译时确定了释放位置,从而消除了对垃圾收集器的需求和运行时成本。当然,这是以学习Rust的所有权系统为代价的。

就竞争而言,在我看来,Go是Java(通常是JVM语言)的直接竞争对手,但不是Rust的竞争对手。另一方面,Rust是Java、Go、C和C++的有力竞争者。

因为它们的效率,我认为我自己将会用Go和Rust写更多的程序,但很可能用Rust写得更多。它们都很适合于Web服务、CLI、系统程序等等的开发。然而,Rust比Go有一个根本的优势。它不是一种垃圾收集的语言,而且与C和C++相比,它的设计是为了安全地编写代码。例如,Go并不特别适合用来写操作系统内核,这也是Rust的优势所在,它与C/C++竞争,因为它们是长期存在的、事实上的写操作系统的语言。Rust与C/C++竞争的另一个层面是在嵌入式世界,我们将在以后再讨论这个。

感谢您的阅读!

【原文链接】 Comparison between Java, Go, and Rust 翻译:冯旭松


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK