关于serverless:从-0-到-1打造新一代开源函数计算平台

32次阅读

共计 8782 个字符,预计需要花费 22 分钟才能阅读完成。

本文是依据霍秉杰在 2021 稀土开发者大会分享的内容整顿而来。

作者:霍秉杰,云原生 FaaS 我的项目 OpenFunction Founder;FluentBit Operator 的发起人;他还是几个可观测性开源我的项目的发起人,如 Kube-Events、Notification Manager 等;酷爱云原生和开源技术,是 Prometheus Operator, Thanos, Loki, Falco 的贡献者。

无服务器计算,即通常所说的 Serverless,曾经成为以后云原生畛域煊赫一时的名词,是继 IaaS,PaaS 之后云计算倒退的下一波浪潮。Serverless 强调的是一种架构思维和服务模型,让开发者无需关怀基础设施(服务器等),而是专一到应用程序业务逻辑上。加州大学伯克利分校在论文 A Berkeley View on Serverless Computing 中给出了两个对于 Serverless 的外围观点:

  • 有服务的计算并不会隐没,但随着 Serverless 的成熟,有服务计算的重要性会逐步升高。
  • Serverless 最终会成为云时代的计算范式,它可能在很大水平上代替有服务的计算模式,并给 Client-Server 时代划上句号。

那么什么是 Serverless 呢?

Serverless 介绍

对于什么是 Serverless,加州大学伯克利分校在之前提到的论文中也给出了明确定义:Serverless computing = FaaS + BaaS。云服务按形象水平从底层到下层传统的分类是硬件、云平台根本组件、PaaS、利用,但 PaaS 层的现实状态是具备 Serverless 的能力,因而这里咱们将 PaaS 层替换成了 Serverless,即下图中的黄色局部。

Serverless 蕴含两个组成部分 BaaSFaaS,其中对象存储、关系型数据库以及 MQ 等云上根底撑持服务属于 BaaS(后端即服务),这些都是每个云都必备的根底服务,FaaS(函数即服务)才是 Serverless 的外围。

现有开源 Serverless 平台剖析

KubeSphere 社区从 2020 年下半年开始对 Serverless 畛域进行深度调研。通过一段时间的调研后,咱们发现:

  • 现有开源 FaaS 我的项目绝大多数启动较早,大部分都在 Knative 呈现前就曾经存在了;
  • Knative 是一个十分卓越的 Serverless 平台,然而 Knative Serving 仅仅能运行利用,不能运行函数,还不能称之为 FaaS 平台;
  • Knative Eventing 也是十分优良的事件治理框架,然而设计有些过于简单,用户用起来有肯定门槛;
  • OpenFaaS 是比拟风行的 FaaS 我的项目,然而技术栈有点老旧,依赖于 Prometheus 和 Alertmanager 进行 Autoscaling,在云原生畛域并非最业余和麻利的做法;
  • 近年来云原生 Serverless 相干畛域陆续涌现出了很多优良的开源我的项目如 KEDA、Dapr、Cloud Native Buildpacks(CNB)、Tekton、Shipwright 等,为创立新一代开源 FaaS 平台打下了根底。

综上所述,咱们调研的论断就是:现有开源 Serverless 或 FaaS 平台并不能满足构建古代云原生 FaaS 平台的要求,而云原生 Serverless 畛域的最新进展却为构建新一代 FaaS 平台提供了可能。

新一代 FaaS 平台框架设计

如果咱们要从新设计一个更加古代的 FaaS 平台,它的架构应该是什么样子呢?现实中的 FaaS 框架应该依照函数生命周期分成几个重要的局部:函数框架 (Functions framework)、函数构建 (Build)、函数服务 (Serving) 和事件驱动框架 (Events Framework)。

作为 FaaS,首先得有一个 Function Spec 来定义函数该怎么写,有了函数之后,还要转换成利用,这个转换的过程就是靠 函数框架 来实现;如果利用想在云原生环境中运行,就得构建容器镜像,构建流程依赖 函数构建 来实现;构建完镜像后,利用就能够部署到 函数服务 的运行时中;部署到运行时之后,这个函数就能够被外界拜访了。

上面咱们将重点论述函数框架、函数构建和函数服务这几个局部的架构设计。

函数框架 (Functions framework)

为了升高开发过程中学习函数标准的老本,咱们须要减少一种机制来实现从函数代码到可运行的利用之间的转换。这个机制须要制作一个通用的 main 函数来实现,这个函数用于解决通过 serving url 函数进来的申请。主函数中具体蕴含了很多步骤,其中一个步骤用于关联用户提交的代码,其余的用于做一些一般的工作(如解决上下文、处理事件源、解决异样、解决端口等等)。

在函数构建的过程中,构建器会应用主函数模板渲染用户代码,在此基础上生成利用容器镜像中的 main 函数。咱们间接来看个例子,假如有这样一个函数。

