第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理

第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理

tags:

  • k8s
  • 源码学习

categories:

  • 源码学习
  • 二次开发

文章目录

  • 第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理
    • 第一节 Operator初识
      • 1.1 Operator介绍
      • 1.2 Operator诞生和发展历程
      • 1.3 Operator的发展
      • 1.4 Operator 的社区与生态
    • 第二节 kubebuilder介绍使用
      • 2.1 kubebuilder介绍
      • 2.2 kubebuilder安装
      • 2.3 kubebuilder项目创建
      • 2.4 kubebuilder项目运行测试
    • 第三节 controller-runtime原理之控制器
      • 3.1 Controller 的实现
      • 3.2 Watch函数实现
      • 3.3 Start 函数实现
    • 第四节 controller-runtime 原理之 manager
      • 4.1 Manager 如何使用
      • 4.2 Manager 实例化
      • 4.3 启动 Manager

第一节 Operator初识

1.1 Operator介绍

  1. 基于 Kubernetes 平台,我们可以轻松的搭建一些简单的无状态应用,比如对于一些常见的 web apps 或是移动端后台程序,开发者甚至不用十分了解 Kubernetes 就可以利用 Deployment,Service 这些基本单元模型构建出自己的应用拓扑并暴露相应的服务。由于无状态应用的特性支持其在任意时刻进行部署、迁移、升级等操作,Kubernetes 现有的 ReplicaSets、Deployment、Services 等资源对象已经足够支撑起无状态应用对于自动扩缩容、实例间负载均衡等基本需求。
  2. 在管理简单的有状态应用时,我们可以利用社区原生的 StatefulSet 和 PV 模型来构建基础的应用拓扑,帮助实现相应的持久化存储,按顺序部署、顺序扩容、顺序滚动更新等特性。
  3. 而随着 Kubernetes 的蓬勃发展,在数据分析,机器学习等领域相继出现了一些场景更为复杂的分布式应用系统,也给社区和相关应用的开发运维人员提出了新的挑战:
    • 不同场景下的分布式系统中通常维护了一套自身的模型定义规范,如何在 Kubernetes 平台中表达或兼容出应用原先的模型定义?
    • 当应用系统发生扩缩容或升级时,如何保证当前已有实例服务的可用性?如何保证它们之间的可连通性?
    • 如何去重新配置或定义复杂的分布式应用?是否需要大量的专业模板定义和复杂的命令操作?是否可以向无状态应用那样用一条 kubectl 命令就完成应用的更新?
    • 如何备份和管理系统状态和应用数据?如何协调系统集群各成员间在不同生命周期的应用状态?
  4. 而所有的这些正是 Operator 希望解决的问题,本文我们将首先了解到 Operator 是什么,之后逐步了解到 Operator 的生态建设,Operator 的关键组件及其基本的工作原理,下面让我们来一探究竟吧。

1.2 Operator诞生和发展历程

  1. CoreOS 在 2016 年底提出了 Operator 的概念,当时的一段官方定义如下:
    “An Operator represents human operational knowledge in software, to reliably manage an application.”
  2. 对于普通的应用开发者或是大多数的应用 SRE 人员,在他们的日常开发运维工作中,都需要基于自身的应用背景和领域知识构建出相应的自动化任务满足业务应用的管理、监控、运维等需求。在这个过程中,Kubernetes 自身的基础模型元素已经无法支撑不同业务领域下复杂的自动化场景。
  3. 与此同时,在云原生的大背景下,生态系统是衡量一个平台成功与否的重要标准,而广大的应用开发者作为 Kubernetes 的最直接用户和服务推广者,他们的业务需求更是 Kubernetes 的生命线。于是,谷歌率先提出了 Third Party Resource 的概念,允许开发者根据业务需求以插件化形式扩展出相应的 K8s API 对象模型,同时提出了自定义 controller 的概念用于编写面向领域知识的业务控制逻辑,基于 Third Party Resource,Kubernetes 社区在 1.7 版本中提出了custom resources and controllers 的概念,(CRD+控制器)这正是** Operator 的核心概念**。
  4. 简单来看,Operator 定义了一组在 Kubernetes 集群中打包和部署复杂业务应用的方法,它可以方便地在不同集群中部署并在不同的客户间传播共享;同时 Operator 还提供了一套应用在运行时刻的监控管理方法,应用领域专家通过将业务关联的运维逻辑编写融入到 operator 自身控制器中,而一个运行中的 Operator 就像一个 7*24 不间断工作的优秀运维团队,它可以时刻监控应用自身状态和该应用在 Kubernetes 集群中的关注事件,并在毫秒级别基于期望终态做出对监听事件的处理,比如对应用的自动化容灾响应或是滚动升级等高级运维操作。
  5. 进一步讲,Operator 的设计和实现并不是千篇一律的,开发者可以根据自身业务需求,不断演进应用的自定义模型,同时面向具体的自动化场景在控制器中扩展相应的业务逻辑。很多 Operator 的出现都是起源于一些相对简单的部署和配置需求,并在后续演进中不断完善补充对复杂运维需求的自动化处理。

1.3 Operator的发展

  1. CoreOS 是最早的一批基于 Kubernetes 平台提供企业级容器服务解决方案的厂商之一,他们很敏锐地捕捉到了 TPR 和控制器模式对企业级应用开发者的重要价值;并很快基于 TPR 实现了历史上第一个 Operator:etcd-operator。它可以让用户通过短短的几条命令就快速的部署一个 etcd 集群,并且基于 kubectl 命令行一个普通的开发者就可以实现 etcd 集群滚动更新、灾备、备份恢复等复杂的运维操作,极大的降低了 etcd 集群的使用门槛,在很短的时间就成为当时 K8s 社区关注的焦点项目。
  2. 与此同时,Operator 以其插件化、自由化的模式特性,迅速吸引了大批的应用开发者,一时间很多市场上主流的分布式应用均出现了对应的 Operator 开源项目;而很多云厂商也迅速跟进,纷纷提出基于 Operator 进行应用上云的解决方案。Operator 在 Kubernetes 应用开发者中的热度大有星火燎原之势。
  3. 虽然 Operator 的出现受到了大量应用开发者的热捧,但是它的发展之路并不是一帆风顺的。对于谷歌团队而言,Controller 和控制器模式一直以来是作为其 API 体系内部实现的核心,从未暴露给终端应用开发者,Kubernetes 社区关注的焦点也更多的是集中在 PaaS 平台层面的核心能力。而 Operator 的出现打破了社区传统意义上的格局,对于谷歌团队而言,Controller 作为 Kubernetes 原生 API 的核心机制,应该交由系统内部的 Controller Manager 组件进行管理,并且遵从统一的设计开发模式,而不是像 Operator 那样交由应用开发者自由地进行 Controller 代码的编写。
  4. 另外 Operator 作为 Kubernetes 生态系统中与终端用户建立连接的桥梁,作为 Kubernetes 项目的设计和捐赠者,谷歌当然也不希望错失其中的主导权。同时 Brendan Burns 突然宣布加盟微软的消息,也进一步加剧了谷歌团队与 Operator 项目之间的矛盾。
  5. 于是,2017 年开始谷歌和 RedHat 开始在社区推广 Aggregated apiserver,应用开发者需要按照标准的社区规范编写一个自定义的 apiserver,同时定义自身应用的 API 模型;通过原生 apiserver 的配置修改,扩展 apiserver 会随着原生组件一同部署,并且限制自定义 API 在系统管理组件下进行统一管理。之后,谷歌和 RedHat 开始在社区大力推广使用聚合层扩展 Kubernetes API,同时建议废弃 TPR 相关功能。
  6. 然而,巨大的压力并没有让 Operator 昙花一现,就此消失。相反,**社区大量的 Operator 开发和使用者仍旧拥护着 Operator 清晰自由的设计理念,继续维护演进着自己的应用项目;**同时很多云服务提供商也并没有放弃 Operator,Operator 简洁的部署方式和易复制,自由开放的代码实现方式使其维护住了大量忠实粉丝。在用户的选择面前,强如谷歌,红帽这样的巨头也不得不做出退让。最终,TPR 并没有被彻底废弃,而是由 Custom Resource Definition(简称 CRD)这个如今已经广为人知的资源模型范式代替。
  7. 2018 年初,RedHat 完成了对 CoreOS 的收购,并在几个月后发布了Operator Framework,通过提供 SDK 等管理工具的方式进一步降低了应用开发与 Kubernetes 底层 API 知识体系之间的依赖。至此,Operator 进一步巩固了其在 Kubernetes 应用开发领域的重要地位

