什么是serverless

啥?怎么就serverless了?

要说serverless,得先从最近几年的XaaS说起。

AWS,阿里云,Azure,GCP等等,最先提供的,是IaaS,基础设施即服务,卖卖ECS各种云服务器,用户不用自己去买硬件服务器、建设机房各种操心了,点点鼠标就有了。

后来出现了PaaS,Platform即服务。PaaS的典型代表就是k8s,用户不用管理具体的服务器,只要描述自己需要的资源就行,由Platform来做调度。用户关心的只有自己的一亩三分地,不用管操作系统。

IaaS和PaaS其实不是非常好分割,比如也有一些厂家用Docker做出来了虚拟机的体验,这怎么归类呢?但无论如何,用户(即开发者)购买服务的时候,仍然是以CPU、内存、存储来计费的,而到底应该购买什么规格,非常考验用户。

serverless就不一样了。

来想一下这个场景,比如我开发一个天气预报的小app。业务上来说,其实就是手机端的软件调用服务器上的api,那么我需要买个ECS来跑后端服务,需要考虑大概会有多少用户、预计消耗多少资源,应该购买什么规格的主机等等。

但,本质上,我想提供的不就是api吗?为什么不能按api的调用来计费呢?

serverless就是这个思路。

以AWS的Lambda为例。

lambda

按两个维度计费:请求次数、计算时间总长。

Lambda通过函数计算的内存来区分不同质量的服务。

具体来看Lambda一个例子。

如果您向您的函数分配 512MB 的内存,一个月执行其 300 万次,且它每次运行 1 秒,您的费用计算如下:

月度计算费用

月度计算价格为每 GB-s 0.00001667 USD,免费套餐提供 400 000 GB-s。

总计算(秒)= 3M * (1s) = 3 000 000 秒

总计算 (GB-s) = 3 000 000 * 512MB/1024 = 1 500 000 GB-s

总计算 – 免费套餐计算 = 月度计费计算 GB- s

1 500 000 GB-s – 400 000 免费套餐 GB-s = 1 100 000 GB-s

月度计算费用 = 1 100 000 * 0.00001667 USD = 18.34 USD

月度请求费用

月度请求价格为每 100 万个请求 0.20 USD,免费套餐每月提供 100 万个请求。

总请求 – 免费套餐请求 = 月度计费请求

3M 请求 – 1M 免费套餐请求 = 2M 月度计费请求

月度请求费用 = 2M * 0.2 USD/M = 0.40 USD

月度总费用 总费用 = 计算费用 + 请求费用 = 18.34 USD + 0.40 USD = 18.74 USD/月