package hello

import (
    "fmt"
    "net/http"
)

func HelloWorld(w http.ResponseWriter, r *http.Request) {fmt.Fprint(w, "Hello, World!\n")
}

经函数框架转换后会生成如下的利用代码:

package main

import (
    "context"
    "errors"
    "fmt"
    "github.com/OpenFunction/functions-framework-go/functionframeworks"
    ofctx "github.com/OpenFunction/functions-framework-go/openfunction-context"
    cloudevents "github.com/cloudevents/sdk-go/v2"
    "log"
    "main.go/userfunction"
    "net/http"
)

func register(fn interface{}) error {ctx := context.Background()
    if fnHTTP, ok := fn.(func(http.ResponseWriter, *http.Request)); ok {if err := functionframeworks.RegisterHTTPFunction(ctx, fnHTTP); err != nil {return fmt.Errorf("Function failed to register: %v\n", err)
        }
    } else if fnCloudEvent, ok := fn.(func(context.Context, cloudevents.Event) error); ok {if err := functionframeworks.RegisterCloudEventFunction(ctx, fnCloudEvent); err != nil {return fmt.Errorf("Function failed to register: %v\n", err)
        }
    } else if fnOpenFunction, ok := fn.(func(*ofctx.OpenFunctionContext, []byte) ofctx.RetValue); ok {if err := functionframeworks.RegisterOpenFunction(ctx, fnOpenFunction); err != nil {return fmt.Errorf("Function failed to register: %v\n", err)
        }
    } else {err := errors.New("unrecognized function")
        return fmt.Errorf("Function failed to register: %v\n", err)
    }
    return nil
}

func main() {if err := register(userfunction.HelloWorld); err != nil {log.Fatalf("Failed to register: %v\n", err)
    }

    if err := functionframeworks.Start(); err != nil {log.Fatalf("Failed to start: %v\n", err)
    }
}

其中高亮的局部就是后面用户本人写的函数。在启动利用之前,先对该函数进行注册,能够注册 HTTP 类的函数,也能够注册 cloudevents 和 OpenFunction 函数。注册实现后,就会调用 functionframeworks.Start 启动利用。

函数构建 (Build)

有了利用之后,咱们还要把利用构建成容器镜像。目前 Kubernetes 曾经废除了 dockershim,不再把 Docker 作为默认的容器运行时,这样就无奈在 Kubernetes 集群中以 Docker in Docker 的形式构建容器镜像。还有没有其余形式来构建镜像?如何治理构建流水线?

Tekton 是一个优良的流水线工具,原来是 Knative 的一个子项目,起初捐给了 CD 基金会 (Continuous Delivery Foundation)。Tekton 的流水线逻辑其实很简略,能够分为三个步骤:获取代码,构建镜像,推送镜像。每一个步骤在 Tekton 中都是一个 Task,所有的 Task 串联成一个流水线。

作容器镜像有多种抉择,比方 Kaniko、Buildah、BuildKit 以及 Cloud Native Buildpacks(CNB)。其中前三者均依赖 Dockerfile 去制作容器镜像,而 Cloud Native Buildpacks(CNB)是云原生畛域最新涌现进去的新技术,它是由 Pivotal 和 Heroku 发动的,不依赖于 Dockerfile,而是能自动检测要 build 的代码,并生成合乎 OCI 规范的容器镜像。这是一个十分惊艳的技术,目前曾经被 Google Cloud、IBM Cloud、Heroku、Pivotal 等公司采纳,比方 Google Cloud 下面的很多镜像都是通过 Cloud Native Buildpacks(CNB)构建进去的。

面对这么多可供选择的镜像构建工具,如何在函数构建的过程中让用户自由选择和切换镜像构建的工具?这就须要用到另外一个我的项目 Shipwright,这是由 Red Hat 和 IBM 开源的我的项目,专门用来在 Kubernetes 集群中构建容器镜像,目前也捐给了 CD 基金会。应用 Shipwright,你就能够在上述四种镜像构建工具之间进行灵便切换,因为它提供了一个对立的 API 接口,将不同的构建办法都封装在这个 API 接口中。

咱们能够通过一个示例来了解 Shipwright 的工作原理。首先须要一个自定义资源 Build 的配置清单:

apiVersion: shipwright.io/v1alpha1
kind: Build
metadata:
  name: buildpack-nodejs-build
spec:
  source:
    url: https://github.com/shipwright-io/sample-nodejs
    contextDir: source-build
  strategy:
    name: buildpacks-v3
    kind: ClusterBuildStrategy
  output:
    image: docker.io/${REGISTRY_ORG}/sample-nodejs:latest
    credentials:
      name: push-secret

