写点什么

Java、Go 和 Rust 的比较

  • 2021 年 12 月 20 日
  • 本文字数:3541 字

    阅读完需:约 12 分钟

原载于:公众号「云原生技术爱好者社区」


这是一篇 Java、Go 和 Rust 之间的比较。不仅仅在基准指标的意义上,更多是在输出可执行文件大小、内存使用、CPU 使用、运行时要求之间的比较,当然还有一些简单性能测试。

为了更贴近现实,我已经用这种比较中的每种语言编写了一个 Web 服务。Web 服务非常简单,它为三个 REST http 服务。



文件大小

在 Java 打包构建的场景下下,我使用 maven-shade-plugin 将所有内容构建到一个 jar 中,并使用了 mvn package 打成 jar 包。在 Go 的情况下,我使用了 go build. 最后,对于 Rust,我使用了 cargo build —release.


每个程序的编译大小(以兆字节为单位)。工程编译的大小还取决于所选的库/依赖项,在我的具体情况下,以上是编译后的程序大小。

在单独的部分中,我将构建所有三个程序并将其打包为 docker 映像,并将列出它们的大小以及显示每种语言所需的运行时开销。更多详情如下。

内存使用情况

没有任何请求的情况下



  • 每个应用程序在内存中空闲时的内存使用情况。

Go 和 Rust 版本在空闲时显示内存占用几乎看不到,只是当 JVM 启动程序并闲置不做任何事情时,Java 消耗了 160 MB 以上的空间。在 Go 的情况下,程序使用 0.86 MB,在 Rust 的情况下使用 0.36 MB。这是一个很大的不同!因为这是在内存中什么也不做情况下,Java 内存占用比 Go 和 Rust 对应物多两个数量级,所以这是对资源的巨大浪费。

REST 请求

让我们使用 wrk 通过请求访问 API 并观察内存和 CPU 使用情况,以及我的机器上针对程序的三个版本的每个请求地址请求的 qps。

wrk -t2 -c400 -d30s http://127.0.0.1:8080/hello wrk -t2 -c400 -d30s http://127.0.0.1:8080/greeting/Janewrk -t2 -c400 -d30s http://127.0.0.1:8080/fibonacci/35
复制代码

关于上面的 wrk 命令说如下,使用两个线程(用于 wrk)并在池中保持 400 个打开的连接,并在 30 秒的持续时间内重复调用 GET 请求。这里我只使用了两个线程,因为 wrk 和被测程序都在同一台机器上运行,所以我不希望它们在可用资源上(尤其是 CPU)相互竞争。

每个 Web 服务都分别进行了测试,并且在每次运行之间重新启动了 Web 服务。下面这个请求是该程序每个版本的三个运行中最好的一个。

/hello
复制代码

此请求返回 Hello, World! 信息。它分配字符串“Hello, World!” 并将其序列化并以 JSON 格式返回。


请求 /hello 时的 CPU 使用率

访问 /hello 时的内存使用情况

访问 /hello qps

/greeting/{name}

此请求接受段路径参数 { name },然后格式化字符串“ Hello, {name}!” , 将其序列化并作为 JSON 格式的问候消息返回。


请求 /greeting/{name} 时的 CPU 使用率

访问 /greeting/{name} 时的内存使用情况

访问 /greeting/{name} qps

/fibonacci/{number}

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

这个特定接口我选择以递归形式实现它。毫无疑问通过迭代实现会产生更好的性能结果,并且出于生产目的,应该选择迭代形式,但是在生产代码中存在必须使用递归的情况(不一定是专门用于计算第 n 个斐波那契数)。另外为了测试性能对比,通过该实现能够使大量 CPU 参与堆栈分配。


请求 /fibonacci/{number} 时的 CPU 使用率

访问 /fibonacci/{number} 时的内存使用情况

访问 /fibonacci/{number} qps 在 Fibonacci 接口期间,Java 实现是唯一一个在 150 个请求上出现超时的编程语言,如下面 wrk 的输出所示。



