理解向。课后总结和个人理解。如有认知错误误跪求指正。
背景
云原生的兴起离不开容器化的部署,而实现容器化部署需要一个核心需求就是实现程序的自动扩缩,来应对和保证在不同请求压力下提供正常且高质量的服务能力。
而自动扩缩意味着提供服务的程序的即启即停能力要求特别迅速、特别敏捷。因为请求峰值可能就在那一两秒钟,如果程序的启动却要耗费的时间比高压请求的时间还要长,那么扩缩就完全失去了意义。虽然目前使用架构大多是微服务,已经将一个复杂的单体系统,拆成了多个独立的模块,但是每个模块提供的服务仍然不少,假如A服务模块提供功能接口有100个,但是其中的热点接口只有5个,那么其余95个接口相当于占用了较大资源,但是并没有做太多事情。这里想表达的意思是,微服务的粒度仍然不够小,如果一个接口就是一个微服务,我们再依据不同服务所需要提供服务的能力要求来分配资源,那么我们服务器的资源利用率将会比单个整体的100个接口的模块来将要高很多,而且更为合理。且单个接口的微服务程序的启停效率,相比100个接口的模块启停效率的提升也将是巨大的。这个也是Serverlesss的思想核心吧。
总结,拥抱云原生,要求程序合理运用资源、实现自动扩缩,而自动扩缩的核心是①启动的服务要最够小而准确②服务要能高效的启动和停止否则自动扩缩将没有意义。因此提出了Serverless的无服务思想(Serverless的思想不只是这个,详细还是得自行)。
java在自动扩缩上的尴尬处境
由于java的跨平台特性,他是一门编译+解释的语言。打个比方,云南人杨仔要想和广东人靓仔交流,首先杨仔要把自己想说的(编译)先转化成普通话(解释)说给靓仔听。靓仔这边也需要将自己想说的(编译)转化成普通话(解释)再向杨仔进行传达。云南话、粤语是两个不同平台的语言,通过“想说”将思想编译成了普通话(class文件),传达到对方的脑中时通过解析(jvm解释class文件中的代码形成机器码)完成了相互的理解。
由于jvm是在运行时编译(JIT),导致其最大的问题就是程序启动和运行效率不高。启动一个java程序首先要先启动jvm,然后jvm再扫描文件夹将class文件进行加载,jvm加载class文件之后再将其解释成机器码,最终才能执行相关的逻辑。
而对于go语言来讲,它可以直接将高级语言翻译成对应操作系统可执行的机器码,没有jvm这些花里胡哨的加载过程。因此它的启停效率特别的高。
所以要解决java程序在jvm启动缓慢的尴尬现状。必须要弃掉JIT,改为直接翻译成可执行机器码文件。
是spring的窘境,也是java生态的窘境
由于java生态框架绝大部分使用了JIT即时编译的特性。比如反射、动态代理,都是在运行时编译,这些操作不只是在spring上大量使用,也是咱们java程序员的基本操作。咱把JIT干掉了转为直接编译为机器码,之前写的反射和动态代理咋整,是不是拥抱云原生意味着之前写的所有程序都不可用了?
JIT不要了?没事GraalVM会出手
GraalVM是一个基于Java HotSpot VM,保留了JIT的特性的虚拟机,简单来说GraalVM具备将java代码直接转成对应平台的机器码的功能,该功能可保证启停的高效率,也由于它是基于Java HotSpot VM,保留了JIT的特性,因此直接使用它作为开发时运行java的虚拟机也没有什么问题。
运行时无法编译了,没事我在编译时直接生成!GraalVM实际上就是这么干的,无论如何GraalVM在编译时都会现将java文件转化为class文件,解析代码中是否涉及到反射调用?是否涉及到动态代理,如果有涉及动态代理,在编译时直接生成对应代理类的class再转化成机器码,如果涉及反射调用,会将反射调用的类和方法,再转化成机器码(这里可能说的不对)。但是,对于一些不确定的加载,GraalVM也无能为力。比如:
AService.class.newInstance().getMethod(System.getProperty("就不告诉你"), null);
上面这个方法必须通过运行时传参“就不告诉你”才能确定,在编译时根本无法确定。如果是这样:
AService.class.newInstance().getMethod(”getName”, null);这个方式GraalVM就能判断。
当前GraalVM编译成机器码的时间相当长,特别长,究极长。编译一个只暴露单接口的springboot项目,编译为机器码耗时为5min+-,执行exe机器码文件启动只需0.149s,通过idea普通流程启动需要2s~3s。当然编译慢也可以理解,在Serverless无服务思想下,一个程序规模铁定比当下微服务模块的体量小得多多,畅享未来也可以接受。
不确定类的加载,要怎么搞?
针对于类似这种:AService.class.newInstance().getMethod(System.getProperty("就不告诉你"), null);
不确定类的加载,可以通过配置文件的方式告诉GraalVM:哎哎哎,我有堆类需要被反射调用或者动态代理,我放在约定好的指定配置文件(reflect-config.json)里了,记得在编译成机器码的时候去读一下给它加载喽,把这些类和方法转成机器码。
Spring6和Spirngboot3在摒弃动态编译做的努力——AOT
全称ahead-of-time,核心思想就是运行时你不给我动态编译生成,好的,我在编译时就提前把那些要动态生成的类给你弄出来。而且springboot3还做了一定的优化工作,它不仅仅是把需要反射注册的一些bean解析放到了reflect-config.json文件里供给GraalVM加载,而且在编译时直接提前做了单例bean的扫描,生成了一个一个的ServiceXX__BeanDefinition.class文件(注意这里不只是会生成__BeanDefinitin.class一种文件,还会生成引导加载这堆ServiceXX__BeanDefinition的一些类,通过这些引导加载的类Xx__ApplicationContextInitializer和Xx__BeanFactoryRegistrations,对这些BeanDefinition做收集和统一的加载),并解释成了机器码,这些操作都是通过sprng-aot模块进行的。在Spirngboot3项目在启动流程中直接加载了这些ServiceXX__BeanDefinition对象,省去了扫描的耗时,进一步提升了启动速度。
spring6自身的组件,如果存在反射、动态代理,它可以做识别收集,自动写入reflect-config.json文件中,但是我们自己业务上使用的反射、动态代理Spirngboot3是帮不了我们的,除非我们告诉springboot3,我有这样一堆类,他后续一定会被反射调用或是动态代理,请你帮我把它们写到reflect-config.json配置文件里去或是我们自己手动追加,后者当然是我们会去考虑的,那么如何告知springboot3这些类呢?有三个方法(请参考官方文档spring-aot的使用):
① 实现RuntimeHintsRegistrar接口,并交于spring管理,重写接口里面的逻辑,注册要强制编译的类和方法,他们最终会被springboot3写到reflect-config.json去。
② 类上加上注解@RegisterReflectionForBinding,标识整个类的所有方法均可能被反射使用。他们最终会被springboot3写到reflect-config.json去。
③ @Reflective,在方法上加上这个注解,标识这个类的这个方法,可能被反射调用,你得帮我写到reflect-config.json文件里。注意,这个注解必须也要至少加在一个构造器上,否则通过ASerivce.class.newInstance().Method(..., ...)时,newInstance()无法创建对象,因为在编译时就没考虑到要把这个构造方法编译成机器码。