龙空技术网

现实世界中的原生 Java

明月陪我玩 107

前言:

此时各位老铁们对“口袋妖怪java”都比较注意,我们都需要分析一些“口袋妖怪java”的相关内容。那么小编同时在网上汇集了一些对于“口袋妖怪java””的相关文章,希望姐妹们能喜欢,同学们快快来了解一下吧!

Kubernetes 上的微服务是原生 Java 的最佳业务案例,因为它们具有最重要的框架和 Java 运行时开销。

原生 Java 的采用可以逐步进行,一次一个微服务。

应用程序框架应该在生产环境中完全支持原生 Java。

Native Java 需要更多的精力来构建、调试、测试、部署和配置文件。

仅当应用程序的所有应用程序库都支持本机 Java 时,才能将应用程序转换为本机 Java。

介绍

微服务架构的日益流行让人想起著名的“壮志凌云”电影名言,“我觉得需要,需要速度”。更小的容器、更快的启动时间和更好的资源利用率对于运行云服务变得越来越重要。

Java 因其缓慢的启动时间、大量的依赖项(无论是完全使用还是未使用)以及大量的资源需求而一直受到批评。将 JVM 和应用程序服务器添加到组合中,需求变得更加沉重。

对于真正的微服务平台,尤其是无服务器 API,传统的解释型 Java 服务并不理想。

这就是原生 Java 真正闪耀的地方......

寻找甜蜜点

Native Java 非常适合 Kubernetes、微服务和无服务器组件。在开发新服务或将较大的单体应用程序分解为较小的服务时,这也是一个理想的机会。

采用本机 Java 不一定是一种“大爆炸”的方法——它可以一次完成一项服务。这种方法可以最大程度地降低风险,并且随着技术随着时间的推移进一步成熟,将有助于建立信心。

开始行动可能看起来势不可挡,但它与今天的传统 Java 开发并没有什么不同。

Logicdrop开发了一个用于业务自动化和数据智能的一体化平台,使企业能够设计自己的解决方案并将其部署到云中。我们的平台最初是使用 Spring Boot 和 Drools 开发的,现在已经从头开始重新设计,只使用Quarkus和 Kogito,并主要部署原生 Java 可执行文件。

在切换到原生 Java 之前,在云原生基础架构中运行越来越多的 Spring Boot 服务变得越来越具有挑战性,更不用说成本高昂了。无论功能如何,容器的大小始终约为 1GB+,因为它们需要 JVM 并包含一整套依赖项(无论是否使用)。启动时间平均为 15-30 秒,由于资源已经很紧张,每个节点只能运行少数几个。

迁移到 Quarkus 后,生成的本机可执行文件明显更小,启动速度明显更快,总体上使用的资源更少。容器的大小小于 50MB(压缩),并且可以在不到 1 秒的时间内准备好接受请求。这些优势使原生 Java 非常适合在成本和性能方面对大小和启动时间至关重要的环境。

吞吐量不那么令人担忧,我们在跳跃后发现它大致相同。由于扩展速度更快,并且可以将更多服务打包到更少的节点中,因此水平扩展可以适应任何差异。

这是我们节省成本的一个例子。亚马逊 Kubernetes 服务 EKS 中的单个集群运行多个 Spring Boot 服务的五个节点每年花费近 5,000 美元。迁移到原生 Java 将成本降低了近 50%,因为只需要一半的资源。这转化为我们所有集群的显着成本节约!

何时使用本机 Java

Native Java 令人印象深刻:GraalVM 将 Java 与其他“更轻、更快”的堆栈相提并论,同时保持了我们都知道的熟悉的 Java 结构。而“更轻、更快”在云中至关重要!

原生 Java 可执行文件也可以更安全:GraalVM 通过剥离未使用的类、方法和字段来减少漏洞利用的表面积。

新的微服务是原生 Java 的理想起点,因为它们可以从头开始编写,以利用已建立的经过验证的原生库。

在决定将什么迁移到原生 Java 时,这些先决条件是一个很好的起点:

该服务是独立的吗?

启动时间和扩展是否重要?

外部依赖项是否与本机 Java 兼容?

正如本系列中的GraalVM 文章所解释的,可能需要额外的配置才能正确处理动态 Java 功能(例如反射)。如果没有这些额外的元数据,库在用作本机可执行文件的一部分时可能会失败!因此,根据我们的经验,Java 库要么与本机 Java 兼容,要么不兼容。

使用提供一组精心策划的库的框架有助于了解哪些在原生 Java 中有效,哪些无效。不幸的是,对于其他 Java 库来说,事情变得更加困难:目前,判断一个库是否与本机 Java 兼容的唯一方法是在本机可执行文件中运行它。大多数时候,如果有任何故障,它们会很快出现。

