后端程序员一定要看的语言大比拼:Java vs. Go vs. Rust
这是Java,Go和Rust之间的比较。这不是基准测试,更多是对可执行文件大小、内存使用率、CPU使用率、运行时要求等的比较,当然还有一个小的基准测试,可以看到每秒处理的请求数量,我将尝试对这些数字进行有意义的解读。
为了尝试尽可能公平比较,我在此比较中使用每种语言编写了一个Web服务。Web服务非常简单,它提供了三个REST服务端点(endpoint)。
这三个Web服务的代码仓库托管在github上。
编译后的二进制文件尺寸
有关如何构建二进制文件的一些信息。对于Java,我使用maven-shade-plugin和mvn package
命令将所有内容构建到一个大的jar中。对于Go,我使用go build。最后,我使用了cargo build –release构建Rust服务的二进制文件。
编译后的文件大小还取决于所选的库/依赖项,因此,如果依赖项的身躯臃肿,则编译后的程序也将难以幸免。在我的特定情况下,针对我选择的特定库,以上是程序编译后的大小。
在后续的一个单独小节中,我会把这三个程序都构建并打包为docker镜像,并列出它们的大小,以显示每种语言所需的运行时开销。下面有更多详细信息。
内存使用情况
空闲状态
什么?Go和Rust版本显示空闲时内存占用量的条形图在哪里?好了,它们在那里,只有JVM启动的程序在空闲状态时消耗160 MB以上的内存,它什么也没做。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命令使用两个线程并在连接池中保持400个打开的连接,并重复调用GET端点,持续30秒。这里我仅使用两个线程,因为wrk和被测程序都在同一台计算机上运行,所以我不希望它们在可用资源(尤其是CPU)上相互竞争(太多)。
每个Web服务都经过单独测试,并且在每次运行之间都重新启动了Web服务。
以下是该程序的每个版本的三个运行中的最佳结果。
- /hello
该端点返回Hello,World!信息。它分配字符串“ Hello,World!” 并将其序列化并以JSON格式返回。
- /greeting/{name}
该端点接受一个段路径参数{name},然后格式化字符串“Hello,{name}!”,序列化并以JSON格式的问候消息返回。
- /fibonacci/{number}
该端点接受一个段路径参数{number},并返回序列化为JSON格式的斐波纳契数和输入数。
对于这个特定的端点,我选择以递归形式实现它。我毫不怀疑,迭代实现会产生更好的性能结果,并且出于生产目的,应该选择一种迭代形式,但是在生产代码中,有些情况下必须使用递归(并非专门用于计算第n个斐波那契数 )。为此,我希望该实现涉及大量CPU栈分配。
在Fibonacci端点测试期间,Java是唯一一个有150个请求超时的实现,如下面wrk的输出所示。
运行时大小
为了模拟现实世界中的云原生应用程序,并避免“它仅可以在我的机器上运行!”,我分别为这三个应用程序创建了一个docker镜像。
Docker文件的源代码包含在代码库相应程序文件夹下。
作为我使用过的Java应用程序的基础镜像,openjdk:8-jre-alpine是已知大小最小的镜像之一,但是,这附带了一些警告,这些警告可能适用于您的应用程序,也可能不适用于您的应用程序,主要是alpine镜像在处理环境变量名称方面不是posix兼容的,因此您不能在Dockerfile中使用ENV中的(点)字符(不过这没什么大不了的),另一个是alpine Linux镜像是使用musl libc而不是glibc编译的,这意味着如果您的应用程序依赖于需要glibc,它可能无法正常工作。不过,在这里,alpine镜像工作是正常的。
至于应用程序的Go版本和Rust版本,我已经对其进行了静态编译,这意味着它们不希望在运行时镜像中存在libc(glibc,musl…等),这也意味着它们不需要运行OS的基本镜像。因此,我使用了scratch docker镜像,这是一个no-op镜像,以零开销托管已编译的可执行文件。
我使用的Docker镜像的命名约定为{lang}/webservice。该应用程序的Java,Go和Rust版本的镜像大小分别为113、8.68和4.24 MB。
结论
在得出任何结论之前,我想指出这三种语言之间的关系。Java和Go都是支持垃圾回收的语言,但是Java会提前编译为在JVM上运行的字节码。启动Java应用程序时,JIT编译器会被调用以通过将字节码编译为本地代码来优化字节码,以提高应用程序的性能。
Go和Rust都提前编译为本地代码,并且在运行时不会进行进一步的优化。
Java和Go都是支持垃圾收集的语言,具有STW(停止世界)的副作用。这意味着,每当垃圾收集器运行时,它将停止应用程序,进行垃圾收集,并在完成后从停止的地方恢复应用程序。大多数垃圾收集器需要停止运行,但是有些实现似乎不需要这样做。
当Java语言在90年代创建时,其最大的卖点之一是一次编写,可在任何地方运行。当时这非常好,因为市场上没有很多虚拟化解决方案。如今,大多数CPU支持虚拟化,这种虚拟化抵消了使用某种语言进行开发的诱惑(该语言承诺可以运行在任何平台上)。Docker和其他解决方案以更为低廉的代价提供虚拟化。
在整个测试中,应用程序的Java版本比Go或Rust对应版本消耗了更多的内存,在前两个测试中,Java使用的内存大约增加了8000%。这意味着对于实际应用程序,Java应用程序的运行成本会更高。
对于前两个测试,Go应用程序使用的CPU比Java少20%,同时处理比java版多出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编程语言已经存在了将近30年,这使得在市场上寻找Java开发人员变得相对容易。另一方面,Go和Rust都是相对较新的语言,因此与Java相比,自然而然的开发人员的数量更少些。不过,Go和Rust都拥有很大的吸引力,许多开发人员正在将它们用于新项目,并且有许多使用Go和Rust的生产中正在运行的项目,因为简单地说,就资源而言,它们比Java更有效。
在编写本文的程序时,我同时学习了Go和Rust。就我而言,Go的学习曲线很短,因为它是一种相对容易掌握的语言,并且与其他语言相比语法很小。我只用了几天就用Go编写了程序。关于Go需要注意的一件事是编译速度,我不得不承认,与Java/C/C++/Rust等其他语言相比,它的速度非常快。该程序的Rust版本花了我大约一个星期的时间来完成,我不得不说,大部分时间都花在弄清borrow checker向我要什么上。Rust具有严格的所有权规则,但是一旦掌握了Rust的所有权和借用概念,编译器错误消息就会突然变得更加有意义。违反借阅检查规则时,Rust编译器对您大吼的原因是因为编译器希望在编译时证明已分配内存的寿命和所有权。这样做可以保证程序的安全性(例如:没有悬挂的指针,除非使用了不安全(unsafe)的代码逃离检查),并且在编译时确定了释放位置,从而消除了垃圾收集器的需求和运行时成本。当然,这是以学习Rust的所有权系统为代价的。
在竞争方面,我认为Go是Java(通常是JVM语言)的直接竞争对手,但不是Rust的竞争对手。另一方面,Rust是Java,Go,C和C ++的重要竞争对手。
由于他们的效率,我看到了自己将会在Go和Rust中编写更多的程序,但是很可能在Rust中编写更多的程序。两者都非常适合Web服务,CLI,系统程序(..etc)开发。但是,Rust比Go具有根本优势。它不是垃圾收集的语言,与C和C++相比,它可以安全地编写代码。例如,Go并不是特别适合用于编写OS内核,而这里又是Rust的亮点,并与C/C ++竞争,因为它们是使用OS编写的长期存在和事实上的语言。Rust与C/C++竞争的另一种方式在嵌入式世界中,我将继续进行讨论。
感谢您的阅读!
本文翻译自《Comparison between Java, Go, and Rust》。
我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!
我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily
我的联系方式:
微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite
微信赞赏:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
© 2020, bigwhite. 版权所有.
Related posts:
写的真不错,我是这样理解的,从资源占用来看,java真的被新生代语言go,rust完爆了,资本家当时希望节省机器成本。然而开发效率大部分时候都比机器成本要珍贵,毕竟人力成本现在远远大于机器成本的,而java到现在,框架和库多如繁星,浩如烟海,给开发效率带来很多的提升,而且很多老系统大而不倒,这个没法说直接退去,而是逐渐逐渐漫漫隐去,这个时间可能是以百年为时间跨度的。而go 作为机器高效和人力高效的替代品,真的很有吸引力了,很多新公司都选择以go为技术栈。在系统编程方面,有垃圾回收器的都不能用,rust在这方面就是挑战c和c++的,特别是c++,太难掌握了,内存回收,带来很大的心智负担。
Java 适合企业应用,如果一个应用如下:查询购物车里商品,查询各个商品的价格和优惠,查询当前用户的优惠券,查询库存,然后生成订单,扣减库存,给用户发送通知,这里牵扯到很多次查库和写库,甚至有可能是多个系统之间的调用,这种场景下,一个操作少几毫秒意义并不大,因为数据库操作或者跨系统调用才是性能瓶颈
Java的GC不是全程需要STW的,不管是现在默认的G1还是实验中的ZGC。
其中ZGC可以控制STW在10ms以内
而且Java也不是纯编译语言,基于虚拟机可以实现动态代理等设计,极限需要性能的地方,也可以利用HikariCp的实现方法直接优化字节码
我建议博主把Java、go和rust的测试代码都展示出来,看那份比对表,我有些好奇博主是怎么测试Java,是使用 tomcat 吗?还是Nettry?还是自己使用 socket 写的一个简易的 HTTP server?
java的多线程并发模型与go和rust的差距非常大。
go的处理模式是来一个请求直接使用go关键字创建线程就行,因为创建线程的开销很小。
但是Java不是这样的,Java对于请求的到来是从线程池中已经创建好的线程抽调一个线程来处理请求。而不是一个请求来了再临时去建立线程(这是GO的做法,但是没有Java开发者会做这样愚蠢的行为)。
此外,在线程池的基础上,再搭配NIO,让一个线程去处理多个Socket连接。
线程池 + NIO + HASH时间轮 一套组合才能让 Java 的并发达到极致,虽然我承认必须要非常扎实的基础,才能真正的把这一套玩好。
博主字节也说了,平常主要使用和学习 GO 和 Rust,我并没有和楼主吵的意思,只是想要看看博主测试的代码,同时呢,Java的高并发门槛比GO要高的多,拿GO高并发模型,套用到Java,这是一个很愚蠢的行为。
别激动,这是一篇译文!可以打开原文链接,问问原作者:)。
另外我再说一点,博主说的Java的那100MB内存是平白占用的观点也是不对的。
这个和Java GC的特点有关系。Java GC的特点是:占尽所有可用的内存。
我举个例子:
在控制台执行一下指令:
java -Xms214M -Xmx214M Demo
上面指令中,参数 -Xms是最小可用内存,-Xmx是最大可用内存,如果Java应用程序使用的内存超过-Xmx参数所指定最大可用内存,那么就会出现java.lang.OutOfMemoryError: Java heap space。
public class Demo {
public static void main(String[] args) {
while(true) {
new Object();
}
}
}
以上的代码会导致程序把214MB内存占用完毕。但是程序会崩溃?当然不会,在前期 new Object()的时候,GC会向操作系统要新的内存,直到GC管理的内存达到214MB(参数 -Xmx指定的),GC便不再索取新的内存,但是已经占有的内存,GC也不会归还给系统。
这214MB内存会被GC用于新的 new Object(),以此来避免频繁的向系统申请内存和销毁内存所带来的开销。不知道博主会不会C语言,C语言的malloc()函数(申请内存)和free()函数(释放内存),这可是内核函数,应用程序调用内核,造成的内核调用,带来的开销是C语言老生长谈的问题,GO同样是基于C发展来的,同样无法避开这个魔咒。
回到博主所说的100MB内存占用问题上,这100MB内存同样也属于预先占用的内存,具体情况与上诉类似。
从这些角度来看,我是认为Java GC是要比GO的GC机制要更加强大的,虽然看上去平白占用的内存很多。
所以C/C++发展到后来的性能关键点,会自己去实现内存池/对象池。go的GC我没有深入了解过,但是go的语言维护者完全也可以有他们一套自己的内存申请策略,而不会随意调用系统级的内存分配函数。最后补充一点,malloc和free并不是直接操作内核级函数的,这两个函数一样是C标准库的封装函数,系统级的内存都是按页去申请的。
你扯一堆也改变不了java吃内存的事实.
话说作者不要什么都随便翻译过来啊,去原来的博客那边去看了一眼,下面全都在吐槽原作者对 JVM 不熟悉而且做了很多不公平的比拼。甚至原作者自己编写的 java 测试代码都出现啦 OOM 的情况,JVM 的参数也很随意,在这种情况下比性能太没意义了。
结论:这个完全算不上后端必看,感觉更像一场不公平的斗蛐蛐
语言争论总是会被人吐槽的:)