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

自动扩缩容是 Serverless 的核心特征,更好、更快的冷启动是所有 Serverless 平台的极致追求,本文基于网易杭州研究院云计算团队的探索,针对热门 Serverless 平台 Knative,解析其与自动扩容密切相关的流量实现机制,希望能够帮助从业者更好地理解 Knative autoscale 功能。

引子——从自动扩缩容说起

服务接收到流量请求后,从 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(ksvc)后,Knative 会自动创建 Route(route)、Configuration(cfg)资源,然后 cfg 会创建对应的 Revision(rev)版本。rev 实际上又会创建 Deployment 提供服务,流量最终会根据 route 的配置,导入到相应的 rev 中。

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

冷启动时的流量转发

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

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

复制代码

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: route-f8c50d56-3f47-11e9-9a9a-08002715c9e6
spec:
  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/v1alpha3
kind: VirtualService
metadata:
  name: route-f8c50d56-3f47-11e9-9a9a-08002715c9e6
spec:
 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

我们可以很明显的看出,Knative 就是通过修改 vs 的 destination host 来实现冷启动中的流量保持和转发。

相信目前你在网上能找到资料,也基本上停留在该阶段。不过,由于 Knative 的快速迭代,这里的一些实现细节分析已经过时。

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

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

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

复制代码

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

virtualservice 的变化

笔者的环境可简单的认为是一个标准的 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 -> kingress(ingress.networking.internal.knative.dev) -> vs

在网关这一层涉及到的 CRD 资源就是如上这些。这里 kingress 的意义在于增加一层抽象,如果我们使用的是 Gloo 等其他网关,则会将 kingress 转换成相应的网关资源配置。最新的版本中,负责 kingress 到 Istio vs 的控制器部分代码已经独立出一个项目,可见如今的 Knative 对 Istio 已经不是强依赖。

现在,我们已经了解到 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 域名入手。

revision service 探索

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

复制代码

$ kubectl -n faas get svc
NAME                     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 域名。

根据 Service 的 annotation 我们可以发现,Knative 对 hello-go-fpmln、hello-go-fpmln-m9mmg 、hello-go-fpmln-metrics 这三个 Service 的定位分别为 public Service、private Service 和 metric Service(最新版本已经将 private 和 metrics Service 合并)。

private Service 和 metric Service 其实不难理解。问题的关键就在这里的 public Service,仔细研究 hello-go-fpmln Service,我们可以发现这是一个没有 labelSelector 的 Service,它的 Endpoint 不是 kubernetes 自动创建的,需要额外生成。

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

复制代码

$ kubectl -n faas get ep
NAME                     ENDPOINTS                               AGE
hello-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 的副本数越多这里也会相应的增加。并且由上面的分析可以看到,vs 的 destination 指向的就是 public Service。

输入几次 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>:80

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

复制代码

$ kubectl -n faas get ep
NAME                     ENDPOINTS                         
helloworld-go-mpk25      172.31.16.121: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

原来,现在新版本的冷启动流量转发机制已经不再是通过修改 vs 来改变网关的流量转发配置了,而是直接更新服务的 public Service 后端 Endpoint,从而实现将流量从 Activator 转发到实际的服务 Pod 上。

通过将流量的转发功能内聚到 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 sks
NAME             MODE    SERVICENAME      PRIVATESERVICENAME     READY   REASON
hello-go-fpmln   Proxy   hello-go-fpmln   hello-go-fpmln-m9mmg   True

复制代码

$ kubectl -n faas get sks
NAME             MODE    SERVICENAME      PRIVATESERVICENAME     READY   REASON
hello-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、Service Mesh、Serverless 让 Knative 充满了魅力,但同时也导致了它的复杂性。

网络流量的稳定保障是 Serverless 服务真正生产可用性的关键因素,Knative 也还在高速的更新迭代中,相信 Knative 会在未来对网络方面的性能和稳定性投入更多的优化。

作者简介:

傅轶,网易杭州研究院云计算技术部高级研发工程师,目前负责网易轻舟容器云和微服务平台研发,致力于网易容器技术及其生态体系建设,对 Kubernetes、Serverless 有深入研究,具有丰富的云原生分布式架构设计开发经验与项目实践。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章