暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

云原生的Java

Share and Fun喜来分 2020-06-05
571

Java 诞生于 25 年前,是最受欢迎的编程语言之一。自 2000 年起 Java 一直保持在 TIOBE 编程语言指数头两位。Java 有非常大的开发者和用户基数,有赖于其完整而成熟的生态系统。这个生态系统提供丰富的涵盖各个领域的库和开发框架,完善的构建和依赖管理工具,功能强大的 IDE 等。Java 程序可运行在各种环境如手机,IoT 设备或者服务器端。除了 Java 语言,Java 虚拟机还支持多种语言如 Kotlin,Groovy,Scala 等。很多大型的中间件都是运行于 JVM 上,如 Hadoop,Spark,Kafka,Cassandra,Flink 等。

Java 的劣势

在云原生的时代,当运行环境都作为交付的一部分时,Java 的一次编写到处运行已经没有太大的意义。而支撑这个理念发展出来的 Java 虚拟器更是成为了应用性能的屏障和各种问题的来源。Java 一直在为成为云原生的编程语言而努力,比如 JDK9 引入的 JPMS(Java Platform Module System,模块系统),它其中一个目的是通过模块化让应用开发者可以有选择地引用不同的组件而不是动辄加载整个 JRE,以此来对应用做瘦身和启动速度优化。另一项改进是 JDK10 引入的对容器环境的感知,修正了 JVM 不能识别通过 cgroup 控制的 CPU 和内存限制(这个增强后来也在 JDK8u191 中引入),开发者再也不用痛苦地在修改容器编排环境对资源的限制时一并修改一堆的 JVM 参数了。

但是 Java 依然不是云原生的首选。

Java 开发的应用程序很多都需要 GB 级别的内存和几十秒的启动时间,而云上一切的资源都是有约束的。CPU 和内存等常规资源直接和价钱挂钩,用 Java 开发的应用程序明显比使用其他语言如 C,Go 或 Javascript 开发的程序更加消耗内存,因而带来更高的运行成本。当 Java 运行在 Kubernetes 上时,大内存消耗加大了 Pod 调度的困难,而启动和停止的速度也是一种资源。Java 那极其缓慢的启动速度加长了系统对 readiness 和 liveness 探针的时间需求,大大减慢系统扩缩容和滚动升级的速度,更是阻碍它作为 Serverless 开发语言的主要因素。

更云原生的 Java

不断提升开发效率和可扩展性一直是 Java 编程语言发展的主要目标之一。动态类加载(Dynamic Class Loading)和反射(Reflection)可以在运行时加载各种未知的类和资源文件并运行它们,这让我们很容易通过插件方式扩展程序的功能。动态代理(Dynamic Proxy)使我们可以拦截方法的调用,面向切面编程(AOP)因此成为可能。标注(Annotation)则可以传达更多元数据和指令到运行时,以此省略了很多之前需要代码和配置文件才能完成的任务,并使应用运行得更加灵活。这些一系列的改进催生了一大批优秀的编程框架,大大简化了开发者的工作和加速了生产速度,也培养了一个超级社区和生态。但是这些为开发者提供便利的动态语言特性却带来了明显的缺点:程序启动速度越来越慢,消耗的内存越来越多。这些框架启动时需要扫描 classpath 来加载所有可达的资源,通过反射这些类来获得标注和元数据,生成需要的代理对象,并 cache 这些反射的类和元数据在内存中,当框架使用得越来越多,启动变得越来越慢,内存也越用越多。怎么破?

Micronaut

Micronaut 是一个目标于微服务,Serverless 和 GraalVM 的应用程序框架,由 Grails 的创始人 Graeme Rocher 等创建,背后支持是 Object Computing Inc 公司,第一个版本 1.0 发布于 2018 年末,目前将要发布 2.0 版本。Micronaut 是一个全功能的框架,它使用自定义的 API,除了支持 RESTful 和一众存储如 JDBC,Hibernate,MongoDB,Cassandra,Redis,Elastic Search 等外,消息通讯支持 Kafka 和 RabbitMQ,编程模型支持 Rxjava 和 Reactor,也对 Micrometer 和 Jaeger 有支持。如果不是有很特殊的需求,它提供的特性基本可以满足日常开发需要。Micronaut 应用程序有秒级的启动速度和相比于常规框架更小的内存 footprint,因为它使用了一种叫 AOT(Ahead of Time)的方式将需要在运行时完成的操作放在了编译期,比如类扫描,反射,应用程序配置,依赖注射,AOP 对象生成等都在编译时完成。Micronaut 使用配套的 Maven 或 Gradle 插件来完成 AOT,并直接支持将应用打包成 GraalVM 原生镜像。这个插件还提供了一个开发者模式(dev mode)来加速开发速度,它运行应用并实时 watch 改动的代码和做重加载,开发者可以即时看到改动生效。

