开源 serverless 产品原理剖析(二) - Fission

背景

本文是开源 serverless 产品原理剖析系列文章的第二篇,关于 serverless 背景知识的介绍可参考文章开源 serverless 产品原理剖析(一) - Kubeless,这里不再赘述。

Fission 简介

Fission 是由私有云服务提供商 Platform9 领导开源的 serverless 产品,它借助 kubernetes 灵活强大的编排能力完成容器的管理调度工作,而将重心投入到 FaaS 功能的开发上,其发展目标是成为 AWS lambda 的开源替代品。从 CNCF 视角,fission 属于 serverless 平台型产品。

核心概念

Fission 包含 FunctionEnvironmentTrigger 三个核心概念,其关系如下图所示:

开源 serverless 产品原理剖析(二) - Fission

  1. Function - 代表用特定语言编写的需要被执行的代码片段。
  2. Environment- 用于运行用户函数的特定语言环境。
  3. Trigger - 用于关联函数和事件源。如果把事件源比作生产者,函数比作执行者,那么触发器就是联系两者的桥梁。

关键组件

Fission 包含 Controller、Router、Executor 三个关键组件:

  1. Controller - 提供了针对 fission 资源的增删改查操作接口,包括 functions、triggers、environments、Kubernetes event watches 等。它是 fission CLI 的主要交互对象。
  2. Router - 函数访问入口,同时也实现了 HTTP 触发器。它负责将用户请求以及各种事件源产生的事件转发至目标函数。
  3. Executor - fission 包含 PoolManager 和 NewDeploy 两类执行器,它们控制着 fission 函数的生命周期。

原理剖析

本章节将从以下几个方面介绍 fission 的基本原理:

  1. 函数执行器 - 它是理解 fission 工作原理的基础。
  2. Pod 特化 - 它是理解 fission 如何根据用户源码构建出可执行函数的关键。
  3. 触发器 - 它是理解 fission 函数各种触发原理的入口。
  4. 自动伸缩 - 它是理解 fission 如何根据负载动态调整函数个数的捷径。
  5. 日志处理 - 它是理解 fission 如何处理各函数日志的有效手段。

本文所作的调研基于kubeless 0.12.0k8s 1.13

函数执行器

CNCF 对函数生命周期的定义如下图所示,它描绘了函数构建、部署、运行的一般流程。

开源 serverless 产品原理剖析(二) - Fission

要理解 fission,首先需要了解它是如何管理函数生命周期的。Fission 的函数执行器是其控制函数生命周期的关键组件。Fission 包含 PoolManager 和 NewDeploy 两类执行器,下面分别对两者进行介绍。

PoolManager

Poolmgr 使用了池化技术,它通过为每个 environment 维持了一定数量的通用 pod 并在函数被触发时将 pod 特化,大大降低了函数的冷启动的时间。同时,poolmgr 会自动清理一段时间内未被访问的函数,减少闲置成本。该执行器的原理如下图所示。

开源 serverless 产品原理剖析(二) - Fission

此时,函数的生命周期如下:

  1. 使用 fission CLI 向 controller 发送请求,创建函数运行时需要的特定语言环境。例如,以下命令将创建一个 python 运行环境。

    fission environment create --name python --image fission/python-env
  2. Poolmgr 定期同步 environment 资源列表,参考 eagerPoolCreator
  3. Poolmgr 遍历 environment 列表,使用 deployment 为每个 environment 创建一个通用 pod 池,参考 MakeGenericPool
  4. 使用 fission CLI 向 controller 发送创建函数的请求。此时,controller 只是将函数源码等信息持久化存储,并未真正构建好可执行函数。例如,以下命令将创建一个名为 hello 的函数,该函数选用已经创建好的 python 运行环境,源码来自 hello.py,执行器为 poolmgr。

    fission function create --name hello --env python --code hello.py --executortype poolmgr
  5. Router 接收到触发函数执行的请求,加载目标函数相关信息。
  6. Router 向 executor 发送请求获取函数访问入口,参考 GetServiceForFunction
  7. Poolmgr 从函数指定环境对应的通用 pod 池里随机选择一个 pod 作为函数执行的载体,这里通过更改 pod 的标签让其从 deployment 中“独立”出来,参考 _choosePod。K8s 发现 deployment 所管理 pod 的实际副本数少于目标副本数后会对 pod 进行补充,这样便实现了保持通用 pod 池中的 pod 个数的目的。
  8. 特化处理被挑选出来的 pod,参考 specializePod
  9. 为特化后的 pod 创建 ClusterIP 类型的 service,参考 createSvc
  10. 将函数的 service 信息返回给 router,router 会将 serviceUrl 缓存以避免频繁向 executor 发送请求。
  11. Router 使用返回的 serviceUrl 访问函数。
  12. 请求最终被路由至运行函数的 pod。
  13. 如果该函数一段时间内未被访问会被自动清理,包括该函数的 pod 和 service,参考 idleObjectReaper

