![GraalVM与Java静态编译:原理与应用](https://wfqqreader-1252317822.image.myqcloud.com/cover/558/41992558/b_41992558.jpg)
1.2 冷启动问题
冷启动,即全新启动一个Java应用程序。由1.1节介绍可知,Java程序从启动到抵达性能峰值需要经过VM初始化、应用初始化、应用预热3个阶段,会耗费一定的时间,我们可以将这3个阶段的耗时统称为冷启动的开销。
JVM初始化对应用的启动有多大影响呢?javac程序可以作为一个良好的实验对象。javac程序把Java源码编译到字节码的编译器,其本身就是一个纯Java应用,所以我们可以将它作为Java应用的典型代表。
javac有两种运行方式:一是通过本地启动器程序(也就是我们平时常见的$JAVA_HOME/bin/javac)启动编译器独立运行,这种方式每次执行javac时都会启动一个独立的JVM;二是作为库函数通过API被其他Java程序调用,这种方式会在调用方已有的JVM中执行,无须再次启动JVM。通过这两种方式启动的编译在功能性上没有任何区别,只有是否需要另外启动JVM的区别,所以我们只要分别通过这两种方式使用javac编译同一段Java程序,然后对比它们的性能数据就可以看到JVM初始化开销了。
图1-3给出了分别用这两种方式编译同一段HelloWorld程序源码的耗时对比,左侧条柱为通过API调用的耗时,右侧条柱为独立执行方式的耗时。从图中我们可以看到这两者之间有近200ms的差距——可以将其视为JVM初始化开销。从这个实验对比中,我们可以说在使用javac编译HelloWorld时有近50%的冷启动开销。当然对于不同的应用,或者同一个应用的不同工作负载而言,冷启动的开销并不是固定的200ms或者50%。应用和工作负载的不同,JVM初始化时需要加载的类的数量也会不同,类越多耗时越长。工作负载越大,工作本身的耗时越长,冷启动开销所占的比重就越小;工作负载本身的耗时越小,冷启动开销的问题就越突出。
![021-01](https://epubservercos.yuewen.com/0AE46C/22010258901905506/epubprivate/OEBPS/Images/021-01.jpg?sign=1739647256-dTMS3Ft6muWc7nJjkDM5fOQGeBeuQOox-0-c507e1c13d5c153908a900d70450576a)
图1-3 javac的两种调用方式编译HelloWorld程序的执行性能对比
由于一般的Serverless应用自身的工作负载较小,因此冷启动的开销问题就显得尤为突出。而除了JVM初始化外,Serverless应用的冷启动中一般还包含服务框架的初始化,这也会对冷启动造成更显著的影响。我们可以通过greeting-service的例子直观地了解冷启动开销对应用程序的影响。
当greeting-service服务启动后,该服务会接受用户发来的greeting请求并返回计数值和一个固定的字符串,具体返回值如下所示。
{ "id":1, "content":"Hola, World!" }
我们将这个服务部署到阿里云的函数计算平台[1](具体部署方法可以参考第13章的介绍,也可以通过函数计算的官方文档了解[2]),然后在函数计算平台的控制台上执行函数调用,以消除从本地到阿里云的网络延迟影响,依次执行两次函数调用可以得到如图1-4所示的RT(response time,响应时间)对比图。这里的RT取自函数计算平台的执行日志,指从发起请求到接收到结果的全部时间。
![022-01](https://epubservercos.yuewen.com/0AE46C/22010258901905506/epubprivate/OEBPS/Images/022-01.jpg?sign=1739647256-N4xOeYaSQkDeHhtGdW0oAlPsm6d5grU8-0-46f3610b805193707c51489b33fd915b)
图1-4 greeting-service服务冷启动执行时间对比图
从图1-4可以看到,第一次请求和第二次请求之间存在着十几倍的巨大差异,除了VM初始化的冷启动开销外,时间主要消耗在应用初始化,即初始化Spring Boot框架上。在用户发起第一次请求之前,计算函数平台上并没有可用的greeting-service服务。当收到用户的首次请求时,函数计算平台才会初始化JVM,启动greeting-service的Spring Boot框架和应用服务,然后才能响应用户的greeting请求。当greeting-service的服务经过冷启动准备后,对第二次及以后的请求的响应就会非常快。
上述的冷启动问题在传统的将应用部署在自建服务器场景下同样存在,但是影响并不明显,因为应用服务提供商可以在自己的服务器上提前启动好应用程序,为其做好充分的预热并保持其时刻处于待命的状态,以便当客户的访问请求到来时提供最好的服务响应。但是在Serverless云计算场景下,由于以下几点原因,冷启动问题会表现得格外突出。
- Serverless服务本身执行时间短。Serverless应用强调微服务架构,服务的粒度小,耗时短。与短暂的应用执行时间相比,冷启动的开销耗时所占比重增大,甚至可能比程序执行时间还要长,因此冷启动对应用的影响也到了不可忽视的程度。
- Spring Boot等框架启动时间长。Spring Boot应用在启动时要扫描所有代码并注册bean,启动时间与代码量成正比。
- Serverless服务会变“冷”。云计算的一大特点是按需使用,当不再有新的使用需求时,云服务会在执行完用户的最后一次请求后的一段时间关闭,此后的第一次服务请求会再次遭遇冷启动问题。
- 预热需要额外的费用花销。当前各个云服务提供商对冷启动问题的解决方案是提供付费预热,应用服务提供商可以购买预热服务提前将自己的服务启动起来,或者通过在服务变冷后定时唤起的方式,让自己的应用保持一定的热度。这就要求应用服务提供商对其用户的使用模式有较为准确的预测,能够在恰到好处的时候预热程序,否则就会多付出不必要的费用。
- 服务扩容时又会面临冷启动。当服务请求数量激增时,云服务提供商会为服务进行弹性扩容,但是新扩容的服务还是会冷启动,被分流到冷启动应用上的请求的响应时间就会上升,从而不能为用户提供最佳的体验。
由此可见,冷启动问题已经是Serverless云原生应用必须面对的重大挑战,业界也提出了多种提高Java启动速度、降低冷启动开销的方案,比如将通用类的数据保存下来并在不同Java进程之间共享,以提高启动速度的AppCDS(Application Class Data Sharing[3])技术,就是OpenJDK社区提出的一个解决方案。但是由于冷启动问题的本质是由JVM初始化、启动时的类加载、程序的解释执行以及JIT编译开销等Java基础性技术综合造成的,所以在传统Java的体系下无法彻底解决。
[1]参见https://www.aliyun.com/product/fc。