1.4 Operator 的社区与生态

  1. 一时间,基于不同种类的业务应用涌现了一大批优秀的开源 Operator 项目,我们可以找到其中很多的典型案例,例如对于运维要求较高的数据库集群,我们可以找到像 etcd、Mysql、PostgreSQL、Redis、Cassandra 等很多主流数据库应用对应的 Operator 项目,这些 Operator 的推出有效的简化了数据库应用在 Kubernetes 集群上的部署和运维工作;在监控方向,CoreOS 开发的 prometheus-operator 早日成为社区里的明星项目,Jaeger、FluentD、Grafana 等主流监控应用也或由官方或由开发者迅速推出相应的 Operator 并持续演进;在安全领域,Aqua、Twistlock、Sisdig 等各大容器安全厂商也不甘落后,通过 Operator 的形式简化了相对门槛较高的容器安全应用配置,另外社区中像 cert-manager、vault-operator 这些热门项目也在很多生产环境上得到了广泛应用。
  2. 可以说 Operator 在很短的时间就成为了分布式应用在 Kubernetes 集群中部署的事实标准,同时 Operator 应用如此广泛的覆盖面也使它超过了分布式应用这个原始的范畴,成为了整个 Kubernetes 云原生应用下一个重要存在。
  3. 随着 Operator 的持续发展,已有的社区共享模式已经渐渐不能满足广大开发者和 K8s 集群管理员的需求,如何快速寻找到业务需要的可用 Operator?如何给生态中大量的 Operator 定义一个统一的质量标准?这些都成为了刚刚完成收购的 RedHat 大佬们眼中亟需解决的问题。
  4. 于是我们看到 RedHat 在年初联合 AWS、谷歌、微软等大厂推出了 OperatorHub.io,希望其作为 Kubernetes 社区的延伸,向广大 operator 用户提供一个集中式的公共仓库,用户可以在仓库网站上轻松的搜索到自己业务应用对应的 Operator 并在向导页的指导下完成实例安装;同时,开发者还可以基于 Operator Framework 开发自己的 Operator 并上传分享至仓库中。
    第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理_第1张图片
  5. Operator 开源生命周期流程图, 主要流程包括:
    • 开发者首先使用 Operator SDK 创建一个 Operator 项目;
    • 利用 SDK 我们可以生成 Operator 对应的脚手架代码,然后扩展相应业务模型和 API,最后实现业务逻辑完成一个 Operator 的代码编写;
    • 参考社区测试指南进行业务逻辑的本地测试以及打包和发布格式的本地校验;
    • 在完成测试后可以根据规定格式向社区提交PR,会有专人进行 review;
    • 待社区审核通过完成 merge 后,终端用户就可以在 OperatorHub.io 页面上找到业务对应的 Operator;
    • 用户可以在 OperatorHub.io 上找到业务 Operator 对应的说明文档和安装指南,通过简单的命令行操作即可在目标集群上完成 Operator 实例的安装;
    • Operator 实例会根据配置创建所需的业务应用,OLM 和 Operator Metering 等组件可以帮助用户完成业务应用对应的运维和监控采集等管理操作。

第二节 kubebuilder介绍使用

2.1 kubebuilder介绍

  1. 在 Kubernetes 中开发 Operator 的时候,我们肯定需要使用到 CRD 以及对应的 Controller ,我们可以通过 CRD 定义业务相关的资源,并利用 controller 实现对应的业务逻辑,例如创建/删除 deployment,并根据资源变化做出相应的动作。但是如果全都去手动生成代码,然后再来编写业务代码显得有点麻烦,为此在社区中,为我们提供了基于 CRD 开发的框架,主要有 kubebuilder 以及 operator-sdk 两个框架,这两者大同小异,都是利用兴趣小组提供的 controller-runtime 项目实现的 Controller 逻辑,不同的是 CRD 资源的创建过程。

2.2 kubebuilder安装

  1. 我们先来简单介绍下 kubebuilder,kubebuilder 由 Kubernetes 特殊兴趣组(SIG) API Machinery 拥有和维护,能够帮助开发者创建 CRD 并生成 controller 脚手架,安装一下kubebuilder。
  2. 官方文档:https://book.kubebuilder.io/quick-start.html
  3. 中文手册: https://cloudnative.to/kubebuilder/quick-start.html
# 下载 kubebuilder 并解压
wget https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz
# 将 kubebuilder 移动 PATH 路径中 
sudo mv kubebuilder /usr/local/bin/kubebuilder
# 查看版本
kubebuilder version

2.3 kubebuilder项目创建

  1. 创建一个目录builder-demo,然后在里面运行 kubebuilder init 命令,初始化一个新项目。示例如下。
mkdir gitee.com/qnk8s/builder-demo
cd gitee.com/qnk8s/builder-demo
# 开启 go modules
export GO111MODULE=on
export GOPROXY=https://goproxy.cn
# 初始化项目 domian公司域名 owner作者 repo git地址
kubebuilder init --domain ydzs.io --owner qnhyn --repo gitee.com/qnk8s/builder-demo
  1. 新建一个 API,运行下面的命令,创建一个新的 API(组/版本)为 “webapp/v1”,并在上面创建新的 Kind(CRD) “Guestbook”。
    • kubebuilder create api
# 创建CRD webapp/v1 Book 
# y y
kubebuilder create api --group webapp --version v1 --kind Book
  1. 上面的命令会创建文件 api/v1/guestbook_types.go ,该文件中定义相关 API ,而针对于这一类型 (CRD) 的业务逻辑生成在 controller/guestbook_controller.go 文件中。
  2. 可以根据自己的需求去修改资源对象的定义结构,修改 api/v1/guestbook_types.go 文件:** 修改完成后,make重新生成代码**。
// 修改结构体字段
type BookSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// Foo is an example field of Book. Edit Book_types.go to remove/update
	Price int32 `json:"price"`
	Title string `json:"title"`
}
  1. controllers/book_controller.go下Reconcile写一些业务逻辑。这里只是日志输出测试流程。