NewDeploy

Poolmgr 很好地平衡了函数的冷启动时间和闲置成本,但无法让函数根据度量指标自动伸缩。NewDeploy 执行器实现了函数 pod 的自动伸缩和负载均衡,该执行器的原理如下图所示。

开源 serverless 产品原理剖析(二) - Fission

此时,函数的生命周期如下:

  1. 使用 fission CLI 向 controller 发送请求,创建函数运行时需要的特定语言环境。
  2. 使用 fission CLI 向 controller 发送创建函数的请求。例如,以下命令将创建一个名为 hello 的函数,该函数选用已经创建好的 python 运行环境,源码来自 hello.py,执行器为 newdeploy,目标副本数在 1 到 3 之间,目标 cpu 使用率是 50%。

    fission fn create --name hello --env python --code hello.py --executortype newdeploy --minscale 1 --maxscale 3 --targetcpu 50
  3. Newdeploy 会注册一个 funcController 持续监听针对 function 的 ADD、UPDATE、DELETE 事件,参考 initFuncController
  4. Newdeploy 监听到了函数的 ADD 事件后,会根据 minscale 的取值判断是否立即为该函数创建相关资源。

    1. minscale > 0,则立即为该函数创建 service、deployment、HPA(deployment 管理的 pod 会特化)。
    2. minscale <= 0,延迟到函数被真正触发时创建。
  5. Router 接收到触发函数执行的请求,加载目标函数相关信息。
  6. Router 向 newdeploy 发送请求获取函数访问入口。如果函数所需资源已被创建,则直接返回访问入口。否则,创建好相关资源后再返回。
  7. Router 使用返回的 serviceUrl 访问函数。
  8. 如果该函数一段时间内未被访问,函数的目标副本数会被调整成 minScale,但不会删除 service、deployment、HPA 等资源,参考 idleObjectReaper

执行器比较

实际使用过程中,用户需要从延迟和闲置成本两个角度考虑选择何种类型的执行器。不同执行器的特点如下表所示。

执行器类型 最小副本数 延迟 闲置成本
Newdeploy 0 非常低 - pods 一段时间未被访问会被自动清理掉。
Newdeploy >0 中等 - 每个函数始终会有一定数量的 pod 在运行。
Poolmgr 0 低 - 通用池中的 pod 会一直运行。

小结

Fission 将函数执行器的概念暴露给了用户,增加了产品的使用成本。实际上可以将 poolmgr 和 newdeploy 技术相结合,通过创建 deployment 将特化后的 pod 管理起来,这样可以很自然地利用 HPA 来实现对函数的自动伸缩。

Pod 特化

在介绍函数执行器时多次提到了 pod 特化,它是 fission 将环境容器变成函数容器的奥秘。Pod 特化的本质是通过向容器发送特化请求让其加载用户函数,其原理如下图所示。

开源 serverless 产品原理剖析(二) - Fission

一个函数 pod 由下面两种容器组成:

  • Fetcher - 下载用户函数并将其放置在共享 volume 里。不同语言环境使用了相同的 fetcher 镜像,fetcher 的工作原理可参考代码 fetcher.go
  • Env - 用户函数运行的载体。当它成功加载共享 volume 里的用户函数后,便可接收用户请求。

具体步骤如下:

  1. 容器 fetcher 接收到拉取用户函数的请求。
  2. Fetcher 从 K8s CRD 或 storagesvc 处获取用户函数。
  3. Fetcher 将函数文件放置在共享的 volume 里,如果文件被压缩还会负责解压。
  4. 容器 env 接收到加载用户函数的命令。
  5. Env 从共享 volume 中加载 fetcher 为其准备好的用户函数。
  6. 特化流程结束,容器 env 开始处理用户请求。

