原文链接:https://ieevee.com/tech/2018/06/28/serverless.html

什么是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为例。

IMG\_256

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

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 函数了。

IMG\_257

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

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

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

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

kubeless架构

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
\# kubectl get crd functions.kubeless.io -o yamlapiVersion:
apiextensions.k8s.io/v1beta1kind: CustomResourceDefinitionmetadata:

name: functions.kubeless.iospec:

group: kubeless.io

names:

kind: Function

listKind: FunctionList

plural: functions

singular: function

scope: Namespaced

version: v1beta1

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

\~\> kubectl get functionNAME 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的具体配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
\~\> kubectl get function xxx -o yamlapiVersion:
kubeless.io/v1beta1kind: Functionmetadata:

clusterName: \"\"

finalizers:

\- kubeless.io/function

generation: 1

name: xxx

namespace: defaultspec:

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,还是很精彩的,虽然很长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
\~\> kubectl get deployment xxx -o yamlapiVersion:
extensions/v1beta1kind: Deploymentmetadata:

labels:

created-by: kubeless

function: xxx

name: xxx

namespace: default

ownerReferences:

\- apiVersion: kubeless.io/v1beta1

kind: Function

name: xxx

uid: fabbed3a-7b69-11e8-8251-6c0b84ace257spec:

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,用来集群内访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
\~\> kubectl get svc xxx -o yamlapiVersion: v1kind: Servicemetadata:

labels:

created-by: kubeless

function: xxx

name: xxx

namespace: default

ownerReferences:

\- apiVersion: kubeless.io/v1beta1

kind: Function

name: xxx

uid: fabbed3a-7b69-11e8-8251-6c0b84ace257spec:

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:8080Hello world!

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