func (r *BookReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
	_ = context.Background()
	log := r.Log.WithValues("book", req.NamespacedName)

	// your logic here
	log.Info("book reconciling")

	return ctrl.Result{}, nil
}

2.4 kubebuilder项目运行测试

  1. 项目中config/samples/webapp_v1_book.yaml修改,想集群中注册CRD。
apiVersion: webapp.ydzs.io/v1
kind: Book
metadata:
  name: book-sample
spec:
  # Add fields here
  price: 100
  title: kubernetes
  1. Makefile文件中命令install可以安装CRD到本地集群中。
# 安装CRD到本地集群
make install
kubectl get crd
  1. 把控制器安装在集群中go run main.go, Makefile中可以看到命令make run。
# make run执行go run main.go控制器安装在集群中
make run
  1. 运行项目中config/samples/webapp_v1_book.yaml查看日志输出。
kubectl apply -f webapp_v1_book.yaml
kubectl delete -f webapp_v1_book.yaml
  1. Makefile中命令docker-build, 可以把当前控制器打包成一个docker镜像。
# docker-build 构建镜像
# docker-push 上传到镜像仓库
make docker-build docker-push IMG=<some-registry>/<project-name>:tag
# deploy 部署到k8s的pod中
make deploy IMG=<some-registry>/<project-name>:tag
# uninstall 删除CRD
make uninstall

第三节 controller-runtime原理之控制器

3.1 Controller 的实现

  1. controller-runtime(https://github.com/kubernetes-sigs/controller-runtime) 框架实际上是社区帮我们封装的一个控制器处理的框架,底层核心实现原理和我们前面去自定义一个 controller 控制器逻辑是一样的,只是在这个基础上新增了一些概念,开发者直接使用这个框架去开发控制器会更加简单方便。包括 kubebuilder、operator-sdk 这些框架其实都是在 controller-runtime 基础上做了一层封装,方便开发者快速生成项目的脚手架而已。下面我们就来分析下 controller-runtime 是如何实现的控制器处理。
  2. 首先我们还是去查看下控制器的定义以及控制器是如何启动的。控制器的定义结构体如下所示:
    • 下载代码:https://github.com/kubernetes-sigs/controller-runtime
// pkg/internal/controller/controller.go

type Controller struct {
	// Name 用于跟踪、记录和监控中控制器的唯一标识,必填字段
	Name string

	// 可以运行的最大并发 Reconciles 数量,默认值为1
	MaxConcurrentReconciles int

	// Reconciler 是一个可以随时调用对象的 Name/Namespace 的函数
  // 确保系统的状态与对象中指定的状态一致,默认为 DefaultReconcileFunc 函数
	Do reconcile.Reconciler

	// 一旦控制器准备好启动,MakeQueue 就会为这个控制器构造工作队列
	MakeQueue func() workqueue.RateLimitingInterface

	// 队列通过监听来自 Infomer 的事件,添加对象键到队列中进行处理
	// MakeQueue 属性就是来构造这个工作队列的
  // 也就是前面我们讲解的工作队列,我们将通过这个工作队列来进行消费
	Queue workqueue.RateLimitingInterface

	// SetFields 用来将依赖关系注入到其他对象,比如 Sources、EventHandlers 以及 Predicates
	SetFields func(i interface{}) error

	// 控制器同步信号量
	mu sync.Mutex

	// 允许测试减少 JitterPeriod,使其更快完成
	JitterPeriod time.Duration

	// 控制器是否已经启动
	Started bool

	// TODO(community): Consider initializing a logger with the Controller Name as the tag

	// startWatches 维护了一个 sources、handlers 以及 predicates 列表以方便在控制器启动的时候启动
	startWatches []watchDescription

	// 日志记录
	Log logr.Logger
}
  1. 上面的结构体就是 controller-runtime 中定义的控制器结构体,我们可以看到结构体中仍然有一个限速的工作队列,但是看上去没有资源对象的 Informer 或者 Indexer 的数据,实际上这里是通过下面的 startWatches 属性做了一层封装,该属性是一个 watchDescription 队列,一个 watchDescription 包含了所有需要 watch 的信息:
// pkg/internal/controller/controller.go

// watchDescription 包含所有启动 watch 操作所需的信息
type watchDescription struct {
	src        source.Source
	handler    handler.EventHandler
	predicates []predicate.Predicate
}
  1. 整个控制器中最重要的两个函数是 Watch 与 Start,下面我们就来分析下他们是如何实现的。

3.2 Watch函数实现

// pkg/internal/controller/controller.go

func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prct ...predicate.Predicate) error {
	c.mu.Lock()
	defer c.mu.Unlock()

	// 注入 Cache 到参数中
	if err := c.SetFields(src); err != nil {
		return err
	}
	if err := c.SetFields(evthdler); err != nil {
		return err
	}
	for _, pr := range prct {
		if err := c.SetFields(pr); err != nil {
			return err
		}
	}

	// Controller 还没启动,把 watches 存放到本地然后返回
	//
	// 这些 watches 会被保存到控制器结构体中,直到调用 Start(...) 函数
	if !c.Started {
		c.startWatches = append(c.startWatches, watchDescription{src: src, handler: evthdler, predicates: prct})
		return nil
	}

	c.Log.Info("Starting EventSource", "source", src)
	// 调用 src 的 Start 函数
	return src.Start(evthdler, c.Queue, prct...)
}
  1. 上面的 Watch 函数可以看到最终是去调用的 Source 这个参数的 Start 函数,Source 是事件的源,如对资源对象的 Create、Update、Delete 操作,需要由 event.EventHandlersreconcile.Requests 入队列进行处理。
    • 使用 Kind 来处理来自集群的事件(如 Pod 创建、Pod 更新、Deployment 更新)。
    • 使用 Channel 来处理来自集群外部的事件(如 GitHub Webhook 回调、轮询外部 URL)。
// pkg/source/source.go

