吴波bruce_wu
2018-12-01
7207浏览量
Serverless 架构的出现让开发者不用过多地考虑传统的服务器采购、硬件运维、网络拓扑、资源扩容等问题,可以将更多的精力放在业务的拓展和创新上。
随着 serverless 概念的深入人心,各大云计算厂商纷纷推出了各自的 serverless 产品,其中比较有代表性的有 AWS lambda、Azure Function、Google Cloud Functions、阿里云函数计算等。
另外,CNCF 也于 2016 年创立了 Serverless Working Group,它致力于 cloud native 和 serverless 技术的结合。下图是 CNCF serverless 全景图,它将这些产品分成了工具型、安全型、框架型和平台型等类别。
同时,容器以及容器编排工具的出现,大大降低了 serverless 产品的开发成本,促进了一大批优秀开源 serverless 产品的诞生,它们大多构建于 kubernetes 之上,如下图所示。
本文将要介绍的 kubeless 便是这些开源 serverless 产品的典型代表。根据官方的定义,kubeless 是 kubernetes native 的无服务计算框架,它可以让用户在 kubernetes 之上使用 FaaS 构建高级应用程序。从 CNCF 视角,kubeless 属于平台型产品。
Kubless 有三个核心概念:
本章节将以 kubeless 为例介绍 serverless 产品需要具备的基本能力,以及 kubeless 是如何利用 K8s 现有功能来实现它们的。这些基本能力包括:
本文所作的调研基于kubeless v1.0.0
和k8s 1.13
。
CNCF 对函数生命周期的定义如下图所示。用户只需提供源码和函数说明,构建部署等工作通常由 serverless 平台完成。 因此,基于用户提交的源码迅速构建可执行函数是 serverless 产品必须具备的基础能力。
在 kubeless 里,创建函数非常简单:
kubeless function deploy hello --runtime python2.7 \
--from-file test.py \
--handler test.hello
该命令各参数含义如下:
hello
:将要部署的函数名称;--runtime python2.7
: 指定使用 python 2.7 作为运行环境。Kubeless 可供选择的运行环境请参考链接 runtimes。--from-file test.py
:指定函数源码文件(支持 zip 格式)。--handler test.hello
:指定使用 test.py 中的 hello 方法处理请求。Kubeless 函数是一个自定义 K8s 对象,本质上是 k8s operator。k8s operator 原理如下图所示:
下面以 kubeless 函数为例,描述 K8s operator 的一般工作流程:
functions.kubeless.io
的 CRD 来代表 kubeless 函数;function-controller
的 CRD controller,该 controller 会监听针对 function 的 ADD、UPDATE、DELETE 事件,并绑定 handler(参阅 AddEventHandler);除了函数外,下文将要介绍的 trigger 也是一个 k8s operator。
Kubeless 的 function-controller
监听到针对 function 的 ADD 事件后,会触发相应 handler 创建函数。一个函数由若干 K8s 对象组成,包括 ConfigMap、Service、Deployment、Pod 等,其结构如下图所示:
函数中的 ConfigMap 用于描述函数源码和依赖。
apiVersion: v1
data:
handler: test.hello
# 函数依赖的第三方 python 库
requirements.txt: |
kubernetes==2.0.0
# 函数源码
test.py: |
def hello(event, context):
print event
return event['data']
kind: ConfigMap
metadata:
labels:
created-by: kubeless
function: hello
# 该 ConfigMap 名称
name: hello
namespace: default
...
函数中的 Service 用于描述该函数的访问方式。该 Service 会与执行 function 逻辑的 Pods 相关联,类型是 ClusterIP。
apiVersion: v1
kind: Service
metadata:
labels:
created-by: kubeless
function: hello
# 该 Service 名称
name: hello
namespace: default
...
spec:
clusterIP: 10.109.2.217
ports:
- name: http-function-port
port: 8080
protocol: TCP
targetPort: 8080
selector:
created-by: kubeless
function: hello
# Service 类型
type: ClusterIP
...
函数中的 Deployment 用于编排执行函数逻辑的 Pods,通过它可以描述函数期望的个数。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
created-by: kubeless
function: hello
name: hello
namespace: default
...
spec:
# 指定函数期望的个数
replicas: 1
...
函数中的 Pod 包含真正执行函数逻辑的容器。
Pod 中的 volumes 段指定了该函数的 ConfigMap。这会将 ConfigMap 中的源码和依赖添加到 volumeMounts.mountPath 指定的目录里面。从容器视角来看,文件路径为/src/test.py
和 /src/requirements
。
...
volumeMounts:
- mountPath: /kubeless
name: hello
- mountPath: /src
name: hello-deps
volumes:
- emptyDir: {}
name: hello
- configMap:
defaultMode: 420
name: hello
...
Pod 中的 Init Container 主要作用如下:
Pod 中的 Func Container 会加载 Init Container 准备好的源码和依赖并执行函数。不同 runtime 加载代码的方式大同小异,可参考 kubeless.py,Handler.java。
一款成熟的 serverless 产品需要具备灵活触发能力,以满足事件源的多样性需求,同时需要能够方便快捷地接入新事件源。CNCF 将函数的触发方式分成了如下图所示的几种类别,关于它们的详细介绍可参考链接 Function Invocation Types。
对于 kubeless 的函数,最简单的触发方式是使用 kubeless CLI,另外还支持通过各种触发器。下表展示了 kubeless 函数目前支持的触发方式以及它们所属的类别。
触发方式 | 类别 |
---|---|
kubeless CLI | Synchronous Req/Rep |
Http Trigger | Synchronous Req/Rep |
Cronjob Trigger | Job (Master/Worker) |
Kafka Trigger | Async Message Queue |
Nats Trigger | Async Message Queue |
Kinesis Trigger | Message Stream |
下图展示了 kubeless 函数部分触发方式的原理:
如果希望通过发送 HTTP 请求触发函数执行,需要为函数创建 HTTP 触发器。 Kubeless 利用 K8s ingress 机制实现了 http trigger。Kubeless 创建了一个名为httptriggers.kubeless.io
的 CRD 来代表 http trigger 对象。同时,kubeless 包含一个名为http-trigger-controller
的 CRD controller,它会持续监听针对 http trigger 和 function 的 ADD、UPDATE、DELETE 事件,并执行对应的操作。
以下命令将为函数 hello 创建一个名为http-hello
的 http trigger,并指定选用 nginx 作为 gateway。
kubeless trigger http create http-hello --function-name hello --gateway nginx --path echo --hostname example.com
该命令会创建如下 ingress 对象,可以参考 CreateIngress 深入了解 ingress 的创建逻辑。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
# 该 Ingress 的名字,即创建 http trigger 时指定的 name
name: http-hello
...
spec:
rules:
- host: example.com
http:
paths:
- backend:
# 指向 kubeless 为函数 hello 创建的 ClusterIP 类型的 Service
serviceName: hello
servicePort: 8080
path: /echo
Ingress 只是用于描述路由规则,要让规则生效、实现请求转发,集群中需要有一个正在运行的 ingress controller。可供选择的 ingress controller 有 Contour、F5 BIG-IP Controller for Kubernetes、Kong Ingress Controllerfor Kubernetes、NGINX Ingress Controller for Kubernetes、Traefik 等。这种路由规则描述和路由功能实现相分离的思想很好地提现了 K8s 始终坚持的需求和供给分离的设计理念。
上文中的命令在创建 trigger 时指定了 nginx 作为 gateway,因此需要部署一个 nginx-ingress-controller。该 controller 的基本工作原理如下:
想要更深入地了解 nginx-ingress-controller 的工作原理可参考文章 how-it-works。
完成上述工作后,我们便可以通过发送 HTTP 请求触发函数 hello 的执行:
样例如下:
curl --data '{"Another": "Echo"}' \
--header "Host: example.com" \
--header "Content-Type:application/json" \
example.com/echo
# 函数返回
{"Another": "Echo"}
如果希望定期触发函数执行,需要为函数创建 cronjob 触发器。K8s 支持通过 CronJob 定期运行任务,kubeless 利用这个特性实现了 cronjob trigger。Kubeless 创建了一个名为cronjobtriggers.kubeless.io
的 CRD 来代表 cronjob trigger 对象。同时,kubeless 包含一个名为cronjob-trigger-controller
的 CRD controller,它会持续监听针对 cronjob trigger 和 function 的 ADD、UPDATE、DELETE 事件,并执行对应的操作。
以下命令将为函数 hello 创建一个名为scheduled-invoke-hello
的 cronjob trigger,该触发器每分钟会触发函数 hello 执行一次。
kubeless trigger cronjob create scheduled-invoke-hello --function=hello --schedule="*/1 * * * *"
该命令会创建如下 CronJob 对象,可以参考 EnsureCronJob 深入了解 CronJob 的创建逻辑。
apiVersion: batch/v1beta1
kind: CronJob
metadata:
# 该 CronJob 的名字,即创建 cronjob trigger 时指定的 name
name: scheduled-invoke-hello
...
spec:
# 该 CronJob 的执行计划,即创建 cronjob trigger 时指定的 schedule
schedule: */1 * * * *
...
jobTemplate:
spec:
activeDeadlineSeconds: 180
template:
spec:
containers:
- args:
- curl
- -Lv
# HTTP headers,包含 event-id、event-time、event-type、event-namespace 等信息
- ' -H "event-id: xxx" -H "event-time: yyy" -H "event-type: application/json" -H "event-namespace: cronjobtrigger.kubeless.io"'
# kubeless 会为 function 创建一个 ClusterIP 类型的 Service
# 可以根据 service 的 name、namespace 拼出 endpoint
- http://hello.default.svc.cluster.local:8080
image: kubeless/unzip
name: trigger
restartPolicy: Never
...
如果发现 kubeless 默认提供的触发器无法满足业务需求,可以自定义新的触发器。新触发器的构建流程如下:
为该 CRD 创建一个 CRD controller。
我们可以看到,自定义 trigger 的流程遵循了 K8s Operator 设计模式。
K8s 通过 Horizontal Pod Autoscaler 实现 pod 的自动水平伸缩。Kubeless 的 function 通过 K8s deployment 部署运行,因此天然可以利用 HPA 实现自动伸缩。
自动伸缩的第一步是要让 HPA 能够获取度量数据。目前,kubeless 中的函数支持基于 cpu 和 qps 这两种指标进行自动伸缩。下图展示了 HPA 获取这两种度量数据的途径。
CPU 使用率属于内置度量指标,对于这类指标 HPA 可以通过 metrics API 从 Metrics Server 中获取数据。Metrics Server 是 Heapster 的继承者,它可以通过kubernetes.summary_api
从 Kubelet、cAdvisor 中获取度量数据。
QPS 属于自定义度量指标,想要获取这类指标的度量数据需要完成下列步骤。
完成上述步骤后,HPA 就可以通过 custom metrics API 从 Prometheus Adapter 中获取 qps 度量数据。详细配置步骤可参考文章 kubeless-autoscaling。
有时基于 cpu 和 qps 这两种度量指标对函数进行自动伸缩还远远不够。如果希望基于其它度量指标,需要了解 K8s 定义的度量指标类型及其获取方式。
目前,K8s 1.13 版本支持的度量指标类型如下:
类型 | 简介 | 获取方式 |
---|---|---|
Object | 代表 k8s 对象的度量指标,例如上文提到的 Service 对象的 function_calls 指标。 |
custom.metrics.k8s.io 度量数据采集后,需要通过已有适配器或自己实现适配器获取数据。目前已有的适配器包括 k8s-prometheus-adapter、azure-k8s-metrics-adapter、k8s-stackdriver 等。 |
Pod | 代表伸缩目标中每个 pod 的自定义度量指标,例如 pod 每秒处理的事务数。在与目标值比较前需要除以 pod 个数。 | 同上 |
Resource | 代表伸缩目标中每个 pod 的 K8s 内置资源指标(如 CPU、Memory)。在与目标值比较前需要除以 pod 个数。 |
metrics.k8s.io 从 Metrics Server 或 Heapster 中获取度量数据。 |
External | External 是一个全局度量指标,它与任何 K8s 对象无关。它允许伸缩目标基于来自 cluster 外部的信息进行伸缩(如外部负载均衡器的 QPS,云消息服务中的队列长度)。 |
external.metrics.k8s.io 度量数据采集后,需要云平台厂商提供适配器或自己实现。目前已有的适配器包括 azure-k8s-metrics-adapter、k8s-stackdriver 等。 |
准备好相应的度量数据和获取数据的组件,HPA 就能基于它们对函数进行自动伸缩。更多关于 K8s 度量指标的介绍可参考文章 hpa-external-metrics。
知道了 HPA 获取度量数据的途径后,下面描述 HPA 如何基于这些数据对函数进行自动伸缩。
假设已经存在一个名为 hello 的函数,以下命令将为该函数创建一个基于 cpu 使用率的 HPA,它将运行该函数的 pod 数量控制在 1 到 3 之间,并通过增加或减少 pod 个数使得所有 pod 的平均 cpu 使用率维持在 70%。
kubeless autoscale create hello --metric=cpu --min=1 --max=3 --value=70
Kubeless 使用的是 autoscaling/v2alpha1 版本的 HPA API,该命令将要创建的 HPA 如下:
kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2alpha1
metadata:
name: hello
namespace: default
labels:
created-by: kubeless
function: hello
spec:
scaleTargetRef:
kind: Deployment
name: hello
minReplicas: 1
maxReplicas: 3
metrics:
- type: Resource
resource:
name: cpu
targetAverageUtilization: 70
该 HPA 计算目标 pod 数量的公式如下:
TargetNumOfPods = ceil(sum(CurrentPodsCPUUtilization) / Target)
以下命令将为函数 hello 创建一个基于 qps 的 HPA,它将运行该函数的 pod 数量控制在 1 到 5 之间,并通过增加或减少 pod 个数确保所有挂在服务 hello 后的 pod 每秒能处理的请求次数之和达到 2000。
kubeless autoscale create hello --metric=qps --min=1 --max=5 --value=2k
该命令将要创建的 HPA 如下:
kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2alpha1
metadata:
name: hello
namespace: default
labels:
created-by: kubeless
function: hello
spec:
scaleTargetRef:
kind: Deployment
name: hello
minReplicas: 1
maxReplicas: 5
metrics:
- type: Object
object:
metricName: function_calls
target:
apiVersion: autoscaling/v2beta1
kind: Service
name: hello
targetValue: 2k
如果计划基于多项度量指标对函数进行自动伸缩,需要直接为运行 function 的 deployment 创建 HPA。
使用如下 yaml 文件可以为函数 hello 创建一个名为hello-cpu-and-memory
的 HPA,它将运行该函数的 pod 数量控制在 1 到 10 之间,并尝试让所有 pod 的平均 cpu 使用率维持在 50%,平均 memory 使用量维持在 200MB。对于多项度量指标,K8s 会计算出每项指标需要的 pod 数量,取其中的最大值作为最终的目标 pod 数量。
kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2alpha1
metadata:
name: hello-cpu-and-memory
namespace: default
labels:
created-by: kubeless
function: hello
spec:
scaleTargetRef:
kind: Deployment
name: hello
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
targetAverageUtilization: 50
- type: Resource
resource:
name: memory
targetAverageValue: 200Mi
一个理想的自动伸缩策略应当处理好下列场景:
Kubeless 依赖的 HPA 充分考虑了上述情形,不断改进和完善其使用的自动伸缩策略。下面以 K8s 1.13 版本为例描述该策略。如果想要更加深入地了解策略原理请参考链接 horizontal。
HPA 每隔一段时间会根据获取的度量数据同步一次和该 HPA 关联的 RC / Deployment 中的 pod 个数,时间间隔通过 kube-controller-manager 的参数--horizontal-pod-autoscaler-sync-period
指定,默认为 15s。在每一次同步过程中,HPA 需要经历如下图所示的计算流程。
分别计算 HPA 列表中每项指标需要的 pod 数量,记为 replicaCountProposal。选择其中的最大值作为 metricDesiredReplicas。在计算每项指标的 replicaCountProposal 过程中会考虑下列因素:
--horizontal-pod-autoscaler-tolerance
指定,默认值是 0.1。--horizontal-pod-autoscaler-cpu-initialization-period
(默认为 5 分钟)和--horizontal-pod-autoscaler-initial-readiness-delay
(默认为 30 秒)调整 pod 被认为处于 unready 状态的时间。将最近一段时间计算出的 metricDesiredReplicas 记录下来,取其中的最大值作为 stabilizedRecommendation。这样做是为了让缩容过程变得平滑,消除度量数据异常波动造成的影响。该时间段可以通过参数--horizontal-pod-autoscaler-downscale-stabilization-window
指定,默认为 5 分钟。
如果通过上述步骤计算出的 desiredReplicas 不等于 currentReplicas,则“执行”扩容缩容操作。这里所说的执行只是将 desiredReplicas 赋值给 RC / Deployment 中的 replicas,pod 的创建销毁会由 kube-scheduler 和 worker node 上的 kubelet 异步完成的。
Kubeless 基于 K8s 提供了较为完整的 serverless 解决方案,但和一些商业 serverless 产品还存在一定差距:
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。
阿里云存储基于飞天盘古2.0分布式存储系统,产品多种多样,充分满足用户数据存储和迁移上云需求。