我们想要试验一下,看看Java微服务是否可以像Go微服务一样运行得那么快。Java“老”、“慢”、“乏味”是业界普遍的共识。Go是“快”、“新”和“酷”的。但我们想知道这些功能是否得到实际性能数据的保证或支持。我们想要一个公平的测试,所以我们创建了一个非常简单的微服务,没有外部依赖(例如数据库)和非常短的代码路径(只处理字符串)。我们确实包含了指标和日志记录,因为它们似乎总是包含在任何真正的微服务中。我们使用了小型轻量级框架(HelidonforJava和Go-KitforGo),还试验了用于Java的纯JAX-RS。我们尝试了不同版本的Java和不同的JVM。我们对堆大小和垃圾收集器进行了一些基本调整。我们在测试运行之前预热微服务。Java的历史Java由SunMicrosystems开发,后来被Oracle收购。它的1.0版本于1996年发布,最新版本为2020年的Java15。主要设计目标是Java虚拟机和字节码的可移植性和带垃圾回收的内存管理。它仍然是最流行的语言之一(根据StackOverflow和TIOBE等来源),并且正在以开源方式开发。让我们谈谈“Java问题”。多年来,Java有许多不同的垃圾收集算法,包括串行、并行、并发标记/清除、G1和新的ZGC垃圾收集器。现代垃圾收集器旨在最大限度地减少垃圾收集“停止世界”暂停。Oracle实验室开发了一种名为GraalVM的新Java虚拟机,它是用Java编写的,具有新的编译器和一些令人兴奋的新特性,例如能够将Java字节码转换为VM可以在其上运行的本机映像。GoGo的历史由Google的RobertGriesemer、RobPike和KenThomson创建。Go受到C、Python、Javascript和C++的影响。它旨在成为高性能网络和多处理的最佳语言。正如我们所说,StackOverflow有27,872个问题被标记为“Go”,而Java有1,702,730个问题。Go是一种静态类型的编译语言。Go是许多CNCF项目的首选语言,例如Kubernetes、Istio、Prometheus和Grafana(大部分)都是用Go编写的。它旨在具有快速构建时间和快速执行。Go的优点是什么(与Java相比)——这是我根据我的经验得出的个人观点:易于实现函数模式,如组合、纯函数、不可变状态。更少的样板代码(但仍然太多)。它仍处于生命周期的早期,因此它没有向后兼容性的沉重负担——他们仍然可以打破现状来改进它。它编译为本机静态链接二进制文件——没有vm层——二进制文件包含运行程序所需的一切,这对于“从头开始”的容器来说非常有用。具有体积小、启动快、执行快的特点。没有OOP、继承、泛型、断言、指针算法。更少的括号,例如没有循环依赖,没有未使用的变量或导入,没有隐式类型转换的强制。那么,Go的“问题”是什么?工具生态不成熟,尤其是依赖管理——有多种选择,没有一种是完美的,特别是对于非开源开发;代码非常慢(就像Maven著名的“下载Internet”问题。将绑定代码导入存储库,使其成为移动的噩梦。IDE非常适合编程、文档查找、自动完成等。指针!没有Java风格的try/捕获异常(如果err!=nil被频繁使用,你最终会写它),没有像列表、映射函数等函数式风格的原语。因为它还不可用,你通常最终会实现一些基本的algorithm...最近,我写了一些代码,用sloe比较两个字符串(列表),并转换它们。在函数式语言中,我可以使用像map这样的内置函数来做这个。没有动态链接!(你问“谁在乎?”。)如果你打算使用带有“感染”静态链接代码的GPL许可证的代码,这可能是一个真正的问题。调整执行或垃圾收集、分析执行或优化算法的旋钮并不多——Java有数百个垃圾收集调整选项,Go有一个——启用或禁用。负载测试方法我们使用JMeter进行负载测试。测试对服务的多次调用并收集有关响应时间、吞吐量(每秒事务数)和内存使用情况的数据。对于Go,我们收集常驻集合大小;对于Java,我们跟踪本机内存。在许多测试中,我们在与被测应用程序相同的计算机上运行JMeter。如果我们在另一台机器上运行JMeter,结果似乎没有任何干扰或差异,因此简化了设置。当我们稍后将应用程序部署到Kubernetes中时,JMeter运行在集群外部的远程机器上。在进行测量之前,我们通过1,000次服务调用来预热应用程序。应用程序本身的源代码以及负载测试的定义位于以下GitHub存储库中:https://github.com/markxnelson/go-java-go第一轮测试在本例中测试于一台2.5GHz双核IntelCorei7笔记本电脑,配备16GBRAM,运行macOS。结果如下:我们宣布Go成为第一轮的获胜者!以下是我们基于这些结果的观察:日志记录似乎是一个主要的性能问题,尤其是java.util.logging。所以我们在有和没有日志记录的情况下都进行了测试。我们还注意到,日志记录是影响Go应用程序性能的一个重要因素。Java版本具有明显更大的内存占用,即使对于如此小而简单的应用程序也是如此。预热对JVM有很大的影响——我们知道JVM在运行时进行优化,所以这在这里很有意义在测试中,我们正在比较不同的执行模型——Go应用程序被编译为本机可执行二进制文件,而Java应用程序被编译为字节代码,然后在虚拟机上运行。GraalVM原生图像GraalVM具有原生图像功能,允许您获取Java应用程序并将其编译为原生可执行代码。此可执行文件包括应用程序类、来自其依赖项的类、运行时库类以及来自JDK的静态链接本机代码。下面是再次添加GraalVM原生镜像测试的第一轮结果(使用GraalVMEE20.1.1-JDK11构建的原生镜像):在这种情况下,使用GraalVM原生镜像并没有看到吞吐量或响应时间有任何显着改善,但内存占用较小。以下是一些测试的响应时间图:>第一轮的响应时间图请注意,在所有三个Java变体中,第一个请求的响应时间要长得多(请参见右上角左轴对面的蓝线)).下一轮,我们决定在更大的计算机上运行测试。与第一轮一样,我们使用了100个线程,每个线程10,000个循环,启动时间为10秒,使用相同版本的Go、Java、Helidon和GraalVM。以下是结果:我们宣布GraalVM本机映像成为第二轮的获胜者!以下是这些测试的响应时间图:>启用日志记录但没有预热的测试运行的响应时间>没有日志记录和没有预热的测试运行的响应时间>有预热但没有日志记录的测试运行的响应时间第2轮的一些观察结果:Java变体在这个测试中表现得更好,并且在没有日志记录的情况下比Go表现得更好Java似乎更能够使用硬件提供的多核和多线程执行(与Go相比)——这是有道理的,因为Go是旨在作为一种系统和网络编程语言,它是一种较年轻的语言,所以有趣的是,Java是在多核处理器不常见的时候设计的,而Go是在多核处理器不常见的时候设计的。特别是,Java日志记录似乎已成功卸载到其他线程/核心,并且对性能的影响要小得多。这一轮的最佳性能来自GraalVM原生图像,平均响应时间为0.25毫秒和每秒82,426个事务,而Go中的最佳结果是1.59毫秒和39,227tps,但这比以牺牲内存使用为代价!Java变体似乎具有更一致的响应时间,但有更多尖峰——我们认为这意味着Go正在做更多、更小的垃圾收集第3轮——Kubernetes在第3轮,我们决定在Kubernetes集群中运行应用程序——a您可能会说,更自然的微服务运行时环境。对于这一轮,我们使用了一个Kubernetes1.16.8集群,该集群具有三个工作节点,每个节点有两个内核(每个内核有两个执行线程)、14GBRAM和OracleLinux7.8。在某些测试中,我们为每个变体运行一个pod,在其他测试中,我们运行100个pod。应用程序访问是通过Traefik入口控制器进行的,JMeter在Kubernetes集群外部运行以进行某些测试,对于其他测试,我们使用ClusterIP并在集群内部运行JMeter。与之前的测试一样,我们使用了100个线程,每个线程10,000个循环,启动时间为10秒。以下是每个变体的容器大小:Continued11.6MBJava/Helidon1.41GBJava/HelidonJLinked150MBNativeimage25.2MB结果如下:以下是一些响应时间:>Kubernetes测试的响应时间在这一轮中,我们观察到Go有时更快,而GraalVM原生图像有时更快,但两者之间的差异很小(通常小于5%)那么我们学到了什么?我们反思了所有这些测试和结果,这里有一些结论:有更多的内核和内存;Go在更小/功能更弱的机器上表现更好。Go的性能通常更一致-可能是由于Java的垃圾收集在“生产规模”机器上,Java运行速度与Go一样快,如果不是更快的话,日志记录似乎是我们在Go和Java瓶颈中遇到的主要问题Java的现代版本,如以及像Helidon这样的较新框架,在消除/减少Java一些众所周知的长期存在的问题(例如冗长、GC性能、启动时间等)方面取得了长足的进步。下一步是什么?这是一个非常有趣的练习,我们打算继续进行,特别是:、多个服务和电路中断,并了解网络如何影响性能以及如何调整微服务网络。还想查看日志记录问题,看看我们可以做些什么来消除瓶颈。想看看目标代码并比较正在执行的实际指令,看看是否可以在代码路径中进行一些进一步的优化。我们想知道JMeter是否可以产生足够的负载而不会成为瓶颈,但我们的测试表明这根本不是一个因素,它可以轻松跟上Go和Java实现的步伐。想要更详细地测量容器启动时间、内存占用等。
