Knative流量的秘密

knative在service里面实现了serverless的功能,其中最重要的莫过于按需来启动服务,并基于流量来弹性伸缩。在社区的文档里面找到这样一张架构设计图,先初略理解一下,详细介绍一下其流量转发流程。

该文基于knative v0.6.0版本。

模型抽象

这里说的模型,其实就是knative的service里面关于CRD的定义。其中声明在外的莫属serviceconfigurationrevision以及route。但是要真正搞明白整个模型的原作原理,理解清楚所有CRD的关联关系是非常有必要的。

上图展示了从service衍生出来的所有CRD的血缘关系,下面简述其流程和个CRD的作用。

  1. 当用户创建一个knative的service的时候,其controller会对应创建出configurationroute;这一块较简单,因为service的spec里面其实是包含了对configuration和traffic的定义的。
  2. 花开两支,configuration一方面基于配置创建对应的revision;另一方面,route除了创建externalService类型的service用于将流量指向istio网关外,同时还创建了clusterIngress(作用很重要)。
  3. clusterIngress是对各种可用于knative流量入口组件的抽象。对于底层是istio的环境,networkController会将clusterIngress资源转化为istio的virtualService配置,从而提供将外部流量转发到集群内部的功能。
  4. 对于revision来讲,其controller一方面基于资源描述创建出deploymentimageCache等资源;另一方面,为了提供serverless功能,controller还对应创建了podAutoScaler。需要知道的是podAutoScaler有两种实现,分别对应kpa和hpa。
  5. autoscaler controller又实现了基于podAutoScaler创建对应的sks(serverlessService)。如果策略是基于kpa的,就需要一套监控流量和并发请求量的机制,于是又创建了private和public的service专用于访问实例上监控组件sidecar的端口。

代码实现

上面的分析,具体的代码实现流程见下图。

流量转发

当外部流量需要访问内部服务时,其流量的转发流程如何?接下来分小节介绍主要的流量转发逻辑。

流量入口

服务映射

之前在将CRD资源的时候有提到clusterIngress资源,以及对应的的ExternalService类型的service,这里是一个简单的实例。

1
2
3
4
5
6
7
8
9
10
11
[root@k8s-master knative]# kc describe svc autoscale-go
Name: autoscale-go
Namespace: default
Labels: serving.knative.dev/route=autoscale-go
Annotations: <none>
Selector: <none>
Type: ExternalName
IP:
External Name: istio-ingressgateway.istio-system.svc.cluster.local
Session Affinity: None
Events: <none>

该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有两种模式,proxyservice。其中proxy会将流量转发到activator上,而service则会将流量转发到对应的后端实例上真正处理业务。

我们先假设此时后端的deployment并没有启动起来,或者是很久没有请求流量,pod已经被autoscaler出于节约资源消耗的目的干掉了,即serverlessservice处于proxy模式。此时istio的配置如下。

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 ~]# istioctl pc listener istio-ingressgateway-67cbb7f6c6-bqv2h.istio-system
ADDRESS PORT TYPE
0.0.0.0 80 HTTP
0.0.0.0 15090 HTTP

[root@k8s-master ~]# istioctl pc route istio-ingressgateway-67cbb7f6c6-bqv2h.istio-system -o json
[
{
"name": "http.80",
"virtualHosts": [
{
"name": "autoscale-go.default.example.com:80",
"domains": [
"autoscale-go.default.example.com",
"autoscale-go.default.example.com:80"
],
"routes": [
{
"match": {
"prefix": "/",
"headers": [
{
"name": ":authority",
"regexMatch": "^autoscale-go\\.default(?::\\d{1,5})?$"
}
]
},
"route": {
"cluster": "outbound|80||autoscale-go-52f52.default.svc.cluster.local",
"timeout": "600s",
"retryPolicy": {
"retryOn": "connect-failure,refused-stream,unavailable,cancelled,resource-exhausted,retriable-status-codes",
"numRetries": 3,
"perTryTimeout": "600s",
"retryHostPredicate": [
{
"name": "envoy.retry_host_predicates.previous_hosts"
}
],
"hostSelectionRetryMaxAttempts": "3",
"retriableStatusCodes": [
503
]
},
"maxGrpcTimeout": "600s"
},
...
}
]
}
]
}
]

查看EDS的配置信息,发现其对应的路由为IP10.244.0.234的8012端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[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.234",
"portValue": 8012
}
},
...
}
]
}

在来看此时该IP是activator对应的pod IP地址。

1
2
[root@k8s-master ~]# kc get pod -o wide --all-namespaces | grep 10.244.0.234
knative-serving activator-5b7d897458-xv4tp 1/1 Running 0 132m 10.244.0.234 k8s-master <none> <none>

