本次 D2 分享的话题叫 《Serverless 函数应用架构升级》,主要讲述的是阿里集团内部从传统一步步抽离出 midway-faas 框架,并赋予其核心能力,解决用户诉求的一些思考和实践。


社区的 Serverless 在不断升温,今年成了阿里经济体前端委员会的四大方向之一,给了前端非常大的机遇和挑战,业务在不断尝试之余,也逐步沉淀出了一套可迭代,可维护,可扩展,可复用的开发模型。如今云厂商不断发力,无声的战争正在打响。


现在,随着 Serverless 的深入人心,云厂商都在说,“我们在定义 Serverless”,而开发者都说”我们在做 Serverless“,用户都是“在用 Serverless",人人都在往 Serverless 体系上靠,或多或少的沾点边,使得整个社区欣欣向荣。


Serverless 函数应用架构升级_java


而对于我们前端本身来说,引进 Serverless,也能在各个场景下带来帮助。目前 Serverless 体系,首选的是 FaaS + BaaS 模型,使用了 FaaS。


首先是更快的开发速度,基于时间驱动模型,能够更方便的进行开发和迭代,让用户快速上线;其次是更加安全的隔离环境,用户再也不能,也不希望登录服务去排查问题;第三是按量付费,由于计费模型的变化,比传统的按实例付费,在访问率低的时候,更能降低成本;最后是弹性实例,在部署服务时,不再考虑峰值等情况,不需要提前预估流量,预先准备实例。


Serverless 函数应用架构升级_java_02


这些优势,看到了一个新的契机,让前端能够减少开发成本,弱化运维,同时又能更多的关注数据,真正的向着后端、一体化发展,也为跨技术栈研发效能提升提供了可能。


Serverless 函数应用架构升级_java_03



社区背景



现有的社区,有非常多的云厂商提供函数服务。不管是 Amazon,微软,还是谷歌,以及国内的阿里云和腾讯云,都推出了自己的云函数,同时,每个云厂商都有着一套自己的标准和规范,同时,在这些标准之上,云厂商为了吸引用户,更快的占领市场,做了很多创新,阿里云的 CustomRuntime,以及前段时间腾讯云推出的 Service 2.0 概念,就是比较好的例子。


Serverless 函数应用架构升级_java_04


作为普通用户,如果想快速的使用 Serverless,在这么多平台面前,光是选择就得斟酌一番,在各个平台之间反复对比,同时,如果想多尝试几个平台,就会发现每个平台的规范,代码行为,启动方式,触发器都不尽相同,对于用户来说,能有跨平台的方案,可以在不同的厂商之间做一些迁移,互调等有意思的尝试。


Serverless 函数应用架构升级_java_05


在社区中已经有一些能够帮助我们更好的部署到多云的工具链,Serverless Framework 就是其中做的不错的代表,已经有不少云厂商通过开发插件接入了 serverless 工具链。它通过 serverless.yml 文件,定义了多云的能力,使用云厂商开发的插件,一定程度上解决了跨平台的问题。


Serverless 函数应用架构升级_java_06


这其中没有很好的解决云厂商之前代码层面的不一致(因为是云厂商自己做的),第二,这些插件是云厂商自己提供,如果某些厂商不做支持,那么能力就会缺乏。


Serverless 函数应用架构升级_java_07


而另一家 ZEIT,推出了 now 系列工具,以轻量简单快速闻名,他们的体验非常棒,目前支持部署到他们接入的平台,示例基本以前端项目为主,和我们 FaaS 本身的实践有所区别,定位也有一些差异。


Serverless 函数应用架构升级_java_08


在调研完社区的生态之后,我们发现,没有非常契合我们的诉求的产品,我们觉得自己设计一套符合我们定位,且能够满足场景,又面向未来的框架。经过讨论,我们将框架的特性归纳、总结出以下四点:


  • 第一,需要能够防厂商锁定,由于 FaaS 目前没有成熟的标准,导致社区平台割裂,同时因为集团内部也有多套环境的需求,所以我们决定把这个特定放到第一位。

  • 第二,是灵活性,我们称之为 Flexibility,传统的 FaaS 部署模型和计费模型,在某些场景下,成本和性能都有所欠缺,同时,在扩展性、复用性上也不够灵活。

  • 第三,在团队规模越来越大的今天,标准,复用和扩展性已经变的越来越重要,如何解决这些问题,我们也进行了深入思考

  • 最后,是一些实际的诉求,比如在多个函数之前,复用一些逻辑,统一埋点,监控等,我们需要在这一方面做一些扩展,目前的社区平台无法满足,我们需要设计出一套生命周期,来完善整个体系,这样内外的逻辑保持一致,也可以把我们的实践输出出去。


Serverless 函数应用架构升级_java_09



防厂商锁定



