gloo 是一款基于 envoy 的云原生的API网关,它能够非常方便的和 K8S 进行集成,通过监听相关的 CRD,基于 envoy 的 xDS 接口对 envoy 配置进行 hot reload。同时也能很方便的集成 knative。另外它的文档也比较完善,设计比较简单,易于上手。
关于如何上手并使用 gloo,相信网上也能搜到一些中文资料,当然最好的方式当然是直接去 gloo 官方文档 查看。本文主要深入 gloo 的源码,探究 gloo 是如何实现对 envoy 的配置下发的。
从最基本意义上讲,gloo 是转化引擎,Envoy xDS 服务器为 Envoy 提供高级配置(包括 gloo 的自定义 Envoy 过滤器)。gloo 遵循基于事件的体系结构,监视各种配置源以进行更新,并立即通过 v2 版本的 gRPC 接口更新 Envoy 配置。
本文基于 gloo 1.14.0 对 gloo 源码进行分析。
核心代码结构
gloo 的核心概念
整体上,gloo 的核心概念如下图所示:
可以看到,主要核心控制器有 3 个:Emitter
、EventLoop
和 Syncer
。其中 Emitter
实现了 Snapshots()
函数,这个函数会返回一个 Snapshot
channel,然后在主循环 EventLoop
中会执行一个 Run()
函数去监听 Snapshot
channel,一旦接收到新的 Snapshot
,就会把这个 Snapshot
发送给 Syncer
实现的 Sync()
函数去处理,而这个 Sync()
函数就是最主要的将 gloo 配置同步到 envoy 的核心函数。
在上面我们可以频繁提到一个 Snapshot
的概念,这个就是 gloo 的最核心的数据结构。Snapshot
是一堆资源的集合,包括 gloo 的各种 CR 以及 K8S 的 Services 等资源,下面是 ApiSnapshot
的一个定义,可以参考源码:
1 | type ApiSnapshot struct { |
上面的 ApiSnapshot
里储存了很多东西,包括 gloo 自己的 CRD,例如 UpstreamList
、VirtualServiceList
、RouteTableList
等,也有 K8S 自己的资源(当然 gloo 在这里稍微做了一层转换),例如 EndpointList
、SecretList
等。
Snapshot
的作用如下:
- gloo 会通过一些 informer 接收到 K8S 中相应的 CRD 信息,并把这些信息汇总到
Snapshot
中 - gloo 会在
Snapshots()
函数中一秒轮训一次,周期性的把Snapshot
送入Emitter
的Snapshots()
接口返回的Snapshot
Channel 中
核心 Translate 流程
下图是一个总体的服务入口和 Translate 流程
gloo 如何把 K8S 的资源汇总转换到 Snapshot 中
在这里我们主要查看一下 Emitter
的逻辑,下面是一个 apiEmitter
的定义
1 | type apiEmitter struct { |
可以看到 Emitter
中存了很多 Client
,需要注意的是这些 Client
并不是 K8S 的 client-go,而是 gloo 自己封装的 client。
Emitter
的 Snapshots()
方法会做以下事情
- 初始化各
Client
并 watch 其中数据变化 - 一旦 Watch 到数据,对数据进行部分处理,然后塞入临时的
currentSnapshot
结构体中 - 每一秒一次,把
currentSnapshot
送入Snapshot
channel
下面针对这 3 个过程从源码进行解析
1. 初始化各 Client
并 watch 其中数据变化
第一步,apiEmitter
的 Register()
函数会调用各个 client 的 Register
接口,这些 Register
接口会对各个 client 对应的 K8S informer 进行初始化。这些 Register
会最终调用到 ResourceClient
这个接口的 Register()
,这部分代码已经不在 gloo 里了,是在 gloo 依赖的 solo-kit 中。然后这个 Register()
最重要的是实现方式在 kube/resource_client.go,最终会调用到 kube/resource_client_factory.go 中。这个函数里的逻辑就很清晰了,里面针对需要监听的 namespace
初始化了对应资源的 sharedInformer 并加入缓存中。
第二步,会调用各个 client 的 List
接口,对 Register()
中注册的 informer 进行 Start
,等待数据 sync 到本地 cache 之后顺便 List
一份返回。这里是一个 artifact 资源的 List
调用的地方,往里看也会调用到 solo-kit 的 kube/resource_client.go 或 kubesecret/resource_client.go 的 List
代码。
第三部,会调用各个 client 的 Watch
接口,监听该资源的情况,一旦有变化会反馈到 Watch
接口返回的 channel 中。例如这里是一个 artifact 资源的 Watch
调用的地方,最终也会调用到 solo-kit,此处不在赘述。
2. Watch 到数据,对数据进行部分处理
上面的 Watch
接口会返回一个 artifactNamespacesChan
,在下面会监听这个 channel,一旦有数据过来,就会汇总发布到 artifactChan
中,然后另一个 goroutine 也在监听 artifactChan
,一旦有数据产生,就会放入 currentSnapshot
中。
gloo 通过这种方式,把所有需要的资源逗整合到了 Snapshot
结构体中。
3. 把 currentSnapshot
送入 Snapshot
channel
每一秒一次,将上面 List
以及 Watch
得到的结果都放入 currentSnapshot
中,最后把 currentSnapshot
深拷贝一份,放入 Snapshot
channel 中。
通过上面的三步,就实现了最核心的 K8S 资源到 Snapshot
的转换,最后会将这个 Snapshot
资源传入 Sync()
函数中,实现最核心的 K8S 配置到 envoy 的 xDS 转换流程。
xDS 流程
gloo 的 Translate 最终做的工作是把 ApiSnapshot
转化为下面的 EnvoySnapshot
:
1 | // Snapshot is an internally consistent snapshot of xDS resources. |
Translate 转化完成之后,会调用 SetSnapshot
方法把 EnvoySnapshot
放到本地缓存中。一旦这个缓存被 Set,就会触发 xDS 服务和 envoy 的数据同步。
gloo 在初始化的时候会初始化一个 xdsServer
,这个 xdsServer
会实现下面的 interface:
1 | // Server is a collection of handlers for streaming discovery requests. |
然后会需要利用这个 xdsServer
生成一个 EnvoyServerV3
,这个才是真正和 Envoy 做交互的,这个 Server 需要实现 Envoy xDS 相关接口
1 | // Server is a collection of handlers for streaming discovery requests. |
如果仔细查看这些接口的实现的话,会发现基本都是使用上面的 StramEnvoyV3 来实现的,这里就不列举了。
总结
以上就是 gloo 的一些源码的主要流程了,总体来说架构和思路还算清晰,但是代码可能有些复杂,我们在阅读 gloo 代码的时候需要注意的是,*.sk.go
的代码都是生成的代码,所以不用过于在乎这部分代码的写法,大致知道逻辑即可。