Serverless开源项目分享-Spring Cloud Function
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.
关键词: Serverless
, FAAS
, SpringCloud
阅读本文需要如下知识点
- 了解基本SpringBoot的注解,可以参考《SpringBoot揭秘》
- 掌握注解扫描/处理器与动态代理技术(又是如何写一个Parser)
- 熟练使用Stream与FluxAPI,否则连Demo都看不懂
本文要做的事
- 本文不分析前台JAX-RS的路由实现,它只是一个由Map与Regex构造的模式匹配
- 本文主要分析Function的注册、编译与执行流程,不分析supplier/consumer
什么是Lambda函数
Lambda函数最开始来自于Lisp,是一种匿名函数
;; 定义一个f(x)=7*x
(lambda (number) (* 7 number))
;; 调用Lambda函数
((lambda (number) (* 7 number) 3)
=> 21
在Groovy/Ruby中也有类似的设计,比如Groovy中代码中{}
就是lambda的语法糖
// 定义一个f(x)=7*x
def closure = {number-> 7 * number}
// 调用Lambda函数
{number-> 7 * number} 3
=> 21
其实上面有一个潜规则,Lambda函数应该尽量避免与外界接触,最好是“无副作用”的纯函数,以免引入全局变量使架构变复杂。
目前的AWS,阿里云等云服务商,通过提供一个云函数的运行环境,用户提供函数代码,按执行时间收费。这就是所谓的Serverless架构,或者称作FAAS架构(FunctionAsAService),比如CI构建编译等低频高性能要求的业务就适合这种架构。
当然,尽管网上说的神乎其神,不过在技术实现上我也觉得FAAS和JMX、在线OJ并没有本质区别
OOP编程与FP编程范式
它们俩个都属于编程范式,但是稍微有点不同,同时这两种也不是对立的。参考如下
最主要的区别:对函数的理解不同。比如在Java中用接口来模拟Lambda函数,而不是直接作为入参
拉通主流程
测试如下Shell
chmod +x ./script/*.sh
cd ./script
# 前端编译器,端口为8080
./function-registry.sh
# 用于生成文件到/tmp/function-registry
.registerFunction.sh -n uppercaseStr -f "s->s.toString().toUpperCase()" -i 'String' -o 'String'
# 函数Proxy,端口为9000
# 启动时讲通过FunctionProxyApplicationListener扫描spring.cloud.function.import
# 并读取 /tmp/function-registry 下的字节码
./web.sh -f uppercaseStr -p 9000
# 客户端调用
curl -H "Content-Type: text/plain" -H "Accept: text/plain" localhost:9000/uppercaseStr -d foo
最终返回了FOO,说明就成功了
从上面也可以看出,现在的架构太复杂了,要专门开一个编译前端
断点分析
现在开始考验你的代码阅读量了,一般是按照“日志”,“断点”,“JMX”,“源码”进行分析的。
编译流程分析
此部分主要是
- 对文本脚本进行组装为java源文件
- 并通过sun闭源工具(com.sun.tools.javac.api.JavacTool)生成class
首先在下面打上断点
org.springframework.cloud.function.compiler.app.CompilerController#registerFunction
通过分析CompilerController
的REST路由,可以发现此部分是一个纯生成文件的模块
JAX-RS(请求)
|
CompilerController
|
调用 ToolProvider.getSystemJavaCompiler() 进行 javac 源码
|
存储Class文件到/tmp/function-registry
此部门代码主要难点在于源码的拼装与检查,而编译是通过sun闭源的JavacTool中的javac实现(本文不分析)
我们甚至可以把在/tmp目录下的文件给拷贝出来
cp /tmp/function-registry/functions/uppercase.fun ./uppercase.class
用IDEA打开反编译的结果是
package org.springframework.cloud.function.compiler;
import java.io.Serializable;
import java.util.function.Function;
import org.springframework.cloud.function.compiler.FunctionFactory;
public class UppercaseFunctionFactory implements FunctionFactory {
public UppercaseFunctionFactory() {
}
public Function<String, String> getResult() {
return (Function)((Serializable)((var0) -> {
return var0.toString().toUpperCase();
}));
}
}
可以发现,通过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中加入
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005
接着配置IDE的remote,点击debug即可
由于现在不清楚JAX-RS的路由,无法快速上手,有个技巧就是在String中打上断点
public String toUpperCase() {
+ return toUpperCase(Locale.getDefault());
}
通过断点与curl,可以发现此框架是通过类似RxJava的框架折腾出来的,断点很难打,接着通过分析stacktrace,可以定位出通过url路由到function的方法为
org.springframework.cloud.function.web.flux.FunctionHandlerMapping#findFunctionForPost
其中url与Function的映射处理为
public <T, R> Function<T, R> lookupFunction(String name) {
return (Function<T, R>) Stream.of(StringUtils.tokenizeToStringArray(name, ","))
// 从HashMap获取缓存
.map(functions::get)
.filter(f -> f != null)
.reduce(null, (f1, f2) -> f1 == null ? f2 : f1.andThen((Function)f2));
}
并最终返回FluxFunction<ByteCodeLoadingFunction>
,这里的FluxFunction类似于Akka的Actor,而内部的ByteCodeLoadingFunction是实现类,实现将同步函数转换为Flux响应式调用(断点难度也直线上升)
接着,通过一系列的onNext
最终调用到toUpperCase
总的来说,断点比以前的分析更难打,而且项目本身还不成熟,不建议花时间分析这个项目。
自己实现FAAS
总的来说,这次分析主流程并不难,因为我在以前的JMX文章中写过几行代码就能搞定动态执行JS等脚本的方法
下面是一个基于jsr223精简版FAAS实现
class FAAS{
Closure eval = {script ->
//jsr223,此处内置了定制GroovyClassLoader
ScriptEngine engine = new ScriptEngineManager().getEngineByName("groovy");
Object eval = null
try {
eval = engine.eval(script)
}catch (Exception e){
e.printStackTrace();
}
}.memoize()/*记忆化执行优化*/
// 可以用etcd/zk/redis封装代替
def map = [:]
void reg(name,func){
map.put(name,func)
}
Object run(funcName, Object... args){
def func = map.get(funcName)
def toRun = func + ' ' + args?.join(',')
println "toRun = $toRun"
eval toRun
}
}
FAAS faas = new FAAS()
faas.reg('sum','{a,b->a+b}')
assert faas.run('sum','1','2') == 3
faas.reg('uppercase','{s->s.toUpperCase()}')
assert faas.run('uppercase','"Hell"') == 'HELL'
在这个基础上加入API网关、缓存优化、JIT优化、热部署等,可以比SpringCloud更快更好。
当然这个实现的并不安全,比如
// 缺乏管理权限导致被rm
faas.reg('attack','{a-> a.execute().text}')
faas.run('attack','"rm -rf ~"')
就是代码任意执行漏洞,把整个机器给黑掉了
总结
通过对开源框架SpringCloudFunction进行分析,可以发现它目前只属于玩具级别
- 语言上只支持纯Java8,还要精通Stream/Flux层层包装操作,无法大范围推广
- 此框架总体上就是重构了一个ScriptEngine,最底层甚至还是用文件进行共享,还不如二次封装ScriptEngine执行JVM语言(js/jruby/groovy/kotin)来的痛快
- SpringCloudFunction不支持在方法中调用其它已注册的Function,无法复用代码,这点绝对是硬伤
- 引入了一堆学习成本较高的注解/接口来模拟【闭包】,这个是Java本身的缺点。动态代理+注解的形式,比不上动态语言的methodMissing
- 我认为更好的设计是使用Redis+JVM动态语言,后期我会以Groovy为例详细说下GroovyClassLoader
- 无论是Spring还是Groovy,都没解决一个重要问题: 无断点调试的IDE提供支撑
总的来说,开源的FAAS尚不成熟,需要后期跟进。如果Spring项目更新了,本文后期也会更新。