type Source interface {
	// Start 是一个内部函数
  // 只应该由 Controller 调用,向 Informer 注册一个 EventHandler
  // 将 reconcile.Request 放入队列
	Start(handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error
}
  1. 我们可以看到 source.Source 是一个接口,它是 Controller.Watch 的一个参数,所以要看具体的如何实现的 Source.Start 函数,我们需要去看传入 Controller.Watch 的参数,在 controller-runtime 中调用控制器的 Watch 函数的入口实际上位于 pkg/builder/controller.go 文件中的 doWatch() 函数:
// pkg/builder/controller.go

func (blder *Builder) doWatch() error {
	// Reconcile type
	src := &source.Kind{Type: blder.forInput.object}
	hdler := &handler.EnqueueRequestForObject{}
	allPredicates := append(blder.globalPredicates, blder.forInput.predicates...)
	err := blder.ctrl.Watch(src, hdler, allPredicates...)
	if err != nil {
		return err
	}
  ......
	return nil
}
  1. 可以看到 Watch 的第一个参数是一个 source.Kind 的类型,该结构体就实现了上面的 source.Source 接口:
// pkg/source/source.go

// Kind 用于提供来自集群内部的事件源,这些事件来自于 Watches(例如 Pod Create 事件)
type Kind struct {
	// Type 是 watch 对象的类型,比如 &v1.Pod{}
	Type runtime.Object

	// cache 用于 watch 的 APIs 接口
	cache cache.Cache
}

// 真正的 Start 函数实现
func (ks *Kind) Start(handler handler.EventHandler, queue workqueue.RateLimitingInterface,
	prct ...predicate.Predicate) error {

	// Type 在使用之前必须提前指定
	if ks.Type == nil {
		return fmt.Errorf("must specify Kind.Type")
	}

	// cache 也应该在调用 Start 之前被注入了
	if ks.cache == nil {
		return fmt.Errorf("must call CacheInto on Kind before calling Start")
	}

	// 从 Cache 中获取 Informer
  // 并添加一个事件处理程序来添加队列
	i, err := ks.cache.GetInformer(context.TODO(), ks.Type)
	if err != nil {
		if kindMatchErr, ok := err.(*meta.NoKindMatchError); ok {
			log.Error(err, "if kind is a CRD, it should be installed before calling Start",
				"kind", kindMatchErr.GroupKind)
		}
		return err
	}
	i.AddEventHandler(internal.EventHandler{Queue: queue, EventHandler: handler, Predicates: prct})
	return nil
}
  1. 从上面的具体实现我们就可以看出来 Controller.Watch 函数就是实现的获取资源对象的 Informer 以及注册事件监听函数。Informer 是通过 cache 获取的,cache 是在调用 Start 函数之前注入进来的,这里其实我们不用太关心;下面的 AddEventHandler 函数中是一个 internal.EventHandler 结构体,那这个结构体比如会实现 client-go 中提供的 ResourceEventHandler 接口,也就是我们熟悉的 OnAdd、OnUpdate、OnDelete 几个函数:
// pkg/source/internal/eventsource.go

// EventHandler 实现了 cache.ResourceEventHandler 接口
type EventHandler struct {
	EventHandler handler.EventHandler
	Queue        workqueue.RateLimitingInterface
	Predicates   []predicate.Predicate
}

func (e EventHandler) OnAdd(obj interface{}) {
  // kubernetes 对象被创建的事件
	c := event.CreateEvent{}

	// 获取对象 metav1.Object
	if o, err := meta.Accessor(obj); err == nil {
		c.Meta = o
	} else {
		log.Error(err, "OnAdd missing Meta",
			"object", obj, "type", fmt.Sprintf("%T", obj))
		return
	}

	// 断言 runtime.Object
	if o, ok := obj.(runtime.Object); ok {
		c.Object = o
	} else {
		log.Error(nil, "OnAdd missing runtime.Object",
			"object", obj, "type", fmt.Sprintf("%T", obj))
		return
	}
	
  // Predicates 用于事件过滤,循环调用 Predicates 的 Create 函数
	for _, p := range e.Predicates {
		if !p.Create(c) {
			return
		}
	}

	// 调用 EventHander 的 Create 函数
	e.EventHandler.Create(c, e.Queue)
}

func (e EventHandler) OnUpdate(oldObj, newObj interface{}) {
	u := event.UpdateEvent{}

	// Pull metav1.Object out of the object
	if o, err := meta.Accessor(oldObj); err == nil {
		u.MetaOld = o
	} else {
		log.Error(err, "OnUpdate missing MetaOld",
			"object", oldObj, "type", fmt.Sprintf("%T", oldObj))
		return
	}

	// Pull the runtime.Object out of the object
	if o, ok := oldObj.(runtime.Object); ok {
		u.ObjectOld = o
	} else {
		log.Error(nil, "OnUpdate missing ObjectOld",
			"object", oldObj, "type", fmt.Sprintf("%T", oldObj))
		return
	}

	// Pull metav1.Object out of the object
	if o, err := meta.Accessor(newObj); err == nil {
		u.MetaNew = o
	} else {
		log.Error(err, "OnUpdate missing MetaNew",
			"object", newObj, "type", fmt.Sprintf("%T", newObj))
		return
	}

	// Pull the runtime.Object out of the object
	if o, ok := newObj.(runtime.Object); ok {
		u.ObjectNew = o
	} else {
		log.Error(nil, "OnUpdate missing ObjectNew",
			"object", oldObj, "type", fmt.Sprintf("%T", oldObj))
		return
	}

	for _, p := range e.Predicates {
		if !p.Update(u) {
			return
		}
	}

	// 调用 EventHandler 的 Update 函数
	e.EventHandler.Update(u, e.Queue)
}

func (e EventHandler) OnDelete(obj interface{}) {
	d := event.DeleteEvent{}

	// Deal with tombstone events by pulling the object out.  Tombstone events wrap the object in a
	// DeleteFinalStateUnknown struct, so the object needs to be pulled out.
	// Copied from sample-controller
	// This should never happen if we aren't missing events, which we have concluded that we are not
	// and made decisions off of this belief.  Maybe this shouldn't be here?
	var ok bool
	if _, ok = obj.(metav1.Object); !ok {
		// 假设对象没有 Metadata,假设是一个 DeletedFinalStateUnknown 类型的对象
		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
		if !ok {
			log.Error(nil, "Error decoding objects.  Expected cache.DeletedFinalStateUnknown",
				"type", fmt.Sprintf("%T", obj),
				"object", obj)
			return
		}

		// Set obj to the tombstone obj
		obj = tombstone.Obj
	}

	// Pull metav1.Object out of the object
	if o, err := meta.Accessor(obj); err == nil {
		d.Meta = o
	} else {
		log.Error(err, "OnDelete missing Meta",
			"object", obj, "type", fmt.Sprintf("%T", obj))
		return
	}

	// Pull the runtime.Object out of the object
	if o, ok := obj.(runtime.Object); ok {
		d.Object = o
	} else {
		log.Error(nil, "OnDelete missing runtime.Object",
			"object", obj, "type", fmt.Sprintf("%T", obj))
		return
	}

	for _, p := range e.Predicates {
		if !p.Delete(d) {
			return
		}
	}

	// 调用 EventHandler 的 delete 函数
	e.EventHandler.Delete(d, e.Queue)
}
  1. 上面的 EventHandler 结构体实现了 client-go 中的 ResourceEventHandler 接口,实现过程中我们可以看到调用了 Predicates 进行事件过滤,过滤后才是真正的事件处理,不过其实真正的事件处理也不是在这里去实现的,而是通过 Controller.Watch 函数传递进来的 handler.EventHandler 处理的,这个函数通过前面的 doWatch() 函数可以看出来它是一个 &handler.EnqueueRequestForObject{} 对象,所以真正的事件处理逻辑是这个函数去实现的:
// pkg/handler/enqueue.go

// EnqueueRequestForObject 是一个包含了作为事件源的对象的 Name 和 Namespace 的入队列的 Request
//(例如,created/deleted/updated 对象的 Name 和 Namespace)
// handler.EnqueueRequestForObject 几乎被所有关联资源(如 CRD)的控制器使用,以协调关联的资源
type EnqueueRequestForObject struct{}

// Create 函数实现
func (e *EnqueueRequestForObject) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
	if evt.Meta == nil {
		enqueueLog.Error(nil, "CreateEvent received with no metadata", "event", evt)
		return
	}
  // 添加一个 Request 对象到工作队列
	q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
		Name:      evt.Meta.GetName(),
		Namespace: evt.Meta.GetNamespace(),
	}})
}

