Knative 全链路流量机制探索与揭秘

架构之美

共 9516字,需浏览 20分钟

 ·

2021-04-22 08:11



-     前言    -


从自动扩缩容说起

服务接收到流量请求后,从0自动扩容为N,以及没有流量时自动缩容为0,是一个Serverless平台最本质的特征。


可以说,自动扩缩容机制是那颗皇冠,戴上之后你才能被称之为Serverless。


当然了解Kubernetes的人会有疑问,HPA不就是用来干自动扩缩容的事儿的吗?难道我用了HPA就可以摇身一变成为Serverless了。


这里最关键的区别在于,Serverless语义下的自动扩缩容是可以让服务从0到N的,但是HPA不能。HPA的机制是检测服务Pod的metrics数据(例如CPU等)然后把Deployment扩容,但当你把Deployment副本数置为0时,流量进不来,metrics数据永远为0,此时HPA也无能为力。


所以,HPA只能让服务从1到N,而从0到1的这个过程,需要额外的机制帮助hold住请求流量,扩容服务,再转发流量到服务,这就是我们常说的冷启动。


可以说,冷启动是Serverless皇冠中的那颗明珠,如何实现更好、更快的冷启动,是所有Serverless平台极致追求的目标。


Knative作为目前被社区和各大厂商如此重视和受关注的Serverless平台,当然也在不遗余力的优化自动扩缩容和冷启动功能。


不过,本文并不打算直接介绍Knative自动扩缩容机制,而是先探究一下Knative中的流量实现机制,流量机制和自动扩容密切相关,只有了解其中的奥秘,才能更好的理解Knative autoscale功能。


由于Knative其实包括Building(Tekton)、Serving和Eventing,这里只专注于Serving部分。另外需要提前说明的是,Knative并不强依赖Istio,Serverless网关的实际选择除了集成Istio,还支持Gloo、Ambassador。同时,即使使用了Istio,也可以选择是否使用envoy sidecar注入。本文介绍的时候,我们默认使用的是Istio和注入sidecar的部署方式。



-     简单但是有点过时的老版流量机制    -


先回顾一下Knative官方的一个简单的原理示意图如下所示。用户创建一个Knative Service(ksv c)后,Knative会自动创建Route(route),Configuration(cfg)资源,然后cfg会创建对应的Revision(rev)版本。rev实际上又会创建Deployment提供服务,流量最终会根据route的配置,导入到相应的rev中。



这是简单的CRD视角,实际上Knative的内部CRD会多一些层次结构,相对更复杂一点。下文会详细描述。


从冷启动和自动扩缩容的实现角度,可以参考一下下图 。从图中可以大概看到,有一个Istio Route充当网关的角色,当服务副本数为0时,自动将流量转发到Activator组件,Activator会hold住流量,同时Autoscaler组件会负责将副本数扩容,之后Activator再将流量导入到实际的Pod,并且在副本数不为0时,Istio Route会直接将流量负载均衡到Pod,不再走Activator组件。这也是Knative实现冷启动的一个基本思路。



在集成使用Istio部署时,Knative Route默认采用的是Istio Ingress Gateway实现,大概在Knative 0.6版本之前,我们可以发现,Route的流量转发本质上是由Istio virtualservice(vs)控制。副本数为0时,vs如下所示,其中destination指向的是Activator组件。此时Activator会帮助转发冷启动时的请求。


apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata:  name: route-f8c50d56-3f47-11e9-9a9a-08002715c9e6spec:  gateways:  - knative-ingress-gateway  - mesh  hosts:  - helloworld-go.default.example.com  - helloworld-go.default.svc.cluster.local  http:  - appendHeaders:    route:    - destination:        host: Activator-Service.knative-serving.svc.cluster.local        port:          number: 80      weight: 100


当服务副本数不为0之后,vs会变为如下所示,将Ingress Gateway的流量直接打到服务Pod上。


apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata:  name: route-f8c50d56-3f47-11e9-9a9a-08002715c9e6spec: hosts:  - helloworld-go.default.example.com  - helloworld-go.default.svc.cluster.local  http:  - match:    route:    - destination:        host: helloworld-go-2xxcn-Service.default.svc.cluster.local        port:          number: 80      weight: 100


无非是通过修改vs的destination来实现冷启动中的流量保持和转发。


相信目前你在网上能找到资料,也基本上停留在该阶段。不过,knative的发展是如此的迅速,以至于,上面分析的细节已经过时。


下面以0.9版本为例,我们仔细探究一下现有的实现方式,和关于Knative流量的真正秘密。



-     复杂但是更优异的新版流量机制    -


