迈向serverless
by 伊布
什么是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一个例子。
如果您向您的函数分配 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,开源的有kubeless、openfaas、openwhisk等等,更详细的名单可以看awesome-cloud-nativ。
下面以kubeless为例,看看怎么用。
kubeless
kubeless安装
kubeless安装很简单,按官方指导 来就行了。
为了方便使用,还可以再装一个kubeless-ui,安装之后,登录到kubeless-ui的界面上,就可以创建第一个 serverless 函数了。
用过Lambda的同学会觉得非常熟悉,操作方式基本是一致的。
这样,开发者只要创建函数就可以了,剩下的事情(编译,runtime,动态横向扩展,等等),kubeless会来搞定。这就是所谓的 FaaS。
Python类型的简单一点,再创建一个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:
Subscribe via RSS