前面我们提到,不同的云厂商提供的云函数服务,标准不同,写法不同,特别是出入参,会有一些差异,给我们的同学带来了不少困扰。参数这一块这是由不同的运行时决定,像阿里云,就会有 req, res, context 以及 event, context, callback 两种写法,腾讯云和 aws 也有一些异步处理以及参数个数上的差异。


Serverless 函数应用架构升级_java_10


社区的 Serverless 由于起步比较早,各家云厂商都对它有一定程度的支持,通过 serverless.yml 来定义每个平台的配置、资源、能力,前面也提到,由于是社区化,各家云厂商对插件的支持力度不一,更多的云厂商还是会着重打造自己的 cli 工具链,以及结合自家平台或者 IDE。


我们基于 Serverless Framework 的想法,希望能进一步做标准化和扩展,这样既可以复用社区生态,也可以进一步扩展我们自己的能力。由此,我们的方向分为两部分:


  • 1、进一步标准化 serverless.yml

  • 2、针对不同的云厂商,标准化代码层面的出入参



Serverless 函数应用架构升级_java_11


severless.yml 的标准化其实比较困难,这一块会参考现有的 serverless 体系,在他们没有做好的地方继续做规范和定义,我们的优势在于,会结合实际场景去考虑,也没有云厂商的包袱,相对来说可以自由一些。


我们设想不同的云平台,都会有类似的能力,比如 http,api gateway,以及 timer,对象存储,消息队列等等,把这些常用的都定义为相同的字段,通过构建来生成出各个平台自己的字段,可以帮用户省下了解的时间。通过这样的变化,我们直接将一份 yml 文件,转变为不同平台自己的 yml 文件,这样缓冲层的设计,也屏蔽了后续因为平台的变化导致用户使用层面的差异性。


Serverless 函数应用架构升级_java_12


标准化出入参,只是代码上的包裹,相对来说就容易许多。


  • 1、把 callback 变为 Promise(async)

  • 2、把原有的入参做一些抽象,变为一个叫 FaaSContext 的请求上下文(包含了原有 event,context 的部分内容,更像 koa 模型)

  • 3、把原有的 context 变为 FaaSContext 上的属性(保留,后续可能有用)


这样做完之后,针对不同的平台,统一将上下文作为第一个参数传入(必选),event 第二个参数(可选),同时整个方法直接是 async 函数。


Serverless 函数应用架构升级_java_13


实现层面相对简单,我们把这些内容做成了参数包裹器(parameter wrapper),和之前的 yml 文件一起,结合用户代码的构建产物,分发部署到各个云平台,目前社区我们还只完成了阿里云和腾讯云,这种模型相当轻量,和原有的平台能力也不冲突,后续我们将会继续支持其他平台。


Serverless 函数应用架构升级_java_14


同时,在这些过程之中,我们采用了 interface 的定义方式,产出了几份针对不同平台的 yml 标准化定义,后续可以用它们快速的格式化,校验,减少用户开发成本。


Serverless 函数应用架构升级_java_15


下面是我们发布到多平台的演示,展示的是同一份代码文件,在不修改代码本身的情况下,发布到多云。


Serverless 函数应用架构升级_java_16



灵活性



我们也把灵活性是灵活性(Flexibility)作为第二个设想的能力,理想情况下,是部署模型的延伸,可以让函数在水平和垂直两个层面自由扩展。


Serverless 函数应用架构升级_java_17


我们以一个传统 Web 栈作为示例。


传统 Web 应用,路由会写在一个固定的文件中,每个路由一般会关联到一个 Controller 上,在函数的体系下,每个路由都将是一个 http 触发器,都将拆为一个独立的的函数,有着独立的代码、配置等。由于函数的热度不同,调用的频次也会不同。同时,如果原来的路由和 Controller,每个函数可以绑定多个路由。


所以说,FaaS 栈和传统 Web 栈有着很高的相似度,但是不同的是,这些函数都将部署到不同的容器中(隔离性),再加上平台本身宣传的那样,按调用量付费,看起来十分完美。


Serverless 函数应用架构升级_java_18


殊不知,函数整体的计费模型,除了传统的调用次数,还有一个 CU 的概念,一般来说,CU 会和容器本身的 CPU、内存消耗相关,调用次数容易计算,CU 要优化就比较困难。如果按请求调用量付费,每次调用都启动一个新的实例的话,创建实例本身的开销加上函数调用其实是比较浪费的。


Serverless 函数应用架构升级_java_19


为此,我们希望在某些场景下,能够尽可能的节省成本。这就产生了一些可能性,如果我们能复用函数实例,并发处理业务,或者共享一些资源,在一定程度上就能降低实例重复创建本身的开销。


我们设想,如果把多个请求在一个实例内处理,创建新实例的次数会变少,如果多个函数的逻辑在一个函数里完成,这样函数冷启动概率是不是就会降低,从而在另一个层面降低成本呢?


如果多个函数在一起,是否可以选择性聚合,假如某个函数请求量变高(热点函数),是否能够随时拆分出来呢?


