Java从诞生到现在已经走过了26个年头。在这26年里,Java应用从未停止过,从最初的单机版到Web应用,再到现在,凭借着强大的生态,它依然占据着当今语言争论中“世界第一”的宝座。但在如今的云原生Serverless时代,Java应用遇到了前所未有的挑战。在云原生时代,云原生技术利用公共云、私有云和混合云等新的动态环境来构建和运行可弹性扩展的应用程序。而我们的应用越来越呈现出以下特点:在基于容器镜像的Java诞生之初,凭借着“一次编译,随处运行”的口号,采用语言层虚拟化的方式,操作系统平台尚未1980年代统一,建立了优势。但如今进入云原生时代,以Docker为首的容器技术也提出了“一次构建,到处运行”的口号,通过操作系统虚拟化为应用提供环境兼容性和平台无关性。因此,在如今的云原生时代,Java“一次编译,到处运行”的优势已经被容器技术大大削弱,不再是大多数服务器开发者在技术选型上的主要考虑因素。另外,由于是基于镜像,云原生时代对镜像的大小非常敏感,包含JDK的Java应用上百兆的镜像大小无疑越来越不符合时代的要求。生命周期缩短,经常需要弹性伸缩。灵活性和弹性可以说是云原生应用的一个显着特征,而这也意味着应用需要有更短的冷启动时间来满足灵活性和弹性的要求。Java应用程序通常是为长期的大型程序设计的。JVM的JIT和分层编译优化技术将使Java应用程序在持续运行中自我优化,并在一段时间后达到性能峰值。但与运行时性能相反,Java应用程序的启动时间通常很慢。Spring等流行框架中广泛的类加载、字节码增强和初始化逻辑加剧了这个问题。这无疑与云原生时代的理念背道而驰。对计算资源的使用敏感在公有云时代,应用往往按使用量付费,这意味着应用所需的计算资源变得非常重要。Java应用占用大量内存的先天劣势在云原生时代被放大了。与其他语言相比,它的使用变得更加“昂贵”。可见,在云原生时代,Java应用的优势在不断被侵蚀,劣势也在不断被放大。因此,如何让我们的应用更符合时代的发展,让Java语言在云原生时代发挥更大的价值,成为了一个值得探讨的话题。为此,笔者将尝试跳出语言比较的固有思维,从更全局的角度,来看看在云原生应用发布的全过程中,我们可以做哪些优化。镜像构建优化Dockerfile从Dockerfile入手,因为它是最基础最简单的优化,可以简单的加快我们应用构建和拉取镜像的时间。以一个Springboot应用为例,我们通常看到的Dockerfile是这样的:FROMopenjdk:8-jdk-alpineCOPYapp.jar/ENTRYPOINT["java","-jar","/app.jar"]很简单明了,但显然,这不是一个很好的Dockerfile,因为它没有使用Image层来进行更高效的缓存。我们都知道Docker有足够高效的缓存机制,但是如果我们没有很好地利用这个特性,只是简单地将Jar包打包成一个单层镜像,就会导致即使应用只改变一行代码,我们需要重新构建整个SpringbootJar包,其中Spring庞大的依赖类库实际上并没有发生变化,无疑是一种得不偿失的做法。因此,将应用程序的所有依赖库都视为一个单独的层显然是更好的解决方案。因此,一个更合理的Dockerfile应该是这样的:FROMopenjdk:8-jdk-alpineARGDEPENDENCY=target/dependencyCOPY${DEPENDENCY}/BOOT-INF/lib/app/libCOPY${DEPENDENCY}/META-INF/app/META-INFCOPY${DEPENDENCY}/BOOT-INF/classes/appENTRYPOINT["java","-cp","app:app/lib/*","HelloApplication"]这样我们就可以充分利用Image层缓存以加快构建和拉取图像的时间。在Docker对镜像构建拥有绝对话语权的今天,我们在实际开发过程中往往忽略了构建组件的选择,但其实选择高效的构建组件往往可以让我们的构建更加高效。传统的dockerbuild有什么问题?在Dockerv18.06之前,dockerbuild会存在一些问题:更改Dockerfile中的任何一行都会使所有后续行的缓存失效#假设只更改了此Dockerfile中的EXPOSE端口号#那么接下来的RUN命令cachewillbeinvalidFROMdebianEXPOSE80RUNaptupdate&&aptinstall–yHEAVY-PACKAGES多阶段并行构建效率不好#即使stage0和stage1之间没有依赖关系#docker也不能并行构建,而是选择serialFROMopenjdk:8-jdkASstage0RUN./gradlewcleanbuildFROMopenjdk:8-jdkASstage1RUN./gradlewcleanbuildFROMopenjdk:8-jdk-alpineCOPY--from=stage0/app-0.jar/COPY--from=stage1/app-1.jar/不能提供编译历史缓存#简单的RUN命令不能提供编译历史缓存#并且RUN--mount新语法不支持老版本docker下的RUN./gradlewbuild#sinceDockerv18.06#语法=docker/dockerfile:1.1-experimentalRUN--mount=type=cache,target=/.cache./gradlewbuild镜像push和pull过程中存在固有的压缩和解压耗时如上图所示,在传统的dockerpullpush阶段,有一个pack和unpack的耗时过程,但这部分不是必须的。业界一直在积极讨论这些先天不足,一些能够适应新时代的构建工具已经诞生。下一代建筑组件:选择最好的新一代建筑工具没有银弹,但通过一些简单的比较,我们仍然可以选择最合适的建筑工具。我们认为,一款适用于云原生平台的构建工具至少应该具备以下特点:能够支持完整的Dockerfile语法,实现应用的平滑迁移;能够弥补上述传统Docker构建的不足;能够在非root权限模式下执行(在基于Kubernetes的CICD环境中显得尤为重要)。因此,Buildkit脱颖而出。这个由Docker开发,目前由社区和Docker维护的新一代构建工具是“含着金钥匙诞生的”。它具有良好的可扩展性,大大提高了构建速度,并提供了更好的安全性。Buildkit支持所有Dockerfile语法,可以更高效的命中构建缓存,增量转发构建上下文,多并发直接将镜像层推送到镜像仓库。(Buildkit与其他构建组件的对比)(Buildkit的构建效率)镜像大小为了控制镜像拉取和推送的耗时,我们通常会尽可能的减小镜像的大小。AlpineLinux是许多Docker容器的首选基础镜像,因为它只有5MB大小,比其他CentOS、Debain等超过100MB的发行版更适合容器环境。不过AlpineLinux为了尽可能瘦身,默认使用musl作为C标准库,而不是传统的glibc(GNUC库)。因此,要制作基于AlpineLinux的OpenJDK镜像,必须先安装glibc。此时,基本映像大约为12MB。在JEP386中,OpenJDK将上游代码移植到了musl中,并通过了兼容性测试。Java16已经发布了这个特性,这种方式生成的镜像只有41MB,不仅远低于CentOS的OpenJDK(约396MB),也远小于官方的slim版本(约200MB).应用程序启动加速让我们先来看看Java应用程序在启动过程中会有哪些阶段。这张图代表了Java运行时各个阶段的生命周期。你可以看到它经历了五个阶段。第一个是VMinit虚拟化。机器的初始化阶段,然后是Appinit应用程序的初始化阶段,然后是App活跃(warmup)的应用预热期,经过一段时间的预热后,进入App活跃(steady)达到性能峰值期,最后应用结束,完成整个生命周期。从上图使用AppCDS不难发现,蓝色的CL(ClassLoad)部分其实占据了Java应用启动阶段的很大一部分。Java也一直致力于减少应用程序启动的ClassLoad时间。从JDK1.5开始,HotSpot提供了CDS(ClassDataSharing)功能。长期以来,其功能非常有限,仅部分商业化。早期的CDS致力于“共享”同样需要在同一主机上的JVM实例之间加载一次的类,但不幸的是早期的CDS无法处理AppClassloader加载的类,这使得它出现在了实际的开发实践中。比较“鸡肋”。但是从OpenJDK10(2018)开始,AppCDS[JEP310]在CDS的基础上增加了对AppClassloader的适配。它的出现使得CDS技术得到广泛应用和应用。特别是对于每次需要加载上千个类的SpringBoot程序,因为JVM不需要在每个实例每次启动时都加载(解析和验证)这些类,所以启动应该变得更快,内存占用应该更小.小的。看起来AppCDS里的一切都很美好,但是实际使用中真的如此吗?当我们尝试使用AppCDS时,它应该包含以下步骤:使用-XX:DumpLoadedClassList参数获取我们想要在应用程序实例之间共享的类;使用-Xshare:dump参数将类存储到适合内存映射的存档(.jsa文件)中;使用-Xshare:on参数在启动时将存档附加到每个应用程序实例。乍一看,使用AppCDS似乎很简单,只需3个简单的步骤。但是在实际使用中,你会发现每一步都可能变成一个应用程序启动,带有特定的JVMOptions,我们不能简单地通过一次启动启动一个可复用的类加载归档文件。尽管在JDK13中,提供了一个新的动态CDS[JEP350]将上述步骤1和2合并为一个步骤。但是在目前流行的JDK11中,我们还是逃不过上面的三个步骤(三次启动)。因此,使用AppCDS往往意味着应用程序启动过程的复杂修改,伴随着较长的初始编译和启动时间。另请注意,在使用AppCDS时,许多应用程序的类路径会变得更加混乱:它们既在原始位置(JAR包)又在新的共享存档(.jsa文件)中。在我们应用开发的过程中,我们会不断的改变和删除原来的类,JVM会从新的类中解析出来。这种情况带来的危险是显而易见的:如果类档案保持不变,类迟早会不匹配,我们将遇到经典的“类路径地狱”问题。JVM不能阻止类更改,但它至少应该能够在适当的时候检测到类不匹配。然而,在JVM的实现中,它没有检测每个单独的类,而是选择比较整个类路径。因此,在AppCDS的官方描述中,我们可以看到这样一句话:Theclasspathusedwith-Xshare:dumpmustbethesame,orbeaprefixof,theclasspathusedwith-Xshare:on。否则,JVM将打印一条错误消息。路径相同(或者前者是后者的前缀)。但这是一个比较含糊的说法,因为类路径可能有几种不同的构成方式,例如:直接从有Jar包的目录加载.class文件,如javacom.example.Main;使用通配符,扫描带有Jar包的A目录,如java-cpmydir/*com.example.Main;使用明确的Jar包路径,如java-cplib1.jar:lib2.jarcom.example.Main。在这些方法中,AppCDS唯一支持的方法是第三种方法,即显式列出Jar包路径。这使得使用大量Jar包依赖的应用程序的启动语句非常繁琐。同时还要注意的是,这种显式列出Jar包路径的方法并没有进行递归查找,即只会在包含所有class文件的FatJar中生效。这意味着使用SpringBoot框架的嵌套Jar包结构,将难以享受到AppCDS技术带来的便利。因此,SpringBoot要想在云原生环境下使用AppCDS,就必须进行应用侵入式改造。没有使用SpringBoot默认的嵌套Jar启动结构,而是使用类似mavenshade的插件,重新打FatJar,在程序中显示。允许程序自然关闭的接口或参数的声明,通过Volume挂载或Dockerfile改造存储和加载类的归档文件。下面是一个修改后的Dockerfile的例子:#这里假设我们已经做了FatJar改造,Jar包中包含了应用程序运行所需的所有类文件FROMeclipse-temurin:11-jreasAPPCDSCOPYtarget/helloworld.jar/helloworld.jar#运行应用程序,并设置一个'--appcds'参数,使程序运行后可以停止RUNjava-XX:DumpLoadedClassList=classes.lst-jarhelloworld.jar--appcds=true#使用类上一步得到的list生成类归档文件RUNjava-Xshare:dump-XX:SharedClassListFile=classes.lst-XX:SharedArchiveFile=appcds.jsa--class-pathhelloworld.jarFROMeclipse-temurin:11-jre#同时复制Jar包和class归档文件COPY--from=APPCDS/helloworld.jar/helloworld.jarCOPY--from=APPCDS/appcds.jsa/appcds.jsa#使用-Xshare:on参数启动应用程序ENTRYPOINTjava-Xshare:on-XX:SharedArchiveFile=appcds.jsa-jarhelloworld.jar可见,使用AppCDS还是需要大量的学习和改造成本的,很多改造都会侵入我们的应用。JVM优化除了构建阶段和启动阶段,我们还可以从JVM本身入手,根据云原生环境的特点进行针对性的优化。使用了解容器内存资源的JDK在虚拟机和物理机中,对于CPU和内存分配,JVM从公共位置(例如Linux中的/proc/cpuinfo和/proc/meminfo)查找它可以使用的CPU和内存)记忆。但是,在容器中运行时,CPU和内存限制存储在/proc/cgroups/...中。旧版本的JDK会继续查找/proc(而不是/proc/cgroups),这会导致CPU和内存使用超过分配的上限,从而导致几个严重的问题:线程太多,因为线程pool的大小由Runtime.availableProcessors()配置JVM的内存使用量超过容器内存限制。并导致容器被OOMKilled。JDK8u131首先实现了UseCGroupMemoryLimitForHeap参数。但是这个参数有缺陷。在为应用程序添加UnlockExperimentalVMOptions和UseCGroupMemoryLimitForHeap参数后,JVM确实可以感知容器内存,控制应用程序的实际堆大小。但这并没有充分利用我们为容器分配的内存。因此,JVM提供了-XX:MaxRAMFraction标志来帮助更好地计算堆大小。MaxRAMFraction的默认值为4(即除以4),但它是一个分数,而不是百分比,因此很难设置一个能够有效利用可用内存的值。价值。JDK10为容器环境提供了更好的支持。如果在Linux容器中运行Java应用程序,JVM将使用UseContainerSupport选项自动检测内存限制。然后,通过InitialRAMPercentage、MaxRAMPercentage和MinRAMPercentage进行内存控制。在这种情况下,我们使用百分比而不是分数,这样会更准确。默认情况下,激活UseContainerSupport参数,MaxRAMPercentage为25%,MinRAMPercentage为50%。需要注意的是,MinRAMPercentage并不是用来设置最小堆大小的,而是只有当物理服务器(或容器)中的总可用内存小于250MB时,JVM才会使用这个参数来限制堆大小。同样,MaxRAMPercentage是当物理服务器(或容器)中的总可用内存大小超过250MB时,JVM将使用此参数来限制堆的大小。这几个参数已向下移植到JDK8u191。UseContainerSupport默认激活。我们可以设置-XX:InitialRAMPercentage=50.0-XX:MaxRAMPercentage=80.0让JVM感知并充分利用容器的可用内存。需要注意的是,指定-Xms-Xmx时,InitialRAMPercentage和MaxRAMPercentage将无效。关闭优化编译器默认情况下,JVM有多个JIT编译阶段。虽然这些阶段可以逐步提高应用程序的效率,但它们也会增加内存使用的开销并增加启动时间。对于短期运行的云原生应用,可以考虑使用以下参数关闭优化阶段,以牺牲长期运行效率换取更短的启动时间。JAVA_TOOL_OPTIONS="-XX:+TieredCompilation-XX:TieredStopAtLevel=1"关闭类验证JVM将类加载到内存中执行时,会验证该类没有被篡改,没有被恶意修改或破坏。但是在云原生环境下,CI/CD流水线通常由云原生平台提供,这意味着我们应用的编译部署是可信的,所以我们应该考虑使用如下参数来关闭校验。如果你在启动时加载了很多类,关闭验证可能会提高启动速度。JAVA_TOOL_OPTIONS="-noverify"减小线程堆栈大小大多数JavaWeb应用程序都基于每个连接一个线程的模型。每个Java线程都消耗本机内存(不是堆内存)。这称为线程堆栈,默认为每个线程1MB。如果您的应用程序处理100个并发请求,它可能至少有100个线程,这相当于使用100MB的线程堆栈空间。此内存不计入堆大小。我们可以使用以下参数来减少线程堆栈大小。JAVA_TOOL_OPTIONS="-Xss256k"需要注意的是,如果减少太多,会出现java.lang.StackOverflowError。您可以分析您的应用程序并找到要配置的最佳线程堆栈大小。使用TEM对Java应用进行零修改云原生优化通过上面的分析,我们可以看出,要想我们的Java应用在云原生时代发挥出最大的实力,我们需要付出很多侵入性的改造和优化的操作。那么有没有办法帮助我们对Java应用进行零修改的云原生优化呢?腾讯云TEM弹性微服务为Java开发者提供了应用零修改的最佳实践,帮助您的Java应用以最佳姿态快速上云。使用TEM,您可以享受以下优势:零构建部署。直接选择使用Jar包/War包进行交付,无需自己构建镜像。默认情况下,TEM提供了一个可以充分利用构建缓存的构建过程。使用新一代构建工具Buildkit进行高速构建,构建速度优化50%以上。整个构建过程可追溯,构建日志可查,简单高效。(直接使用Jar包部署)(可以查看构建日志)(构建速度对比)零改造加速。直接使用KONAJdk11/OpenJdk11进行应用加速,默认支持SpringBoot应用零修改加速。无需修改原有的SpringBoot嵌套Jar包结构,TEM直接提供Java应用加速最佳实践,实例扩展时启动时间缩短10%~40%。(无应用加速,规格1c2g)(有应用加速,规格1c2g)(应用启动速度对比,以springpetclinic为例,规格1c2g)零运维监控。使用SkyWalking在应用程序级别监控您的Java应用程序。可以直观的查看JVM堆内存、GC次数/耗时、接口RT/QPS等关键参数,帮助您甚至发现应用性能瓶颈。(应用程序JVM监控)极大的灵活性。TEM默认提供高利用率的定时弹性策略和基于资源的弹性策略,为您的应用提供秒级弹性性能,帮助您应对流量高峰,在实例闲置时及时节省资源。(指标弹性策略)(时序弹性策略)总结欲善其事,必先利其器。在如今的云原生时代,如何最大限度地提高Java应用的部署效率和运行性能,是摆在所有开发者面前的挑战。TEM作为微服务应用的ServerlessPaaS平台,将成为你手中的“云工具”。TEM将致力于服务于企业和开发者,以最快捷、最便捷、最省心的态度,为您的企业助一臂之力。无忧上云,享受云原生时代的便利。腾讯云弹性微服务(TEM)是面向微服务应用的Serverless平台,实现了Serverless与微服务的完美结合,提供开箱即用的微服务解决方案。目前,腾讯云最新一代Serverless微服务应用管理平台TEM限时免费,快来官网体验吧!参考文档http://seanthefish.com/2020/1...https://www.infoq.cn/article/...https://medium.com/@toparvion...https://events19。linux位于...https://static.sched.com/host...