Quarkus

2019 年初,红帽(Red Hat)推出了一个全栈开源应用程序框架 Quarkus。官网对这个框架的次标题用”Supersonic Subatomic Java“(超音速亚原子的 Java)标榜其大小和速度。Quarkus 是一个容器优先,为 HotSpot 和 GraalVM 量身打造的框架,第一个版本 1.0 发布于 2019 年末,目前最新版本是 1.4.2。Quarkus 也是一个使用 AOT 预编译的框架,原理和 Micronaut 异曲同工,也是通过 Maven 或 Gradle 插件将所需要的类预先加载,生成和初始化。由于有红帽的加持,它直接支持 Java EE 各种标准如 JAX-RS 和 CDI(Context and Dependency Injection),无缝连接自家和各大开源库如 MicroProfile,Vert.X,Netty,Hibernate,RestEasy,Undertow,Keycloak,Camel 等,对各种云服务如 Kubernetes API,AWS Lambda,Azure Functions,Vault 等也有直接支持。开发者可以使用已经熟悉的库和 API 来在 Quarkus 上开发,而不需要再学习另一套新的 API。此外,Quarkus 使用一个 Extension 的架构用于帮助开发者方便地集成现有的工具库和代码到 Quarkus 中以利用其 AOT 的优势。Quarkus 也提供一个和 Micronaut 类似的 dev mode 来取悦开发者。和 Micronaut 相比,Quarkus 支持的库和特性更广泛,也更有利于形成生态,这和红帽的投资密不可分。

Micronaut vs Quarkus vs Spring Boot

我使用了一个最简单的 RESTful Hello World 场景在个人笔记本上(JDK_11.0.7)对比 Micronaut,Quarkus 和 Spring Boot 的启动时间和内存消耗。JVM 运行时内存包含了堆内存(heap)和堆外内存(non-heap),只测量堆内存并不全面,而事实上 AOT 节省的很大一部分是堆外内存(如 Metaspace)。因此使用 RSS(Resident Set Size,常驻内存集)测量更准确,也是在容器环境中衡量内存消耗的方式。为了让程序充分运行来反映真实情况,在程序启动后,我对它们产生了一个持续 5 分钟的 5tps 的压力并在完成后通过 JConsole 观察测试结果(算是模拟一个 Serverless 函数实例支持 5 个并发的场景吧 (smile))。3 个项目都是各自官网生成下载并稍作修改。

Micronaut (2.0.0.M3)

  • 代码

  1. @Controller("/")

  2. @Validated

  3. public class GreetingController {

  4. @Get(uri = "/hello/{name}", produces = MediaType.TEXT_PLAIN)

  5. public Single<String> hello(@NotBlank String name) {

  6. return Single.just("Hello " + name + "!");

  7. }

  8. }

  • 启动

  1. windhong > java -jar target/micronaut-demo-0.1.jar -Xmx64m

  2. 17:23:10.351 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 1444ms. Server Running: http://localhost:8080

  • 内存

  1. windhong > ps x -o pid,rss,command -p 15905

  2. PID RSS COMMAND

  3. 15905 158052 /usr/bin/java -jar target/micronaut-demo-0.1.jar -Xmx64m

Quarkus (1.4.2.Final)

  • 代码

  1. @Path("/hello")

  2. public class ExampleResource {

  3. @GET

  4. @Path("/{name}")

  5. @Produces(MediaType.TEXT_PLAIN)

  6. public String hello(@PathParam("name") String name) {

  7. return "hello " + name + "!";

  8. }

  9. }

  • 启动

  1. windhong > java -jar target/quarkus-demo-1.0.0-SNAPSHOT-runner.jar -Xmx64m

  2. __ ____ __ _____ ___ __ ____ ______

  3. --/ __ \/ / / / _ | / _ \/ //_/ / / / __/

  4. -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \

  5. --\___\_\____/_/ |_/_/|_/_/|_|\____/___/

  6. 2020-05-23 16:46:49,368 INFO [io.quarkus] (main) quarkus-demo 1.0.0-SNAPSHOT (powered by Quarkus 1.4.2.Final) started in 1.207s. Listening on: http://0.0.0.0:8080

  7. 2020-05-23 16:46:49,426 INFO [io.quarkus] (main) Profile prod activated.

  8. 2020-05-23 16:46:49,426 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]

  • 内存

  1. windhong > ps x -o pid,rss,command -p 14227

  2. PID RSS COMMAND

  3. 14227 159364 /usr/bin/java -jar target/quarkus-demo-1.0.0-SNAPSHOT-runner.jar -Xmx64m