查看service上面的流量以及对应的转发目标变为activator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@k8s-master knative]# kc get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
alertmanager-operated ClusterIP None <none> 9093/TCP,6783/TCP 7d2h
autoscale-go ExternalName <none> istio-ingressgateway.istio-system.svc.cluster.local <none> 148m
autoscale-go-52f52 ClusterIP 10.108.134.58 <none> 80/TCP 148m
autoscale-go-52f52-metrics ClusterIP 10.109.214.47 <none> 9090/TCP 148m
autoscale-go-52f52-priv ClusterIP 10.106.251.157 <none> 80/TCP 148m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 66d
prometheus-operated ClusterIP None <none> 9090/TCP 7d2h


[root@k8s-master knative]# kc get ep
NAME ENDPOINTS AGE
autoscale-go-52f52 10.244.0.234:9090,10.244.0.234:8012,10.244.0.234:8013 148m
autoscale-go-52f52-metrics <none> 148m
autoscale-go-52f52-priv <none> 148m

所以,我们可以判断:当很久没有流量请求的时候,serverlessservice会切换到proxy模式,其autoscaler逻辑会将pod全干掉;而此时,为了检测到外部的流量请求,Istio将流量转发到activator上面。

弹性伸缩

通过上面的分析,相信大家对serverlessservice的弹性伸缩都有了一个直观的了解。接下来该部分通过两个小节介绍弹性伸缩的工作原理。

弹性伸缩分为hpakpa:

  • hpa:基于一些监控指标,比如CPU、memory等的情况来决定是否需要扩缩容pod的数量,另外hpa不支持将实例数降到0个。
  • kpa:基于监控到的并发请求数来自动弹性伸缩实例数,当并发搞的时候就扩容,当持续一段时间没有请求的时候,就让pod休假,将流量转给看门人activator来中转。一旦有新请求,立马召回pod,并对外服务。

触发器

前面提到的activator就是触发器,它的作用就是为那些几乎没有流量访问的服务充当看门人。显然这个看门人并不是只为某个VIP客户服务,而是为大家一起服务的。那activator都做了些什么?当流量到了的时候,它又是如何通知正处于放空状态的服务实例的呢?

作用

  1. 当service的实例数为0的时候,activator代替service接收流量,统计流量和并发数量,并通过websocket上报到autoscaler服务。
  2. 当service的实例启动起来后,activator通过获取revision和serverlessservice来找到对应的service,并探测该service的服务是否已经可以访问,一旦准备好,就发送流量。
  3. 通过一系列的handler链来打印日志,记录trace信息,限速,零时存储请求内容,响应probe和healthcheck等。

核心工作流程

  1. 作为代理看门人,activator收到外部访问service的请求后,第一件事请就是去除报文中的header信息,按照revision name作为key产生一条event。
  2. 如果从来没有改key的event,就可以判断这是到某一个客户的第一条请求,此时看门人需要立刻通过websocket上报到autoscaler(autoscaler的逻辑会创建service的实例)。
  3. 当然,activator并非只做这些,它还记录日志,trace信息等,最重要的一点,activator会基于报文header中的信息来知道其所请求的revision以及sks。
  4. 通过查询sks的privateService来找到该service的healthCheck probe地址,探测其服务是否ready。一旦probe成功就会将缓存的报文发送到目标服务。

代码实现

其代码住逻辑分为三块,也对应三个goroutine,分别是:

  1. 监控上报
  2. 监控数据统计
  3. 报文处理(包含多个处理链)

弹性伸缩逻辑

在触发器的流程中,我们提到当触发器发现某个报文是对某个revision的第一个请求时,会通过websocket上报到autoscaler controller。接下来,我们看看autoscaler的处理流程。

功能

  1. autoscalr的核心作用就是做弹性伸缩的决策,为了做决策,它需要实现对上报监控指标的采集,这又分为两种形式。
    • 通过websocket上报来收集activator的报文请求指标;
    • 通过定时pull的形势来从metrics service获取queue中统计的指标;
  2. 基于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的作用非常重要。

  1. 基于一层反向代理,收集访问业务container的流量情况,对外暴露9090(即:metrics的访问端口)
  2. 在反向代理上,基于配置的并发数来限制外部访问的请求速率;
  3. 对外提供对主容器进行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
2
[root@k8s-master ~]# kc get pod -o wide --all-namespaces | grep 10.244.0.235
knative-serving autoscaler-74f47bfff8-7znzv 1/1 Running 0 15d 10.244.0.235 k8s-master <none> <none>

当服务启动后,各服务的具体内容如下:

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
}
},
...
}
]
}

0%