概述
- contour 是一个开源的 k8s ingress controller
- 使用 envoy 提供反向代理
- 提供动态配置更新
- 支持多集群网络代理
特性
- 基于高性能的L7代理和负载均衡Envoy作为数据面
- 灵活的架构
- TLS安全代理
为什么选择Contour
- 动态配置更新,最大限度减少连接中断
- 安全的支持多集群,多配置
- 使用 HTTPProxy CRD 增强 ingress 核心配置
安装
kubectl
1 | bash复制代码# yaml 安装 |
helm
1 | bash复制代码# 添加仓库 |
资源
部署完成后查看安装的资源信息,包括:
- contour deployment:counter controller,作为控制面
- envoy daemonset:envoy,作为数据面
1 | bash复制代码$ kubectl get deploy,svc,ds,job -n projectcontour |
CRD
contour 安装完成后,查看 crd 有哪些:
1 | bash复制代码$ kubectl get crd|grep contour |
其中最核心的 CRD 是 HTTPProxy,等效于 k8s 提供的原生 Ingress,提供路由配置功能。
看一个简单的 httpproxy 配置示例,当访问 foo1.bar.com 时,将代理到后端 s1 服务的 80 端口
将 foo1.bar.com 域名和任意一个Node节点(daemonset部署)的ip做域名绑定后,即可通过域名访问服务
1 | yaml复制代码apiVersion: projectcontour.io/v1 |
架构
概述
contour 主要包含两个组件:
- Envoy:提供高性能的反向代理
- Contour:充当 Envoy 的后台管理服务,提供相关配置
两个组件是分开部署的,Contour 以 Deployment 的方式部署,Envoy 以 DaemonSet 的方式部署
原理
数据面:envoy侧
envoy pod 中包含两个 container
- initContainer container:执行
contour bootstrap
命令,生成一个配置文件到临时目录,envoy container共享这个目录,并作为启动的配置文件,里面配置了 contour server 地址,作为 envoy 的管理服务 - envoy container:根据配置文件启动envoy,建立 GRPC连接,并从服务端接收配置请求
控制面:contour 侧
- contour 是 k8s api 的一个客户端,通过 informer 机制监听 ingress,service,endpoint,secret等k8s原生资源,和httpproxy这个CRD
- contour 缓存k8s的资源信息,并最终转换为 Envoy 需要的 XDS 信息。
其他
从图中我们还看到其他的一些信息
- contour job:自动生成证书,使得grpc上的xds安全传输
- contour 内部还提供了选主模式
HTTPProxy配置
概述
Ingress 资源在 k8s 1.1 版本引入,从那以后,Ingress API 保持相对稳定,需要使用特殊的能力需要借助于 Annotation 实现。
HttpProxy CRD 的目标是扩展 Ingress API 的能力,为用户提供更丰富的功能体验,以及解决 Ingress 在多租户环境下的限制。
对比 Ingress 的优势
- 安全地支持多 Kubernetes 集群,能够限制哪些命名空间可以配置虚拟主机和 TLS 凭据。
- 能够包括来自另一个 HTTPProxy 的路径或域的路由配置(这个HTTPProxy可能在另一个命名空间中)
- 允许单个路由中接受多个服务并负载均衡它们之间的流量
- 天生支持定义服务权重和负载策略,不需要使用注解
- 支持创建时校验配置的合法性
配置文件对比
Ingress
1 | yaml复制代码# ingress.yaml |
HTTPProxy
1 | yaml复制代码# httpproxy.yaml |
源码分析
原理回顾
通过前面 contour 的基本原理和架构的介绍,总结如下:
数据流转原理
- contour 通过 informer 机制监听 k8s 资源
- 将k8s资源转换为 envoy 需要的 xds 资源
- 通过grpc将xds配置下发到envoy节点
流量流转原理
- 带有域名的请求经过任意一个Node节点,进入Envoy(以 hostNetWork=true 部署方式为例)
- envoy将请求路由到 HTTPProxy 中配置的后端 service
数据流转实现
概述
实际上在将 k8s 资源转换为最终 xds 资源的过程中,还经过了其他的数据转换。
- k8s资源变化,触发 informer 通知机制,注册的回调函数将事件放入 channel
- EventHandler协程消费 channel,将channel中的每种资源以一个map形式保存在KubernetesCache中,map的key是该资源的namespace + name唯一确定的
- KubenetesCache中的资源,经过一序列 processor 的处理之后,生成 DAG
- DAG变更,会通知一序列 Observer,依次调用 OnChange方法,传入DAG,每个 Observer对应一个 XDS协议,将DAG中需要的资源转换为XDS配置,保存在Cache中
- GRPC读取Cache中的数据,通过Stream将XDS下发到Envoy。下发这一步使用了
go-control-plane
这个开源框架
源码调用图
ResourceEventHandler
k8s 提供了 informer 机制,可以 watch kubernetes资源的变更。contour中 watch 的资源包括两类:
k8s原生资源:
- ingress
- service
- endpoint
- namespace
crd资源:
- httpproxy
1 | go复制代码// contour/cmd/contour/serve.go |
不管是 原生资源,还是 crd 资源,Informer 监听的原理都是一样的,用户只需要编写事件处理函数,对应的接口是 ResourceEventHandler,该接口提供了增加、删除、更新回调函数。
1 | go复制代码// k8s.io/client-go/tools/cache/controller.go |
Contour 中所有的资源复用了两个 ResourceEventHandler 的实现类:
- EndpointsTranslator:处理 endpoint 资源
- EventHandler:处理除了 endpoint 外的其他资源。
EventHandler 将得到的资源信息,放入channel中,由单独的协程去消费以解耦
1 | go复制代码func (e *EventHandler) OnAdd(obj interface{}) { |
放入channel的信息,由谁去消费呢?
监听 channel 的协程,拿到事件后,取出资源信息,保存到 KubernetesCache 中。
1 | go复制代码// contour/cmd/contour/server.go |
EndpointsTranslator 监听的 endpoint 资源,处理比较麻烦一些,以 add 为例分析
1 | go复制代码// contour/internal/xdscache/v3/endpointstranslator.go |
KubernetesCache
类中定义了多个map类型的资源对象,分别存储不同的资源,key是 namespace 和 name 组成的 type.NamespacedName,将原生k8s中不同 namespace 的同一资源都存储到一个map中,多种资源的多个map,共同组成 KubernetesCache
1 | go复制代码// contour/internal/dag/cache.go |
KubernetesCache 插入流程:
- 首先判断资源类型
- 将不同的资源放入不同的map
- 有些资源的变更,还需要触发其他资源变更。比如:service变化了,对应的 ingress或者httpproxy也需要联动变化
1 | go复制代码// contour/internal/dag/cache.go |
DAG
构造好 KubernetesCache 之后,contour 通过 一些列的 processor,将 KubernetesCache 变成 DAG,DAG 代表了 k8s 不同资源关系的一个有向无环图。核心代码如下:
1 | go复制代码// contour/internal/contour/handler.go |
DAG 数据结构如下,根节点 roots 是一个 Vertex 的列表,Vertex 只定义了Visit接口。即:所有实现了 Visit
1 | go复制代码// contour/internal/dag/dag.go |
build过程分析:
1 | go复制代码// contour/internal/dag/builder.go |
processor 列表包括:
- IngressProcessor
- ExtensionServiceProcessor
- HTTPProxyProcessor
- GatewayAPIProcessor
- ListenerProcessor
1 | go复制代码func getDAGBuilder(ctx *serveContext, clients *k8s.Clients, clientCert, fallbackCert *types.NamespacedName, log logrus.FieldLogger) dag.Builder { |
每个processor 实现 Run 方法,接收 DAG 和 KubernetesCache 参数,通过 KubernetesCache 修改 DAG,以 HTTPProxyProcessor 为例分析。
1 | go复制代码func (p *HTTPProxyProcessor) Run(dag *DAG, source *KubernetesCache) { |
DAG 中的节点(Vertex)有:
- VirtualHost:对应 LDS
- SecureVirtualHost:对应 LDS 和 CDS
- ExtensionCluster:对应 CDS
- ServiceCluster:对应 EDS
- Cluster:对应 CDS,一部分SDS
- Listener:对应 RDS
其中某些 Vectex 又包含子 Vectex,因此用图这种数据结构串联起来。
ResourceCache
前面分析 DAG 代码时,在生成 DAG 后,会通知所有的 Observer,即e.Observer.OnChange
1 | go复制代码// contour/internal/contour/handler.go |
这里的 e.Observer 是 一堆 Observer的组合
1 | go复制代码// contour/cmd/contour/serve.go |
ResourceCache 是一个接口,聚合了两个接口:dag.Observer 和 xds.Resource。该接口定义了如何处理 DAG,以及处理完成后如何返回给调用方。
- Observer接口:
- OnChange:参数为DAG,当 DAG 有任何变更,都会触发这个函数,可以拿到最新的DAG做处理
- Resource接口:
- Contents:返回xds资源的接口
- Query:查询xds资源的接口
- Register:注册xds资源的接口
- TypeURL:xds资源类型
1 | go复制代码// contour/internal/xdscache/resources.go |
不同的 xds 资源类型都实现了ResourceCache 接口:
- RouteCache:对应 RDS
- ClusterCache:对应 CDS
- ListenerCache:对应 LDS
- SecretCache:对应 SDS
- EndpointsTranslator:对应 EDS
转换后的 xds 资源都保存在 values
字段中。
- Observer的OnChange接口:将 DAG 转换为 Envoy xds 配置保存在 values 中
- Resource的接口:提供查询 values 的方法,即可得到 envoy 的xds配置
1 | go复制代码// contour/internal/xdscache/v3/cluster.go |
以 RouteCache 的 OnChange 为例,查看实现逻辑
1 | go复制代码// contour/internal/xdscache/v3/route.go |
DAG 转换为 RDS 的逻辑实现
1 | go复制代码// contour/internal/xdscache/v3/route.go |
GRPC 数据下发
从前面的分析我们知道,数据最终被转换为多个 ResourceCache 的列表,这些数据最终有两个用途:
- 通过 GRPC 下发给数据面侧的 Envoy,最终达到流量路由的效果
- 传给 SnapshotHander,用于生成快照
1 | go复制代码func doServe(log logrus.FieldLogger, ctx *serveContext) error { |
下面分析数据经过 grpc 下发的流程,这部分主要是使用 go-control-plane
sdk
注册 ADS、SDS、CDS、EDS、LDS、RDS 等都使用了 srv 对象,可以推断 Server 继承了 各种 XDS的接口
1 | go复制代码// contour/internal/xds/v3/server.go |
contourServer
1 | go复制代码// contour/internal/xds/v3/contour.go |
把 ResourceCache 传给 contourServer,contourServer作为GRPC的server注册到GRPC上,go-control-plane 框架封装了各个XDS数据传输的接口,contourServer 实现这些接口,从而实现数据下发
1 | go复制代码func (s *contourServer) StreamClusters(srv envoy_service_cluster_v3.ClusterDiscoveryService_StreamClustersServer) error { |
所有的 XDS Stream方法内部都调用了 s.stream 方法
1 | go复制代码func (s *contourServer) stream(st grpcStream) error { |
框架server VS contourServer
contour会根据 ctx.Config.Server.XDSServerType 属性的值,初始化不同的 server。
- envoy:初始化框架默认的server
- contour:初始化 contourServer
Go-control-plane 提供的 pb 接口包括三类:
- StreamXXX
- DeltaXXX
- FetchXXX
XXX 代表不同的 XDS,比如 StreamClusters。
contourServer 只实现了所有 XDS 的 Stream 相关方法,并没有实现 Delta,Fetch 相关方法
Envoy 连接到 Contour服务端
前面介绍了 contour 下发数据到 envoy 的流程,那最开始 envoy是如何连接上 contour的?
initContainer 生成配置文件
查看 envoy daemonset yaml文件,可以看到,内部配置了一个 initContainer,用于生成本地配置文件,并和 envoy 业务容器共享,envoy 启动时根据配置文件启动。启动命令中还指定了 envoy 连接的 grpc server 地址,即 contour的地址,这里配置的是 k8s svc 名称
1 | yaml复制代码initContainers: |
envoy启动配置文件内容
进入 envoy 容器可以查看生成的配置内容
1 | bash复制代码kubectl -n projectcontour exec -it envoy-65qfq -c envoy -- cat /config/envoy.json |
1 | yaml复制代码{ |
总结
k8s本身没有内置 ingress 控制器,ingress 控制器呈现百花齐放的状态,参考官方文档:ingress 控制器。
不同的控制器,底层使用的网络代理不同,主要有:nginx、envoy、kong、haproxy等。envoy 作为云原生时代的网络代理,被越来越多的开源项目使用。比如:istio、contour、gloo、。这些项目的本质,都是简化 envoy 的配置,封装更简单的路由规则,通过内部的逻辑转换,变成envoy 的xds配置。
go-control-plane 框架封装了基于grpc下发xds配置,可以方便开发者开发自己的控制面。
本文转载自: 掘金