本文结构
本文是在阅读Fission源代码的过程中,对Fission实现思想的总结,最重要的原则是忠于源代码。将从Fission创建的K8s自定义资源和Fission的关键组件上对Fission进行介绍。Fission 架构图如下(按照自己理解画的,如有错误,欢迎指正)。
备注:
- 不仅仅是Controller和Executor和etcd与关联,其他fission组件都和etcd有着种种的关联
- Client 仅仅和Controller之间进行交互,和Executor之间是隔离的
Fission CRDs
Fission 在自定义资源中定义了其需要的额外资源类型,如下:
- Function
- Environment
- Trigger
- HTTPTrigger
- KubernetesWatchTrigger
- TimeTrigger
- MessageQueueTrigger
- Package
- CanaryConfig
Fission Router
Fission的Router模块是HTTPTrigger的具体实现,主要思想如下:
- Router通过拉取在K8s中存储
httptriggers
和functions
两类CRD来缓存路径和函数之间的对应关系,代码 - Router具有监听K8s中资源变动的能力,从而实现路由配置的动态调整,代码
- Router支持根据name或者function-weights两种方式查找对应函数的UID,代码
- Router通过和Executor申请,获得function和ip之间的对应关系,代码
- Router实际上是对镜像中的HTTP服务进行了代理,具有重试、限时等功能,代码
在阅读Fission Router代码部分的时候接触到了两个十分有趣的组件,cache和throttler。
- cache可以理解是一个简单版的redis map服务,可以设置过期时间。通过channel的方式,串行请求,减少了锁的使用,加快了缓存的速度。
- throttler可以理解是一个任务处理队列,可以为任务标记上是否是重复执行。在高并发的情况下Router和Executor的交互,通过throttler的控制,减少了不必要的重复请求开销。
Fission Executor
Fission Executor的主要功能是对函数进行调度,具体策略分为poolmgr
和newdeploy
两种。
Executor在启动时(代码):
- 和其他组件一样,部署fission自定义资源,并确保资源都已经配置到了K8s中。
- 可以通过参数
ADOPT_EXISTING_RESOURCES
选择适配已经存在的旧函数或者清空掉已经存在的函数。 - 订阅K8s中的ConfigMap和Secret的变化,更新相关联的函数
- 订阅env的变化,处理env相关的事项
- 定期清理Fission自身不需要的Service Account。
Executor 订阅Configmap和Secert的变化,更新关联函数
此处将会涉及到Fission的功能改造:添加全局的configmap和secret中心,这些配置发生变化,也会执行函数的更新操作。函数的更新操作过程如下:
- 在全局函数中找到使用了发生变化的函数列表,调用执行器的refreshPods函数,进行清理。poolmgr的清理过程如下:
- 找到函数使用的env,在env关联的函数缓存中找到函数实例,删除env中关于这个函数实例的缓存
- 记录函数实例存活周期等信息
- 构建函数实例的label selector,在k8s中删除函数
- 之后Router的访问会因为函数没有响应,而重新请求Executor来获取函数的地址,Executor会实例化新的函数,使用新的配置进行运行
存在的问题:函数删除的时间比较长,会存在因为函数删除(k8s上真正的删除)不及时,造成router请求的依旧是旧函数。
解决办法:设置ENV的删除等待时间
Executor 订阅env的变化,处理相关事项
Executor在订阅到env的创建时,会在对应的命名空间中给fission组件需要的服务帐号进行权限绑定,在此处我添加了服务帐号对公共配置的命名空间的权限授予。
Executor对外提供的API
/v2/getServiceForFunction
传入的参数是namespace+func_name,返回函数的IP地址,步骤如下:
拉取函数的配置信息
根据ExecutorType,选择不同的策略执行器
执行器检查缓存中是否存在函数实例,并验证缓存的有效性
- 缓存设置
- 缓存的有效时长是无限的,因此需要有效性验证
- GenericPoolManager 实例有效性检查,内容如下
- Pod在K8s中存在,并且处于运行状态
- Pod不处于Terminating状态(K8s的issue,属于PodRunning状态的Pod,也有可能是Terminating阶段)
- Pod IP存在
- Pod中所有容器都处于Ready状态
- 启用了Istio 或者缓存中的IP和Pod对应的IP一致
- NewDeploy 实例有效性检查,内容如下
- 检查service有效性
- 检车service绑定的development后端的存在
- 检查development可用副本的数量是否大于0
- 若缓存有效,直接返回缓存的IP,否则清理掉缓存和旧资源,执行创建过程(代码)
- 申请创建函数的请求,会在channel中排队成串行执行,和cache的原理一样
- 创建函数将会保证每个函数的创建过程都是并行的
- 但是并行多请求触发的函数创建只会有一次,通过WaitGroup,记录同一函数创建请求的多个request,在创建完成后,将信息返回给各个request
- 缓存设置
具体创建函数的过程
GenericPoolManager 创建函数的过程
- 从K8s中获取函数中声明的运行环境信息
- 若没有运行环境的Poll,先创建运行环境的Poll(代码),Poll的概念和K8s中的Deployment概念一致
- 如果环境声明的命名空间是default,则运行环境在function-namespace中创建,否则在对应的命名空间中创建,代码。
- 检查目标命名空间是否有Fetcher所需的ServiceAccount,没有则进行创建。
- 创建Pod的数量会根据env中指定的spec.Poolsize参数创建(若spec.Version<3,spec.Poolsize默认为3)。
- 在创建之前检查命名空间中是否已经包含了对应的Deployment,若已经包含,则更改已经存在的Deployment的EXECUTOR_INSTANCEID_LABEL属性,报错退出。
- 从Poll中选出可以使用的pod(代码),若运行环境中标明运行环境不可搭载多个function,则会将选出的pod打上函数的标签,以独立于Pool。
- 对Pod进行特化,具体过程见
Fission Fetcher
部分。 - 根据参数决定是否创建service代理
/v2/tapServices
标记函数最近被访问的时间
poolmgr
功能特点
poolmgr除了负责上述创建Pool和实例话函数的过程,还负责
- 监视K8s中的Function和Package的资源变化,进行运维,Function Watcher,Package Watcher。
- 扫描闲置的函数,超过函数指定闲置时长进行清理,代码,若函数的
Spec.AllowedFunctionsPerContainer
为infinite,会忽略掉。 - 周期性同步Environment资源,提前创建Environment的Pool池。
工作流程
此外poolmgr为了追求极短的冷启动时长和控制限制的时间成本,不会创建service资源,不配置弹性伸缩。整体过程如下图(图片来源于参考资料,粗略阅读过源代码,比较赞成下图的流程,其中Service组件在没有配置Istio的时候是不会被创建的)
newdeploy
功能特点
newdeploy 追求的是可扩展的弹性伸缩的服务能力,基于K8s的HPA功能实现,集群需要提供metric service的服务,来让HPA知晓目前的pod压力,作出伸缩操作。
工作流程
图片来自参考资料,认同该工作流程图。
存在的问题:在参考资料中,提到了fission把函数执行器的概念暴露给了用户,这给用户造成了困扰。但我认为用户可以根据这种两种执行器,更自由的构造想要的函数,例如,我根据env环境中提供的cache功能,便捷的实现了一个带有缓存功能的长周期存在的用于缓存功能的实例,虽然这种部署思想和函数化的思想不太一致,但确实在使用过程中,给我带来了很多的便利。
Fission Fetcher
特化
Fetcher主要的工作是特化Pod,即指将环境容器变成运行函数容器的过程,该过程决定着Fission冷启动的时长。Executor 调用Fetcher代码,fetcher特化入口主要流程如下:
向目标pod发送拉取资源请求(fetcher)和加载资源请求(load),通过HTTP进行请求。
拉取资源
- 拉取代码
- 获取代码的Package信息
- 检查本地是否已经存在了所需要的文件
- 若不存在,支持两种方式拉取代码,拉取完成之后,根据配置和文件情况选择是否进行解压
- size<256k的时候,使用的k8s中存储的package中的代码
- size>256k的时候,从存储引擎中进行下载
- 将拉取下来的内容放置到指定的位置
- 拉取secrets 和 configmaps
拉取配置在k8s中的secrets,存储到本Pod的
1
sharedSecretPath/secret.namespace/secret.name
目录下,文件名为key,内容为value
拉取配置在k8s中的configmaps,存储到本Pod的
目录下,同样的,文件名为key,内容为value1
sharedSecretPath/config.namespace/config.name
Q:不会被覆盖掉么?
A:配置和密钥的目录位置可以设置成不一样的,所以是不会冲突的
默认fetcher的配置
1
/fetcher -secret-dir /secrets -cfgmap-dir /configs ...
- 拉取代码
加载函数
- fetcher向env中的接口发送加载函数的请求,由环境负责加载函数,官方Python 的env不会将secrets和configmaps主动拉进来,在使用的时候需要注意。
上传
为builder容器准备的,压缩上传编译好的文件。
Fission Mqtrigger
通过消息队列的方式触发函数,实现的思想是通过部署一个消费组件,消费消息队列中的消息,调用router组件触发函数,得到结果,并写入指定的topic中,实现消息流的处理。
在fission 1.10的版本中kafka也可以同时进行安装部署
Fission CLI
Fission CLI调用了Controller的restful接口,实现交互。
参考资料
开源 serverless 产品原理剖析(二) - Fission
附录
HTTPTrigger yaml demo
1 | Name: welcome-func-trigger |
Package yaml demo
1 | Name: welcome-87ee5696-1e54-4cf2-a8e0-57d13d576c4b |
Function yaml demo
1 | Name: welcome |