前言

云计算时代出现了大量XaaS形式的概念,从IaaS(Infrastructure as a Service)、PaaS(Platform as a Service)、SaaS(Software as a Service)到容器云引领的CaaS(Containers as a Service),再到现在比较火热的微服务架构,其实他们做的都是尝试把各种软硬件资源抽象成一种服务提供给开发和运维,减少对基础设施、资源需求、中间件和中控调度等等的担心,更专注于业务的开发。FaaS即(Function as a Service)的简称,相较于微服务,它往往和无服务架构(Serverless Architecture)一同被提起。

一、时光回溯

介绍无服务框架前,让我们回顾下部署的发展历史

1、传统部署时代

早期在物理服务器上运行应用程序,应用程序都在同一台物理机器上,无法为物理服务器中的应用程序定义资源边界,这会导致资源分配问题。 例如,如果在物理服务器上运行多个应用程序,则可能会出现一个应用程序占用大部分资源的情况, 结果可能导致其他应用程序的性能下降。 一种解决方案是在不同的物理服务器上运行每个应用程序,但是由于资源利用不足而无法扩展, 并且组织维护许多物理服务器的成本很高。

2、虚拟化部署时代

作为解决方案,引入了虚拟化。虚拟化技术允许你在单个物理服务器的CPU上运行多个虚拟机(VM)。虚拟化允许应用程序在VM之间隔离,并提供一定程度的安全,因为一个应用程序的信息不能被另一应用程序随意访问。
虚拟化技术能够更好地利用物理服务器上的资源,并且因为可轻松地添加或更新应用程序 而可以实现更好的可伸缩性,降低硬件成本等等。每个VM是一台完整的计算机,在虚拟化硬件之上运行所有组件,包括其自己的操作系统。因此需要一层Hypervisor(虚拟机监视器)来统一监控、管理应用。

3、容器部署时代

容器类似于VM,但是它们具有被放宽的隔离属性,可以在应用程序之间共享操作系统(OS)。 因此,容器被认为是轻量级的。容器与VM 类似,具有自己的文件系统、CPU、内存、进程空间等。 由于它们与基础架构分离,因此可以跨云和 OS 发行版本进行移植。
其中在容器部署中会有一层Container Runtime(容器运行时)的组件,运行时一般是用来支持程序运行的实现,例如JVM就是一种运行时。具体到容器运行时:就是运行容器所需要的一系列程序。docker在一开始是把这些所需都一起解决了,打包在每个容器里的镜像里,后来为了标准化,方便使用各种不同的实现,就分别进行了拆分,成立了Open Container Initiative (OCI)。推出了两个标准:
容器运行时标准:这个标准主要是指定容器的运行状态,和runtime需要提供的命令。
容器镜像标准:这个主要是说明容器的镜像的格式。这个一般是以OCI runtime filesytem bundle的形式存在。
简单来理解就是把容器运行所需的文件存放在文件夹里,用chroot控制访问,创建好cgroups和namespace实现容器隔离、运行docker和下载、管理、更新镜像以及提供RPC接口,用于远程提供服务。

二、什么是FaaS

FaaS作为无服务架构Serverless的一种,Serverless的概念刚刚出现在HackerNews时并不为大众所接受。后来随着微服务和事件驱动架构的发展才慢慢引起关注。微服务架构近年来是一个非常火爆的话题,大大小小的公司都开始逐步分拆原来的单体应用,试着转换到由各个模块服务组合成大型的复杂应用。
Serverless可以看作是比微服务架构更细粒度的架构模式,即FaaS。它允许用户仅仅上传代码而无需提供和管理服务器,由它负责代码的执行、高可用扩展,支持从别的AWS服务或其他Web应用直接调用等;以电子商务应用为例,微服务中可以将浏览商品、添加购物车、下单、支付、查看物流等拆分为解耦的微服务。在FaaS里,它可以拆分到用户的所有CRUD操作代码,当发生“下单”事件时,只将触发相应的Functions。
现有以下的js代码

