作者 | 张挺
这次分享的内容分为两部分, 一块是 Midway Serverless 的能力介绍,第二块是这些能力的设计、思考、沉淀。
Background
当前业界的 Serverless 化方向如火突入,有如阿里正在利用 Serverless 将原有业务迁移,降低成本的,也有正在向这些方向努力前进的,我首先会介绍一下当前 Serverless 的一些背景,前端使用 Serverless 的一些场景和方向。第二块会简单介绍一下 Midway Serverless 的一些基础上手和使用。第三块会介绍 Midway Serverless 在抹平平台差异,架构防腐层的一些设计与心得,最后是对未来的一些期望,方向的思考。
Midway 从 2014 年开始一直在集团承担 Node.js 应用的基础开发框架,最开始是 express,到后面的 koa,egg 体系等,将集团业务承载至今。最开始的前后端分离,到如今的函数化,都在不断的开拓前端职能,让业务更聚焦,开发更提效。
之前的 midway v1 版本,我们认为 midway 是一个 Web 全栈框架,提供 Web 服务,增加了依赖注入之后,也适合于大型应用的开发,灵活性和应用的可维护性也得到了验证。
而到了现在 Midway Serverless 时期,整个 Midway 框架的定位在逐步的变化。
首先,Midway Serverless 是一个 serverless framework,可以在让代码在多云平台部署,在用户选择时可以减少一些顾虑。
第二是能够方便的让传统的应用迁移上现有的弹性服务,毕竟在集团内,还有非常多的传统应用,不管是在什么场景,这些应用都还需要人维护,需要占用大量的资源,如果能上弹性,对节省成本有非常大的好处。
第三是让应用本身能够在传统应用和函数之间切换,传统的 midway 是基于装饰器加上依赖注入的特性构建出来的,在函数体系下上,也可以这样做,甚至于通过构建将不同的场景结合到一起,我们希望最后能达到代码不变的情况下,不同场景都可用的状态。
结构
这是我们经典的目录结构,最简单的,抛开 ts 的一些文件,只需要 f.yml 这个配置以及 index.ts 这个逻辑文件,而复杂一点,也只是增加了目录,增加了不同逻辑的分层,和传统的写法契合。
这里的 f.yml 就承载了之前的路由层的功能,在 Serverless(FaaS)体系中,路由交给了网关处理,那么我们只需要在项目代码中写对应的原 Controller 的内容即可。
如图所示,f.yml 中每一个服务都会对应一个接口,每个接口都由一个方法承载由 handler字段去映射绑定,而实际运行中,通过依赖注入的方式,框架只根据当前执行的逻辑动态初始化其中方法,所以也不需要担心执行的性能问题。
f.yml 通过标准化适配多云平台,最简单的来说,可以通过定义 http 触发器的 path 和 method 具体的指定接口地址,也可以简化到默认值,自动变为通配路由等等。
工具链和能力
除了 f.yml 这套标准定义文件,我们还提供了 faas-cli ,一个精简的本地开发工具,帮助函数体系开发的更好。在开发层面,我们只精简的提供了 create,invoke,test,deploy 四个命令,对应了整个研发流程的四个周期,而剩下的部分,则交给了对应平台自身的能力来完成,同时,我们后续也会提供一些后置管理,让 Node.js 开发本身更加的高效。
从 v1.0 之后,我们也提供了一系列示例,不管是和前端集成的 React/Vue,还是场景化示例,博客,Todo list 等等。
原理解析
虽然给大家展示了开发的工具链,开发的标准,解释了运行时机制,大家是不是还是很疑惑,依赖注入是如何把 f.yml 中的 handler 字段如何与代码中对应的装饰器连接的,而函数整个原来的参数是如何和云平台对接,做到一套代码跨多平台的?
为了方便理解,我们拿 Midway v1 里的依赖注入容器来解释。
整个 Midway v1 是基于 EggJS 往上扩展,增加 IoC(依赖注入) 容器的初始化部分,并且将装饰器的能力注册到其中,和整个路由体系结合到一起。
右边是我们核心的伪代码,在初始化时,容器会做一次扫描,把当前用户的代码都加载到内存中,并分析其中的装饰器组成一个”依赖图“,在每次执行逻辑的时候,从其中拿到对应的实例(get),并将其依赖,子依赖统一初始化。
路由部分也是这种逻辑的其中一层,在调用路由时,获取到对应的 Controller key,找到对应的方法,整个 Midway v1 都是如此运行起来的。
在之后的迭代过程中,我们发现这样和单一框架依赖会比较深,很难去灵活的调整功能,并且在 Web 场景的能力,很难去适配到其他场景,这就给逻辑的复用和扩展造成了不少困难。
我们希望不同的场景的代码,能够在一定程度上能够复用,比如常见的 router/orm/graphql 等等,都是可以横跨不同的场景去复用的,甚至于用户的服务层代码本身也是可以去多处复用的。另外一块,我们希望传统全栈到 Serverless 的过程是有延续性的,不希望代码的写法有比较大的区别,既能在不同的平台通用,又能在不同的技术栈大部分通用。
这也迫使我们的去思考不同的代码设计,找到最佳的路径。
Design
我们从最原始的函数写法给大家讲起。
架构防腐
整个原始的入口函数,社区的写法都非常简单,是一个传统的方法,其参数在不同平台根据不同触发器略有不同。
在执行时,通过网关调度到其内部的运行时,然后拼装参数执行到用户的入口函数中。
为了和之前的框架结合,以及屏蔽不同平台之间的差异,我们在社区的运行时执行之后,用户入口函数之前,做了一层架构抽象,即我们所谓的”防腐层“。这层一共包括两个功能,一是运行时防腐的部分,屏蔽出入参数差异,屏蔽异步差异,错误处理等等。第二部分是 API 传承,将传统的 Midway v1 的容器初始化,根据 yml 里的信息实例化对应的函数方法。
这样说有一点抽象,我们找一段实际的代码来理解一下。
下面是实际生成的代码入口 index.js 的示例。
初始化的时候,我们会做两个事情,一个就是每个平台的适配器,会自动根据 f.yml 中配置的 provider.name 来生成,我们会自动提供支持的平台启动器(现在已经有阿里云,腾讯云,以及即将完成的 aws)。
另外一个就是 Midway Serverless 框架的入口(FaaSStarter),通过它,来调用到实际的用户代码(src/index.ts)。其余的 asyncWrapper 和 asyncEvent 则是用于对异步函数的包裹,让代码可以统一用上 async 关键字。
看到这里,大家是否好奇我们的运行时适配器(防腐层)内部是如何运作的?这就来稍微详细一点的讲讲。
整个运行时处在最中间的部分,往上承接事件带来的数据,接收,中转,往下调用到业务代码,把中转的参数传入,在整个容器中占了非常重要的部分 。一般来说,整个运行时包括几个部分,一是语言的 VM,基建的 SDK 等等,比如 Node.js 10/12,日志采集模块等等,二是运维的脚本,用来控制启停,健康检查等等,第三块就是运行时实际代码,简单的实现的话,可以理解起了个 http 服务,并在其中加载业务函数的代码。
生命周期
传统的社区平台都会默认埋入自己的运行时,而我们的运行时则是在这些平台内置的运行时之上的封装,并且将运行时和业务代码通过自定义生命周期进行关联,将整个代码 run 起来。
整个生命周期分为几部分阶段,外围运行时包括 RuntimeStart、FunctionStart、Invoke、Close 等阶段,而在这些周期中,还提供了 before 和 after 的钩子,方便对这些阶段进行扩展。
我们来一下实际的运行时扩展的例子,看看我们是如何抹平不同的云平台的。
这是一个阿里云运行时适配器的例子,我们接着上面的业务代码调用的路径来观察,asyncEvent 用来包裹真实的入参,在接受到参数之后,我们做了一些不同触发器的类型判断,将其分为了 Web 和非 Web 两种类型。
在 Web 的处理方法中,进一步细化内容,比如判断是否是网关的类型,构造出一个类似 koa ctx 的结构,处理 body 参数等。
做完这些事情之后,就开始把规则化好的参数传递给用户的真正的逻辑了,这个时候,由于生命周期的存在,开始执行才看到的 invoke 过程,并在内部调用 before 和 after 过程。
除此之外,我们还提供了一个默认执行拦截的能力,这个能力在传统应用迁移的时候起了巨大的作用。
应用迁移
现在,所有的扩展(Layer)都可以复用在所有的运行时适配上,整个函数体系和应用迁移体系基于这套生命周期和扩展机制,将能力复用,结构分层表现的淋漓尽致。
而用户所需要做的,仅仅增加一个 f.yml,写入这 4 行即可。整个 midway 构建器 会生成所有需要的入口和适配代码。
这套函数和应用统一的方案,如果在企业内部,私有化部署也是非常适合的,阿里集团内部的函数体系也是如此被加载起来,和社区保持了高度的一致,也减少了很多的维护成本。
小结
从 midway serverless 的基础到入口的生成,生命周期以及应用迁移的方案介绍,这里涵盖的是 midway serverless 的架构防腐的一小部分,后续也将会有其他文章介绍不同的部分,感谢大家阅读,也欢迎大家关注 Midway。
关注「Alibaba F2E」
把握阿里巴巴前端新动向