前言
上一篇比较侧重源码,讲了 rancher 中的 remotedialer 模块是如何实现一个双工隧道的。这里再简单的总结一下。安全性催生了隧道,双工隧道可采用四层连接实现,但处理四层连接的数据收发相对处理七层麻烦一些;而七层协议中 websocket 支持双工,故将七层协议作为一个四层实现,即可实现这条双工隧道。
这一篇,我们将分析一下基于这条隧道,rancher 是怎么做集群注册和集群管理的。
集群注册
rancher server: 集群创建
假设我们不对接 IaaS,使用 import 已有集群的方式,那么在集群注册之前,还需要在 server 上进行一次集群导入,或者更准确的说:集群创建。
在导入集群之后,实际上只是在 rancher 的管理面,创建一个 cluster 实例。该实例没有真正的 cluster 信息,只有一些 meta 信息,比如集群的 id,集群的 display name 等等。集群的实际信息,比如 cluster endpoint 等等,需要后续由 agent 在集群注册阶段填充。
当然,rancher 会做一些额外的事情,比如说
- 有 service account 被对应创建
- 有 cluster registration token 被相应创建,用作 agent 的认证用
我们现在可以暂时忽略这些部分,从 agent 发起的集群注册流程开始分析。
rancher agent: 集群注册
在集群创建完毕之后,用户可以生成一份部署 agent 的 yaml 文件,在目标 k8s 集群上部署 agent 之后,agent 将发起集群注册。
agent 的逻辑入口在 cmd/agent/main.go
中。忽略一些无用的、辅助性的代码,我们可以直接看 run()
这个函数。在阅读整个 agent 逻辑的过程中,我们需要知晓如下的背景知识:
- agent 分为 cluster agent 和 node agent,在 rancher 的架构说明中,rancher 会优先尝试连接 cluster agent,在 cluster agent 不可用时,将尝试访问 node agent。而 node agent 和 cluster agent 本质上并没有太大差别,我们只看 cluster agent 即可。
- 一些环境变量,以便于我们判断代码逻辑的分支。
CLUSTER_CLEANUP=false
,CATTLE_WRITE_CERT_ONLY=false
,CATTLE_TOKEN
是 rancher 签发的一个 token,CATTLE_SERVER
是 rancher 的服务端地址。
整个流程分为下列步骤:
- 获取 k8s 的 root CA cert、service account token、APIServer endpoint。这些信息将被序列化,封装在 agent 发起的请求的 header 中,其 key 是
X-API-Tunnel-Params
。 - 获取 cluster registration token 和 rancher server。token 会被封装为
X-API-Tunnel-Token
中。 - 持续的向 server 发起 websocket 连接,连接上之后,调用特定的回调函数
onConnect
。要注意的是这条 websocket 连接被封装在 remotedialer 的 client 中,一方面 websocket 建立连接之后,调用回调函数;另一方面连接之后,client 侧就可以接收 server 端转发来的请求,转发到后端的 k8s 集群。
回调函数中启动了一些 k8s operator,其代码比较 trivial,这里不做展开。
通过上面的叙述,我们看到 cluster agent 通过携带的 token 完成服务端的隧道认证;并在集群注册中,上报了 k8s 集群的信息。
集群管理
集群管理大致包含了如下职能:
- 管理集群的元数据,管理元数据与集群资源的映射管理
- 同步集群的状态,包括健康状态、认证信息等
- 管理与各个集群建立的隧道
- 管理路由转发规则
rancher server 通过 multi-cluster-manager 组件实现集群管理与路由功能。
multi-cluster-manager
rancher 的 multi-cluster-manager 比较乱,许多代码分散在各个地方,可读性很差。这里简单做个总结。multi-cluster-manager 做了下面的事情:
- 管理集群的 CA、endpoint、健康状态等信息,用来访问后端集群
- 管理隧道状态
- 管理集群路由,用作决定什么请求向哪儿转发
- 管理转发单元,用作转发请求时,添加各种附加信息
- 管理集群状态,用作判断集群的健康状态
mcm 中封装了一个 DeferredServer
,对这个 server 我们不用过分关心,只要知道它里面注册了许多 API handler。具体的路由注册见 pkg/multiclustermanager/routes.go
这个文件,其中我们需要关注的是
- connectHandler:处理集群注册的 API handler,它其实就是 remotedialer 的 server 端
- proxy handler:用来将请求转发到后端集群的 API handler
这俩是 rancher 进行隧道管理的核心。
remotedialer server
connect handler 作为 remotedialer server,在上一篇有详细的介绍,这里就不再多说了。读者需要记得 remotedialer 中可以注册自定义的 authorizer
,用作隧道认证。rancher 声明了自己的 authorizer,见 pkg/tunnelserver/tunnel.go
文件的 Authorize
函数。这里简单说一下流程:
- 获取
X-API-Tunnel-Token
header,查看 cluster registration token 是否在数据库内。这一步还实现了通过 token 查找 cluster 的操作,agent 不需要携带 cluster id,只需要携带 token 即可。这个信息用作隧道认证用 - 获取
X-API-Tunnel-Params
header,解析出 k8s 集群信息,及时更新到 cluster CRD 中。这些信息在转发请求时被用到
每一次 agent 重新连接,都会上报这些信息,rancher 就可以及时更新集群信息。
在 token 管理方面,rancher 没有用业界通用的 jwt,而是选择自己生成一串 token,属实没太大必要。但是这里暂且不说认证相关的内容了。
proxy service
其实就是 "github.com/rancher/rancher/pkg/k8sproxy"
就是包,感兴趣的同学可以简单阅读它的源码,经过一堆弯弯绕绕的逻辑,最后看 "github.com/rancher/rancher/pkg/clusterrouter/proxy"
这个包即可。
那 agent 上报的集群信息到底是怎么被使用的呢?这个会涉及到认证与鉴权的相关知识,这些背景知识留到认证部分再详细解释。这里只简单的介绍 proxy 实例的关键业务流程:
- 判断集群是否可达,不可达直接报错
- 获取集群的 API endpoint
- 获取 agent 上报的 service account token,持有该 Token 就可以访问 k8s 集群,之后添加相应的 authorization header
- 向后端集群转发
这样请求就通过 remotedialer 的隧道,转到了后端的 k8s 的 APIServer。
对认证有研究的同学应该能发现这里只有认证信息,没有身份信息。rancher 对身份信息的填充放在另一个组件中进行,源码可以看 pkg/auth/requests/filter.go
,采用 k8s 的 impersonation 技术,这里不做展开,之后会详细介绍。
处理高可用
如果 agent 仅仅维持一个长连接,在高并发场景下,这条隧道是可能成为瓶颈的。rancher 在这个点上并没有解决方案。
如果读者还记得的话,remotedialer 包是考虑到支持多副本的,server 端维护的 session 列表是一个数组。这意味着有多个 client 时,server 本可以采用一定的负载均衡算法。对应到实际场景,多个 agent 理应可以同时注册到 rancher,并由 rancher 进行负载均衡,避免这条 websocket 连接称为瓶颈。故其实要支持高可用可能也不难,在隧道做好高可用,即可完成上层无感知的高可用方案建设。
总结
基于这条隧道,rancher 完成了集群注册、隧道认证、集群信息上报,并在 server 端用 mcm 实现了集群管理:
- 上行:上报集群信息与集群认证信息
- 下行:转发请求
这一篇有许多技术细节没有说,大多是关于认证、鉴权的内容,下一篇我们单独将这些技术细节。