// Update 函数实现
func (e *EnqueueRequestForObject) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
	if evt.MetaOld != nil {
    // 如果旧的meta对象不为空,添加到工作队列中
		q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
			Name:      evt.MetaOld.GetName(),
			Namespace: evt.MetaOld.GetNamespace(),
		}})
	} else {
		enqueueLog.Error(nil, "UpdateEvent received with no old metadata", "event", evt)
	}

	if evt.MetaNew != nil {
    // 如果新的meta对象不为空,添加到工作队列中
		q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
			Name:      evt.MetaNew.GetName(),
			Namespace: evt.MetaNew.GetNamespace(),
		}})
	} else {
		enqueueLog.Error(nil, "UpdateEvent received with no new metadata", "event", evt)
	}
}

// Delete 函数实现
func (e *EnqueueRequestForObject) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
	if evt.Meta == nil {
		enqueueLog.Error(nil, "DeleteEvent received with no metadata", "event", evt)
		return
	}
  // 因为前面关于对象的删除状态已经处理了,所以这里直接放入队列中即可
	q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
		Name:      evt.Meta.GetName(),
		Namespace: evt.Meta.GetNamespace(),
	}})
}
  1. 通过 EnqueueRequestForObject 的 Create/Update/Delete 实现可以看出我们放入到工作队列中的元素不是以前默认的元素唯一的 KEY,而是经过封装的 reconcile.Request 对象,当然通过这个对象也可以很方便获取对象的唯一标识 KEY。
    第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理_第2张图片
  2. 总结起来就是 Controller.Watch 函数就是来实现之前自定义控制器中的 Informer 初始化以及事件监听函数的注册。

3.3 Start 函数实现

  1. 上面我们分析了控制器的 Watch 函数的实现,下面我们来分析另外一个重要的函数 Controller.Start 函数的实现。
// pkg/internal/controller/controller.go

func (c *Controller) Start(stop <-chan struct{}) error {
	c.mu.Lock()
	
  // 调用 MakeQueue() 函数生成工作队列
	c.Queue = c.MakeQueue()
  // 函数退出后关闭队列
	defer c.Queue.ShutDown() 

	err := func() error {
		defer c.mu.Unlock()

		// TODO(pwittrock): Reconsider HandleCrash
		defer utilruntime.HandleCrash()

		// NB(directxman12): 在试图等待缓存同步之前启动 sources
    // 这样他们有机会注册他们的目标缓存
		for _, watch := range c.startWatches {
			c.Log.Info("Starting EventSource", "source", watch.src)
			if err := watch.src.Start(watch.handler, c.Queue, watch.predicates...); err != nil {
				return err
			}
		}

		// 启动 SharedIndexInformer 工厂,开始填充 SharedIndexInformer 缓存
		c.Log.Info("Starting Controller")

		for _, watch := range c.startWatches {
			syncingSource, ok := watch.src.(source.SyncingSource)
			if !ok {
				continue
			}
      // 等待 Informer 同步完成
			if err := syncingSource.WaitForSync(stop); err != nil {
				err := fmt.Errorf("failed to wait for %s caches to sync: %w", c.Name, err)
				c.Log.Error(err, "Could not wait for Cache to sync")
				return err
			}
		}

		// 所有的 watches 已经启动,重置
		c.startWatches = nil

		if c.JitterPeriod == 0 {
			c.JitterPeriod = 1 * time.Second
		}

		// 启动 workers 来处理资源
		c.Log.Info("Starting workers", "worker count", c.MaxConcurrentReconciles)
		for i := 0; i < c.MaxConcurrentReconciles; i++ {
			go wait.Until(c.worker, c.JitterPeriod, stop)
		}
		c.Started = true
		return nil
	}()
	if err != nil {
		return err
	}

	<-stop
	c.Log.Info("Stopping workers")
	return nil
}
  1. 上面的 Start 函数很简单,和我们之前自定义控制器中启动控制循环比较类似,都是先等待资源对象的 Informer 同步完成,然后启动 workers 来处理资源对象,而且 worker 函数都是一样的实现方式:
// pkg/internal/controller/controller.go

// worker 运行一个工作线程,从队列中弹出元素处理,并标记为完成
// 强制要求永远不和同一个对象同时调用 reconcileHandler
func (c *Controller) worker() {
	for c.processNextWorkItem() {
	}
}

// processNextWorkItem 将从工作队列中弹出一个元素,并尝试通过调用 reconcileHandler 来处理它
func (c *Controller) processNextWorkItem() bool {
  // 从队列中弹出元素
	obj, shutdown := c.Queue.Get()
	if shutdown {
		// 队列关闭了,直接返回 false
		return false
	}

  // 标记为处理完成
	defer c.Queue.Done(obj)
	// 调用 reconcileHandler 进行元素处理
	return c.reconcileHandler(obj)
}

func (c *Controller) reconcileHandler(obj interface{}) bool {
	// 处理完每个元素后更新指标
	reconcileStartTS := time.Now()
	defer func() {
		c.updateMetrics(time.Since(reconcileStartTS))
	}()

	// 确保对象是一个有效的 request 对象
	req, ok := obj.(reconcile.Request)
	if !ok {
		// 工作队列中的元素无效,所以调用 Forget 函数
    // 否则会进入一个循环尝试处理一个无效的元素
		c.Queue.Forget(obj)
		c.Log.Error(nil, "Queue item was not a Request", "type", fmt.Sprintf("%T", obj), "value", obj)
		// 直接返回 true
		return true
	}

	log := c.Log.WithValues("name", req.Name, "namespace", req.Namespace)

	// RunInformersAndControllers 的 syncHandler,传递给它要同步的资源的 namespace/Name 字符串
  // 调用 Reconciler 函数来处理这个元素,也就是我们真正去编写业务逻辑的地方
	if result, err := c.Do.Reconcile(req); err != nil {
    // 如果业务逻辑处理出错,重新添加到限速队列中去
		c.Queue.AddRateLimited(req)
		log.Error(err, "Reconciler error")
    // Metrics 指标记录
		ctrlmetrics.ReconcileErrors.WithLabelValues(c.Name).Inc()
		ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, "error").Inc()
		return false
	} else if result.RequeueAfter > 0 {
    // 如果调谐函数 Reconcile 处理结果中包含大于0的 RequeueAfter
    // 
    // 需要注意如果 result.RequeuAfter 与一个非 nil 的错误一起返回,则 result.RequeuAfter 会丢失。
		// 忘记元素
		c.Queue.Forget(obj)
    // 加入队列
		c.Queue.AddAfter(req, result.RequeueAfter)
		ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, "requeue_after").Inc()
		return true
	} else if result.Requeue {
    // 重新加入队列
		c.Queue.AddRateLimited(req)
		ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, "requeue").Inc()
		return true
	}

	// 最后如果没有发生错误,我们就会 Forget 这个元素
  // 这样直到发送另一个变化它就不会再被排队了
	c.Queue.Forget(obj)

	// TODO(directxman12): What does 1 mean?  Do we want level constants?  Do we want levels at all?
	log.V(1).Info("Successfully Reconciled")
  
  // metrics 指标记录
	ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, "success").Inc()
	// 直接返回true
	return true
}
  1. 上面的 reconcileHandler 函数就是我们真正执行元素业务处理的地方,函数中包含了事件处理以及错误处理,真正的事件处理是通过 c.Do.Reconcile(req) 暴露给开发者的,所以对于开发者来说,只需要在 Reconcile 函数中去处理业务逻辑就可以了。
  2. 根据 c.Do.Reconcile(req) 函数的返回值来判断是否需要将元素重新加入队列进行处理:
    • 如果返回 error 错误,则将元素重新添加到限速队列中
    • 如果返回的 result.RequeueAfter > 0,则先将元素忘记,然后在 result.RequeueAfter 时间后加入到队列中
    • 如果返回 result.Requeue,则直接将元素重新加入到限速队列中
    • 如果正常返回,则直接忘记这个元素
  3. 到这里其实基本上就实现了和我们自定义控制器一样的逻辑,只是将业务处理的逻辑暴露给了开发者去自己实现。接下来我们就需要了解下 controller-runtime 是如何去控制器控制器的初始化以及启动的。