触发器

前面的章节介绍了 fission 函数的构建、加载和执行的逻辑,本章节主要介绍如何基于各种事件源触发 fission 函数的执行。CNCF 将函数的触发方式分成了如下图所示的几种类别,关于它们的详细介绍可参考链接 Function Invocation Types

开源 serverless 产品原理剖析(二) - Fission

对于 fission 函数,最简单的触发方式是使用 fission CLI,另外还支持通过各种触发器。下表展示了 fission 函数目前支持的触发方式以及它们所属的类别。

触发方式 类别
fission CLI Synchronous Req/Rep
HTTP Trigger Synchronous Req/Rep
Time Trigger Job (Master/Worker)
Message Queue Trigger
1. nats-streaming
2. azure-storage-queue
3. kafka
Async Message Queue
Kubernetes Watch Trigger Async Message Queue

下图展示了 fission 函数部分触发方式的原理:

开源 serverless 产品原理剖析(二) - Fission

HTTP trigger

所有发往 fission 函数的请求都会由 router 转发,fission 通过为 router 创建 NodePortLoadBalancer 类型的 service 让其能够被外界访问。

除了直接访问 router,还可以利用 K8s ingress 机制实现 http trigger。以下命令将为函数 hello 创建一个 http trigger,并指定访问路径为/echo

fission httptrigger create --url /echo --method GET --function hello --createingress --host example.com

该命令会创建如下 ingress 对象,可以参考 createIngress 深入了解 ingress 的创建逻辑。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  # 该 Ingress 的名称
  name: xxx
  ...
spec:
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          # 指向 router service
          serviceName: router
          servicePort: 80
        # 访问路径
        path: /echo

Ingress 只是用于描述路由规则,要让规则生效、实现请求转发,集群中需要有一个正在运行的 ingress controller。想要深入了解 ingress 原理可参考系列文章第一篇中的 HTTP trigger 章节。

Time trigger

如果希望定期触发函数执行,需要为函数创建 time trigger。Fission 使用 deployment 部署了组件 timer,该组件负责管理用户创建的 timer trigger。Timer 每隔一段时间会同步一次 time trigger 列表,并通过 golang 中被广泛使用的 cron 库 robfig/cron 定期触发和各 timer trigger 相关联函数的执行。

以下命令将为函数 hello 创建一个名为halfhourly的 time trigger,该触发器每半小时会触发函数 hello 执行一次。这里使用了标准的 cron 语法定义执行计划。

fission tt create --name halfhourly --function hello --cron "*/30 * * * *"
trigger 'halfhourly' created

Message queue trigger

为了支持异步触发,fission 允许用户创建消息队列触发器。目前可供选择的消息队列有 nats-streamingazure-storage-queuekafka,下面以 kafka 为例描述消息队列触发器的使用方法和实现原理。

以下命令将为函数 hello 创建一个基于 kafka 的消息队列触发器hellomsg。该触发器订阅了主题 input 中的消息,每当有消息到达它便会触发函数执行。如果函数执行成功,会将结果写入主题 output 中,否则将结果写入主题 error 中。

fission mqt create --name hellomsg --function hello --mqtype kafka --topic input --resptopic output --errortopic error 

Fission 使用 deployment 部署了组件 mqtrigger-kafka,该组件负责管理用户创建的 kafka trigger。它每隔一段时间会同步一次 kafka trigger 列表,并为每个 trigger 创建 1 个用于执行触发逻辑的 go routine,触发逻辑如下:

  1. 消费 topic 字段指定主题中的消息;
  2. 通过向 router 发送请求触发函数执行并等待函数返回;
  3. 如果函数执行成功,将返回结果写入 resptopic 字段指定的主题中,并确认消息已被处理;否则,将结果写入 errortopic 字段指定的主题中。

小结

  1. Fission 提供了一些常用触发器,但缺少对 CNCF 规范里提到的Message/Record Streams触发方式的支持,该方式要求消息被顺序处理;
  2. 如果有其它事件源想要接入可以参考 fission 触发器的设计模式自行实现。

自动伸缩