/fibonacci 请求的延迟

运行时大小

为了模拟真实世界的云原生应用程序,并消除“它可以在我的机器上运行!”的问题,我为这三个应用程序中的每一个都创建了一个 docker 镜像。

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

对于 Go 和 Rust 版本的应用程序,我对它们进行了静态编译,这意味着它们不需要 libc(glibc,musl…等)存在于运行时镜像中,这也意味着它们不需要一个带有操作系统的基础镜像来运行。所以我使用了 scratch docker 镜像,它是一个以零开销的方式承载编译后的可执行文件。

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


最终的 Docker 镜像大小

结论



  • 三种语言的比较

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

Go 和 Rust 都提前编译为本机代码,并且在运行时不会发生进一步的优化。

Java 和 Go 都是垃圾收集语言,有一个 STW 的副作用。这意味着每当垃圾收集器运行时,它将停止应用程序,进行垃圾收集,当完成后,它将从它离开的地方恢复应用程序。大多数垃圾收集器需要 STW,但也有一些实现可以减缓这种情况的发生。

当 Java 在 90 年代创建时,它最大的卖点之一就是 “一次编译,到处运行”。这在当时是很接地气的需求,因为当时市场上还没有很多虚拟化解决方案。如今,大多数 CPU 都支持虚拟化,这使得使用 Java 语言开发的诱惑力消失了,因为 Docker 和其他解决方案提供了廉价的虚拟化,它可以在任何地方运行(在任何支持的平台上)。

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

对于前两个测试,Go 应用程序使用的 CPU 比 Java 少约 20%,同时处理的请求多 38%。另一方面,Rust 版本使用的 CPU 比 Go 少 57%,同时处理的请求多 13%。

第三项测试在设计上是 CPU 密集型的,我想通过它来榨取 CPU 的每一个指令集。Go 和 Rust 的 CPU 使用率都比 Java 高 1%。而我认为如果 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 相比,它的编译速度非常快。Rust 版本的程序花了我一周左右的时间来学习,我不得不说,其中大部分时间是在弄清楚借用检查器要我做什么。Rust 有严格的所有权规则,但一旦掌握了 Rust 中所有权和借用的概念,编译器的错误信息就会突然变得更有意义。Rust 编译器之所以在违反借用检查规则时对你大喊大叫,是因为编译器想在编译时证明分配内存的生命周期和所有权。通过这样做,它保证了程序的安全性(例如:没有野指针,除非使用了不安全的代码转义),并且在编译时确定了取消分配,从而消除了对垃圾收集器的需求和运行时成本。当然,这是以学习 Rust 的所有权系统为代价的。

就竞争而言,在我看来,Go 是 Java(一般的 JVM 语言)的直接竞争对手,但不是 Rust 的竞争对手。另一方面,Rust 是 Java、Go、C 和 C++ 的一个严重竞争对手。

由于它们的效率,我将会用 Go 和 Rust 写更多的程序,但最有可能的是用 Rust 写更多的程序。这两种语言对于网络服务、cli、系统程序(……等)的开发都很好。然而,Rust 不是一种垃圾收集语言,这是它跟 Go 相比的天然优势。与 C 和 C++ 相比,它被设计为可以安全地编写代码。例如,Go 并不特别适合用来编写操作系统内核,而这又是 Rust 的优势所在,它可以与 C/C++ 竞争,因为它们是长期存在的、事实上可以用来编写操作系统的语言。Rust 与 C/C++ 竞争的另一个方面是在嵌入式领域。

点击进入获得更多技术信息~~

发布于: 2 小时前阅读数: 9
用户头像

关注百度开发者中心,收获一手技术干货。 2018.11.12 加入

汇聚百度所有对外开放技术、平台和服务资源,提供全方位支持,助力开发者加速成功,实现开发者、消费者和百度三方共赢。https://developer.baidu.com/

评论

发布
暂无评论
Java、Go 和 Rust 的比较