️This article has been over 2 years since the last update.
Spring Cloud Function实现了类似于AWS Lambda的云函数调用,属于Serverless架构。它是基于SpringBoot开发的FAAS项目,目前Star只有100多,可以说是玩具级别。但是功能还是很全的。它基于Reactor进行设计,类似于Akka中的Actor或者RxJava中的Lift管道操作符
The Serverless Architecture
Deploy your applications as independent functions, that respond to events, charge you only when they run, and scale automatically.
阅读本文需要如下知识点
- 了解基本SpringBoot的注解,可以参考《SpringBoot揭秘》
- 掌握注解扫描/处理器与动态代理技术(又是如何写一个Parser)
- 熟练使用Stream与FluxAPI,否则连Demo都看不懂
本文要做的事
- 本文不分析前台JAX-RS的路由实现,它只是一个由Map与Regex构造的模式匹配
- 本文主要分析Function的注册、编译与执行流程,不分析supplier/consumer
函数式编程
无状态函数是指无副作用的函数,也就是函数式编程中所谓的“纯函数”,比如map,filter,它们在LISP语言中叫做Lambda表达式。因为不涉及共享变量,所以总是线程安全的。
- Spring Cloud Function: Spring推出的微服务框架,可以动态注册Java8函数
- AWS Lambda: 云函数调用属于Serverless架构
- Reactor编程范式: 与无状态函数配合有两种形式,一种是CallBack,比如RxJava,Flux, 还有一种是模式匹配 Scala, Clojure, Akka, Erlang
- select: Select系统调用本质也是无副作用,给下游可读或者可写的fd,再通过函数指针进行调用
优点: 容易开发与热部署,测试用例也好实现。
缺点: 代码不容易理解,很难调试,(分布式情况下)全局函数可能命名重复而互相覆盖,目前只在Erlang等电信领域中比较多,纯Java的不成熟。
什么是Lambda函数
Lambda函数最开始来自于Lisp,是一种匿名函数
1 | ;; 定义一个f(x)=7*x |
在Groovy/Ruby中也有类似的设计,比如Groovy中代码中{}
就是lambda的语法糖
1 | // 定义一个f(x)=7*x |
其实上面有一个潜规则,Lambda函数应该尽量避免与外界接触,最好是“无副作用”的纯函数,以免引入全局变量使架构变复杂。
目前的AWS,阿里云等云服务商,通过提供一个云函数的运行环境(比如OpenFAAS),用户提供函数代码,按执行时间收费。这就是所谓的Serverless架构,或者称作FAAS架构(FunctionAsAService),比如CI构建编译等低频高性能要求的业务就适合这种架构。
当然,尽管网上说的神乎其神,不过在技术实现上我也觉得FAAS和JMX、在线OJ并没有本质区别
OOP编程与FP编程范式
它们俩个都属于编程范式,但是稍微有点不同,同时这两种也不是对立的。参考如下
最主要的区别:对函数的理解不同。比如在Java中用接口来模拟Lambda函数,而不是直接作为入参
拉通主流程
测试如下Shell
1 | chmod +x ./script/*.sh |
最终返回了FOO,说明就成功了
从上面也可以看出,现在的架构太复杂了,要专门开一个编译前端
断点分析
现在开始考验你的代码阅读量了,一般是按照“日志”,“断点”,“JMX”,“源码”进行分析的。
编译流程分析
此部分主要是
- 对文本脚本进行组装为java源文件
- 并通过sun闭源工具(com.sun.tools.javac.api.JavacTool)生成class
首先在下面打上断点
1 | org.springframework.cloud.function.compiler.app.CompilerController#registerFunction |
通过分析CompilerController
的REST路由,可以发现此部分是一个纯生成文件的模块
1 | JAX-RS(请求) |
此部门代码主要难点在于源码的拼装与检查,而编译是通过sun闭源的JavacTool中的javac实现(本文不分析)
我们甚至可以把在/tmp目录下的文件给拷贝出来
1 | cp /tmp/function-registry/functions/uppercase.fun ./uppercase.class |
用IDEA打开反编译的结果是
1 | package org.springframework.cloud.function.compiler; |
可以发现,通过registerFunction.sh
发送请求后,在CompilerApplication平台在内部生成了Class文件,并保存为文件到/tmp/function-registry
目录下。
函数代理侧实现
代理侧就是./web.sh
中扫描/tmp/function-registry
并注册为JAX-RS服务的过程
- 定制了FunctionProxyApplicationListener: 监听了ApplicationPreparedEvent事件,当【上下文启动完成而没有刷新】时,触发此事件,并invoke其中的onApplicationEvent方法
- 通过
PropertySourcesBinder
实现对env中的K-V进行反序列化,并获取spring.cloud.function.compile
与spring.cloud.function.import
的value - 通过
registerByteCodeLoadingProxy
构造单例Bean,key为function的名词,value为FunctionProxy的实现类
由于web.sh涉及到很多变量,如何打上断点呢,可以在web.sh中加入
1 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 |
接着配置IDE的remote,点击debug即可
由于现在不清楚JAX-RS的路由,无法快速上手,有个技巧就是在String中打上断点
1 | public String toUpperCase() { |
通过断点与curl,可以发现此框架是通过类似RxJava的框架折腾出来的,断点很难打,接着通过分析stacktrace,可以定位出通过url路由到function的方法为
1 | org.springframework.cloud.function.web.flux.FunctionHandlerMapping#findFunctionForPost |
其中url与Function的映射处理为
1 | public <T, R> Function<T, R> lookupFunction(String name) { |
并最终返回FluxFunction<ByteCodeLoadingFunction>
,这里的FluxFunction类似于Akka的Actor,而内部的ByteCodeLoadingFunction是实现类,实现将同步函数转换为Flux响应式调用(断点难度也直线上升)
接着,通过一系列的onNext
最终调用到toUpperCase
总的来说,目前基本上就是玩具,暂时不建议花时间分析这个项目。
自己实现FAAS
总的来说,这次分析主流程并不难,因为我在以前的JMX文章中写过几行代码就能搞定动态执行JS等脚本的方法
下面是一个基于jsr223精简版FAAS实现
1 | class FAAS{ |
在这个基础上加入API网关、缓存优化、JIT优化、热部署等,可以比SpringCloud更快更好。
当然这个实现的并不安全,比如
1 | // 缺乏管理权限导致被rm |
就是代码任意执行漏洞,把整个机器给黑掉了
总结
通过对开源框架SpringCloudFunction进行分析,可以发现它目前只属于玩具级别
- 语言上只支持纯Java8,还要精通Stream/Flux层层包装操作,无法大范围推广
- 此框架总体上就是重构了一个ScriptEngine,最底层甚至还是用文件进行共享,还不如二次封装ScriptEngine执行JVM语言(js/jruby/groovy/kotin)来的痛快
- SpringCloudFunction不支持在方法中调用其它已注册的Function,无法复用代码,这点绝对是硬伤
- 引入了一堆学习成本较高的注解/接口来模拟【闭包】,这个是Java本身的缺点。动态代理+注解的形式,比不上动态语言的methodMissing
- 我认为更好的设计是使用JVM动态语言,后期我会以Groovy为例详细说下GroovyClassLoader
- 无论是Spring还是Groovy,都没解决一个重要问题: 无断点调试的IDE提供支撑
总的来说,开源的FAAS尚不成熟,需要后期跟进。如果Spring项目更新了,本文后期也会更新。