module.exports = (context, callback) => { callback(200, "Hello, world!
"); }

显然它是一个函数,通过FaaS的方式,我们可以通过访问一个URL的方式调用这个函数。

$ curl -XGET localhost:8080
Hello, world!

三、FaaS特征

1、FaaS里的应用逻辑单元都可以看作是一个函数,开发人员只关注如何实现这些逻辑,而不用提前考虑性能优化,让工作聚焦在这个函数里,而非应用整体。
2、FaaS是无状态的,天生满足云原生(Cloud Native App)应用该满足的12因子(12 Factors)中对状态的要求。无状态意味着本地内存、磁盘里的数据无法被后续的操作所使用。大部分的状态需要依赖于外部存储,比如数据库、网络存储等。
3、FaaS的函数应当可以快速启动执行,并拥有短暂的生命周期。函数在有限的时间里启动并处理任务,并在返回执行结果后终止。如果它执行时间超过了某个阈值,也应该被终止。
4、FaaS函数启动延时受很多因素的干扰。以AWS Lambda为例,如果采用了JS或Python实现了函数,它的启动时间一般不会超过10~100毫秒。但如果是实现在JVM上的函数,当遇到突发的大流量或者调用间隔过长的情况,启动时间会显著变长。
5、FaaS需要借助于API Gateway将请求的路由和对应的处理函数进行映射,并将响应结果代理返回给调用方。
对于一个简单的3层Web应用,在这里后端系统实现了大部分业务逻辑:认证、搜索、事务等,它的架构如下:

采用Serverless架构,将认证、数据库等采用第三方的服务,从原来的单体后端里分拆出来(可能需要在原来的客户端里加入一些业务逻辑)。对于大部分的任务,通过函数的形式进行执行,而不再使用一直在线的服务器进行支持,如此一来它的架构看起来就清晰多了:

三、基于FaaS框架的Fission

Fission是一款基于Kubernetes的FaaS框架。通过Fission可以轻而易举地将函数发布成HTTP服务。它通过读取用户的源代码,抽象出容器镜像并执行。同时它帮助减轻了Kubernetes的学习负担,开发者无需了解太多K8s也可以搭建出实用的服务。Fission可以与HTTP路由、Kubernetes Events和其他的事件触发器结合,所有这些函数都只有在运行的时候才会消耗CPU和内存。
Kubernetes提供了强大的弹性编排系统,并且拥有易于理解的后端API和不断发展壮大的社区。所以Fission将容器编排功能交给了K8s,让自己专注于FaaS的特性。

对于FaaS来说,它最重要的两个特性是将函数转换为服务和管理服务的生命周期。
FaaS优化了函数运行时的资源使用,它的目标是在运行的时候才消费资源。但在冷启动的时候可能会有些资源使用过载,比如对于用户登录的过程,无论多等几秒都是不可接受的。为了改变这个问题,Fission维持了一个面向任何环境容器池。当有函数进来时,Fission无需启动新容器,直接从池里取一个,将函数拷贝到容器里,执行动态加载,并将请求路由到对应的实例。

除了安装在本地的Fission主程序外,Fission-bundle设计为一组微服务构成:

Controller: 记录了函数、HTTP路由、事件触发器和环境镜像
Pool Manager: 管理环境容器,加载函数到容器,函数实例空闲时杀掉
Router: 接受HTTP请求,并路由到对应的函数实例,必要的话从Pool Manager中请求容器实例

当Router收到外部请求,它先去缓存Cache里查看是否在请求一个已经存在的服务。如果没有,要访问请求映射的服务函数,需要向Pool Manager申请一个容器实例执行函数。Pool Manager拥有一个空闲Pod池,它选择一个Pod,并把函数加载到里面(通过向容器里的Sidecar发送请求实现),并且把Pod的地址返回给Router。Router将外部请求代理转发到该Pod,并将响应结果返回。Pod会被缓存起来以应对后续的请求。如果空闲了几分钟,它就会被杀死。

四、执行流程

如果是第一次运行,需要先准备NodeJS的运行环境:

fission env create --name nodejs --image fission/node-env


同样,由fission主程序执行命令function和子命令create,通过–name参数指定函数名为hello,–env参数确定环境,–code参数确定要执行的函数代码。通过POST向/v1/functions发出请求,携带函数信息的JSON。controller拿到JSON后进行函数资源的存储。首先将拿到UUID,然后写到文件名为该UUID的文件里。接着向ETCD的API发送HTTP请求,在file/name路径下有序存放UUID。最后类似上面env命令,将UUID和序列化后的JSON数据写到ETCD里。

fission function create --name hello --env nodejs --code hello.js


fission通过参数–method指定请求所需方法为GET,–url指定API路由为hello,–function指定对应执行的函数为hello。通过POST向/v1/triggers/http发出请求,将路由和函数的映射关系信息发送到controller。controller会在已有的trigger列表里进行重名检查,如果不重复,才会获取UUID并将序列化后的JSON数据写到etcd里。

fission route create --method GET --url /hello --function hello


当执行该curl时,请求发送至router容器。收到请求后会转发到两个对应的handler。一个是用户定义的面向外部的,一个是内部的。实际上它们执行的是同一个handler。任何handler都会先根据funtion名去Cache里查找对应的service名。如果没有命中,将通过poolmgr为函数创建新的Service,并把记录添加到Cache。然后生成一个反向代理,接收外部请求,然后转发至Kubernetes Service。

curl http://$FISSION_ROUTER/hello


云计算时代下的FaaS(21/1/8 tencent)