Apache Ignite 就是这样一个在原生 Java 中失败的库,因为它依赖于低级 Java API。我们仍然使用它在某些 Spring Boot 服务中进行缓存,但现在已在本机可执行文件中将其替换为 Redis。

了解哪些库与本机 Java 兼容可能是决定什么是本机 Java 的良好候选者的重要因素:对于不兼容的库,我们要么使用替代品,要么重新实现功能。

幸运的是,大多数 Java 应用程序通常会依赖于已经随框架提供的类似类型的功能——日志记录、REST API、JSON 等。例如,这些 API 已经存在于 Quarkus 中并且与原生 Java 兼容:

持久性(NoSQL 和 RDBMS)

可观察性(Elastic、Prometheus、Jaeger 等)

AWS 开发工具包

安全

SOAP (Apache CXF)

REST(RESTEasy、Jackson 等)

支持(Swagger、Logging 等)

上面的列表表明,大量常用的库可以并且已经可以与本机 Java 一起使用。而且这个名单还在继续增长!

但是,并非所有服务都适合原生 Java。将有一些库和代码使转换变得比它的价值更麻烦。最好让这些服务保持原样,并在以后重新评估它们。

根据我们的经验,在以下情况下迁移到原生 Java 是没有意义的:

启动时间、扩展和资源要求并不重要

专门的库没有本地等效库或对本地不友好

动态 Java,如反射或动态代理,被大量使用

关于动态 Java 的注意事项:GraalVM 不支持动态代理,因为本机可执行文件需要在构建时拥有所有可用的类。至于反射,它是受支持的,但在构建时无法解析元素的情况下,可以在常规 JVM 上运行一个代理,跟踪反射和动态代理对象的使用。

每当复杂性、工作量和风险超过迁移到本机 Java 的直接好处时,我们就会积压这些服务以备后用。这些只是少数服务。

选择框架

选择一个原生框架就像选择一个入门口袋妖怪:每个都有优点和缺点。所以选择一个需要仔细考虑长期使用。

本机 Java 可用于纯 Java 开发。但是大多数组织应该选择建立在一个框架上,因为它会减少样板代码并提供一组精心策划的 API,从而节省时间和精力。此外,每个框架都使您免于构建本机可执行文件的过程,进一步降低了复杂性和学习曲线。

选择的框架应该完全包含 GraalVM,提供支持原生 Java 的丰富生态系统,并以对您的组织有意义的方式简化原生可执行文件的构建。考虑到这一点,今天只有三个 Java 框架可以做到这一点——Quarkus、Micronaut 和 Helidon。

一些框架甚至可以在 JVM 中“传统地”运行,同时仍然利用一些 GraalVM 优化。当应用程序或服务无法完全原生运行时,这可能是一个很好的后备方案。

在评估了可用框架后,我们选择了 Quarkus。它是启动和运行速度最快的框架,它利用了 Java 标准,文档非常好,它提供了我们开箱即用所需的所有功能,并且社区提供了极大的帮助和支持。这就是为什么在短短几个月内,我们整个后端团队就从 Spring Boot 切换到 Quarkus。

采用本机 Java

跳转到原生 Java 并不像人们想象的那么可怕——开发体验基本保持不变。但是一些流程需要稍微改变以更好地交付本机可执行文件。

对于日常开发,我们像往常一样开发 Java 服务:编写 Java 代码并使用 IDE 或命令行工具对其进行测试和调试。构建本机可执行文件将为此过程引入额外的步骤和新的考虑因素。

针对本机可执行文件的典型生命周期如下所示:

在开发者机器上正常开发、调试和测试服务

执行更严格、更稳健的测试

测试 API 有效负载的结构以确保它们是完整的

测试端点“就像在生产中运行一样”以确保覆盖所有代码

为每个环境和/或操作系统构建、测试和部署本机可执行文件

与传统的 Java 开发不同,构建本机 Java 可执行文件是资源密集型的——构建每个服务可能需要 2-10 分钟,即使在相当大的工作站上也是如此!

与传统的 Java 开发不同,创建单个 WAR 或 JAR 文件是不够的:每个操作系统都需要自己的本机可执行文件。而且由于本机可执行文件内联了它们的代码和属性,因此每个环境还需要自己的本机可执行文件。例如,Swagger 可能在 staging 中暴露,但在生产中没有。因此,需要构建暂存可执行文件,包括 Swagger 依赖项,而生产可执行文件则不需要。对于在运行时无法处理的任何属性或配置也是如此。如果仅针对 Linux 容器,则构建变体的数量会被简化。

编译生成

最好仅在需要时在开发人员机器上构建本机 Java 可执行文件。这可能是在一个重要的特性即将被合并之前,或者当出现需要调试的问题时。相反,依靠 CI/CD 管道来卸载不同目标的构建和测试将减少流程的干扰并减轻开发人员的压力。