这个配置清单分为 3 个局部:

  • source 示意去哪获取源代码;
  • output 示意源代码构建的镜像要推送到哪个镜像仓库;
  • strategy 指定了构建镜像的工具。

其中 strategy 是由自定义资源 ClusterBuildStrategy 来配置的,比方应用 buildpacks 来构建镜像,ClusterBuildStrategy 的内容如下:

这里分为两个步骤,一个是筹备环境,一个是构建并推送镜像。每一步都是 Tekton 的一个 Task,由 Tekton 流水线来治理。

能够看到,Shipwright 的意义在于将镜像构建的能力进行了形象,用户能够应用对立的 API 来构建镜像,通过编写不同的 strategy 就能够切换不同的镜像构建工具。

函数服务 (Serving)

函数服务 (Serving) 指的是如何运行函数 / 利用,以及赋予函数 / 利用基于事件驱动或流量驱动的主动伸缩的能力 (Autoscaling)。CNCF Serverless 白皮书定义了函数服务的四种调用类型:

咱们能够对其进行精简一下,次要分为两种类型:

  • 同步函数:客户端必须发动一个 HTTP 申请,而后必须等到函数执行实现并获取函数运行后果后才返回。
  • 异步函数:发动申请之后间接返回,无需期待函数运行完结,具体的后果通过 Callback 或者 MQ 告诉等事件来告诉调用者,即事件驱动 (Event Driven)。

同步函数和异步函数别离都有不同的运行时来实现:

  • 同步函数方面,Knative Serving 是一个十分优良的同步函数运行时,具备了弱小的主动伸缩能力。除了 Knative Serving 之外,还能够抉择基于 KEDA http-add-on 配合 Kubernetes 原生的 Deployment 来实现同步函数运行时。这种组合办法能够解脱对 Knative Serving 依赖。
  • 异步函数方面,能够联合 KEDA 和 Dapr 来实现。KEDA 能够依据事件源的监控指标来主动伸缩 Deployment 的正本数量;Dapr 提供了函数拜访 MQ 等中间件的能力。

Knative 和 KEDA 在主动伸缩方面的能力不尽相同,上面咱们将开展剖析。

Knative 主动伸缩

Knative Serving 有 3 个次要组件:Autoscaler、Serverless 和 Activator。Autoscaler 会获取工作负载的 Metric(比方并发量),如果当初的并发量是 0,就会将 Deployment 的正本数膨胀为 0。但正本数缩为 0 之后函数就无奈调用了,所以 Knative 在正本数缩为 0 之前会把函数的调用入口指向 Activator

当有新的流量进入时,会先进入 Activator,Activator 接管到流量后会告诉 Autoscaler,而后 Autoscaler 将 Deployment 的正本数扩大到 1,最初 Activator 会将流量转发到理论的 Pod 中,从而实现服务调用。这个过程也叫 冷启动

由此可知,Knative 只能依赖 Restful HTTP 的流量指标进行主动伸缩,但事实场景中还有很多其余指标能够作为主动伸缩的根据,比方 Kafka 生产的音讯积压,如果音讯积压数量过多,就须要更多的副原本解决音讯。要想依据更多类型的指标来主动伸缩,咱们能够通过 KEDA 来实现。

KEDA 主动伸缩

KEDA 须要和 Kubernetes 的 HPA 相互配合来达到更高级的主动伸缩的能力,HPA 只能实现从 1 到 N 之间的主动伸缩,而 KEDA 能够实现从 0 到 1 之间的主动伸缩,将 KEDA 和 HPA 联合就能够实现从 0 到 N 的主动伸缩。

KEDA 能够依据很多类型的指标来进行主动伸缩,这些指标能够分为这么几类:

  • 云服务的根底指标,比方 AWS 和 Azure 的相干指标;
  • Linux 零碎相干指标,比方 CPU、内存;
  • 开源组件特定协定的指标,比方 Kafka、MySQL、Redis、Prometheus。

例如要依据 Kafka 的指标进行主动伸缩,就须要这样一个配置清单:

apiVersion: keda.k8s.io/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-scaledobject
  namespace: default
  labels:
    deploymentName: kafka-consumer-deployment # Required Name of the deployment we want to scale.
spec:
  scaleTargetRef:
    deploymentName: kafka-consumer-deployment # Required Name of the deployment we want to scale.
  pollingInterval: 15
  minReplicaCount: 0
  maxReplicaCount: 10 
  cooldownPeriod: 30
  triggers:
  - type: kafka
    metadata:
      topic: logs
      bootstrapServers: kafka-logs-receiver-kafka-brokers.default.svc.cluster.local
      consumerGroup: log-handler
      lagThreshold: "10"

正本伸缩的范畴在 0~10 之间,每 15 秒查看一次 Metrics,进行一次扩容之后须要期待 30 秒再决定是否进行伸缩。