第四节 controller-runtime 原理之 manager

4.1 Manager 如何使用

  1. 上文我们介绍了 controller-runtime 中的 Controller 的实现,这个控制器的实现和我们自定义控制器的流程基本一致的,那么 controller-runtime 是如何来使用这个 Controller 的呢,本节我们就来详细介绍下。
  2. 在 controller-runtime 中使用了一个 Manager 的接口来管理 Controller,除了控制器其实还可以管理 Admission Webhook,也包括访问资源对象的 client、cache、scheme 等,如下图所示:
    第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理_第3张图片
  3. 首先我们先来查看下 controller-runtime 中的 Manager 是如何使用的,查看 controller-runtime 代码仓库中的示例,位于 https://github.com/kubernetes-sigs/controller-runtime/tree/master/examples/crd,示例中关于 Manager 的使用步骤为:
    • 实例化 manager,参数 config
    • 向 manager 添加 scheme
    • 向 manager 添加 controller,该 controller 包含一个 reconciler 结构体,我们需要在 reconciler 结构体实现逻辑处理
    • 向 manager 添加 webhook,同样需要实现逻辑处理
    • 启动 manager.start()
  4. 代码如下所示:
// 根据 config 实例化 Manager
// config.GetConfigOrDie() 使用默认的配置~/.kube/config
manager.New(config.GetConfigOrDie(), manager.Options{})

// 将 api 注册到 Scheme,Scheme 提供了 GVK 到 go type 的映射。
// 如果多个 crd,需要多次调用 AddToScheme
api.AddToScheme(mgr.GetScheme())

// 注册 Controller 到 Manager
// For:监控的资源,相当于调用 Watches(&source.Kind{Type: apiType},&handler.EnqueueRequestForObject{})
// Owns:拥有的下属资源,如果 corev1.Pod{} 资源属于 api.ChaosPod{},也将会被监控,相当于调用 Watches(&source.Kind{Type: }, &handler.EnqueueRequestForOwner{OwnerType: apiType, IsController: true})
// reconciler 结构体:继承 Reconciler,需要实现该结构体和 Reconcile 方法
// mgr.GetClient()、mgr.GetScheme() 是 Client 和 Scheme,前面的 manager.New 初始化了
err = builder.ControllerManagedBy(mgr).
		For(&api.ChaosPod{}).
		Owns(&corev1.Pod{}).
		Complete(&reconciler{
			Client: mgr.GetClient(),
			scheme: mgr.GetScheme(),
		})
// 构建webhook
err = builder.WebhookManagedBy(mgr).For(&api.ChaosPod{}).Complete()
// 启动manager,实际上是启动controller
mgr.Start(ctrl.SetupSignalHandler())
  1. Manager 是一个用于初始化共享依赖关系的接口,接口定义如下所示(只显示了核心的几个方法):
// pkg/manager/manager.go

// Manager 初始化共享的依赖关系,比如 Caches 和 Client,并将他们提供给 Runnables
type Manager interface {
	// Add 将在组件上设置所需的依赖关系,并在调用 Start 时启动组件
  // Add 将注入接口的依赖关系 - 比如 注入 inject.Client
  // 根据 Runnable 是否实现了 LeaderElectionRunnable 接口判断
  // Runnable 可以在非 LeaderElection 模式(始终运行)或 LeaderElection 模式(如果启用了 LeaderElection,则由 LeaderElection 管理)下运行
  Add(Runnable) error

	// SetFields 设置对象上的所有依赖关系,而该对象已经实现了 inject 接口
  // 比如 inject.Client
	SetFields(interface{}) error

	// Start 启动所有已注册的控制器,并一直运行,直到停止通道关闭
  // 如果启动任何控制器都出错,则返回错误。
  // 如果使用了 LeaderElection,则必须在此返回后立即退出二进制,否则需要 Leader 选举的组件可能会在 Leader 锁丢失后继续运行
	Start(<-chan struct{}) error

	......
}

第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理_第4张图片
6. Manager 可以管理 Runnable 的生命周期(添加/启动),如果您不通过 Manager 启动(需要处理各种常见的依赖关系)。
7. Manager 还保持共同的依赖性:client、cache、scheme 等。
- 提供了getter(例如GetClient())
- 还有一个简单的依赖注入机制(runtime/inject)
8. 此外还支持领导人选举,只需用选项指定即可,还提供了一个用于优雅关闭的信号处理程序。

4.2 Manager 实例化

  1. 然后查看下 Manager 的实例化 New 函数的实现:
// 返回一个新的 Manager,用于创建 Controllers
func New(config *rest.Config, options Options) (Manager, error) {
	if config == nil {
		return nil, fmt.Errorf("must specify Config")
	}
	// 设置 options 属性的默认值
	options = setOptionsDefaults(options)
	......
	return &controllerManager{
		......
	}, nil
}
  1. New 函数中就是为 Manager 执行初始化工作,其中 setOptionsDefaults 函数为 Options 属性设置了默认的一些参数值,最后返回的是一个 controllerManager 的实例,这是因为该结构体是 Manager 接口的一个实现,所以 Manager 的真正操作都是这个结构体去实现的。
  2. 接下来最重要的就是注册 Controller 到 Manager 的过程:
err = builder.ControllerManagedBy(mgr).
		For(&api.ChaosPod{}).
		Owns(&corev1.Pod{}).
		Complete(&reconciler{
			Client: mgr.GetClient(),
			scheme: mgr.GetScheme(),
		})
  1. builder.ControllerManagedBy 函数返回一个新的控制器构造器 Builder 对象,生成的控制器将由所提供的管理器 Manager 启动,函数实现很简单:
// pkg/builder/controller.go

// 控制器构造器
type Builder struct {
	forInput         ForInput
	ownsInput        []OwnsInput
	watchesInput     []WatchesInput
	mgr              manager.Manager
	globalPredicates []predicate.Predicate
	config           *rest.Config
	ctrl             controller.Controller
	ctrlOptions      controller.Options
	log              logr.Logger
	name             string
}