我们之前提到,带有本机可执行文件的容器要小得多,需要的资源也少得多。这使我们能够将多个预览环境部署到集群中,而不是仅仅依赖一个共享环境。开发人员现在可以一起测试所有服务,为他们的特定配置和隔离环境原生构建,而不会“踩到别人的脚趾”。传统的 Java 也可以做到这一点,但由于对已经受限的云资源的需求,成本要高得多。

例如,我们开始使用通常的三个环境:仅开发、测试和生产。使用本机可执行文件,我们现在可以拥有超过 20 个预览环境,每个环境都构建和配置了所需的所有服务(目前约 20 个服务)。因此,我们现在可以有 20 个或更多的预览环境并行运行,而不是共享一个只能提供 20 个服务的开发环境,总共公开 400 个服务。

调试

当问题出在本机可执行文件上时,我们需要调试本机可执行文件。这需要一些额外的设置和工具,并且有一个可用的 GraalVM。但是,一旦设置好,它与使用当今流行的 IDE 调试 Java 并没有太大区别。

调试从附加到正在运行的本机进程开始,将 IDE 链接到 Java 源文件,最后单步执行代码。一旦附加到进程中,所有通常的操作都是可能的:设置断点、创建监视、检查状态等。

幸运的是,自从我们开始原生 Java 之旅以来,这些工具已经取得了长足的进步。例如,Visual Studio Code 为 Quarkus 和 GraalVM 提供了出色的扩展,提供完整的 Java 开发和调试功能,并包括 GraalVM 运行时。此扩展还与 VisualVM 集成,以便可以分析本机可执行文件。

根据GraalVM FAQ,IntelliJ、Eclipse 和 Netbeans 也支持 GraalVM。作为最后的手段,您始终可以使用 GNU 调试器来调试本机可执行文件。

测试

测试本机 Java 可执行文件类似于测试传统 Java 服务。但必须注意细微差别。

测试本机可执行文件的一个直接缺点是它们的静态、封闭世界性质。依赖于 Java 动态特性的久经考验的测试方法,例如模拟库,在这里是不可能使用的。并且对源代码的任何更改都需要首先构建本机可执行文件的新版本,这也是一个比传统 Java 慢得多的过程。

GraalVM 还尝试内联和/或删除尽可能多的代码。这可能会导致很多问题。

错误代码删除的一个例子是 Jackson JSON 序列化。我们的 JUnit 测试报告序列化在开发过程中很好。但是本机可执行文件缺少特定的嵌套模型,没有抛出异常。原因是 GraalVM 从可执行文件中删除了一些模型,因为它认为它们没有被使用。修复很简单:使用 GraalVM 注册 JSON 有效负载中使用的任何类。这阻止了他们被排除在本机可执行文件之外。我们还扩展了我们的测试,彻底检查了有效载荷,并添加了更多的冒烟测试。

动态功能,例如反射,是另一个需要密切关注的领域。在某些情况下,可能不会抛出异常,或者某些功能问题在部署可执行文件之后才会出现。

除此之外,我们发现行使端点的测试是保证预期功能和正确有效载荷在本机可执行文件中的极好方法。在任何给定服务的入口点开始测试,无论是在 JVM 还是本机可执行文件中运行,都是在最重要的地方验证功能的好方法。

概括总结

迁移到原生 Java 从来都不是我们最初的目标之一。我们只想重新架构我们现有的平台,使其更加云原生,为即将推出的功能做好准备,并更好地大规模利用 Kubernetes 集群。

我们相信选择 Quarkus 是我们有史以来最好的决定之一。它使采用本机 Java 变得非常容易。通过一些前期的规划和努力,并在构建了一些原型之后,很快就发现,一头扎进原生 Java 不仅是可能的。它也是以最小的努力有机地发生的!

肯定有挑战。您可以期待传统开发和交付的变化。但它们与今天开发 Java 服务的方式并没有太大区别。迁移到原生 Java 对我们来说只是对现有流程的补充。

归根结底,任何微服务通常都会受益于更快的启动时间和更少的资源需求。使用原生 Java 的优势,尤其是在 Kubernetes 中,加上成本节约和可衡量的效率,是我们转向原生 Java 的原因。

本机 Java 可执行文件将 Java 提升到一个新的水平。如果机会确实出现并且条件合适,那么值得付出额外的努力来跳跃并开始使用 GraalVM!

Java 主导企业应用程序。但在云中,Java 比一些竞争对手更昂贵。本机编译使云中的 Java 更便宜:它创建的应用程序启动速度更快且使用更少的内存。

因此,原生编译为所有 Java 用户提出了许多问题:原生 Java 如何改变开发?我们什么时候应该切换到原生 Java?我们什么时候不应该?我们应该为原生 Java 使用什么框架?本系列将为这些问题提供答案。

标签: #口袋妖怪java