同时还定义了一个触发器,即 Kafka 服务器的“logs”topic。音讯沉积阈值为 10,即当音讯数量超过 10 时,logs-handler 的实例数量就会减少。如果没有音讯沉积,就会将实例数量减为 0。

这种基于组件特有协定的指标进行主动伸缩的形式比基于 HTTP 的流量指标进行伸缩的形式更加正当,也更加灵便。

尽管 KEDA 不反对基于 HTTP 流量指标进行主动伸缩,但能够借助 KEDA 的 http-add-on 来实现,该插件目前还是 Beta 状态,咱们会继续关注该我的项目,等到它足够成熟之后就能够作为同步函数的运行时来代替 Knative Serving。

Dapr

当初的利用基本上都是分布式的,每个利用的能力都不尽相同,为了将不同利用的通用能力给形象进去,微软开发了一个分布式应用运行时,即 Dapr (Distributed Application Runtime)。Dapr 将利用的通用能力形象成了 组件 ,不同的 组件 负责不同的性能,例如服务之间的调用、状态治理、针对输入输出的资源绑定、可观测性等等。这些分布式组件都应用同一种 API 裸露给各个编程语言进行调用。

函数计算也是分布式应用的一种,会用到各种各样的编程语言,以 Kafka 为例,如果函数想要和 Kafka 通信,Go 语言就得应用 Go SDK,Java 语言得用 Java SDK,等等。你用几种语言去拜访 Kafka,就得写几种不同的实现,十分麻烦。

再假如除了 Kafka 之外还要拜访很多不同的 MQ 组件,那就会更麻烦,用 5 种语言对接 10 个 MQ(Message Queue) 就须要 50 种实现。应用了 Dapr 之后,10 个 MQ 会被形象成一种形式,即 HTTP/GRPC 对接,这样就只需 5 种实现,大大加重了开发分布式应用的工作量。

由此可见,Dapr 非常适合利用于函数计算平台。

新一代开源函数计算平台 OpenFunction

联合下面探讨的所有技术,就诞生了 OpenFunction 这样一个开源我的项目,它的架构如图所示。

次要蕴含 4 个组件:

  • Function : 将函数转换为利用;
  • Build : 通过 Shipwright 抉择不同的镜像构建工具,最终将利用构建为容器镜像;
  • Serving : 通过 Serving CRD 将利用部署到不同的运行时中,能够抉择同步运行时或异步运行时。同步运行时能够通过 Knative Serving 或者 KEDA-HTTP 来反对,异步运行时通过 Dapr+KEDA 来反对。
  • Events : 对于事件驱动型函数来说,须要提供事件治理的能力。因为 Knative 事件治理过于简单,所以咱们研发了一个新型事件治理驱动叫 OpenFunction Events

    OpenFunction Events 借鉴了 Argo Events 的局部设计,并引入了 Dapr。整体架构分为 3 个局部:

    • EventSource : 用于对接多种多样的事件源,通过异步函数来实现,能够依据事件源的指标主动伸缩,使事件的生产更加具备弹性。
    • EventBus : EventBus 利用 Dapr 的能力解耦了 EventBus 与底层具体 Message Broker 的绑定,你能够对接各种各样的 MQ。EventSource 生产事件之后有两种解决形式,一种是间接调用同步函数,而后期待同步函数返回后果;另一种形式是将其写入 EventBus,EventBus 接管到事件后会间接触发一个异步函数。
    • Trigger : Trigger 会通过各种表达式对 EventBus 外面的各种事件进行筛选,筛选实现后会写入 EventBus,触发另外一个异步函数。

对于 OpenFunction 的理论应用案例能够参考这篇文章:以 Serverless 的形式用 OpenFunction 异步函数实现日志告警(点击下方图片跳转浏览)。

OpenFunction Roadmap

OpenFunction 的第一个版本于往年 5 月份公布,从 v0.2.0 开始反对异步函数,v0.3.1 开始新增了 OpenFunction Events,并反对了 Shipwright,v0.4.0 新增了 CLI。

后续咱们还会引入可视化界面,反对更多的 EventSource,反对对边缘负载的解决能力,通过 WebAssembly 作为更加轻量的运行时,联合 Rust 函数来减速冷启动速度。

退出 OpenFunction 社区

期待感兴趣的开发者退出 OpenFunction 社区。能够提出任何你对 OpenFunction 的疑难、设计提案与单干提议。

您也能够退出咱们的微信交换群,加群管理员微信:cloud-native-yang,备注进 OpenFunction 交换群。

您能够在这里找到 OpenFunction 的一些典型应用案例:

  • 以 Serverless 的形式实现 Kubernetes 日志告警
  • OpenFunction Serverless Samples
  • OpenFunction Events Samples
  • OpenFunction 官网

正文完
 0