Spring Boot (2.3.0.RELEASE)

  • 代码

  1. @Controller

  2. public class GreetingController {

  3. @GetMapping(value="/hello/{name}", produces = "text/plain")

  4. public @ResponseBody String hello(@PathVariable("name") String name) {

  5. return "Hello " + name + "!";

  6. }

  7. }

  • 启动

  1. windhong > java -jar target/springboot-demo-0.0.1-SNAPSHOT-old.jar -Xmx64m

  2. . ____ _ __ _ _

  3. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \

  4. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

  5. \\/ ___)| |_)| | | | | || (_| | ) ) ) )

  6. ' |____| .__|_| |_|_| |_\__, | / / / /

  7. =========|_|==============|___/=/_/_/_/

  8. :: Spring Boot :: (v2.3.0.RELEASE)


  9. 2020-05-23 17:09:04.640 INFO 14915 --- [ main] c.e.s.SpringbootDemoApplication : Starting SpringbootDemoApplication v0.0.1-SNAPSHOT on EMB-8ENUFVH5 with PID 14915

  10. 2020-05-23 17:09:04.646 INFO 14915 --- [ main] c.e.s.SpringbootDemoApplication : No active profile set, falling back to default profiles: default

  11. 2020-05-23 17:09:06.350 INFO 14915 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)

  12. 2020-05-23 17:09:06.370 INFO 14915 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]

  13. 2020-05-23 17:09:06.370 INFO 14915 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.35]

  14. 2020-05-23 17:09:06.498 INFO 14915 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext

  15. 2020-05-23 17:09:06.498 INFO 14915 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1748 ms

  16. 2020-05-23 17:09:06.782 INFO 14915 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'

  17. 2020-05-23 17:09:07.105 INFO 14915 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''

  18. 2020-05-23 17:09:07.124 INFO 14915 --- [ main] c.e.s.SpringbootDemoApplication : Started SpringbootDemoApplication in 3.436 seconds (JVM running for 4.191)

  19. 2020-05-23 17:09:14.371 INFO 14915 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'

  20. 2020-05-23 17:09:14.371 INFO 14915 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'

  21. 2020-05-23 17:09:14.385 INFO 14915 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 14 ms

  • 内存

  1. windhong > ps x -o pid,rss,command -p 14915

  2. PID RSS COMMAND

  3. 14915 214112 /usr/bin/java -jar target/springboot-demo-0.0.1-SNAPSHOT-old.jar -Xmx64m

对比结果

Micronaut 和 Quarkus 的启动速度都是旗鼓相当的 1 秒钟多一点,Spring Boot 的启动速度是它们的 3 倍。Micronaut 和 Quarkus 也有差不多的 RSS 消耗,Spring Boot 比它们多消耗大约 60MB。Quarkus 在内存管理上有一些亮点,它通过 YGC 可将 heap 降到 10MB,而 Micronaut 和 Spring Boot 在 heap 上是在稳步爬升的状态。Micronaut 和 Quarkus 在 heap 和 non-heap 上都比 Spring Boot 消耗得少一点。可以看到 non-heap 基本是在 metaspace 上省出来的。

GraalVM

无论是 Micronaut 还是 Quarkus,它们的终极目标都是应用于 GraalVM。GraalVM 是 Oracle 开发的支持多语言的虚拟机,它支持 Java 和其他基于 JVM 的语言,JavaScript,Ruby,Python,R,WebAssembly,C/C++ 和其他 LLVM-based 语言。对应 Java 应用,GraalVM 可以作为 JVM 让 Java 执行得更快。更重要的是,GraalVM 提供一个 Native Image(原生镜像)功能,这个功能可以将基于 JVM 的程序编译成本地可执行文件,编译的过程也正正是使用 AOT - 静态分析查找在运行时碰触到的任何代码,并将它们编译成机器码。

使用 Native Image 可以使你的应用获得极快的启动速度,这个优势使它非常适合用于命令行和 Serverless 程序,因为这些程序生命短暂但需要在很短时间内可以启动好多次。为什么它会启动得如此快?因为 Native Image 并不需要在启动时做 Class Loading,所有的类已经在创建镜像时预加载好,有些甚至已经部分初始化了。相应的堆内存也已经预先分配好,不会等到启动时来做。另一个使用 Native Image 的好处是它相比于 JVM 的更低的 Memory Footprint。它可以抛弃很多 JVM 才会用到的内存段,比如已经加载类的 metadata 和用于优化性能的 Segmented Code Cache。