Serverless 函数应用架构升级_java_20


这个答案是肯定的,经过实践,我们已经可以做到函数本身的随意组合部署。这就是我们设想的 Flexibility 的一部分,让函数在部署模型上变的更加自由,更加灵活。每个函数可以按传统的部署模型,部署到多个容器中,也可以按照自己的想法(流量情况)聚合到一起部署,随意的组合。


Serverless 函数应用架构升级_java_21


但是有些同学觉得这样的组合很不错,就会担心成本的问题,毕竟函数的部署模型调整的话,代码也会跟着调整,如果改动复杂的话,这个成本不会接受。


经过我们的设计,我们已经做到了只在 yml (配置)层面做调整,代码层面不需要改动,同时聚合的配置部分不影响原有的函数配置(随意增减),这就是我们 Flexibility 水平聚合能力,我们也叫它 “高密度部署”。


Serverless 函数应用架构升级_java_22



开发效率



第三点,我们考虑的是开发效率,在团队越来越大之后,开发的标注化,扩展性和可维护性一直是关注的重点,而随着 TypeScript 的推出,越来越多人将应用本身迁移到了这个体系中,我们也在不断思考,但是目前为止,没有在 FaaS 上看到这些方案。


Serverless 函数应用架构升级_java_23


前面提到过,我们将传统的 Event 和 Context 字段做了转换,同时,因为对代码做了变化,也需要给用户足够多的可用性提示,为此,我们定义一些 interface(比如 FaaSContext)辅助开发。


将 TypeScript 优秀的特性引入 FaaS 体系一直是我们的目标,从 17 年的 pandora 开始,包括去年我们开源的 midway,都全部使用 TypeScript 进行研发,内部的所有库已经全部迁移到了 TypeScript 体系上,让 FaaS 用上 TypeScript 也并不困难。


Serverless 函数应用架构升级_java_24


为此,我们将 midway 逐步改成了多场景方案,迁移出了名为 midway-faas 的函数框架,在标准的 Class 模型基础上,提供了基于 IoC,装饰器等新的特性,既和原来的体系一脉相承,能力共享,也在 FaaS 场景提供了更多的 Feature。


Serverless 函数应用架构升级_java_25



运行时扩展



在上面这些特性定义完之后,我们觉得函数的开发本身能力差不多了,但是还有些问题没有解决。


Serverless 函数应用架构升级_java_26


在使用一段时间之后,我们发现了一些用户诉求。


  • 现有的函数,用户面对的直接是入口参数,我们如果要在跨函数前做一些参数校验,转换的的事情,目前需要做基类继承,或者直接写多次方法调用,这种类 middleware,或者 AOP 的做法,是否能够有方案支持。

  • 内部运行时还好,运行时是由平台掌控,有些有初始化方法,有些没有,那么能不能在一定程度上抹平这些差异,让用户使用无感。

  • 还有一些统一监控,埋点的需求,以及集团内部一直提的治理的需求



Serverless 函数应用架构升级_java_27


这些能力不一定需要在框架层本身实现,由于我们的构建特性以及隐藏了真实的入口,我们完全可以在整个调用之前加入这些能力。


为此我们设计了一套针对运行时的生命周期,包括 RuntimeStart、FunctionStart、Invoke、Close 四个阶段,以及他们的 before 和 after 的 hook 能力。同时将内部的运行时都基于这套编写,目前已经实现了多个平台的完整运行时。


不过这一套在社区平台稍有不一致,社区的平台基本无法自定义运行时,也就无法执行完整的生命周期,所以我们针对社区平台(例如阿里云 FC),就进行了简化处理。


整个运行时扩展简单来说分为几个部分,最底层的是 runtime-engine,用于整个生命周期的管理和执行。之上的是 LightRuntime,一个轻量的运行时,用于在不同的平台之中完成我们的运行时生命周期,同时适配各个平台的出入参。


Serverless 函数应用架构升级_java_28


而另一块是每个运行时复用的能力,我们从 Lambda 中借鉴了概念,称之为Layer。Layer 在设计上可以和任意一个运行时合并,赋予其额外的能力,我们一般用在多场景兼容(传统应用迁移),统一监控、metrics 等能力上。


Serverless 函数应用架构升级_java_29


经过快一年的迭代更新,我们将淘宝的导购业务迁移到了 Serverless 体系,同时集团其他 BU 也逐步的复制了这套方案,都顺利通过了双促的大考。



我们计划将这套 midway-faas 的能力开放给社区,如今,代码已经提交到了 github,还在飞速迭代中,算是 public beta 阶段,我们希望在明年的一月发布 v1.0,提供更多的 Feature 支持。


Serverless 函数应用架构升级_java_30


Serverless 的路依然还处在起步阶段,不断尝试,降低成本,为前端赋能是我们的目标,也欢迎更多的同学加入到我们的队伍中。