// ControllerManagedBy 返回一个新的控制器构造器
// 它将由提供的 Manager 启动
func ControllerManagedBy(m manager.Manager) *Builder {
	return &Builder{mgr: m}
}
  1. 可以看到 controller-runtime 封装了一个 Builder 的结构体用来生成 Controller,将 Manager 传递给这个构造器,然后是调用构造器的 For 函数:
// pkg/builder/controller.go

// ForInput 表示 For 方法设置的信息
type ForInput struct {
	object     runtime.Object
	predicates []predicate.Predicate
}

// For 函数定义了被调谐的对象类型
// 并配置 ControllerManagedBy 通过调谐对象来响应 create/delete/update 事件
// 调用 For 函数相当于调用:
// Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{})
func (blder *Builder) For(object runtime.Object, opts ...ForOption) *Builder {
	input := ForInput{object: object}
	for _, opt := range opts {
		opt.ApplyToFor(&input)
	}
	blder.forInput = input
	return blder
}
  1. For 函数就是用来定义我们要处理的对象类型的,接着调用了 Owns 函数:
// pkg/builder/controller.go

// OwnsInput 表示 Owns 方法设置的信息
type OwnsInput struct {
	object     runtime.Object
	predicates []predicate.Predicate
}

// Owns 定义了 ControllerManagedBy 生成的对象类型
// 并配置 ControllerManagedBy 通过调谐所有者对象来响应 create/delete/update 事件
// 这相当于调用:
// Watches(&source.Kind{Type: }, &handler.EnqueueRequestForOwner{OwnerType: apiType, IsController: true})
func (blder *Builder) Owns(object runtime.Object, opts ...OwnsOption) *Builder {
	input := OwnsInput{object: object}
	for _, opt := range opts {
		opt.ApplyToOwns(&input)
	}

	blder.ownsInput = append(blder.ownsInput, input)
	return blder
}
  1. Owns 函数就是来配置我们监听的资源对象的子资源,如果想要协调资源则需要调用 Owns 函数进行配置,然后就是最重要的 Complete 函数了:
// pkg/builder/controller.go

func (blder *Builder) Complete(r reconcile.Reconciler) error {
  // 调用 Build 函数构建 Controller
	_, err := blder.Build(r)
	return err
}

// Build 构建应用程序 ControllerManagedBy 并返回它创建的 Controller
func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, error) {
	if r == nil {
		return nil, fmt.Errorf("must provide a non-nil Reconciler")
	}
	if blder.mgr == nil {
		return nil, fmt.Errorf("must provide a non-nil Manager")
	}

	// 配置 Rest Config
	blder.loadRestConfig()

	// 配置 ControllerManagedBy
	if err := blder.doController(r); err != nil {
		return nil, err
	}

	// 配置 Watch
	if err := blder.doWatch(); err != nil {
		return nil, err
	}

	return blder.ctrl, nil
}
  1. Complete 函数通过调用 Build 函数来构建 Controller,其中比较重要的就是 doControllerdoWatch 两个函数,doController 就是去真正实例化 Controller 的函数:
// pkg/builder/controller.go

// 根据 GVK 获取控制器名称
func (blder *Builder) getControllerName(gvk schema.GroupVersionKind) string {
	if blder.name != "" {
		return blder.name
	}
	return strings.ToLower(gvk.Kind)
}

func (blder *Builder) doController(r reconcile.Reconciler) error {
	ctrlOptions := blder.ctrlOptions
	if ctrlOptions.Reconciler == nil {
		ctrlOptions.Reconciler = r
	}

	// 从我们正在调谐的对象中检索 GVK
	gvk, err := getGvk(blder.forInput.object, blder.mgr.GetScheme())
	if err != nil {
		return err
	}

	// 配置日志 Logger
	if ctrlOptions.Log == nil {
		ctrlOptions.Log = blder.mgr.GetLogger()
	}
	ctrlOptions.Log = ctrlOptions.Log.WithValues("reconcilerGroup", gvk.Group, "reconcilerKind", gvk.Kind)

	// 构造 Controller 
  // var newController = controller.New
	blder.ctrl, err = newController(blder.getControllerName(gvk), blder.mgr, ctrlOptions)
	return err
}
  1. 上面的函数通过获取资源对象的 GVK 来获取 Controller 的名称,最后通过一个 newController 函数(controller.New 的别名)来实例化一个真正的 Controller:
// pkg/controller/controller.go

// New 返回一个 Manager 处注册的 Controller
// Manager 将确保共享缓存在控制器启动前已经同步
func New(name string, mgr manager.Manager, options Options) (Controller, error) {
	c, err := NewUnmanaged(name, mgr, options)
	if err != nil {
		return nil, err
	}

	// 将 controller 作为 manager 的组件
	return c, mgr.Add(c)
}

// NewUnmanaged 返回一个新的控制器,而不将其添加到 manager 中
// 调用者负责启动返回的控制器
func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller, error) {
	if options.Reconciler == nil {
		return nil, fmt.Errorf("must specify Reconciler")
	}

	if len(name) == 0 {
		return nil, fmt.Errorf("must specify Name for Controller")
	}

	if options.MaxConcurrentReconciles <= 0 {
		options.MaxConcurrentReconciles = 1
	}

	if options.RateLimiter == nil {
		options.RateLimiter = workqueue.DefaultControllerRateLimiter()
	}

	if options.Log == nil {
		options.Log = mgr.GetLogger()
	}

	// 在 Reconciler 中注入依赖关系
	if err := mgr.SetFields(options.Reconciler); err != nil {
		return nil, err
	}

	// 创建 Controller 并配置依赖关系
	return &controller.Controller{
		Do: options.Reconciler,
		MakeQueue: func() workqueue.RateLimitingInterface {
			return workqueue.NewNamedRateLimitingQueue(options.RateLimiter, name)
		},
		MaxConcurrentReconciles: options.MaxConcurrentReconciles,
		SetFields:               mgr.SetFields,
		Name:                    name,
		Log:                     options.Log.WithName("controller").WithValues("controller", name),
	}, nil
}
  1. 可以看到 NewUnmanaged 函数才是真正实例化 Controller 的地方,终于和前文的 Controller 联系起来来,Controller 实例化完成后,又通过 mgr.Add(c) 函数将控制器添加到 Manager 中去进行管理,所以我们还需要去查看下 Manager 的 Add 函数的实现,当然是看 controllerManager 中的具体实现:
// pkg/manager/manager.go

// Runnable 允许一个组件被启动
type Runnable interface {
	Start(<-chan struct{}) error
}

// pkg/manager/internal.go

// Add 设置i的依赖,并将其他添加到 Runnables 列表中启动
func (cm *controllerManager) Add(r Runnable) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()
	if cm.stopProcedureEngaged {
		return errors.New("can't accept new runnable as stop procedure is already engaged")
	}

	// 设置对象的依赖
	if err := cm.SetFields(r); err != nil {
		return err
	}

	var shouldStart bool

	// 添加 runnable 到 leader election 或者非 leaderelection 列表
	if leRunnable, ok := r.(LeaderElectionRunnable); ok && !leRunnable.NeedLeaderElection() {
		shouldStart = cm.started
		cm.nonLeaderElectionRunnables = append(cm.nonLeaderElectionRunnables, r)
	} else {
		shouldStart = cm.startedLeader
		cm.leaderElectionRunnables = append(cm.leaderElectionRunnables, r)
	}

	if shouldStart {
		// 如果已经启动,启动控制器
		cm.startRunnable(r)
	}

	return nil
}