K8s 通过 Horizontal Pod Autoscaler 实现 pod 的自动水平伸缩。对于 fission,只有通过 newdeploy 方式创建的函数才能利用 HPA 实现自动伸缩。

以下命令将创建一个名为 hello 的函数,运行该函数的 pod 会关联一个 HPA,该 HPA 会将 pod 数量控制在 1 到 6 之间,并通过增加或减少 pod 个数使得所有 pod 的平均 cpu 使用率维持在 50%。

fission fn create --name hello --env python --code hello.py --executortype newdeploy --minmemory 64 --maxmemory 128 --minscale 1 --maxscale 6 --targetcpu 50

Fission 使用的是autoscaling/v1版本的 HPA API,该命令将要创建的 HPA 如下:

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  labels:
    executorInstanceId: xxx
    executorType: newdeploy
    functionName: hello
    ...
  # 该 HPA 名称
  name: hello-${executorInstanceId}
  # 该 HPA 所在命名空间
  namespace: fission-function
  ...
spec:
  # 允许的最大副本数
  maxReplicas: 6
  # 允许的最小副本数
  minReplicas: 1
  # 该 HPA 关联的目标
  scaleTargetRef:
    apiVersion: extensions/v1beta1
    kind: Deployment
    name: hello-${executorInstanceId}
  # 目标 CPU 使用率
  targetCPUUtilizationPercentage: 50

想了解 HPA 的原理可参考系列文章第一篇中的自动伸缩章节,那里详细介绍了 K8s 如何获取和使用度量数据以及目前采用的自动伸缩策略。

小结

  1. 和 kubeless 类似,fission 避免了将创建 HPA 的复杂细节直接暴露给用户,但这是以牺牲功能为代价的;
  2. Fission 目前提供的自动伸缩功能过于局限,只对通过 newdeploy 方式创建的函数有效,且只支持基于 cpu 使用率这一种度量指标(kubeless 支出基于 cpu 和 qps)。本质上是因为 fission 目前仍然使用的是 v1 版本的 HPA,如果用户希望基于新的度量指标或者综合多项度量指标可以直接使用 hpa-v2 提供的功能;
  3. 目前 HPA 的扩容缩容策略是基于既成事实被动地调整目标副本数,还无法根据历史规律预测性地进行扩容缩容。

日志处理

为了能更好地洞察函数的运行情况,往往需要对函数产生的日志进行采集、处理和分析。Fission 日志处理的原理如下图所示。

开源 serverless 产品原理剖析(二) - Fission

日志处理流程如下:

  1. 使用 DaemonSet 在集群中的每个工作节点上部署一个 fluentd 实例用于采集当前机器上的容器日志,参考 logger。这里,fluentd 容器将包含容器日志的宿主机目录/var/log//var/lib/docker/containers挂载进来,方便直接采集。
  2. Fluentd 将采集到的日志存储至 influxdb 中。
  3. 用户使用 fission CLI 查看函数日志。例如,使用命令fission function logs --name hello可以查看到函数 hello 产生的日志。

小结

目前,fission 只做到了函数日志的集中化存储,能够提供的查询分析功能非常有限。另外,influxdb 更适合存储监控指标类数据,无法满足日志处理与分析的多样性需求。

函数是运行在容器里的,因此函数日志处理本质上可归结为容器日志处理。针对容器日志,阿里云日志服务团队提供了成熟完备的解决方案,欲知详情可参考文章面向容器日志的技术实践

总结

在介绍完 fission 的基本原理后,不妨从以下几个方面将其和第一篇介绍的 kubeless 作一个简单对比。

  1. 触发方式 - 两款产品都支持常用的触发方式,但 kubeless 相比 fission 支持的更全面,且更方便接入新的数据源。
  2. 自动伸缩 - 两款产品的自动伸缩能力都还比较基础,支持的度量指标都比较少,且底层都依赖于 K8s HPA。
  3. 函数冷启动时间 - fission 通过池化技术降低了函数冷启动时间,kubeless 在这一块并未作过多优化。
  4. 高级功能 - fission 支持灰度发布自定义工作流等高级功能,kubeless 目前还不支持。

参考资料

上一篇:【shiro】一、基础概念


下一篇:性能诊断利器 JProfiler 快速入门和最佳实践