鉴于官方文档并没有最新的具体实现机制介绍,我们创建一个简单的hello-go ksvc,并以此进行分析。ksvc如下所示:


apiVersion: serving.knative.dev/v1alpha1kind: Servicemetadata:  name: hello-go  namespace: faasspec:  template:    spec:      containers:      - image: harbor-yx-jd-dev.yx.netease.com/library/helloworld-go:v0.1        env:        - name: TARGET          value: "Go Sample v1"

笔者的环境可简单的认为是一个标准的Istio部署,Serverless网关为Istio Ingress Gateway,所以创建完ksvc后,为了验证服务是否可以正常运行,需要发送http请求至网关。由于Gateway资源已经在部署Knative的时候创建了,所以我们只需要关心一下vs。在服务副本数为0的时候,Knative控制器创建的vs关键配置如下:


spec:  gateways:  - knative-serving/cluster-local-gateway  - knative-serving/knative-ingress-gateway  hosts:  - hello-go.faas  - hello-go.faas.example.com  - hello-go.faas.svc  - hello-go.faas.svc.cluster.local  - f81497077928a654cf9422088e7522d5.probe.invalid  http:  - match:    - authority:        regex: ^hello-go\.faas\.example\.com(?::\d{1,5})?$      gateways:      - knative-serving/knative-ingress-gateway    - authority:        regex: ^hello-go\.faas(\.svc(\.cluster\.local)?)?(?::\d{1,5})?$      gateways:      - knative-serving/cluster-local-gateway    retries:      attempts: 3      perTryTimeout: 10m0s    route:    - destination:        host: hello-go-fpmln.faas.svc.cluster.local        port:          number: 80


vs指定了已经创建好的gw,同时destination指向的是一个Service域名。这个Service就是Knative默认自动创建的hello-go服务的Service。

不过细心的我们又发现vs的ownerReferences指向了一个Knative的CRD:


ingress.networking.internal.knative.dev:  ownerReferences:  - apiVersion: networking.internal.knative.dev/v1alpha1    blockOwnerDeletion: true    controller: true    kind: Ingress    name: hello-go    uid: 4a27a69e-5b9c-11ea-ae53-fa163ec7c05f


根据名字可以看到这是一个Knative内部使用的CRD,该CRD的内容其实和vs比较类似,同时ingress.networking.internal.knative.dev的ownerReferences指向了我们熟悉的route,总结下来就是:


route -ingress.networking.internal.knative.dev -vs

在网关这一层体现到的CRD资源就是如上这些。这里ingress.networking.internal.knative.dev的意义在于增加一层抽象,如果我们使用的是Gloo等其他网关,则会将ingress.networking.internal.knative.dev转换成相应的网关资源配置。可见Knative正在计划下一盘大棋。


现在,我们已经了解到Serverless网关是由Knative控制器最终生成的vs生效到Istio Ingress Gateway上,为了验证我们刚才部署的服务是否可以正常的运行,简单的用curl命令试验一下。


和所有的网关或者负载均衡器一样,对于7层http访问,我们需要在Header里加域名Host,用于流量转发到具体的服务。在上面的vs中已经可以看到对外域名和内部Service域名均已经配置。所以,只需要:


curl -v -H'Host:hello-go.faas.example.com'  <IngressIP>:<Port> 

其中,IngressIP即网关实例对外暴露的IP。


对于冷启动来说,目前的Knative需要等十几秒,即会收到请求。根据之前老版本的经验,这个时候vs会被更新,destination指向hello-go的Service。


不过,现在我们实际发现,vs没有任何变化,仍然指向了服务的Service。这时候,我们才想起来,老版本中服务副本数为0时,其实vs的destination指向的是Activator组件的。但现在,不管服务副本数如何变化,vs一直不变。


蹊跷只能从destination的Service域名入手。


创建ksvc后,Knative会帮我们自动创建Service如下所示:


$ kubectl -n faas get svcNAME                     TYPE           CLUSTER-IP     EXTERNAL-IP                                            PORT(S)      hello-go                 ExternalName   <none>         cluster-local-gateway.istio-system.svc.cluster.local   <none>           hello-go-fpmln           ClusterIP      10.178.4.126   <none>                                                 80/TCP             hello-go-fpmln-m9mmg     ClusterIP      10.178.5.65    <none>                                                 80/TCP,8022/TCP  hello-go-fpmln-metrics   ClusterIP      10.178.4.237   <none>                                                 9090/TCP,9091/TCP


hello-go Service是一个ExternalName Service,作用是将hello-go的Service域名增加一个dns CNAME别名记录,指向网关的Service域名。