func (cm *controllerManager) startRunnable(r Runnable) {
	cm.waitForRunnable.Add(1)
	go func() {
		defer cm.waitForRunnable.Done()
		if err := r.Start(cm.internalStop); err != nil {
			cm.errChan <- err
		}
	}()
}
  1. controllerManager 的 Add 函数传递的是一个 Runnable 参数,Runnable 是一个接口,用来表示可以启动的一个组件,而恰好 Controller 实际上就实现了这个接口的 Start 函数,所以可以通过 Add 函数来添加 Controller 实例,在 Add 函数中除了依赖注入之外,还根据 Runnable 来判断组件是否支持选举功能,支持则将组件加入到 leaderElectionRunnables 列表中,否则加入到 nonLeaderElectionRunnables 列表中,这点非常重要,涉及到后面控制器的启动方式。

4.3 启动 Manager

  1. 如果 Manager 已经启动了,现在调用 Add 函数来添加 Runnable,则需要立即调用 startRunnable 函数启动控制器,startRunnable 函数就是在一个 goroutine 中去调用 Runnable 的 Start 函数,这里就相当于调用 Controller 的 Start 函数来启动控制器了。
  2. 到这里就实例化 Controller 完成了,回到前面 Builder 的 build 函数中,doController 函数调用完成,接着是 doWatch 函数的实现:
// pkg/builder/controller.go

func (blder *Builder) doWatch() error {
	// 调谐类型
	src := &source.Kind{Type: blder.forInput.object}
	hdler := &handler.EnqueueRequestForObject{}
	allPredicates := append(blder.globalPredicates, blder.forInput.predicates...)
	// 执行 Watch 操作
  err := blder.ctrl.Watch(src, hdler, allPredicates...)
	if err != nil {
		return err
	}

	// Watches 管理的类型(子类型)
	for _, own := range blder.ownsInput {
		src := &source.Kind{Type: own.object}
		hdler := &handler.EnqueueRequestForOwner{
			OwnerType:    blder.forInput.object,
			IsController: true,
		}
		allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
		allPredicates = append(allPredicates, own.predicates...)
		if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
			return err
		}
	}

	// 执行 watch 请求
	for _, w := range blder.watchesInput {
		allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
		allPredicates = append(allPredicates, w.predicates...)
		if err := blder.ctrl.Watch(w.src, w.eventhandler, allPredicates...); err != nil {
			return err
		}

	}
	return nil
}
  1. 上面的 doWatch 函数就是去将我们需要调谐的资源对象放到 Controller 中进行 Watch 操作,包括资源对象管理的子类型,都需要去执行 Watch 操作,这就又回到了前面 Controller 的 Watch 操作了,其实就是去注册 Informer 的事件监听器,将数据添加到工作队列中去。这样到这里我们就将 Controller 初始化完成,并为我们调谐的资源对象执行了 Watch 操作。
  2. 最后是调用 Manager 的 Start 函数来启动 Manager,由于上面我们已经把 Controller 添加到了 Manager 中,所以这里启动其实是启动关联的 Controller,启动函数实现如下所示:
// pkg/manager/internal.go

func (cm *controllerManager) Start(stop <-chan struct{}) (err error) {
	stopComplete := make(chan struct{})
	defer close(stopComplete)
	// stopComplete 关闭后必须在 deferer 执行下面的操作,否则会出现死锁
	defer func() {
		// https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/gettyimages-459889618-1533579787.jpg
		stopErr := cm.engageStopProcedure(stopComplete)
		if stopErr != nil {
			if err != nil {
				err = utilerrors.NewAggregate([]error{err, stopErr})
			} else {
				err = stopErr
			}
		}
	}()

	cm.errChan = make(chan error)

	// Metrics 服务
	if cm.metricsListener != nil {
		go cm.serveMetrics(cm.internalStop)
	}

	// 健康检测的服务
	if cm.healthProbeListener != nil {
		go cm.serveHealthProbes(cm.internalStop)
	}
	
	// 启动非 LeaderElection 的 Runnables
	go cm.startNonLeaderElectionRunnables()

	go func() {
		if cm.resourceLock != nil {
      // 启动 LeaderElection 选举
			err := cm.startLeaderElection()
			if err != nil {
				cm.errChan <- err
			}
		} else {
			close(cm.elected)
      // 启动 LeaderElection 的 Runnables
			go cm.startLeaderElectionRunnables()
		}
	}()

	select {
	case <-stop:
		// We are done
		return nil
	case err := <-cm.errChan:
		// Error starting or running a runnable
		return err
	}
}
  1. 上面的启动函数其实就是去启动前面我们加入到 Manager 中的 Runnable(Controller),非 LeaderElection 的列表与 LeaderElection 的列表都分别在一个 goroutine 中启动:
// pkg/manager/internal.go

func (cm *controllerManager) waitForCache() {
	if cm.started {
		return
	}

	// Start the Cache. Allow the function to start the cache to be mocked out for testing
	if cm.startCache == nil {
		cm.startCache = cm.cache.Start
	}
	cm.startRunnable(RunnableFunc(func(stop <-chan struct{}) error {
		return cm.startCache(stop)
	}))

	cm.cache.WaitForCacheSync(cm.internalStop)
	cm.started = true
}

// 启动非 LeaderElection Runnables
func (cm *controllerManager) startNonLeaderElectionRunnables() {
	cm.mu.Lock()
	defer cm.mu.Unlock()
	// 等待缓存同步完成
	cm.waitForCache()

	// 开始启动所有的非 leaderelection 的 Runnables
	for _, c := range cm.nonLeaderElectionRunnables {
		cm.startRunnable(c)
	}
}

func (cm *controllerManager) startLeaderElectionRunnables() {
	cm.mu.Lock()
	defer cm.mu.Unlock()
	// 等待缓存同步完成
	cm.waitForCache()

	for _, c := range cm.leaderElectionRunnables {
		cm.startRunnable(c)
	}

	cm.startedLeader = true
}

// 真正的启动一个 Runnable
func (cm *controllerManager) startRunnable(r Runnable) {
	cm.waitForRunnable.Add(1)
	go func() {
		defer cm.waitForRunnable.Done()
		if err := r.Start(cm.internalStop); err != nil {
			cm.errChan <- err
		}
	}()
}

第八课 k8s源码学习和二次开发原理篇-KubeBuilder使用和Controller-runtime原理_第5张图片
6. 可以看到最终还是去调用的 Runnable 的 Start 函数来启动,这里其实也就是 Controller 的 Start 函数,前文我们已经详细介绍过,这个函数相当于启动一个控制循环不断从工作队列中消费数据,然后给到一个 Reconciler 接口进行处理,也就是我们要去实现的 Reconcile(Request) (Result, error) 这个业务逻辑函数。
7. 到这里我们就完成了 Manager 的整个启动过程,包括 Manager 是如何初始化,如何和 Controller 进行关联以及如何启动 Controller 的,了解了整个 controller-runtime 的原理过后,我们再去使用 kubebuilder 来编写 Operator 就更加容易了。

你可能感兴趣的:(kubernetes,容器,云原生)