knative在service里面实现了serverless的功能,其中最重要的莫过于按需来启动服务,并基于流量来弹性伸缩。在社区的文档里面找到这样一张架构设计图,先初略理解一下,详细介绍一下其流量转发流程。
该文基于knative v0.6.0版本。
模型抽象
这里说的模型,其实就是knative的service里面关于CRD的定义。其中声明在外的莫属service
、configuration
、revision
以及route
。但是要真正搞明白整个模型的原作原理,理解清楚所有CRD的关联关系是非常有必要的。
上图展示了从service
衍生出来的所有CRD的血缘关系,下面简述其流程和个CRD的作用。
- 当用户创建一个knative的service的时候,其controller会对应创建出
configuration
和route
;这一块较简单,因为service的spec里面其实是包含了对configuration和traffic的定义的。 - 花开两支,
configuration
一方面基于配置创建对应的revision
;另一方面,route
除了创建externalService类型的service
用于将流量指向istio网关外,同时还创建了clusterIngress
(作用很重要)。 clusterIngress
是对各种可用于knative流量入口组件的抽象。对于底层是istio的环境,networkController会将clusterIngress资源转化为istio的virtualService
配置,从而提供将外部流量转发到集群内部的功能。- 对于
revision
来讲,其controller一方面基于资源描述创建出deployment
、imageCache
等资源;另一方面,为了提供serverless功能,controller还对应创建了podAutoScaler
。需要知道的是podAutoScaler
有两种实现,分别对应kpa和hpa。 - autoscaler controller又实现了基于
podAutoScaler
创建对应的sks
(serverlessService)。如果策略是基于kpa的,就需要一套监控流量和并发请求量的机制,于是又创建了private和public的service
专用于访问实例上监控组件sidecar的端口。
代码实现
上面的分析,具体的代码实现流程见下图。
流量转发
当外部流量需要访问内部服务时,其流量的转发流程如何?接下来分小节介绍主要的流量转发逻辑。
流量入口
服务映射
之前在将CRD资源的时候有提到clusterIngress
资源,以及对应的的ExternalService类型的service
,这里是一个简单的实例。
1 | [root@k8s-master knative]# kc describe svc autoscale-go |
该service的作用是,当请求autoscale-go.default.example.com
时(cluster名字换成环境的cluster name),DNS会直接返回external nameistio-ingressgateway.istio-system.svc.cluster.local
作为响应。相当于就将对autoscale-go服务的访问,重定向到了istio的ingressgateway上。而Istio上早已按照clusterIngress
的要求,配置好了流量转发规则。
转发规则
serverlessservice有两种模式,proxy
和service
。其中proxy
会将流量转发到activator上,而service
则会将流量转发到对应的后端实例上真正处理业务。
我们先假设此时后端的deployment并没有启动起来,或者是很久没有请求流量,pod已经被autoscaler出于节约资源消耗的目的干掉了,即serverlessservice处于proxy
模式。此时istio的配置如下。
1 | [root@k8s-master ~]# istioctl pc listener istio-ingressgateway-67cbb7f6c6-bqv2h.istio-system |
查看EDS的配置信息,发现其对应的路由为IP10.244.0.234
的8012端口。
1 | [root@k8s-master ~]# istioctl pc endpoint istio-ingressgateway-67cbb7f6c6-bqv2h.istio-system -o json |
在来看此时该IP是activator对应的pod IP地址。
1 | [root@k8s-master ~]# kc get pod -o wide --all-namespaces | grep 10.244.0.234 |
查看service上面的流量以及对应的转发目标变为activator。
1 | [root@k8s-master knative]# kc get svc |
所以,我们可以判断:当很久没有流量请求的时候,serverlessservice会切换到proxy
模式,其autoscaler逻辑会将pod全干掉;而此时,为了检测到外部的流量请求,Istio将流量转发到activator上面。
弹性伸缩
通过上面的分析,相信大家对serverlessservice的弹性伸缩都有了一个直观的了解。接下来该部分通过两个小节介绍弹性伸缩的工作原理。
弹性伸缩分为hpa
和kpa
:
hpa
:基于一些监控指标,比如CPU、memory等的情况来决定是否需要扩缩容pod的数量,另外hpa不支持将实例数降到0个。kpa
:基于监控到的并发请求数来自动弹性伸缩实例数,当并发搞的时候就扩容,当持续一段时间没有请求的时候,就让pod休假,将流量转给看门人activator来中转。一旦有新请求,立马召回pod,并对外服务。
触发器
前面提到的activator就是触发器,它的作用就是为那些几乎没有流量访问的服务充当看门人。显然这个看门人并不是只为某个VIP客户服务,而是为大家一起服务的。那activator都做了些什么?当流量到了的时候,它又是如何通知正处于放空状态的服务实例的呢?
作用
- 当service的实例数为0的时候,activator代替service接收流量,统计流量和并发数量,并通过websocket上报到autoscaler服务。
- 当service的实例启动起来后,activator通过获取revision和serverlessservice来找到对应的service,并探测该service的服务是否已经可以访问,一旦准备好,就发送流量。
- 通过一系列的handler链来打印日志,记录trace信息,限速,零时存储请求内容,响应probe和healthcheck等。
核心工作流程
- 作为代理看门人,activator收到外部访问service的请求后,第一件事请就是去除报文中的header信息,按照revision name作为key产生一条event。
- 如果从来没有改key的event,就可以判断这是到某一个客户的第一条请求,此时看门人需要立刻通过websocket上报到autoscaler(autoscaler的逻辑会创建service的实例)。
- 当然,activator并非只做这些,它还记录日志,trace信息等,最重要的一点,activator会基于报文header中的信息来知道其所请求的revision以及sks。
- 通过查询sks的privateService来找到该service的healthCheck probe地址,探测其服务是否ready。一旦probe成功就会将缓存的报文发送到目标服务。
代码实现
其代码住逻辑分为三块,也对应三个goroutine,分别是:
- 监控上报
- 监控数据统计
- 报文处理(包含多个处理链)
弹性伸缩逻辑
在触发器的流程中,我们提到当触发器发现某个报文是对某个revision的第一个请求时,会通过websocket上报到autoscaler controller。接下来,我们看看autoscaler的处理流程。
功能
- autoscalr的核心作用就是做弹性伸缩的决策,为了做决策,它需要实现对上报监控指标的采集,这又分为两种形式。
- 通过websocket上报来收集activator的报文请求指标;
- 通过定时pull的形势来从metrics service获取queue中统计的指标;
- 基于metric指标来做弹性伸缩决策并下发。
代码实现
由于这里重点讲流量,该代码基本是按照功能来实现,这里就不再做讲解。
这里重点讲一下autoscaler将service的pod拉起来了之后的动作,通过代码里面可以看到,ks.applyScale
最后会走到c.reconcileSKS
。这里其实是更改serverlessservice的模式,从proxy
切换到service
,而接下来的又做了什么?请参考service controller的逻辑。
当pod启动起来之后,对应的private Service会基于selector而选中新起来的pod,从而监控流量将从pod的8012端口采集。
流量监控
上面提到,当pod启动之后,autoscaler会从pod的8012端口采集监控指标。这里的监控指标是如何产生的?应用需要关注吗?这些问题在这一节来解答。
作用
首先,knative中的revision在创建deploy的时候,会为其自动加入一个sidecar,这个container就是queue-proxy;在整个平台中,queue-proxy的作用非常重要。
- 基于一层反向代理,收集访问业务container的流量情况,对外暴露9090(即:metrics的访问端口)
- 在反向代理上,基于配置的并发数来限制外部访问的请求速率;
- 对外提供对主容器进行healthcheck以及drain的接口(admin的端口:8022),(业务代理端口: http1:8012,http2:8013);
代码实现
实验
autoscaler通过周期性的抓取每一个业务容器对应queue-proxy的metrics来感知实时流量情况的。当外部往service发包的时候,在pod的queue container网卡上抓包可以发现有很多的prometheus metrics的报文,collector拉取指标的频率为每秒4次,具体指标见下图的抓包细节:
在报文里面发现来收集metrics的IP地址为10.244.0.235
, 通过在k8s里面查询,发现该IP为autoscaler的地址。
1 | [root@k8s-master ~]# kc get pod -o wide --all-namespaces | grep 10.244.0.235 |
当服务启动后,各服务的具体内容如下: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[root@k8s-master ~]# kc get serverlessservice
NAME SERVICENAME PRIVATESERVICENAME READY REASON
autoscale-go-52f52 autoscale-go-52f52 autoscale-go-52f52-priv True
[root@k8s-master ~]# kc get revisions
NAME SERVICE NAME GENERATION READY REASON
autoscale-go-52f52 autoscale-go-52f52 1 True
[root@k8s-master ~]# kc get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
autoscale-go-52f52-deployment 1/1 1 1 141m
[root@k8s-master ~]# kc get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
autoscale-go ExternalName <none> istio-ingressgateway.istio-system.svc.cluster.local <none> 142m
autoscale-go-52f52 ClusterIP 10.108.134.58 <none> 80/TCP 142m
autoscale-go-52f52-metrics ClusterIP 10.109.214.47 <none> 9090/TCP 142m
autoscale-go-52f52-priv ClusterIP 10.106.251.157 <none> 80/TCP 142m
[root@k8s-master ~]# kc get ep
NAME ENDPOINTS AGE
alertmanager-operated <none> 7d2h
autoscale-go-52f52 10.244.0.18:8012 142m
autoscale-go-52f52-metrics 10.244.0.18:9090 142m
autoscale-go-52f52-priv 10.244.0.18:8012 142m
kubernetes 10.200.204.76:6443 66d
prometheus-operated <none> 7d2h
[root@k8s-master ~]# istioctl ps
NAME CDS LDS EDS RDS PILOT VERSION
istio-ingressgateway-67cbb7f6c6-bqv2h.istio-system SYNCED SYNCED SYNCED (100%) SYNCED istio-pilot-75984f55cc-5brpc 1.1.3
[root@k8s-master ~]# istioctl pc endpoint istio-ingressgateway-67cbb7f6c6-bqv2h.istio-system -o json
{
"name": "outbound|80||autoscale-go-52f52.default.svc.cluster.local",
"addedViaApi": true,
"hostStatuses": [
{
"address": {
"socketAddress": {
"address": "10.244.0.18",
"portValue": 8012
}
},
...
}
]
}