仔细研究hello-go-fpmln Service,我们可以发现这是一个没有label selector的Headless Service,它的Endpoint不是kubernetes自动创建的,需要额外去创建。


hello-go-fpmln-m9mmg Service和hello-go-fpmln-metrics则根据revision label selector到对应的服务Pod。


根据这些Service的一些annotation可以看到,Knative对hello-go-fpmln、hello-go-fpmln-m9mmg 、hello-go-fpmln-metrics这三个Service的定位分别为public Service、private Service和metric Service。


private Service和metric Service其实不难理解。问题的关键就在这里的public Service,并且由上面的分析可以看到,vs的destination指向的就是这里的public Service。


在服务副本数为0时,查看一下Service对应的Endpoint,如下所示:


$ kubectl -n faas get epNAME                     ENDPOINTS                               AGEhello-go-fpmln           172.31.16.81:8012                       hello-go-fpmln-m9mmg     172.31.16.121:8012,172.31.16.121:8022   hello-go-fpmln-metrics   172.31.16.121:9090,172.31.16.121:9091   


其中,public Service的Endpoint IP是Knative Activator的Pod IP,实际发现Activator的副本数越多这里也会相应的增加。


输入几次curl命令模拟一下http请求,虽然副本数从0开始增加到1了,但是这里的Endpoint却没有变化,仍然为Activator Pod IP。


接着使用hey来压测一下:


./hey_linux_amd64 -n 1000000 -c 300  -m GET -host helloworld-go.faas.example.com http://<IngressIP>:8

果然,发现Endpoint变化了,通过对比服务的Pod IP,已经变成了新启动的服务Pod IP,不再是Activator Pod的IP。


$ kubectl -n faas get epNAME                     ENDPOINTS                         helloworld-go-mpk25      172.31.16.121:8012hello-go-fpmln-m9mmg     172.31.16.121:8012,172.31.16.121:8022   hello-go-fpmln-metrics   172.31.16.121:9090,172.31.16.121:9091


原来,现在新版本的冷启动流量转发机制已经不再是通过修改vs来改变网关的流量转发配置了,而是直接更新服务的public Service后端Endpoint,从而实现将流量从Activator负载均衡到真正的服务负载Pod上。
这样将流量的转发功能内聚到Kubernetes本身Service/Endpoint层,一方面减小了网关的配置更新压力,一方面Knative可以在对接各种不同的网关时的实现时更加解耦,网关层不再需要关心冷启动时的流量转发机制。
再深入从上述的三个Service入手研究,它们的ownerReference是serverlessservice.networking.internal.knative.dev(sks),而sks的ownerReference是podautoscaler.autoscaling.internal.knative.dev(kpa)。
在压测过程中同样发现,sks会在冷启动过后,会从Proxy模式变为Serve模式:


$ kubectl -n faas get sksNAME             MODE    SERVICENAME      PRIVATESERVICENAME     READY   REASONhello-go-fpmln   Proxy   hello-go-fpmln   hello-go-fpmln-m9mmg   True
$ kubectl -n faas get sksNAME             MODE    SERVICENAME      PRIVATESERVICENAME     READY   REASONhello-go-fpmln   Serve   hello-go-fpmln   hello-go-fpmln-m9mmg   True


这也意味着,当流量从Activator导入的时候,sks为Proxy模式。

服务真正启动起来后会变成Serve模式,网关流量直接流向服务Pod。


从名称上也可以看到,sks和kpa均为Knative内部CRD,实际上也是由于Knative设计上可以支持自定义的扩缩容方式和支持Kubernetes HPA有关,实现更高一层的抽象。


现在为止,我们可以梳理Knative的绝大部分CRD的关系如下图所示:




一个更复杂的实际实现架构图如下所示。



简单来说,服务副本数为0时,流量路径为:网关-> public Service -> Activator经过冷启动后,副本数为N时,流量路径为:网关->public Service -> Pod当然流量到Pod后,实际内部还有Envoy sidecar流量拦截,Queue-Proxy sidecar反向代理,才再到用户的User Container。这里的机制背后实现我们会有另外一篇文章再单独细聊。



-     总结    -


Knative本身的实现可谓是云原生领域里的一个集大成者,融合Kubernetes、ServiceMesh、Serverless让Knative充满了魅力,但同时也导致了Knative的复杂性。网络流量的稳定保障是Serverless服务真正生产可用性的关键因素,Knative也还在高速的更新迭代中,相信Knative会在未来对网络方面的性能和稳定性投入更多的优化。


作者:ethfoo

来源:https://juejin.cn/post/6844904084005191693

浏览 36
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报