AWS Lambda 是一项计算服务,可使您无需预配置或管理服务器即可运行代码。AWS Lambda 只在需要时执行您的代码并自动缩放,从每天几个请求到每秒数千个请求。您只需按消耗的计算时间付费 – 代码未运行时不产生费用。借助 AWS Lambda,您几乎可以为任何类型的应用程序或后端服务运行代码,而且无需执行任何管理。AWS Lambda 在可用性高的计算基础设施上运行您的代码,执行计算资源的所有管理工作,其中包括服务器和操作系统维护、容量预置和自动扩展、代码监控和记录。您只需要以 AWS Lambda 支持的一种语言 (目前为 Node.js、Java、C#、Go 和 Python) 提供您的代码。

真正做到了按量计费。

serverless还有一个名字,叫做 FaaS ,即 Function as a Service。

有哪些serverless选手

商业产品有上面的AWS Lambda,开源的有kubelessopenfaasopenwhisk等等,更详细的名单可以看awesome-cloud-nativ

下面以kubeless为例,看看怎么用。

kubeless

kubeless安装

kubeless安装很简单,按官方指导 来就行了。

为了方便使用,还可以再装一个kubeless-ui安装之后,登录到kubeless-ui的界面上,就可以创建第一个 serverless 函数了。

ui

用过Lambda的同学会觉得非常熟悉,操作方式基本是一致的。

这样,开发者只要创建函数就可以了,剩下的事情(编译,runtime,动态横向扩展,等等),kubeless会来搞定。这就是所谓的 FaaS。

Python类型的简单一点,再创建一个golang的函数。

golang

kubeless也提供了CLI:kubeless,功能比web更完善。

kubeless架构

kubeless最大的特点,是所谓的 kubernetes原生:kubeless在k8s上创建了一个CRD,名为function,而kubeless则是一个监听function的controller,这样的架构,对于k8s的开发者来说会比较亲切。

# kubectl get crd functions.kubeless.io -o yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: functions.kubeless.io
spec:
  group: kubeless.io
  names:
    kind: Function
    listKind: FunctionList
    plural: functions
    singular: function
  scope: Namespaced
  version: v1beta1

所以所有的函数,都可以直接在k8s上直接看到。kubeless还提供了命令行,可以看到更多丰富的信息。

~> kubectl get function
NAME      AGE
abc       2d
hello     2d
xxx       2d
~> kubeless function list
NAME    NAMESPACE       HANDLER         RUNTIME         DEPENDENCIES    STATUS   
abc     default         abc.Foo         go1.10                          1/1 READY
hello   default         hello.boy       python2.7                       1/1 READY
xxx     default         xxx.Foo         go1.10                          1/1 READY

来看看function的具体配置。

~> kubectl get function xxx -o yaml
apiVersion: kubeless.io/v1beta1
kind: Function
metadata:
  clusterName: ""
  finalizers:
  - kubeless.io/function
  generation: 1
  name: xxx
  namespace: default
spec:
  deployment:
    metadata:
      creationTimestamp: null
    spec:
      strategy: {}
      template:
        metadata:
          creationTimestamp: null
        spec:
          containers: null
    status: {}
  deps: ""
  function: |2

    package kubeless

    import (
            "github.com/kubeless/kubeless/pkg/functions"
    )

    func Foo(event functions.Event, context functions.Context) (string, error) {
            return "Hello world!\r\n", nil
    }
  function-content-type: ""
  handler: xxx.Foo
  horizontalPodAutoscaler:
    metadata:
      creationTimestamp: null
    spec:
      maxReplicas: 0
      scaleTargetRef:
        kind: ""
        name: ""
    status:
      conditions: null
      currentMetrics: null
      currentReplicas: 0
      desiredReplicas: 0
  runtime: go1.10
  service: {}
  timeout: ""

基本上也就是ui上提供的一些信息。注意这个例子里没有配置HPA,正常如果要使用的话,HPA必不可少。

函数创建之后,发生了什么呢?

kubeless监听到新的function之后,会创建一个deployment,它会完成后续的 编译、执行。一个deployment怎么搞定两件事的呢?kubeless使用init Container来做编译(是不是很聪明),而函数的执行则在普通的容器中来执行;创建HPA后,如果api访问量高,还可以动态的横向扩展。

serverless该有的,它都有了。

贴一下这个deployment,还是很精彩的,虽然很长。

~> kubectl get deployment xxx -o yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    created-by: kubeless
    function: xxx
  name: xxx
  namespace: default
  ownerReferences:
  - apiVersion: kubeless.io/v1beta1
    kind: Function
    name: xxx
    uid: fabbed3a-7b69-11e8-8251-6c0b84ace257
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      created-by: kubeless
      function: xxx
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      annotations:
        prometheus.io/path: /metrics
        prometheus.io/port: "8080"
        prometheus.io/scrape: "true"
      labels:
        created-by: kubeless
        function: xxx
    spec:
      containers:
      - env:
        - name: FUNC_HANDLER
          value: Foo
        - name: MOD_NAME
          value: xxx
        - name: FUNC_TIMEOUT
          value: "180"
        - name: FUNC_RUNTIME
          value: go1.10
        - name: FUNC_MEMORY_LIMIT
          value: "0"
        - name: FUNC_PORT
          value: "8080"
        image: kubeless/go@sha256:e2fd49f09b6ff8c9bac6f1592b3119ea74237c47e2955a003983e08524cb3ae5
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 3
          periodSeconds: 30
          successThreshold: 1
          timeoutSeconds: 1
        name: xxx
        ports:
        - containerPort: 8080
          protocol: TCP
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /kubeless
          name: xxx
      dnsPolicy: ClusterFirst
      initContainers:
      - args:
        - echo 'f727da8be400e7412981aa011900905151c59c59e6ce959f3cec0f5c2bc00b8f  /src/xxx.go'
          > /tmp/func.sha256 && sha256sum -c /tmp/func.sha256 && cp /src/xxx.go /kubeless/xxx.go
          && cp /src/Gopkg.toml /kubeless
        command:
        - sh
        - -c
        image: kubeless/unzip@sha256:f162c062973cca05459834de6ed14c039d45df8cdb76097f50b028a1621b3697
        imagePullPolicy: IfNotPresent
        name: prepare
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /kubeless
          name: xxx
        - mountPath: /src
          name: xxx-deps
      - args:
        - sed 's/<<FUNCTION>>/Foo/g' $GOPATH/src/controller/kubeless.tpl.go > $GOPATH/src/controller/kubeless.go
          && go build -o /kubeless/server $GOPATH/src/controller/kubeless.go > /dev/termination-log
          2>&1
        command:
        - sh
        - -c
        image: kubeless/go-init@sha256:983b3f06452321a2299588966817e724d1a9c24be76cf1b12c14843efcdff502
        imagePullPolicy: IfNotPresent
        name: compile
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /kubeless
          name: xxx
        workingDir: /kubeless
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext:
        fsGroup: 1000
        runAsUser: 1000
      terminationGracePeriodSeconds: 30
      volumes:
      - emptyDir: {}
        name: xxx
      - configMap:
          defaultMode: 420
          name: xxx
        name: xxx-deps

教科书般的传球Deployment。init container将编译出来的bin文件(server)挂到volume xxx,之后再正常的pod中执行server。

再创建一个svc,用来集群内访问。

 ~> kubectl get svc xxx -o yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    created-by: kubeless
    function: xxx
  name: xxx
  namespace: default
  ownerReferences:
  - apiVersion: kubeless.io/v1beta1
    kind: Function
    name: xxx
    uid: fabbed3a-7b69-11e8-8251-6c0b84ace257
spec:
  clusterIP: 10.43.59.200
  ports:
  - name: http-function-port
    port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    created-by: kubeless
    function: xxx
  sessionAffinity: None
  type: ClusterIP
~> curl 10.43.59.200:8080
Hello world!

如果要从集群外面访问,可以创建ingress。(和kubeless不同,openfaas是内置了一个API Gateway,不过我觉得kubeless的做法更kubernetes)。

kubeless还在快速发展中,周五看到一个bug,提了PR之后很快就merge进去了,效率很高。

Ref: