最近这几年,Go、Rust 收到越来越多的关注,特别是 Go,在国内挺受欢迎的,很多大公司都采用它。而 Rust,作为系统编程语言收到越来越多的人关注,苹果、微软都宣称他们使用 Rust 编写部分业务。而 Java 作为老牌编程语言,长期霸占编程语言排行榜第一或第二位。这篇文章从一些角度就以上三门语言做一个对比。
本文是 Java,Go 和 Rust 之间的比较。但这不是性能测试,主要关注可执行文件大小,内存使用,CPU 使用率,运行时要求等,当然还有一个小的基准测试,看看每秒能处理的请求数,并通过数据展示。
为了更好的进行比较,我使用这三种语言分别编写了一个 Web 服务。该 Web 服务非常简单,它为三个 REST 端点提供服务。
Web服务在Java,Go和Rust中提供服务的端点
这三个 Web 服务的代码托管在 GitHub: https://github.com/dexterdarwich/ws-compare 上。
编译后可执行文件大小
关于如何构建二进制文件。在 Java 中,我已经使用 maven-shade-plugin 将所有内容构建到一个大的 jar 中,并使用了 mvn packagetarget。对于 Go,我使用 go build。而对于 Rust,我使用 build --release
。
每个程序的编译大小(以 Mb 为单位)
编译后的大小还取决于所选的库/依赖项。在本文中,对于我选择的库,以上是程序的编译大小。
在后面,我将把这三个程序构建并打包为 Docker 映像,同时列出它们的大小,以显示每种语言所需的运行时开销。下面有更多详细信息。
内存使用
空闲时的内存使用
每个应用程序在内存空闲时的内存使用情况
什么?Go 和 Rust 版本的条形图在哪里显示空闲时的内存占用量?好了,它们在那里,只有当 JVM 启动程序并处于空闲状态时,Java 才消耗 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 命令表示:使用两个线程(用于 wrk)并在池中保留 400 个打开的连接,并重复使用 GET 方式访问端点,持续 30 秒。这里我仅使用两个线程,因为 wrk 和被测程序都在同一台计算机上运行,所以我不希望它们在可用资源(尤其是 CPU)上相互竞争(太多)。
每个 Web 服务都经过单独测试,并且在每次运行之间都重新启动了 Web 服务。
以下是该程序的每个版本的三个运行中的最佳结果。
/hello
该端点返回 Hello,World!
信息。它生成字符串 “ Hello,World!” 将其序列化并以 JSON 格式返回。
访问hello端点时的CPU使用率
请求hello端点时的内存使用情况
请求hello端点时的每秒请求数
/greeting/{name}
该端点接受路径参数{name},然后格式化字符串 “ Hello,{name}!”
,进行序列化并将其返回为 JSON 格式的问候消息。
请求greeting端点时的CPU使用率
请求greeting端点时的内存使用情况
请求greeting端点时的每秒请求数
/fibonacci/{number}
该端点接受路径路参数 {number},并返回斐波纳契数列并序列化为 JSON 格式。
这个特定的端点我选择以递归形式实现它。毫不怀疑,循环方式实现会产生更好的性能结果,并且出于生产目的,应该选择一种迭代形式,但是在生产代码中,有些情况下必须使用递归(并非专门用于计算第 n 个斐波那契数))。因此,我希望实现大量涉及 CPU、栈内存分配。
请求fibonacci端点时的CPU使用率
请求fibonacci端点时的内存使用情况
请求fibonacci端点时的每秒请求数
在 Fibonacci 端点测试中,Java 实现是唯一一个有 150 个请求出现了超时, wrk 的输出如下所示。
fibonacci端点的时延
运行时大小
为了模拟现实世界中的云原生应用程序,并消除“它可以在我的机器上运行!”,我为这三个应用程序中的每一个创建了一个 Docker 映像。
Docker 文件的源包含在相应程序文件夹下的代码库中。
作为 Java 应用程序的基本运行时镜像,我使用了 openjdk:8-jre-alpine,该镜像被称为是最小的镜像之一,但是,它附带了一些警告,这些警告可能适用于你的应用程序,也可能不适用于你的应用程序,主要是 alpine 镜像在处理环境变量名称方面不符合 posix,因此在 Docker 文件中,你不能使用 .
字符,另一方面是 alpine Linux 镜像是使用 musl libc 而不是 glibc 编译的,这意味着如果你的应用程序依赖需要 glibc,它将无法正常工作。在我这个例子,alpine 可以正常运行。
至于应用程序的 Go 版本和 Rust 版本,我已经对其进行了静态编译,这意味着它们不希望在运行时镜像中存在libc(glibc,musl…等),这也意味着它们不需要运行 OS 的基本映像。因此,我使用了临时 docker 映像,这是一个无操作映像,以零开销托管已编译的可执行文件。
我使用的 Docker 映像的命名约定为 {lang}/webservice
。该应用程序的 Java,Go 和 Rust 版本的镜像大小分别为 113、8.68 和 4.24 MB。
最终Docker映像大小
结论
三种语言的比较
在得出任何结论之前,我想指出这三种语言之间的关系。Java 和 Go 都是垃圾收集语言,但是 Java 会提前编译(AOT)为在 JVM 上运行的字节码。当启动 Java 应用程序时,即时(JIT)编译器将被调用,以通过随时随地将其编译为本机代码来优化字节码,以提高应用程序的性能。
Go 和 Rust 都提前编译为本地代码,并且在运行时不会进行进一步的优化。
Java 和 Go 都是垃圾收集语言,有 Stop-The-World 的副作用。这意味着,每当垃圾收集器运行时,它将停止应用程序,进行垃圾收集,并在完成后从停止的地方恢复应用程序。大多数垃圾收集器需要停止运行,但是有些实现似乎不需要这样做。
Java 语言是 90 年代创建的,其最大的卖点之一是一次编写,可在任何地方运行。当时,这很棒,因为市场上没有很多虚拟化解决方案。如今,各种虚拟化技术存在,特别是 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 编程语言已经存在了将近 30 年,这使得在市场上寻找 Java 开发人员变得相对容易。另一方面,Go 和 Rust 都是相对较新的语言,因此与 Java 相比,自然而然开发人员更少。不过,Go 和 Rust 都获得了很大的吸引力,许多开发人员正在将它们用于新项目,并且有许多在生产环境使用 Go 和 Rust 的项目或公司,因为简单地说,就资源而言,它们比 Java 更有效。(也许是因为它们是新的、酷的语言!)
在编写本文的程序时,我同时学习了 Go 和 Rust。就我而言,Go 的学习曲线很平滑,因为它是一种相对容易掌握的语言,并且与其他语言相比语法很少。我只用了几天就用 Go 编写了程序。关于 Go 需要注意的一件事是编译速度,我不得不承认,与 Java/C/C++/Rust 等其他语言相比,它的速度非常快。该程序的 Rust 版本花了我大约一个星期的时间来完成,我不得不说,大部分时间都花在弄清借阅检查器(borrow checker)向我要什么上。Rust 具有严格的所有权规则,但是一旦掌握了 Rust 的所有权和借用(borrowing)概念,编译器错误消息就会突然变得更加有意义。当违反借阅检查规则时,Rust 编译器对您大吼大叫的原因是,因为编译器要在编译时证明已分配内存的寿命和所有权。这样,它保证了程序的安全性(例如:除非使用了不安全的代码转义,否则就没有悬挂指针),并且在编译时确定了释放位置,从而消除了垃圾收集器的需求和运行时成本。当然,这是以学习 Rust 的所有权系统为代价的。
在竞争方面,我认为 Go 是 Java(通常是 JVM 语言)的直接竞争对手,但不是 Rust 的竞争对手。另一方面,Rust 是Java,Go,C 和 C++ 的重要竞争对手。(注:感觉 Rust 好猛呀!)
由于它们的效率,我想到了自己。并且将会用 Go 和 Rust 编写更多的程序,但是很可能用 Rust 编写更多的程序。两者都非常适合网络服务,CLI,系统程序等的开发。但是,Rust 比 Go 具有根本优势。它不是垃圾收集的语言,与 C 和 C++ 相比,它可以安全地编写代码。例如,Go 并不是特别适合用于编写 OS 内核,而这里又是 Rust 的亮点,并与 C/C++ 竞争,因为它们是使用 OS 编写的长期存在和事实上的语言。Rust 与 C/C++ 竞争的另一种方式是在嵌入式世界中,但我将后续进行讨论。
最后简单介绍下 Rust 语言:
Rust 是一门系统编程语言,专注于安全,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust 在语法上和 C++ 类似,但是设计者想要在保证性能的同时提供更好的内存安全。Rust 最初是由 Mozilla 研究院的 Graydon Hoare 设计创造,然后在 Dave Herman, Brendan Eich以 及很多其他人的贡献下逐步完善的。Rust的设计者们通过在研发 Servo 网站浏览器布局引擎过程中积累的经验优化了 Rust 语言和 Rust编译器。
Rust编译器是在 MIT License 和 Apache License 2.0 双重协议声明下的免费开源软件。Rust 已经连续四年(2016,2017,2018,2019)在 Stack Overflow 开发者调查的“最受喜爱编程语言”评选项目中折取桂冠。
作者:Dexter Darwich