Micronaut 和 Quarkus 都支持通过 Maven 插件将应用打包成 GraalVM Native Image,并预先设置好一些适应框架优化过的参数。打包的时间相比普通的 Java 编译和打包时间长(很多),完成后的输出是一个可执行文件。

  1. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] classlist: 7,653.08 ms

  2. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] (cap): 3,122.82 ms

  3. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] setup: 5,701.56 ms

  4. 17:29:09,610 INFO [org.jbo.threads] JBoss Threads version 3.1.1.Final

  5. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] (typeflow): 39,496.63 ms

  6. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] (objects): 24,782.94 ms

  7. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] (features): 735.23 ms

  8. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] analysis: 66,922.71 ms

  9. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] (clinit): 928.87 ms

  10. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] universe: 4,180.73 ms

  11. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] (parse): 7,463.07 ms

  12. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] (inline): 9,651.46 ms

  13. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] (compile): 69,337.05 ms

  14. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] compile: 89,477.98 ms

  15. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] image: 8,517.05 ms

  16. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] write: 1,836.76 ms

  17. [quarkus-demo-1.0.0-SNAPSHOT-runner:3010] [total]: 184,895.55 ms

将之前的 Quarkus Demo 应用打包成原生镜像需要 3 分多钟的时间。执行这个文件的启动时间是逆天的 44 毫秒。

  1. windhong > ./target/quarkus-demo-1.0.0-SNAPSHOT-runner -Xmx32m

  2. __ ____ __ _____ ___ __ ____ ______

  3. --/ __ \/ / / / _ | / _ \/ //_/ / / / __/

  4. -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \

  5. --\___\_\____/_/ |_/_/|_/_/|_|\____/___/

  6. 2020-05-27 00:31:10,815 INFO [io.quarkus] (main) quarkus-demo 1.0.0-SNAPSHOT (powered by Quarkus 1.4.2.Final) started in 0.044s. Listening on: http://0.0.0.0:8080

  7. 2020-05-27 00:31:10,815 INFO [io.quarkus] (main) Profile prod activated.

  8. 2020-05-27 00:31:10,815 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]

我使用 - Xmx32m 参数将堆内存限制在 32M,并施放同样一个 5 分钟 5tps 的请求来观察它的内存使用情况,结果是 RSS 保持在 25M 左右。

  1. windhong > ps x -o pid,rss,command -p 69373

  2. PID RSS COMMAND

  3. 69373 24820 ./target/quarkus-demo-1.0.0-SNAPSHOT-runner -Xmx32m

作为一个 Java 编写的程序这样的启动速度和内存 footprint 可谓是相当感人。

使用 AOT 和 Native Image 也带来很多局限性。为了产生一个高度优化的本地可执行文件,AOT 使用 Closed World 作为静态分析的假设,这代表在编译时,所有在运行时访问到的类和资源都要可达。说人话就是:它再也不能在运行时加载未知的类,接口或各种资源。如果我们在代码中使用了反射,动态类加载资源和动态代理等之前提到的技术,在做 AOT 时,我们必须要通过配置文件告诉 GraalVM 哪些类和资源会被用到,这些资源都必须在 AOT 编译时可达。为了帮助开发者生成这个配置文件,GraalVM 提供了一个 Java agent 工具。在运行时这个 agent 会观察 Java 应用的行为,看它反射了多少类和加载了哪些资源等,并产生 AOT 需要的配置文件。

以下是一些在运维上的局限。

  • 不支持 JVMTI(Java Virtual Machine Tool Interface):这意味着我们无法再使用类似 JConsole 和 JMX 等工具去 profile 我们的应用程序,这对 debug 来说是一大挑战。

  • 不支持 Java Agent:有一些工具依赖 Java Agent 来做 tracing,安全测试,性能指标收集和暴露等。这些工具都不能使用了。

  • 有限度地支持 Heap Dump 和 Thread Dump:社区版的 GraalVM 支持使用命令行方式获得 Heap Dump, 但如果想使用其配套的 VisualVM 来分析 Thread Dump 则要购买企业版。

GraalVM 在 2019 年 5 月份发布 19.0 版本时官方宣称其 Production Ready,目前的版本是 20.1。

Spring Framework 也在积极参与到和 GraalVM 集成中,为此社区维护了一个 Spring GraalVM Native 项目,它目前还只是有体验性质的输出,只能使用某些特定的库。由于 Spring 框架底层是基于反射和 CGLIB 字节码生成,完整支持 Native Image 并对它进行优化还需要一段时间。

结语

我们都热爱 Java,都希望 Java 有更长的生命力,在未来十年能继续使用 Java 开发应用。在云原生的时代,很多 Java 引以为豪的功能和特性成为阻碍它发展的障碍。Java 需要面向更快更小的应用,很多设计模式(如运行时插件和 SPI)需要重新考虑。在微服务架构盛行的今天,改变和尝试技术栈的成本变得前所未有的低。在开始一个新的工程,或者开发一个新功能时,我们不妨可以思考一下:我真的一定要用 Spring 吗?我真的要用反射和动态加载吗?我的程序可以用 AOT 吗?

References

GraalVM 性能深度解释 https://www.infoq.com/presentations/graalvm-performance/


文章转载自Share and Fun喜来分,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论